This commit is contained in:
Elizabeth Hunt 2025-03-01 12:36:47 -07:00
parent d903bd9a13
commit 8dacee8f73
Signed by: simponic
GPG Key ID: 2909B9A7FF6213EE
21 changed files with 577 additions and 361 deletions

View File

@ -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">&times;</span>
<p>Some text in the Modal..</p>
<hr></hr>
</div>
</div>

View File

@ -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,

View File

@ -12,4 +12,5 @@ export namespace ComponentNames {
export const Text = "Text";
export const LambdaTerm = "LambdaTerm";
export const Life = "Life";
export const Modal = "Modal";
}

View File

@ -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;
}
}

View File

@ -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);

View 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);
}
}

View File

@ -13,3 +13,4 @@ export * from "./GridSpawn";
export * from "./Text";
export * from "./LambdaTerm";
export * from "./Life";
export * from "./Modal";

View File

@ -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,

View File

@ -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();
}
}

View File

@ -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);
};

View File

@ -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));
}
}

View File

@ -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>`,
},
})
);
}
}

View 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();
}
}

View File

@ -8,4 +8,5 @@ export namespace SystemNames {
export const Life = "Life";
export const Music = "Music";
export const Level = "Level";
export const Modal = "Modal";
}

View File

@ -9,3 +9,4 @@ export * from "./Collision";
export * from "./Life";
export * from "./Music";
export * from "./Level";
export * from "./Modal";

View 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
View 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();

View File

@ -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";

View File

@ -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);
}
};

View 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 };
}
};

View File

@ -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);