import { IMAGES, Miscellaneous, SPRITE_SPECS, SpriteSpec, Sprites, } from "../config"; import { Entity, EntityNames } from "."; import { BoundingBox, Colliding, ComponentNames, Grid, Highlight, Interactable, LambdaSpawn, Sprite, Text, } from "../components"; import { Coord2D, Direction } from "../interfaces"; import { openModal, closeModal } from "../utils"; import { EditorState, StateField, StateEffect, Range } from "@codemirror/state"; import { Decoration, EditorView, keymap } from "@codemirror/view"; import { defaultKeymap } from "@codemirror/commands"; import rainbowBrackets from "rainbowbrackets"; import { basicSetup } from "codemirror"; import { parse } from "../../interpreter"; interface CodeEditorState { view: EditorView; editorElement: HTMLElement; } const highlightEffect = StateEffect.define[]>(); 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), }); export class LambdaFactory extends Entity { private static spriteSpec: SpriteSpec = SPRITE_SPECS.get( Sprites.LAMBDA_FACTORY, ) as SpriteSpec; private codeEditorState: CodeEditorState | null; private code: string; private spawns: number; constructor(gridPosition: Coord2D, code: string, spawns: number) { super(EntityNames.LambdaFactory); this.code = code; this.spawns = spawns; this.codeEditorState = null; this.addComponent( new BoundingBox( { x: 0, y: 0, }, { width: LambdaFactory.spriteSpec.width, height: LambdaFactory.spriteSpec.height, }, 0, ), ); this.addComponent(new Text(spawns.toString())); this.addComponent(new Colliding()); this.addComponent(new LambdaSpawn(this.spawns, this.code)); this.addComponent(new Grid(gridPosition)); this.addComponent( new Sprite( IMAGES.get(LambdaFactory.spriteSpec.sheet)!, { x: 0, y: 0 }, { width: LambdaFactory.spriteSpec.width, height: LambdaFactory.spriteSpec.height, }, LambdaFactory.spriteSpec.msPerFrame, LambdaFactory.spriteSpec.frames, ), ); this.addComponent( new Highlight( (direction) => this.onHighlight(direction), () => this.onUnhighlight(), ), ); } private onUnhighlight() { closeModal(); this.removeComponent(ComponentNames.Interactable); } private spawnNewLambda(direction: Direction) { const spawner = this.getComponent(ComponentNames.LambdaSpawn); spawner.spawn(direction); const text = this.getComponent(ComponentNames.Text); text.text = spawner.spawnsLeft.toString(); this.addComponent(text); } private onHighlight(direction: Direction) { if (direction === Direction.LEFT || direction === Direction.RIGHT) { this.addComponent(new Interactable(() => this.spawnNewLambda(direction))); return; } let modalOpen = false; let editorView: EditorView | null = null; const syntaxErrorDecoration = Decoration.mark({ class: "syntax-error", }); const close = () => { if (editorView) { const text = editorView.state.doc.toString(); // remove all text from the editor editorView.dispatch({ changes: { from: 0, to: text.length, insert: "", }, }); // add the new text to the editor editorView.dispatch({ changes: { from: 0, to: 0, insert: text, }, }); try { parse(text); } catch (e: any) { if (!e.location) { return; } const { location: { start: { offset: start }, end: { offset: end }, }, } = e; editorView.dispatch({ effects: highlightEffect.of([ syntaxErrorDecoration.range( start === end ? start - 1 : start, end, ), ]), }); document.getElementById("syntax-error")!.innerText = e.message; return; } const spawner = this.getComponent( ComponentNames.LambdaSpawn, ); spawner.code = text; this.addComponent(spawner); } modalOpen = false; closeModal(); document.getElementById(Miscellaneous.CANVAS_ID)!.focus(); return; }; const interaction = () => { if (modalOpen) { close(); return; } modalOpen = true; openModal( "

", ); const spawner = this.getComponent( ComponentNames.LambdaSpawn, ); const startState = EditorState.create({ doc: spawner.code, extensions: [ basicSetup, keymap.of(defaultKeymap), rainbowBrackets(), highlightExtension, ], }); const codeBox = document.getElementById("code")!; editorView = new EditorView({ state: startState, parent: codeBox, }); editorView.focus(); document.getElementById("close-modal")!.addEventListener("click", close); }; this.addComponent(new Interactable(interaction)); } }