updates
This commit is contained in:
parent
d903bd9a13
commit
8dacee8f73
@ -6,8 +6,7 @@ export const App = () => {
|
||||
<div className="main">
|
||||
<div id={Miscellaneous.MODAL_ID} className="modal">
|
||||
<div id={Miscellaneous.MODAL_CONTENT_ID} className="modal-content">
|
||||
<span className="close">×</span>
|
||||
<p>Some text in the Modal..</p>
|
||||
<hr></hr>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -11,6 +11,7 @@ import {
|
||||
Life,
|
||||
Music,
|
||||
Level,
|
||||
Modal,
|
||||
} from "./systems";
|
||||
|
||||
export class TheAbstractionEngine {
|
||||
@ -33,6 +34,7 @@ export class TheAbstractionEngine {
|
||||
const facingDirectionSystem = new FacingDirection(inputSystem);
|
||||
|
||||
[
|
||||
new Modal(),
|
||||
new Level(LevelNames.LevelSelection),
|
||||
inputSystem,
|
||||
facingDirectionSystem,
|
||||
|
@ -12,4 +12,5 @@ export namespace ComponentNames {
|
||||
export const Text = "Text";
|
||||
export const LambdaTerm = "LambdaTerm";
|
||||
export const Life = "Life";
|
||||
export const Modal = "Modal";
|
||||
}
|
||||
|
@ -1,11 +1,7 @@
|
||||
import { Component, ComponentNames } from ".";
|
||||
|
||||
export class Control extends Component {
|
||||
public isControllable: boolean = true;
|
||||
|
||||
constructor(isControllable = true) {
|
||||
constructor(public isControllable = true) {
|
||||
super(ComponentNames.Control);
|
||||
|
||||
this.isControllable = isControllable;
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { Component, ComponentNames } from ".";
|
||||
import { DebrujinifiedLambdaTerm } from "../../interpreter";
|
||||
|
||||
export class LambdaTerm extends Component {
|
||||
public code: string;
|
||||
public last: null | { data?: DebrujinifiedLambdaTerm; error?: any } = null;
|
||||
|
||||
constructor(code: string) {
|
||||
super(ComponentNames.LambdaTerm);
|
||||
|
10
src/engine/components/Modal.ts
Normal file
10
src/engine/components/Modal.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { Component, ComponentNames } from ".";
|
||||
import { ModalInitState } from "../systems";
|
||||
|
||||
export class Modal extends Component {
|
||||
constructor(
|
||||
public initState: ModalInitState
|
||||
) {
|
||||
super(ComponentNames.Modal);
|
||||
}
|
||||
}
|
@ -13,3 +13,4 @@ export * from "./GridSpawn";
|
||||
export * from "./Text";
|
||||
export * from "./LambdaTerm";
|
||||
export * from "./Life";
|
||||
export * from "./Modal";
|
||||
|
@ -19,7 +19,7 @@ export const LambdaTransformSound: SoundSpec = {
|
||||
volume: 0.3,
|
||||
};
|
||||
|
||||
export const LambdaSave: SoundSpec = {
|
||||
export const EditorSave: SoundSpec = {
|
||||
name: "lambdaSave",
|
||||
url: "/assets/sound/lambda_save.wav",
|
||||
volume: 0.3,
|
||||
@ -83,7 +83,7 @@ export const Music: SoundSpec = {
|
||||
export const SOUND_SPECS: SoundSpec[] = [
|
||||
MovingSound,
|
||||
LambdaTransformSound,
|
||||
LambdaSave,
|
||||
EditorSave,
|
||||
Failure,
|
||||
ModalOpen,
|
||||
ModalClose,
|
||||
|
@ -1,17 +1,13 @@
|
||||
import {
|
||||
Entity,
|
||||
EntityNames,
|
||||
FunctionBox,
|
||||
Key,
|
||||
Particles,
|
||||
makeLambdaTermHighlightComponent,
|
||||
} from ".";
|
||||
import { Entity, EntityNames, FunctionBox, Key, Particles } from ".";
|
||||
import {
|
||||
BoundingBox,
|
||||
Colliding,
|
||||
ComponentNames,
|
||||
Grid,
|
||||
Highlight,
|
||||
Interactable,
|
||||
LambdaTerm,
|
||||
Modal,
|
||||
Sprite,
|
||||
} from "../components";
|
||||
import {
|
||||
@ -26,9 +22,9 @@ import {
|
||||
import { Coord2D, Direction } from "../interfaces";
|
||||
import { Game } from "..";
|
||||
import { Grid as GridSystem, SystemNames } from "../systems";
|
||||
import { colors } from "../utils";
|
||||
import { colors, tryWrap } from "../utils";
|
||||
import {
|
||||
DebrujinifiedLambdaTerm,
|
||||
InvalidLambdaTermError,
|
||||
SymbolTable,
|
||||
emitNamed,
|
||||
interpret,
|
||||
@ -62,8 +58,8 @@ export class FunctionApplication extends Entity {
|
||||
y: 0,
|
||||
},
|
||||
dimension,
|
||||
0,
|
||||
),
|
||||
0
|
||||
)
|
||||
);
|
||||
|
||||
this.addComponent(new Grid(gridPosition));
|
||||
@ -76,13 +72,48 @@ export class FunctionApplication extends Entity {
|
||||
{ x: 0, y: 0 },
|
||||
dimension,
|
||||
FunctionApplication.spriteSpec.msPerFrame,
|
||||
FunctionApplication.spriteSpec.frames,
|
||||
),
|
||||
FunctionApplication.spriteSpec.frames
|
||||
)
|
||||
);
|
||||
|
||||
this.addComponent(new Colliding(this.handleCollision.bind(this)));
|
||||
|
||||
this.addComponent(makeLambdaTermHighlightComponent(this));
|
||||
this.addComponent(
|
||||
new Highlight(this.onHighlight.bind(this), this.onUnhighlight.bind(this))
|
||||
);
|
||||
}
|
||||
|
||||
private onHighlight(_direction: Direction) {
|
||||
this.addComponent(new Interactable(this.interaction.bind(this)));
|
||||
}
|
||||
|
||||
private interaction() {
|
||||
const codeConsumer = (_code: string) => {
|
||||
this.removeComponent(ComponentNames.Modal);
|
||||
return { consumed: true };
|
||||
};
|
||||
const { last, code } = this.getComponent<LambdaTerm>(
|
||||
ComponentNames.LambdaTerm
|
||||
);
|
||||
this.addComponent(
|
||||
new Modal({
|
||||
type: "CODE_EDITOR",
|
||||
codeInit: {
|
||||
code,
|
||||
codeConsumer,
|
||||
readonly: true,
|
||||
result: {
|
||||
error: last?.error && `Error: ${last.error.message}`,
|
||||
data: last?.data && `Last Result: ${emitNamed(last.data)}`,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private onUnhighlight() {
|
||||
this.removeComponent(ComponentNames.Modal);
|
||||
this.removeComponent(ComponentNames.Interactable);
|
||||
}
|
||||
|
||||
public handleCollision(game: Game, entity: Entity) {
|
||||
@ -100,7 +131,7 @@ export class FunctionApplication extends Entity {
|
||||
const gridSystem = game.getSystem<GridSystem>(SystemNames.Grid);
|
||||
const fail = () => {
|
||||
entityGrid.movingDirection = gridSystem.oppositeDirection(
|
||||
entityGrid.previousDirection,
|
||||
entityGrid.previousDirection
|
||||
);
|
||||
entity.addComponent(entityGrid);
|
||||
|
||||
@ -109,18 +140,17 @@ export class FunctionApplication extends Entity {
|
||||
};
|
||||
|
||||
const applicationTerm = this.getComponent<LambdaTerm>(
|
||||
ComponentNames.LambdaTerm,
|
||||
ComponentNames.LambdaTerm
|
||||
);
|
||||
const functionTerm = entity.getComponent<LambdaTerm>(
|
||||
ComponentNames.LambdaTerm,
|
||||
ComponentNames.LambdaTerm
|
||||
);
|
||||
const newCode = applicationTerm.code.replace("_INPUT", functionTerm.code);
|
||||
|
||||
let result: DebrujinifiedLambdaTerm | null = null;
|
||||
try {
|
||||
result = interpret(newCode, this.symbolTable, true);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
const result = tryWrap(() => interpret(newCode, this.symbolTable, true));
|
||||
applicationTerm.last = result;
|
||||
if (result.error || !result.data) {
|
||||
console.error(result.error);
|
||||
fail();
|
||||
return;
|
||||
}
|
||||
@ -128,29 +158,34 @@ export class FunctionApplication extends Entity {
|
||||
const { dimension } = gridSystem;
|
||||
const nextPosition = gridSystem.getNewGridPosition(
|
||||
grid.gridPosition,
|
||||
entityGrid.previousDirection,
|
||||
entityGrid.previousDirection
|
||||
);
|
||||
|
||||
let applicationResultingEntity: Entity | null = null; // this should be its own function
|
||||
if ("abstraction" in result) {
|
||||
const code = emitNamed(result);
|
||||
|
||||
const { data } = result;
|
||||
if ("application" in data) {
|
||||
// if we get an application that means we didn't interpret correctly.
|
||||
// this should "not" happen and should be fatal.
|
||||
throw new InvalidLambdaTermError(
|
||||
"produced term should not be an application"
|
||||
);
|
||||
}
|
||||
if ("abstraction" in data) {
|
||||
const code = emitNamed(data);
|
||||
applicationResultingEntity = new FunctionBox(grid.gridPosition, code);
|
||||
} else if ("name" in result) {
|
||||
const { name } = result;
|
||||
}
|
||||
if ("name" in data) {
|
||||
const { name } = data;
|
||||
const entityFactory = APPLICATION_RESULTS[name];
|
||||
if (entityFactory) {
|
||||
game.addEntity(entityFactory(nextPosition));
|
||||
}
|
||||
} else {
|
||||
fail();
|
||||
return;
|
||||
}
|
||||
|
||||
game.removeEntity(entity.id);
|
||||
if (applicationResultingEntity) {
|
||||
const grid = applicationResultingEntity.getComponent<Grid>(
|
||||
ComponentNames.Grid,
|
||||
ComponentNames.Grid
|
||||
);
|
||||
grid.movingDirection = entityGrid.previousDirection;
|
||||
applicationResultingEntity.addComponent(grid);
|
||||
@ -158,7 +193,7 @@ export class FunctionApplication extends Entity {
|
||||
game.addEntity(applicationResultingEntity);
|
||||
}
|
||||
|
||||
this.playTransformSound();
|
||||
SOUNDS.get(LambdaTransformSound.name)!.play();
|
||||
const particles = new Particles({
|
||||
center: gridSystem.gridToScreenPosition(nextPosition),
|
||||
spawnerDimensions: {
|
||||
@ -183,9 +218,4 @@ export class FunctionApplication extends Entity {
|
||||
});
|
||||
game.addEntity(particles);
|
||||
}
|
||||
|
||||
private playTransformSound() {
|
||||
const audio = SOUNDS.get(LambdaTransformSound.name)!;
|
||||
audio.play();
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,4 @@
|
||||
import {
|
||||
IMAGES,
|
||||
Miscellaneous,
|
||||
ModalClose,
|
||||
ModalOpen,
|
||||
SOUNDS,
|
||||
SPRITE_SPECS,
|
||||
SpriteSpec,
|
||||
Sprites,
|
||||
} from "../config";
|
||||
import { IMAGES, SPRITE_SPECS, SpriteSpec, Sprites } from "../config";
|
||||
import { Entity, EntityNames } from ".";
|
||||
import {
|
||||
BoundingBox,
|
||||
@ -16,18 +7,21 @@ import {
|
||||
Highlight,
|
||||
Interactable,
|
||||
LambdaTerm,
|
||||
Modal,
|
||||
Pushable,
|
||||
Sprite,
|
||||
} from "../components";
|
||||
import { Coord2D } from "../interfaces";
|
||||
import { openModal, closeModal } from "../utils";
|
||||
|
||||
export class FunctionBox extends Entity {
|
||||
private static spriteSpec: SpriteSpec = SPRITE_SPECS.get(
|
||||
Sprites.FUNCTION_BOX,
|
||||
Sprites.FUNCTION_BOX
|
||||
) as SpriteSpec;
|
||||
|
||||
constructor(gridPosition: Coord2D, code: string) {
|
||||
constructor(
|
||||
gridPosition: Coord2D,
|
||||
private readonly code: string
|
||||
) {
|
||||
super(EntityNames.FunctionBox);
|
||||
|
||||
this.addComponent(
|
||||
@ -40,8 +34,8 @@ export class FunctionBox extends Entity {
|
||||
width: FunctionBox.spriteSpec.width,
|
||||
height: FunctionBox.spriteSpec.height,
|
||||
},
|
||||
0,
|
||||
),
|
||||
0
|
||||
)
|
||||
);
|
||||
|
||||
this.addComponent(new Pushable());
|
||||
@ -57,56 +51,43 @@ export class FunctionBox extends Entity {
|
||||
height: FunctionBox.spriteSpec.height,
|
||||
},
|
||||
FunctionBox.spriteSpec.msPerFrame,
|
||||
FunctionBox.spriteSpec.frames,
|
||||
),
|
||||
FunctionBox.spriteSpec.frames
|
||||
)
|
||||
);
|
||||
|
||||
this.addComponent(new LambdaTerm(code));
|
||||
|
||||
this.addComponent(makeLambdaTermHighlightComponent(this));
|
||||
this.addComponent(
|
||||
new Highlight(this.onHighlight.bind(this), this.onUnhighlight.bind(this))
|
||||
);
|
||||
}
|
||||
|
||||
private interaction() {
|
||||
const codeConsumer = (_code: string) => {
|
||||
this.removeComponent(ComponentNames.Modal);
|
||||
return { consumed: true };
|
||||
};
|
||||
const { last } = this.getComponent<LambdaTerm>(ComponentNames.LambdaTerm);
|
||||
this.addComponent(
|
||||
new Modal({
|
||||
type: "CODE_EDITOR",
|
||||
codeInit: {
|
||||
code: this.code,
|
||||
codeConsumer,
|
||||
readonly: true,
|
||||
result: {
|
||||
error: last?.error && `Error: ${last.error.message}`,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public onHighlight() {
|
||||
this.addComponent(new Interactable(this.interaction.bind(this)));
|
||||
}
|
||||
|
||||
public onUnhighlight() {
|
||||
this.removeComponent(ComponentNames.Interactable);
|
||||
}
|
||||
}
|
||||
|
||||
export const makeLambdaTermHighlightComponent = (
|
||||
entity: Entity,
|
||||
text?: string,
|
||||
) => {
|
||||
const onUnhighlight = () => {
|
||||
closeModal();
|
||||
entity.removeComponent(ComponentNames.Interactable);
|
||||
};
|
||||
|
||||
const onHighlight = () => {
|
||||
let modalOpen = false;
|
||||
const doModalClose = () => {
|
||||
SOUNDS.get(ModalClose.name)!.play();
|
||||
modalOpen = false;
|
||||
closeModal();
|
||||
};
|
||||
|
||||
const interaction = () => {
|
||||
if (modalOpen) {
|
||||
doModalClose();
|
||||
return;
|
||||
}
|
||||
|
||||
const code =
|
||||
text ??
|
||||
entity.getComponent<LambdaTerm>(ComponentNames.LambdaTerm)!.code;
|
||||
openModal(
|
||||
`<div style="text-align:center"><p>${code}</p> <br> <button id="close">Close</button></div>`,
|
||||
);
|
||||
modalOpen = true;
|
||||
SOUNDS.get(ModalOpen.name)!.play();
|
||||
|
||||
document.getElementById("close")!.addEventListener("click", () => {
|
||||
doModalClose();
|
||||
document.getElementById(Miscellaneous.CANVAS_ID)!.focus();
|
||||
});
|
||||
};
|
||||
|
||||
entity.addComponent(new Interactable(interaction));
|
||||
};
|
||||
|
||||
return new Highlight(onHighlight, onUnhighlight);
|
||||
};
|
||||
|
@ -1,10 +1,7 @@
|
||||
import {
|
||||
Failure,
|
||||
IMAGES,
|
||||
LambdaSave,
|
||||
LambdaTransformSound,
|
||||
Miscellaneous,
|
||||
ModalOpen,
|
||||
SOUNDS,
|
||||
SPRITE_SPECS,
|
||||
SpriteSpec,
|
||||
@ -19,66 +16,19 @@ import {
|
||||
GridSpawn,
|
||||
Highlight,
|
||||
Interactable,
|
||||
Modal,
|
||||
Sprite,
|
||||
Text,
|
||||
} from "../components";
|
||||
import { Coord2D, Direction } from "../interfaces";
|
||||
import { openModal, closeModal } from "../utils";
|
||||
import {
|
||||
EditorState,
|
||||
StateField,
|
||||
StateEffect,
|
||||
Range,
|
||||
Extension,
|
||||
} from "@codemirror/state";
|
||||
import { Decoration, EditorView, keymap } from "@codemirror/view";
|
||||
import { defaultKeymap } from "@codemirror/commands";
|
||||
import rainbowBrackets from "rainbowbrackets";
|
||||
import { basicSetup } from "codemirror";
|
||||
import { tryWrap } from "../utils";
|
||||
import { parse } from "../../interpreter";
|
||||
|
||||
interface CodeEditorState {
|
||||
view: EditorView;
|
||||
editorElement: HTMLElement;
|
||||
syntaxError: HTMLElement;
|
||||
canvas: HTMLCanvasElement;
|
||||
closeButton: HTMLButtonElement;
|
||||
}
|
||||
|
||||
const highlightEffect = StateEffect.define<Range<Decoration>[]>();
|
||||
const highlightExtension = StateField.define({
|
||||
create() {
|
||||
return Decoration.none;
|
||||
},
|
||||
update(value, transaction) {
|
||||
value = value.map(transaction.changes);
|
||||
|
||||
for (let effect of transaction.effects) {
|
||||
if (effect.is(highlightEffect))
|
||||
value = value.update({ add: effect.value, sort: true });
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
provide: (f) => EditorView.decorations.from(f),
|
||||
});
|
||||
|
||||
const FontSizeTheme = EditorView.theme({
|
||||
$: {
|
||||
fontSize: "16pt",
|
||||
},
|
||||
});
|
||||
const FontSizeThemeExtension: Extension = [FontSizeTheme];
|
||||
const syntaxErrorDecoration = Decoration.mark({
|
||||
class: "syntax-error",
|
||||
});
|
||||
|
||||
export class LambdaFactory extends Entity {
|
||||
private static spriteSpec: SpriteSpec = SPRITE_SPECS.get(
|
||||
Sprites.LAMBDA_FACTORY,
|
||||
Sprites.LAMBDA_FACTORY
|
||||
) as SpriteSpec;
|
||||
|
||||
private codeEditorState: CodeEditorState | null;
|
||||
private spawns: number;
|
||||
private code: string;
|
||||
|
||||
@ -87,7 +37,6 @@ export class LambdaFactory extends Entity {
|
||||
|
||||
this.spawns = spawns;
|
||||
this.code = code;
|
||||
this.codeEditorState = null;
|
||||
|
||||
this.addComponent(
|
||||
new BoundingBox(
|
||||
@ -99,8 +48,8 @@ export class LambdaFactory extends Entity {
|
||||
width: LambdaFactory.spriteSpec.width,
|
||||
height: LambdaFactory.spriteSpec.height,
|
||||
},
|
||||
0,
|
||||
),
|
||||
0
|
||||
)
|
||||
);
|
||||
|
||||
this.addComponent(new Text(spawns.toString()));
|
||||
@ -110,8 +59,8 @@ export class LambdaFactory extends Entity {
|
||||
this.addComponent(
|
||||
new GridSpawn(
|
||||
this.spawns,
|
||||
() => new FunctionBox({ x: 0, y: 0 }, this.code),
|
||||
),
|
||||
() => new FunctionBox({ x: 0, y: 0 }, this.code)
|
||||
)
|
||||
);
|
||||
|
||||
this.addComponent(new Grid(gridPosition));
|
||||
@ -125,27 +74,57 @@ export class LambdaFactory extends Entity {
|
||||
height: LambdaFactory.spriteSpec.height,
|
||||
},
|
||||
LambdaFactory.spriteSpec.msPerFrame,
|
||||
LambdaFactory.spriteSpec.frames,
|
||||
),
|
||||
LambdaFactory.spriteSpec.frames
|
||||
)
|
||||
);
|
||||
|
||||
this.addComponent(
|
||||
new Highlight(
|
||||
(direction) => this.onHighlight(direction),
|
||||
() => this.onUnhighlight(),
|
||||
),
|
||||
new Highlight(this.onHighlight.bind(this), this.onUnhighlight.bind(this))
|
||||
);
|
||||
}
|
||||
|
||||
private codeConsumer(code: string) {
|
||||
const parsed = tryWrap(() => parse(code));
|
||||
if (parsed.error) {
|
||||
return { error: parsed.error };
|
||||
}
|
||||
this.code = code;
|
||||
this.removeComponent(ComponentNames.Modal);
|
||||
return { consumed: true };
|
||||
}
|
||||
|
||||
private onHighlight(direction: Direction) {
|
||||
if (direction === Direction.LEFT || direction === Direction.RIGHT) {
|
||||
this.addComponent(new Interactable(() => this.spawnNewLambda(direction)));
|
||||
return;
|
||||
}
|
||||
|
||||
this.addComponent(new Interactable(this.interaction.bind(this)));
|
||||
}
|
||||
|
||||
private interaction() {
|
||||
if (this.hasComponent(ComponentNames.Modal)) {
|
||||
return;
|
||||
}
|
||||
this.addComponent(
|
||||
new Modal({
|
||||
type: "CODE_EDITOR",
|
||||
codeInit: {
|
||||
code: this.code,
|
||||
codeConsumer: this.codeConsumer.bind(this),
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private onUnhighlight() {
|
||||
closeModal();
|
||||
this.removeComponent(ComponentNames.Modal);
|
||||
this.removeComponent(ComponentNames.Interactable);
|
||||
}
|
||||
|
||||
private spawnNewLambda(direction: Direction) {
|
||||
try {
|
||||
parse(this.code);
|
||||
} catch (e: any) {
|
||||
const parsed = tryWrap(() => parse(this.code));
|
||||
if (parsed.error) {
|
||||
SOUNDS.get(Failure.name)!.play();
|
||||
return;
|
||||
}
|
||||
@ -159,132 +138,4 @@ export class LambdaFactory extends Entity {
|
||||
|
||||
SOUNDS.get(LambdaTransformSound.name)!.play();
|
||||
}
|
||||
|
||||
private openCodeEditor() {
|
||||
const modalContent =
|
||||
"<div class='code'><div id='code'></div><br><p id='syntax-error' class='error'></p><button id='close-modal'>Save</button></div>";
|
||||
openModal(modalContent);
|
||||
|
||||
const startState = EditorState.create({
|
||||
doc: this.code,
|
||||
extensions: [
|
||||
basicSetup,
|
||||
keymap.of(defaultKeymap),
|
||||
rainbowBrackets(),
|
||||
highlightExtension,
|
||||
FontSizeThemeExtension,
|
||||
],
|
||||
});
|
||||
|
||||
const codeBox = document.getElementById("code")!;
|
||||
const syntaxError = document.getElementById("syntax-error")!;
|
||||
const canvas = document.getElementById(
|
||||
Miscellaneous.CANVAS_ID,
|
||||
) as HTMLCanvasElement;
|
||||
const closeButton = document.getElementById(
|
||||
"close-modal",
|
||||
) as HTMLButtonElement;
|
||||
closeButton.addEventListener("click", () => this.saveAndCloseCodeEditor());
|
||||
|
||||
const editorView = new EditorView({
|
||||
state: startState,
|
||||
parent: codeBox,
|
||||
});
|
||||
editorView.focus();
|
||||
|
||||
this.codeEditorState = {
|
||||
view: editorView,
|
||||
editorElement: codeBox,
|
||||
syntaxError,
|
||||
canvas,
|
||||
closeButton,
|
||||
};
|
||||
|
||||
SOUNDS.get(ModalOpen.name)!.play();
|
||||
}
|
||||
|
||||
private refreshCodeEditorText(text: string) {
|
||||
if (!this.codeEditorState) {
|
||||
return;
|
||||
}
|
||||
const { view } = this.codeEditorState;
|
||||
|
||||
view.dispatch({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: text.length,
|
||||
insert: "",
|
||||
},
|
||||
});
|
||||
view.dispatch({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: 0,
|
||||
insert: text,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private saveAndCloseCodeEditor() {
|
||||
if (!this.codeEditorState) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { canvas, view, editorElement, syntaxError } = this.codeEditorState;
|
||||
const text = view.state.doc.toString();
|
||||
this.refreshCodeEditorText(text);
|
||||
syntaxError.innerText = "";
|
||||
|
||||
try {
|
||||
parse(text);
|
||||
} catch (e: any) {
|
||||
if (!e.location) {
|
||||
return;
|
||||
}
|
||||
const {
|
||||
location: {
|
||||
start: { offset: start },
|
||||
end: { offset: end },
|
||||
},
|
||||
} = e;
|
||||
|
||||
view.dispatch({
|
||||
effects: highlightEffect.of([
|
||||
syntaxErrorDecoration.range(start === end ? start - 1 : start, end),
|
||||
]),
|
||||
});
|
||||
|
||||
syntaxError.innerText = e.message;
|
||||
SOUNDS.get(Failure.name)!.play();
|
||||
return;
|
||||
}
|
||||
|
||||
this.code = text;
|
||||
|
||||
view.destroy();
|
||||
editorElement.innerHTML = "";
|
||||
this.codeEditorState = null;
|
||||
closeModal();
|
||||
|
||||
canvas.focus();
|
||||
SOUNDS.get(LambdaSave.name)!.play();
|
||||
}
|
||||
|
||||
private onHighlight(direction: Direction) {
|
||||
if (direction === Direction.LEFT || direction === Direction.RIGHT) {
|
||||
this.addComponent(new Interactable(() => this.spawnNewLambda(direction)));
|
||||
return;
|
||||
}
|
||||
|
||||
const interaction = () => {
|
||||
if (this.codeEditorState) {
|
||||
this.saveAndCloseCodeEditor();
|
||||
return;
|
||||
}
|
||||
|
||||
this.openCodeEditor();
|
||||
};
|
||||
|
||||
this.addComponent(new Interactable(interaction));
|
||||
}
|
||||
}
|
||||
|
@ -1,18 +1,27 @@
|
||||
import { Entity, EntityNames, makeLambdaTermHighlightComponent } from ".";
|
||||
import { BoundingBox, Colliding, Grid, Sprite } from "../components";
|
||||
import { Entity, EntityNames } from ".";
|
||||
import {
|
||||
BoundingBox,
|
||||
Colliding,
|
||||
ComponentNames,
|
||||
Grid,
|
||||
Highlight,
|
||||
Interactable,
|
||||
Modal,
|
||||
Sprite,
|
||||
} from "../components";
|
||||
import { IMAGES, SPRITE_SPECS, SpriteSpec, Sprites } from "../config";
|
||||
import { Coord2D } from "../interfaces";
|
||||
|
||||
export class Sign extends Entity {
|
||||
private static spriteSpec: SpriteSpec = SPRITE_SPECS.get(
|
||||
Sprites.SIGN,
|
||||
Sprites.SIGN
|
||||
) as SpriteSpec;
|
||||
|
||||
private text: string;
|
||||
|
||||
constructor(text: string, gridPosition: Coord2D) {
|
||||
constructor(
|
||||
private readonly text: string,
|
||||
gridPosition: Coord2D
|
||||
) {
|
||||
super(EntityNames.Sign);
|
||||
this.text = text;
|
||||
|
||||
const dimension = {
|
||||
width: Sign.spriteSpec.width,
|
||||
@ -25,8 +34,8 @@ export class Sign extends Entity {
|
||||
{ x: 0, y: 0 },
|
||||
dimension,
|
||||
Sign.spriteSpec.msPerFrame,
|
||||
Sign.spriteSpec.frames,
|
||||
),
|
||||
Sign.spriteSpec.frames
|
||||
)
|
||||
);
|
||||
|
||||
this.addComponent(
|
||||
@ -36,14 +45,40 @@ export class Sign extends Entity {
|
||||
y: 0,
|
||||
},
|
||||
dimension,
|
||||
0,
|
||||
),
|
||||
0
|
||||
)
|
||||
);
|
||||
|
||||
this.addComponent(new Grid(gridPosition));
|
||||
|
||||
this.addComponent(new Colliding());
|
||||
|
||||
this.addComponent(makeLambdaTermHighlightComponent(this, this.text));
|
||||
this.addComponent(
|
||||
new Highlight(this.onHighlight.bind(this), this.onUnhighlight.bind(this))
|
||||
);
|
||||
}
|
||||
|
||||
private onHighlight() {
|
||||
this.addComponent(new Interactable(this.interaction.bind(this)));
|
||||
}
|
||||
|
||||
private onUnhighlight() {
|
||||
this.removeComponent(ComponentNames.Modal);
|
||||
this.removeComponent(ComponentNames.Interactable);
|
||||
}
|
||||
|
||||
private interaction() {
|
||||
if (this.hasComponent(ComponentNames.Modal)) {
|
||||
this.removeComponent(ComponentNames.Modal);
|
||||
return;
|
||||
}
|
||||
this.addComponent(
|
||||
new Modal({
|
||||
type: "CONTENT",
|
||||
contentInit: {
|
||||
content: `<p>${this.text}</p>`,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
93
src/engine/systems/Modal.ts
Normal file
93
src/engine/systems/Modal.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import { Game } from "..";
|
||||
import { System, SystemNames } from ".";
|
||||
import { Miscellaneous, ModalClose, ModalOpen, SOUNDS } from "../config";
|
||||
import { ComponentNames, Modal as ModalComponent } from "../components";
|
||||
import { Entity } from "../entities";
|
||||
import {
|
||||
CodeConsumer,
|
||||
CodeEditorInstance,
|
||||
CodeEditorSingleton,
|
||||
ModalInstance,
|
||||
ModalSingleton,
|
||||
} from "../utils";
|
||||
|
||||
export interface ModalInitState {
|
||||
type: "CONTENT" | "CODE_EDITOR";
|
||||
contentInit?: {
|
||||
content: string;
|
||||
};
|
||||
codeInit?: {
|
||||
code: string;
|
||||
codeConsumer: CodeConsumer;
|
||||
readonly?: boolean;
|
||||
result?: { data?: string; error?: string };
|
||||
};
|
||||
}
|
||||
|
||||
export class Modal extends System {
|
||||
private openingEntity: null | Entity = null;
|
||||
private modalInstance: ModalInstance = ModalSingleton;
|
||||
private codeEditorInstance: CodeEditorInstance = CodeEditorSingleton;
|
||||
|
||||
constructor() {
|
||||
super(SystemNames.Modal);
|
||||
}
|
||||
|
||||
public update(_dt: number, game: Game) {
|
||||
if (this.openingEntity) {
|
||||
if (this.openingEntity.hasComponent(ComponentNames.Modal)) {
|
||||
return;
|
||||
}
|
||||
this.closeCallback();
|
||||
}
|
||||
|
||||
game.forEachEntityWithComponent(ComponentNames.Modal, (entity) => {
|
||||
const modalComponent = entity.getComponent<ModalComponent>(
|
||||
ComponentNames.Modal
|
||||
);
|
||||
if (this.openingEntity) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.openingEntity = entity;
|
||||
SOUNDS.get(ModalOpen.name)!.play();
|
||||
|
||||
if (modalComponent.initState.type === "CONTENT") {
|
||||
const content = `
|
||||
<div style="text-align:center">
|
||||
<div>${modalComponent.initState.contentInit!.content}</div>
|
||||
<br>
|
||||
<button id="close">Close</button>
|
||||
</div>`;
|
||||
this.modalInstance.open(content);
|
||||
document
|
||||
.getElementById("close")
|
||||
?.addEventListener("click", this.closeCallback.bind(this));
|
||||
}
|
||||
|
||||
if (modalComponent.initState.type === "CODE_EDITOR") {
|
||||
this.codeEditorInstance.open(
|
||||
modalComponent.initState.codeInit!.code,
|
||||
(code) => {
|
||||
const result =
|
||||
modalComponent.initState.codeInit!.codeConsumer(code);
|
||||
if (result.consumed) {
|
||||
this.closeCallback();
|
||||
}
|
||||
return result;
|
||||
},
|
||||
!!modalComponent.initState.codeInit!.readonly,
|
||||
modalComponent.initState.codeInit!.result,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private closeCallback() {
|
||||
this.openingEntity = null;
|
||||
this.modalInstance.vanish();
|
||||
this.codeEditorInstance.close();
|
||||
document.getElementById(Miscellaneous.CANVAS_ID)!.focus();
|
||||
SOUNDS.get(ModalClose.name)!.play();
|
||||
}
|
||||
}
|
@ -8,4 +8,5 @@ export namespace SystemNames {
|
||||
export const Life = "Life";
|
||||
export const Music = "Music";
|
||||
export const Level = "Level";
|
||||
export const Modal = "Modal";
|
||||
}
|
||||
|
@ -9,3 +9,4 @@ export * from "./Collision";
|
||||
export * from "./Life";
|
||||
export * from "./Music";
|
||||
export * from "./Level";
|
||||
export * from "./Modal";
|
200
src/engine/utils/CodeEditor.ts
Normal file
200
src/engine/utils/CodeEditor.ts
Normal file
@ -0,0 +1,200 @@
|
||||
import {
|
||||
EditorState,
|
||||
StateField,
|
||||
StateEffect,
|
||||
Range,
|
||||
Extension,
|
||||
} from "@codemirror/state";
|
||||
import { Decoration, EditorView, keymap } from "@codemirror/view";
|
||||
import { defaultKeymap } from "@codemirror/commands";
|
||||
import rainbowBrackets from "rainbowbrackets";
|
||||
import { basicSetup } from "codemirror";
|
||||
import { ModalInstance, ModalSingleton } from ".";
|
||||
import { EditorSave, Failure, ModalOpen, SOUNDS } from "../config";
|
||||
|
||||
export interface CodeEditorError extends Error {
|
||||
location: {
|
||||
start: { offset: number };
|
||||
end: { offset: number };
|
||||
};
|
||||
}
|
||||
|
||||
export type CodeConsumer = (code: string) => {
|
||||
consumed?: boolean;
|
||||
error?: CodeEditorError;
|
||||
};
|
||||
|
||||
interface CodeEditorState {
|
||||
view: EditorView;
|
||||
editorElement: HTMLElement;
|
||||
errorElement: HTMLElement;
|
||||
resultElement: HTMLElement;
|
||||
closeButton: HTMLButtonElement;
|
||||
codeConsumer: CodeConsumer;
|
||||
}
|
||||
|
||||
export class CodeEditorInstance {
|
||||
constructor(
|
||||
private modalInstance: ModalInstance = ModalSingleton,
|
||||
private codeEditorState: CodeEditorState | null = null
|
||||
) {}
|
||||
|
||||
public close() {
|
||||
if (!this.codeEditorState) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.codeEditorState.view.destroy();
|
||||
this.codeEditorState = null;
|
||||
|
||||
SOUNDS.get(EditorSave.name)!.play();
|
||||
}
|
||||
|
||||
public open(
|
||||
initCode: string,
|
||||
codeConsumer: CodeEditorState["codeConsumer"],
|
||||
readonly: boolean = false,
|
||||
initResult: { data?: string; error?: string } = {},
|
||||
) {
|
||||
if (this.codeEditorState) {
|
||||
throw new Error("code editor instance is already owned.");
|
||||
}
|
||||
const modalContent = `
|
||||
<div class='code'>
|
||||
<div id='code'></div>
|
||||
<br>
|
||||
<p id='error' class='error'></p>
|
||||
<p id='result' class='result'></p>
|
||||
<button id='close-modal'>Done</button>
|
||||
</div>
|
||||
`;
|
||||
this.modalInstance.open(modalContent);
|
||||
|
||||
const startState = EditorState.create({
|
||||
doc: initCode,
|
||||
extensions: [
|
||||
basicSetup,
|
||||
keymap.of(defaultKeymap),
|
||||
rainbowBrackets(),
|
||||
highlightExtension,
|
||||
FontSizeThemeExtension,
|
||||
EditorState.readOnly.of(readonly),
|
||||
],
|
||||
});
|
||||
|
||||
const codeBox = document.getElementById("code")!;
|
||||
const errorElement = document.getElementById("error")!;
|
||||
const resultElement = document.getElementById("result")!;
|
||||
|
||||
const closeButton = document.getElementById(
|
||||
"close-modal"
|
||||
) as HTMLButtonElement;
|
||||
closeButton.addEventListener("click", () => this.onSave());
|
||||
|
||||
const editorView = new EditorView({
|
||||
state: startState,
|
||||
parent: codeBox,
|
||||
});
|
||||
editorView.focus();
|
||||
|
||||
this.codeEditorState = {
|
||||
view: editorView,
|
||||
editorElement: codeBox,
|
||||
errorElement,
|
||||
resultElement,
|
||||
closeButton,
|
||||
codeConsumer,
|
||||
};
|
||||
this.setResult(initResult);
|
||||
}
|
||||
|
||||
private refreshCodeEditorText(text: string) {
|
||||
if (!this.codeEditorState) {
|
||||
return;
|
||||
}
|
||||
const { view } = this.codeEditorState;
|
||||
|
||||
view.dispatch({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: text.length,
|
||||
insert: "",
|
||||
},
|
||||
});
|
||||
view.dispatch({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: 0,
|
||||
insert: text,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private onSave() {
|
||||
if (!this.codeEditorState) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
const { view } = this.codeEditorState;
|
||||
const code = view.state.doc.toString();
|
||||
this.refreshCodeEditorText(code);
|
||||
|
||||
const valid = this.codeEditorState.codeConsumer(code);
|
||||
if (!valid.error) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
location: {
|
||||
start: { offset: start },
|
||||
end: { offset: end },
|
||||
},
|
||||
message,
|
||||
} = valid.error;
|
||||
|
||||
view.dispatch({
|
||||
effects: highlightEffect.of([
|
||||
syntaxErrorDecoration.range(start === end ? start - 1 : start, end),
|
||||
]),
|
||||
});
|
||||
|
||||
this.setResult({ error: message });
|
||||
SOUNDS.get(Failure.name)!.play();
|
||||
}
|
||||
|
||||
private setResult(result: { data?: string; error?: string }) {
|
||||
if (!this.codeEditorState) return;
|
||||
this.codeEditorState.resultElement.innerText = result.data ?? "";
|
||||
this.codeEditorState.errorElement.innerText = result.error ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
const highlightEffect = StateEffect.define<Range<Decoration>[]>();
|
||||
const highlightExtension = StateField.define({
|
||||
create() {
|
||||
return Decoration.none;
|
||||
},
|
||||
update(value, transaction) {
|
||||
value = value.map(transaction.changes);
|
||||
|
||||
for (let effect of transaction.effects) {
|
||||
if (effect.is(highlightEffect))
|
||||
value = value.update({ add: effect.value, sort: true });
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
provide: (f) => EditorView.decorations.from(f),
|
||||
});
|
||||
|
||||
const FontSizeTheme = EditorView.theme({
|
||||
$: {
|
||||
fontSize: "14pt",
|
||||
},
|
||||
});
|
||||
const FontSizeThemeExtension: Extension = [FontSizeTheme];
|
||||
const syntaxErrorDecoration = Decoration.mark({
|
||||
class: "syntax-error",
|
||||
});
|
||||
|
||||
export const CodeEditorSingleton = new CodeEditorInstance();
|
45
src/engine/utils/Modal.ts
Normal file
45
src/engine/utils/Modal.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { Miscellaneous } from "../config";
|
||||
|
||||
export class ModalInstance {
|
||||
private modalOpen: boolean = false;
|
||||
private elementId: string = Miscellaneous.MODAL_ID;
|
||||
private contentId: string = Miscellaneous.MODAL_CONTENT_ID;
|
||||
|
||||
public open(content: string) {
|
||||
const modal = document.getElementById(this.elementId);
|
||||
const modalContent = document.getElementById(this.contentId);
|
||||
if (!modal || this.modalOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.modalOpen = true;
|
||||
modal.style.display = "flex";
|
||||
modal.style.animation = "fadeIn 0.25s";
|
||||
|
||||
modalContent!.innerHTML = content;
|
||||
modalContent!.style.animation = "scaleUp 0.25s";
|
||||
}
|
||||
|
||||
public vanish(): Promise<void> {
|
||||
const modal = document.getElementById(this.elementId);
|
||||
const modalContent = document.getElementById(this.contentId);
|
||||
return new Promise((res, _rej) => {
|
||||
if (!(modal && this.modalOpen && modalContent)) {
|
||||
res();
|
||||
return;
|
||||
}
|
||||
modal.style.animation = "fadeOut 0.25s";
|
||||
modalContent.style.animation = "scaleDown 0.25s";
|
||||
|
||||
setTimeout(() => {
|
||||
modalContent.innerHTML = "";
|
||||
modal.style.display = "none";
|
||||
|
||||
this.modalOpen = false;
|
||||
res();
|
||||
}, 200);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const ModalSingleton = new ModalInstance();
|
@ -1,6 +1,8 @@
|
||||
export * from "./clamp";
|
||||
export * from "./dotProduct";
|
||||
export * from "./rotateVector";
|
||||
export * from "./modal";
|
||||
export * from "./colors";
|
||||
export * from "./random";
|
||||
export * from './tryWrap';
|
||||
export * from "./Modal";
|
||||
export * from "./CodeEditor";
|
@ -1,41 +0,0 @@
|
||||
import { Miscellaneous } from "../config";
|
||||
|
||||
let modalOpen = false;
|
||||
|
||||
export const openModal = (
|
||||
content: string,
|
||||
id = Miscellaneous.MODAL_ID,
|
||||
contentId = Miscellaneous.MODAL_CONTENT_ID,
|
||||
) => {
|
||||
const modal = document.getElementById(id);
|
||||
const modalContent = document.getElementById(contentId);
|
||||
if (modal && !modalOpen && modalContent) {
|
||||
modal.style.display = "flex";
|
||||
modal.style.animation = "fadeIn 0.25s";
|
||||
|
||||
modalContent.innerHTML = content;
|
||||
modalContent.style.animation = "scaleUp 0.25s";
|
||||
|
||||
modalOpen = true;
|
||||
}
|
||||
};
|
||||
|
||||
export const closeModal = (
|
||||
id = Miscellaneous.MODAL_ID,
|
||||
contentId = Miscellaneous.MODAL_CONTENT_ID,
|
||||
) => {
|
||||
const modal = document.getElementById(id);
|
||||
const modalContent = document.getElementById(contentId);
|
||||
|
||||
if (modal && modalOpen && modalContent) {
|
||||
modal.style.animation = "fadeOut 0.25s";
|
||||
modalContent.style.animation = "scaleDown 0.25s";
|
||||
|
||||
setTimeout(() => {
|
||||
modalContent.innerHTML = "";
|
||||
modal.style.display = "none";
|
||||
|
||||
modalOpen = false;
|
||||
}, 200);
|
||||
}
|
||||
};
|
7
src/engine/utils/tryWrap.ts
Normal file
7
src/engine/utils/tryWrap.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export const tryWrap = <T>(supplier: () => T): { data?: T; error?: any } => {
|
||||
try {
|
||||
return { data: supplier() };
|
||||
} catch (error) {
|
||||
return { error: error as any };
|
||||
}
|
||||
};
|
@ -249,8 +249,8 @@ export const emitNamed = (term: DebrujinifiedLambdaTerm): string => {
|
||||
export const interpret = (
|
||||
term: string,
|
||||
symbolTable = new SymbolTable(),
|
||||
allowUnderscores = false, // in our world, underscores should be internal to the game.
|
||||
maxDepth = 15,
|
||||
allowUnderscores = false
|
||||
): DebrujinifiedLambdaTerm => {
|
||||
const ast = parse(term, allowUnderscores);
|
||||
const debrujined = debrujinify(ast, symbolTable);
|
||||
|
Loading…
x
Reference in New Issue
Block a user