Compare commits
No commits in common. "main" and "moar-levels" have entirely different histories.
main
...
moar-level
@ -6,7 +6,8 @@ export const App = () => {
|
|||||||
<div className="main">
|
<div className="main">
|
||||||
<div id={Miscellaneous.MODAL_ID} className="modal">
|
<div id={Miscellaneous.MODAL_ID} className="modal">
|
||||||
<div id={Miscellaneous.MODAL_CONTENT_ID} className="modal-content">
|
<div id={Miscellaneous.MODAL_CONTENT_ID} className="modal-content">
|
||||||
<hr></hr>
|
<span className="close">×</span>
|
||||||
|
<p>Some text in the Modal..</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -21,11 +22,7 @@ export const App = () => {
|
|||||||
<div className="footer">
|
<div className="footer">
|
||||||
<span>
|
<span>
|
||||||
built by{" "}
|
built by{" "}
|
||||||
<a
|
<a href="https://github.com/simponic" target="_blank" className="tf">
|
||||||
href="https://git.simponic.xyz/simponic"
|
|
||||||
target="_blank"
|
|
||||||
className="tf"
|
|
||||||
>
|
|
||||||
simponic
|
simponic
|
||||||
</a>{" "}
|
</a>{" "}
|
||||||
| inspired by{" "}
|
| inspired by{" "}
|
||||||
|
@ -11,11 +11,7 @@ export interface GameCanvasProps {
|
|||||||
export const GameCanvas = ({ width, height }: GameCanvasProps) => {
|
export const GameCanvas = ({ width, height }: GameCanvasProps) => {
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
const [game, setGame] = useState<TheAbstractionEngine>();
|
const [game, setGame] = useState<TheAbstractionEngine>();
|
||||||
// TODO: go back to this after done
|
const [ready, setReady] = useState(false);
|
||||||
// const [ready, setReady] = useState(false);
|
|
||||||
const [ready, setReady] = useState(
|
|
||||||
document.location.hostname.includes("localhost"),
|
|
||||||
);
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -17,10 +17,10 @@ export const Title = ({ setReady }: TitleProps) => {
|
|||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<br />
|
<br />
|
||||||
<h3 className="warning">
|
<p>
|
||||||
WASD/arrow keys to move. SPACE/ENTER to interact after highlighting with
|
WASD/arrow keys to move, space/enter to interact after highlighting with
|
||||||
the mouse.
|
the mouse
|
||||||
</h3>
|
</p>
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
<hr />
|
<hr />
|
||||||
|
@ -20,8 +20,7 @@ body {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background:
|
background: radial-gradient(ellipse at top, var(--bg), transparent),
|
||||||
radial-gradient(ellipse at top, var(--bg), transparent),
|
|
||||||
radial-gradient(ellipse at left, var(--blue), transparent),
|
radial-gradient(ellipse at left, var(--blue), transparent),
|
||||||
radial-gradient(ellipse at right, var(--purple), transparent),
|
radial-gradient(ellipse at right, var(--purple), transparent),
|
||||||
radial-gradient(ellipse at bottom, var(--bg), transparent);
|
radial-gradient(ellipse at bottom, var(--bg), transparent);
|
||||||
|
@ -11,8 +11,6 @@ import {
|
|||||||
Life,
|
Life,
|
||||||
Music,
|
Music,
|
||||||
Level,
|
Level,
|
||||||
Modal,
|
|
||||||
RadialObserve,
|
|
||||||
} from "./systems";
|
} from "./systems";
|
||||||
|
|
||||||
export class TheAbstractionEngine {
|
export class TheAbstractionEngine {
|
||||||
@ -34,11 +32,8 @@ export class TheAbstractionEngine {
|
|||||||
|
|
||||||
const facingDirectionSystem = new FacingDirection(inputSystem);
|
const facingDirectionSystem = new FacingDirection(inputSystem);
|
||||||
|
|
||||||
const isDev = document.location.hostname.includes("localhost");
|
|
||||||
[
|
[
|
||||||
new RadialObserve(),
|
new Level(LevelNames.LevelSelection),
|
||||||
new Modal(),
|
|
||||||
new Level(isDev ? LevelNames.CarCadr : LevelNames.LevelSelection),
|
|
||||||
inputSystem,
|
inputSystem,
|
||||||
facingDirectionSystem,
|
facingDirectionSystem,
|
||||||
new Grid(
|
new Grid(
|
||||||
|
@ -8,10 +8,8 @@ export namespace ComponentNames {
|
|||||||
export const Interactable = "Interactable";
|
export const Interactable = "Interactable";
|
||||||
export const Pushable = "Pushable";
|
export const Pushable = "Pushable";
|
||||||
export const Colliding = "Colliding";
|
export const Colliding = "Colliding";
|
||||||
export const RadialObserve = "RadialObserve";
|
|
||||||
export const GridSpawn = "GridSpawn";
|
export const GridSpawn = "GridSpawn";
|
||||||
export const Text = "Text";
|
export const Text = "Text";
|
||||||
export const LambdaTerm = "LambdaTerm";
|
export const LambdaTerm = "LambdaTerm";
|
||||||
export const Life = "Life";
|
export const Life = "Life";
|
||||||
export const Modal = "Modal";
|
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
import { Component, ComponentNames } from ".";
|
import { Component, ComponentNames } from ".";
|
||||||
|
|
||||||
export class Control extends Component {
|
export class Control extends Component {
|
||||||
constructor(public isControllable = true) {
|
public isControllable: boolean = true;
|
||||||
|
|
||||||
|
constructor(isControllable = true) {
|
||||||
super(ComponentNames.Control);
|
super(ComponentNames.Control);
|
||||||
|
|
||||||
|
this.isControllable = isControllable;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
import { Component, ComponentNames } from ".";
|
import { Component, ComponentNames } from ".";
|
||||||
import { DebrujinifiedLambdaTerm } from "../../interpreter";
|
|
||||||
|
|
||||||
export class LambdaTerm extends Component {
|
export class LambdaTerm extends Component {
|
||||||
public code: string;
|
public code: string;
|
||||||
public last: null | { data?: DebrujinifiedLambdaTerm; error?: any } = null;
|
|
||||||
|
|
||||||
constructor(code: string) {
|
constructor(code: string) {
|
||||||
super(ComponentNames.LambdaTerm);
|
super(ComponentNames.LambdaTerm);
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
import { Component, ComponentNames } from ".";
|
|
||||||
import { ModalInitState } from "../systems";
|
|
||||||
|
|
||||||
export class Modal extends Component {
|
|
||||||
constructor(public initState: ModalInitState) {
|
|
||||||
super(ComponentNames.Modal);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
import { Component, ComponentNames } from ".";
|
|
||||||
import { Game } from "..";
|
|
||||||
import { Entity } from "../entities";
|
|
||||||
|
|
||||||
export class RadialObserve extends Component {
|
|
||||||
constructor(
|
|
||||||
public onObservation?: (game: Game, entity: Entity) => void,
|
|
||||||
public radius: number = 0,
|
|
||||||
) {
|
|
||||||
super(ComponentNames.RadialObserve);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
import { Component, ComponentNames } from ".";
|
|
||||||
import { Game } from "..";
|
|
||||||
import { Entity } from "../entities";
|
|
||||||
|
|
||||||
export class Colliding extends Component {
|
|
||||||
public onCollision?: (game: Game, entity: Entity) => void;
|
|
||||||
|
|
||||||
constructor(onCollision?: (game: Game, entity: Entity) => void) {
|
|
||||||
super(ComponentNames.RadialObserve);
|
|
||||||
|
|
||||||
this.onCollision = onCollision;
|
|
||||||
}
|
|
||||||
}
|
|
@ -55,7 +55,7 @@ export class Sprite extends Component implements Renderable {
|
|||||||
|
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.translate(center.x, center.y);
|
ctx.translate(center.x, center.y);
|
||||||
if (typeof rotation !== "undefined" && rotation != 0) {
|
if (rotation != undefined && rotation != 0) {
|
||||||
ctx.rotate(rotation * (Math.PI / 180));
|
ctx.rotate(rotation * (Math.PI / 180));
|
||||||
}
|
}
|
||||||
ctx.translate(-center.x, -center.y);
|
ctx.translate(-center.x, -center.y);
|
||||||
@ -64,12 +64,6 @@ export class Sprite extends Component implements Renderable {
|
|||||||
ctx.globalAlpha = opacity;
|
ctx.globalAlpha = opacity;
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.drawImage(
|
|
||||||
this.sheet,
|
|
||||||
...this.getSpriteArgs(),
|
|
||||||
...this.getDrawArgs(drawArgs),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (backgroundText) {
|
if (backgroundText) {
|
||||||
// draw text
|
// draw text
|
||||||
const { fillStyle, font, textAlign, text } = backgroundText;
|
const { fillStyle, font, textAlign, text } = backgroundText;
|
||||||
@ -81,6 +75,12 @@ export class Sprite extends Component implements Renderable {
|
|||||||
ctx.fillText(text, center.x, center.y + height / 2);
|
ctx.fillText(text, center.x, center.y + height / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx.drawImage(
|
||||||
|
this.sheet,
|
||||||
|
...this.getSpriteArgs(),
|
||||||
|
...this.getDrawArgs(drawArgs),
|
||||||
|
);
|
||||||
|
|
||||||
if (tint) {
|
if (tint) {
|
||||||
ctx.globalAlpha = 0.5;
|
ctx.globalAlpha = 0.5;
|
||||||
ctx.globalCompositeOperation = "source-atop";
|
ctx.globalCompositeOperation = "source-atop";
|
||||||
|
@ -8,10 +8,8 @@ export * from "./Control";
|
|||||||
export * from "./Highlight";
|
export * from "./Highlight";
|
||||||
export * from "./Interactable";
|
export * from "./Interactable";
|
||||||
export * from "./Pushable";
|
export * from "./Pushable";
|
||||||
export * from "./RadialObserve";
|
|
||||||
export * from "./Colliding";
|
export * from "./Colliding";
|
||||||
export * from "./GridSpawn";
|
export * from "./GridSpawn";
|
||||||
export * from "./Text";
|
export * from "./Text";
|
||||||
export * from "./LambdaTerm";
|
export * from "./LambdaTerm";
|
||||||
export * from "./Life";
|
export * from "./Life";
|
||||||
export * from "./Modal";
|
|
||||||
|
@ -27,8 +27,6 @@ export namespace KeyConstants {
|
|||||||
|
|
||||||
" ": Action.INTERACT,
|
" ": Action.INTERACT,
|
||||||
enter: Action.INTERACT,
|
enter: Action.INTERACT,
|
||||||
|
|
||||||
r: Action.RESET,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// value -> [key] from KeyActions
|
// value -> [key] from KeyActions
|
||||||
|
@ -19,7 +19,7 @@ export const LambdaTransformSound: SoundSpec = {
|
|||||||
volume: 0.3,
|
volume: 0.3,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EditorSave: SoundSpec = {
|
export const LambdaSave: SoundSpec = {
|
||||||
name: "lambdaSave",
|
name: "lambdaSave",
|
||||||
url: "/assets/sound/lambda_save.wav",
|
url: "/assets/sound/lambda_save.wav",
|
||||||
volume: 0.3,
|
volume: 0.3,
|
||||||
@ -83,7 +83,7 @@ export const Music: SoundSpec = {
|
|||||||
export const SOUND_SPECS: SoundSpec[] = [
|
export const SOUND_SPECS: SoundSpec[] = [
|
||||||
MovingSound,
|
MovingSound,
|
||||||
LambdaTransformSound,
|
LambdaTransformSound,
|
||||||
EditorSave,
|
LambdaSave,
|
||||||
Failure,
|
Failure,
|
||||||
ModalOpen,
|
ModalOpen,
|
||||||
ModalClose,
|
ModalClose,
|
||||||
|
@ -11,5 +11,4 @@ export namespace EntityNames {
|
|||||||
export const Portal = "Portal";
|
export const Portal = "Portal";
|
||||||
export const Grass = "Grass";
|
export const Grass = "Grass";
|
||||||
export const Sign = "Sign";
|
export const Sign = "Sign";
|
||||||
export const Piston = "Piston";
|
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,17 @@
|
|||||||
import { Entity, EntityNames, FunctionBox, Key, Particles } from ".";
|
import {
|
||||||
|
Entity,
|
||||||
|
EntityNames,
|
||||||
|
FunctionBox,
|
||||||
|
Key,
|
||||||
|
Particles,
|
||||||
|
makeLambdaTermHighlightComponent,
|
||||||
|
} from ".";
|
||||||
import {
|
import {
|
||||||
BoundingBox,
|
BoundingBox,
|
||||||
Colliding,
|
Colliding,
|
||||||
ComponentNames,
|
ComponentNames,
|
||||||
Grid,
|
Grid,
|
||||||
Highlight,
|
|
||||||
Interactable,
|
|
||||||
LambdaTerm,
|
LambdaTerm,
|
||||||
Modal,
|
|
||||||
Sprite,
|
Sprite,
|
||||||
} from "../components";
|
} from "../components";
|
||||||
import {
|
import {
|
||||||
@ -22,20 +26,16 @@ import {
|
|||||||
import { Coord2D, Direction } from "../interfaces";
|
import { Coord2D, Direction } from "../interfaces";
|
||||||
import { Game } from "..";
|
import { Game } from "..";
|
||||||
import { Grid as GridSystem, SystemNames } from "../systems";
|
import { Grid as GridSystem, SystemNames } from "../systems";
|
||||||
import { colors, tryWrap } from "../utils";
|
import { colors } from "../utils";
|
||||||
import {
|
import {
|
||||||
InvalidLambdaTermError,
|
DebrujinifiedLambdaTerm,
|
||||||
SymbolTable,
|
SymbolTable,
|
||||||
emitNamed,
|
emitNamed,
|
||||||
interpret,
|
interpret,
|
||||||
} from "../../interpreter";
|
} from "../../interpreter";
|
||||||
|
|
||||||
const APPLICATION_RESULTS: Record<
|
const APPLICATION_RESULTS: Record<string, (gridPosition: Coord2D) => Entity> = {
|
||||||
string,
|
|
||||||
(gridPosition: Coord2D) => null | Entity
|
|
||||||
> = {
|
|
||||||
_KEY: (gridPosition: Coord2D) => new Key(gridPosition),
|
_KEY: (gridPosition: Coord2D) => new Key(gridPosition),
|
||||||
_EMPTY: (_gridPosition: Coord2D) => null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export class FunctionApplication extends Entity {
|
export class FunctionApplication extends Entity {
|
||||||
@ -82,42 +82,7 @@ export class FunctionApplication extends Entity {
|
|||||||
|
|
||||||
this.addComponent(new Colliding(this.handleCollision.bind(this)));
|
this.addComponent(new Colliding(this.handleCollision.bind(this)));
|
||||||
|
|
||||||
this.addComponent(
|
this.addComponent(makeLambdaTermHighlightComponent(this));
|
||||||
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) {
|
public handleCollision(game: Game, entity: Entity) {
|
||||||
@ -151,39 +116,35 @@ export class FunctionApplication extends Entity {
|
|||||||
);
|
);
|
||||||
const newCode = applicationTerm.code.replace("_INPUT", functionTerm.code);
|
const newCode = applicationTerm.code.replace("_INPUT", functionTerm.code);
|
||||||
|
|
||||||
const result = tryWrap(() => interpret(newCode, this.symbolTable, true));
|
let result: DebrujinifiedLambdaTerm | null = null;
|
||||||
applicationTerm.last = result;
|
try {
|
||||||
if (result.error || !result.data) {
|
result = interpret(newCode, this.symbolTable, true);
|
||||||
console.error(result.error);
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
fail();
|
fail();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { dimension } = gridSystem;
|
||||||
const nextPosition = gridSystem.getNewGridPosition(
|
const nextPosition = gridSystem.getNewGridPosition(
|
||||||
grid.gridPosition,
|
grid.gridPosition,
|
||||||
entityGrid.previousDirection,
|
entityGrid.previousDirection,
|
||||||
);
|
);
|
||||||
|
|
||||||
let applicationResultingEntity: Entity | null = null; // this should be its own function
|
let applicationResultingEntity: Entity | null = null; // this should be its own function
|
||||||
const { data } = result;
|
if ("abstraction" in result) {
|
||||||
if ("application" in data) {
|
const code = emitNamed(result);
|
||||||
// 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);
|
applicationResultingEntity = new FunctionBox(grid.gridPosition, code);
|
||||||
}
|
} else if ("name" in result) {
|
||||||
if ("name" in data) {
|
const { name } = result;
|
||||||
const { name } = data;
|
|
||||||
const entityFactory = APPLICATION_RESULTS[name];
|
const entityFactory = APPLICATION_RESULTS[name];
|
||||||
if (entityFactory) {
|
if (entityFactory) {
|
||||||
const entity = entityFactory(nextPosition);
|
game.addEntity(entityFactory(nextPosition));
|
||||||
entity && game.addEntity(entity);
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
fail();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
game.removeEntity(entity.id);
|
game.removeEntity(entity.id);
|
||||||
@ -197,8 +158,7 @@ export class FunctionApplication extends Entity {
|
|||||||
game.addEntity(applicationResultingEntity);
|
game.addEntity(applicationResultingEntity);
|
||||||
}
|
}
|
||||||
|
|
||||||
SOUNDS.get(LambdaTransformSound.name)!.play();
|
this.playTransformSound();
|
||||||
const { dimension } = gridSystem;
|
|
||||||
const particles = new Particles({
|
const particles = new Particles({
|
||||||
center: gridSystem.gridToScreenPosition(nextPosition),
|
center: gridSystem.gridToScreenPosition(nextPosition),
|
||||||
spawnerDimensions: {
|
spawnerDimensions: {
|
||||||
@ -223,4 +183,9 @@ export class FunctionApplication extends Entity {
|
|||||||
});
|
});
|
||||||
game.addEntity(particles);
|
game.addEntity(particles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private playTransformSound() {
|
||||||
|
const audio = SOUNDS.get(LambdaTransformSound.name)!;
|
||||||
|
audio.play();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,13 @@
|
|||||||
import { IMAGES, SPRITE_SPECS, SpriteSpec, Sprites } from "../config";
|
import {
|
||||||
|
IMAGES,
|
||||||
|
Miscellaneous,
|
||||||
|
ModalClose,
|
||||||
|
ModalOpen,
|
||||||
|
SOUNDS,
|
||||||
|
SPRITE_SPECS,
|
||||||
|
SpriteSpec,
|
||||||
|
Sprites,
|
||||||
|
} from "../config";
|
||||||
import { Entity, EntityNames } from ".";
|
import { Entity, EntityNames } from ".";
|
||||||
import {
|
import {
|
||||||
BoundingBox,
|
BoundingBox,
|
||||||
@ -7,22 +16,18 @@ import {
|
|||||||
Highlight,
|
Highlight,
|
||||||
Interactable,
|
Interactable,
|
||||||
LambdaTerm,
|
LambdaTerm,
|
||||||
Modal,
|
|
||||||
Pushable,
|
Pushable,
|
||||||
RadialObserve,
|
|
||||||
Sprite,
|
Sprite,
|
||||||
} from "../components";
|
} from "../components";
|
||||||
import { Coord2D } from "../interfaces";
|
import { Coord2D } from "../interfaces";
|
||||||
|
import { openModal, closeModal } from "../utils";
|
||||||
|
|
||||||
export class FunctionBox extends Entity {
|
export class FunctionBox extends Entity {
|
||||||
private static spriteSpec: SpriteSpec = SPRITE_SPECS.get(
|
private static spriteSpec: SpriteSpec = SPRITE_SPECS.get(
|
||||||
Sprites.FUNCTION_BOX,
|
Sprites.FUNCTION_BOX,
|
||||||
) as SpriteSpec;
|
) as SpriteSpec;
|
||||||
|
|
||||||
constructor(
|
constructor(gridPosition: Coord2D, code: string) {
|
||||||
gridPosition: Coord2D,
|
|
||||||
private readonly code: string,
|
|
||||||
) {
|
|
||||||
super(EntityNames.FunctionBox);
|
super(EntityNames.FunctionBox);
|
||||||
|
|
||||||
this.addComponent(
|
this.addComponent(
|
||||||
@ -41,8 +46,6 @@ export class FunctionBox extends Entity {
|
|||||||
|
|
||||||
this.addComponent(new Pushable());
|
this.addComponent(new Pushable());
|
||||||
|
|
||||||
this.addComponent(new RadialObserve());
|
|
||||||
|
|
||||||
this.addComponent(new Grid(gridPosition));
|
this.addComponent(new Grid(gridPosition));
|
||||||
|
|
||||||
this.addComponent(
|
this.addComponent(
|
||||||
@ -60,37 +63,50 @@ export class FunctionBox extends Entity {
|
|||||||
|
|
||||||
this.addComponent(new LambdaTerm(code));
|
this.addComponent(new LambdaTerm(code));
|
||||||
|
|
||||||
this.addComponent(
|
this.addComponent(makeLambdaTermHighlightComponent(this));
|
||||||
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,7 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
Failure,
|
Failure,
|
||||||
IMAGES,
|
IMAGES,
|
||||||
|
LambdaSave,
|
||||||
LambdaTransformSound,
|
LambdaTransformSound,
|
||||||
|
Miscellaneous,
|
||||||
|
ModalOpen,
|
||||||
SOUNDS,
|
SOUNDS,
|
||||||
SPRITE_SPECS,
|
SPRITE_SPECS,
|
||||||
SpriteSpec,
|
SpriteSpec,
|
||||||
@ -16,19 +19,66 @@ import {
|
|||||||
GridSpawn,
|
GridSpawn,
|
||||||
Highlight,
|
Highlight,
|
||||||
Interactable,
|
Interactable,
|
||||||
Modal,
|
|
||||||
Sprite,
|
Sprite,
|
||||||
Text,
|
Text,
|
||||||
} from "../components";
|
} from "../components";
|
||||||
import { Coord2D, Direction } from "../interfaces";
|
import { Coord2D, Direction } from "../interfaces";
|
||||||
import { tryWrap } from "../utils";
|
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 { parse } from "../../interpreter";
|
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 {
|
export class LambdaFactory extends Entity {
|
||||||
private static spriteSpec: SpriteSpec = SPRITE_SPECS.get(
|
private static spriteSpec: SpriteSpec = SPRITE_SPECS.get(
|
||||||
Sprites.LAMBDA_FACTORY,
|
Sprites.LAMBDA_FACTORY,
|
||||||
) as SpriteSpec;
|
) as SpriteSpec;
|
||||||
|
|
||||||
|
private codeEditorState: CodeEditorState | null;
|
||||||
private spawns: number;
|
private spawns: number;
|
||||||
private code: string;
|
private code: string;
|
||||||
|
|
||||||
@ -37,6 +87,7 @@ export class LambdaFactory extends Entity {
|
|||||||
|
|
||||||
this.spawns = spawns;
|
this.spawns = spawns;
|
||||||
this.code = code;
|
this.code = code;
|
||||||
|
this.codeEditorState = null;
|
||||||
|
|
||||||
this.addComponent(
|
this.addComponent(
|
||||||
new BoundingBox(
|
new BoundingBox(
|
||||||
@ -79,52 +130,22 @@ export class LambdaFactory extends Entity {
|
|||||||
);
|
);
|
||||||
|
|
||||||
this.addComponent(
|
this.addComponent(
|
||||||
new Highlight(this.onHighlight.bind(this), this.onUnhighlight.bind(this)),
|
new Highlight(
|
||||||
);
|
(direction) => this.onHighlight(direction),
|
||||||
}
|
() => this.onUnhighlight(),
|
||||||
|
),
|
||||||
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() {
|
private onUnhighlight() {
|
||||||
this.removeComponent(ComponentNames.Modal);
|
closeModal();
|
||||||
this.removeComponent(ComponentNames.Interactable);
|
this.removeComponent(ComponentNames.Interactable);
|
||||||
}
|
}
|
||||||
|
|
||||||
private spawnNewLambda(direction: Direction) {
|
private spawnNewLambda(direction: Direction) {
|
||||||
const parsed = tryWrap(() => parse(this.code));
|
try {
|
||||||
if (parsed.error) {
|
parse(this.code);
|
||||||
|
} catch (e: any) {
|
||||||
SOUNDS.get(Failure.name)!.play();
|
SOUNDS.get(Failure.name)!.play();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -138,4 +159,132 @@ export class LambdaFactory extends Entity {
|
|||||||
|
|
||||||
SOUNDS.get(LambdaTransformSound.name)!.play();
|
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,61 +0,0 @@
|
|||||||
import { Entity, EntityNames } from ".";
|
|
||||||
import {
|
|
||||||
Colliding,
|
|
||||||
ComponentNames,
|
|
||||||
FacingDirection,
|
|
||||||
Grid,
|
|
||||||
RadialObserve,
|
|
||||||
} from "../components";
|
|
||||||
import { Game } from "../Game";
|
|
||||||
import { Coord2D, Direction } from "../interfaces";
|
|
||||||
|
|
||||||
export class Piston extends Entity {
|
|
||||||
constructor(gridPosition: Coord2D, direction: Direction) {
|
|
||||||
super(EntityNames.Piston);
|
|
||||||
|
|
||||||
const radius = 1;
|
|
||||||
this.addComponent(new RadialObserve(this.onObservation.bind(this), radius));
|
|
||||||
|
|
||||||
this.addComponent(new FacingDirection(direction));
|
|
||||||
|
|
||||||
this.addComponent(new Grid(gridPosition));
|
|
||||||
|
|
||||||
this.addComponent(new Colliding());
|
|
||||||
}
|
|
||||||
|
|
||||||
private onObservation(_game: Game, entity: Entity) {
|
|
||||||
const facingDirection = this.getComponent<FacingDirection>(
|
|
||||||
ComponentNames.FacingDirection,
|
|
||||||
);
|
|
||||||
|
|
||||||
const myPosition = this.getComponent<Grid>(
|
|
||||||
ComponentNames.Grid,
|
|
||||||
).gridPosition;
|
|
||||||
const observingGrid = entity.getComponent<Grid>(ComponentNames.Grid);
|
|
||||||
const observingPosition = observingGrid.gridPosition;
|
|
||||||
|
|
||||||
const [dx, dy] = [
|
|
||||||
myPosition.x - observingPosition.x,
|
|
||||||
myPosition.y - observingPosition.y,
|
|
||||||
].map((x) => Math.round(x));
|
|
||||||
const v: Record<typeof dx, Record<typeof dy, Direction>> = {
|
|
||||||
[-1]: {
|
|
||||||
[dy]: Direction.RIGHT,
|
|
||||||
},
|
|
||||||
[1]: {
|
|
||||||
[dy]: Direction.LEFT,
|
|
||||||
},
|
|
||||||
[0]: {
|
|
||||||
[-1]: Direction.UP,
|
|
||||||
[1]: Direction.DOWN,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (facingDirection.currentDirection !== v[dx][dy]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
observingGrid.movingDirection = facingDirection.currentDirection;
|
|
||||||
entity.addComponent(observingGrid);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,14 +1,5 @@
|
|||||||
import { Entity, EntityNames } from ".";
|
import { Entity, EntityNames, makeLambdaTermHighlightComponent } from ".";
|
||||||
import {
|
import { BoundingBox, Colliding, Grid, Sprite } from "../components";
|
||||||
BoundingBox,
|
|
||||||
Colliding,
|
|
||||||
ComponentNames,
|
|
||||||
Grid,
|
|
||||||
Highlight,
|
|
||||||
Interactable,
|
|
||||||
Modal,
|
|
||||||
Sprite,
|
|
||||||
} from "../components";
|
|
||||||
import { IMAGES, SPRITE_SPECS, SpriteSpec, Sprites } from "../config";
|
import { IMAGES, SPRITE_SPECS, SpriteSpec, Sprites } from "../config";
|
||||||
import { Coord2D } from "../interfaces";
|
import { Coord2D } from "../interfaces";
|
||||||
|
|
||||||
@ -17,11 +8,11 @@ export class Sign extends Entity {
|
|||||||
Sprites.SIGN,
|
Sprites.SIGN,
|
||||||
) as SpriteSpec;
|
) as SpriteSpec;
|
||||||
|
|
||||||
constructor(
|
private text: string;
|
||||||
private readonly text: string,
|
|
||||||
gridPosition: Coord2D,
|
constructor(text: string, gridPosition: Coord2D) {
|
||||||
) {
|
|
||||||
super(EntityNames.Sign);
|
super(EntityNames.Sign);
|
||||||
|
this.text = text;
|
||||||
|
|
||||||
const dimension = {
|
const dimension = {
|
||||||
width: Sign.spriteSpec.width,
|
width: Sign.spriteSpec.width,
|
||||||
@ -53,32 +44,6 @@ export class Sign extends Entity {
|
|||||||
|
|
||||||
this.addComponent(new Colliding());
|
this.addComponent(new Colliding());
|
||||||
|
|
||||||
this.addComponent(
|
this.addComponent(makeLambdaTermHighlightComponent(this, this.text));
|
||||||
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>`,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,6 @@ export interface Coord2D {
|
|||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
}
|
}
|
||||||
export const cartesianDistance = (a: Coord2D, b: Coord2D) =>
|
|
||||||
Math.sqrt((b.y - a.y) ** 2 + (b.x - a.x) ** 2);
|
|
||||||
|
|
||||||
export interface Dimension2D {
|
export interface Dimension2D {
|
||||||
width: number;
|
width: number;
|
||||||
@ -17,3 +15,11 @@ export interface Velocity2D {
|
|||||||
};
|
};
|
||||||
dTheta: number;
|
dTheta: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Force2D {
|
||||||
|
fCartesian: {
|
||||||
|
fx: number;
|
||||||
|
fy: number;
|
||||||
|
};
|
||||||
|
torque: number;
|
||||||
|
}
|
||||||
|
@ -1,80 +0,0 @@
|
|||||||
import { Level, LevelNames } from ".";
|
|
||||||
import { Game } from "..";
|
|
||||||
import {
|
|
||||||
Curry,
|
|
||||||
FunctionApplication,
|
|
||||||
Grass,
|
|
||||||
LambdaFactory,
|
|
||||||
LockedDoor,
|
|
||||||
Player,
|
|
||||||
Wall,
|
|
||||||
} from "../entities";
|
|
||||||
import { Piston } from "../entities/Piston";
|
|
||||||
import { Direction } from "../interfaces";
|
|
||||||
import { Grid, SystemNames } from "../systems";
|
|
||||||
import { normalRandom } from "../utils";
|
|
||||||
|
|
||||||
export class CarCadr extends Level {
|
|
||||||
constructor() {
|
|
||||||
super(LevelNames.CarCadr);
|
|
||||||
}
|
|
||||||
|
|
||||||
public init(game: Game) {
|
|
||||||
const grid = game.getSystem<Grid>(SystemNames.Grid);
|
|
||||||
const dimensions = grid.getGridDimensions();
|
|
||||||
|
|
||||||
const grasses = Array.from({ length: dimensions.width })
|
|
||||||
.fill(0)
|
|
||||||
.map(() => {
|
|
||||||
// random grass
|
|
||||||
return new Grass({
|
|
||||||
x: Math.floor(
|
|
||||||
normalRandom(dimensions.width / 2, dimensions.width / 4, 1.5),
|
|
||||||
),
|
|
||||||
y: Math.floor(
|
|
||||||
normalRandom(dimensions.height / 2, dimensions.height / 4, 1.5),
|
|
||||||
),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const entities = [
|
|
||||||
...grasses,
|
|
||||||
new Player({ x: 9, y: 5 }),
|
|
||||||
// Cadr
|
|
||||||
new Wall({ x: 2, y: 3 }),
|
|
||||||
new Wall({ x: 2, y: 4 }),
|
|
||||||
new Wall({ x: 5, y: 3 }),
|
|
||||||
new LambdaFactory({ x: 4, y: 4 }, "(\\ (x) . x)", 1),
|
|
||||||
new FunctionApplication({ x: 3, y: 5 }, "(_INPUT _KEY)"),
|
|
||||||
new Wall({ x: 2, y: 5 }),
|
|
||||||
new Wall({ x: 4, y: 5 }),
|
|
||||||
new FunctionApplication({ x: 2, y: 6 }, "(_INPUT _EMPTY)"),
|
|
||||||
new Wall({ x: 4, y: 7 }),
|
|
||||||
new Wall({ x: 3, y: 7 }),
|
|
||||||
new Wall({ x: 2, y: 7 }),
|
|
||||||
new Wall({ x: 9, y: 3 }),
|
|
||||||
// Car
|
|
||||||
new LambdaFactory({ x: 10, y: 4 }, "(\\ (x) . x)", 1),
|
|
||||||
new Wall({ x: 12, y: 4 }),
|
|
||||||
new FunctionApplication({ x: 11, y: 5 }, "(_INPUT _EMPTY)"),
|
|
||||||
new Wall({ x: 10, y: 5 }),
|
|
||||||
new Wall({ x: 12, y: 5 }),
|
|
||||||
new Wall({ x: 12, y: 3 }),
|
|
||||||
new FunctionApplication({ x: 12, y: 6 }, "(_INPUT _KEY)"),
|
|
||||||
new Wall({ x: 10, y: 7 }),
|
|
||||||
new Wall({ x: 11, y: 7 }),
|
|
||||||
new Wall({ x: 12, y: 7 }),
|
|
||||||
// solve!
|
|
||||||
new LockedDoor({ x: 7, y: 8 }),
|
|
||||||
new LockedDoor({ x: 7, y: 9 }),
|
|
||||||
new Curry({ x: 7, y: 10 }),
|
|
||||||
new Wall({ x: 6, y: 9 }),
|
|
||||||
new Wall({ x: 8, y: 9 }),
|
|
||||||
new Wall({ x: 6, y: 10 }),
|
|
||||||
new Wall({ x: 8, y: 10 }),
|
|
||||||
new Wall({ x: 7, y: 11 }),
|
|
||||||
];
|
|
||||||
|
|
||||||
entities.forEach((entity) => game.addEntity(entity));
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,5 +1,4 @@
|
|||||||
export namespace LevelNames {
|
export namespace LevelNames {
|
||||||
export const Tutorial = "0";
|
export const Tutorial = "0";
|
||||||
export const CarCadr = "1";
|
|
||||||
export const LevelSelection = "LevelSelection";
|
export const LevelSelection = "LevelSelection";
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,9 @@
|
|||||||
import { LEVELS, Level, LevelNames } from ".";
|
import { LEVELS, Level, LevelNames } from ".";
|
||||||
import { Game } from "..";
|
import { Game } from "..";
|
||||||
import { Grass, Player, Portal } from "../entities";
|
import { Player, Portal } from "../entities";
|
||||||
import { Grid, Level as LevelSystem, SystemNames } from "../systems";
|
import { Grid, Level as LevelSystem, SystemNames } from "../systems";
|
||||||
import { normalRandom } from "../utils";
|
|
||||||
|
|
||||||
export class LevelSelection extends Level {
|
export class LevelSelection extends Level {
|
||||||
public static RADIUS = 5;
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(LevelNames.LevelSelection);
|
super(LevelNames.LevelSelection);
|
||||||
}
|
}
|
||||||
@ -14,43 +11,23 @@ export class LevelSelection extends Level {
|
|||||||
public init(game: Game): void {
|
public init(game: Game): void {
|
||||||
const gridSystem = game.getSystem<Grid>(SystemNames.Grid);
|
const gridSystem = game.getSystem<Grid>(SystemNames.Grid);
|
||||||
const center = gridSystem.getCenterGrid();
|
const center = gridSystem.getCenterGrid();
|
||||||
const dimensions = gridSystem.getGridDimensions();
|
|
||||||
|
|
||||||
const levelSystem = game.getSystem<LevelSystem>(SystemNames.Level);
|
const levelSystem = game.getSystem<LevelSystem>(SystemNames.Level);
|
||||||
const unlocked = levelSystem.getUnlockedLevels();
|
const unlocked = levelSystem.getUnlockedLevels();
|
||||||
|
|
||||||
const renderableLevels = LEVELS.filter(
|
LEVELS.forEach((level, i) => {
|
||||||
({ name }) => name !== LevelNames.LevelSelection,
|
if (
|
||||||
);
|
!unlocked.has(level.name) ||
|
||||||
const radiansPerLevel = (2 * Math.PI) / renderableLevels.length;
|
level.name === LevelNames.LevelSelection
|
||||||
renderableLevels
|
) {
|
||||||
.filter(({ name }) => unlocked.has(name))
|
return;
|
||||||
.map((level, i) => {
|
}
|
||||||
const radians = i * radiansPerLevel;
|
|
||||||
const coords = {
|
const portal = new Portal(level.name, { x: i, y: 7 });
|
||||||
x: Math.floor(Math.cos(radians) * LevelSelection.RADIUS + center.x),
|
game.addEntity(portal);
|
||||||
y: Math.floor(Math.sin(radians) * LevelSelection.RADIUS + center.y),
|
});
|
||||||
};
|
|
||||||
return new Portal(level.name, coords);
|
|
||||||
})
|
|
||||||
.forEach((e) => game.addEntity(e));
|
|
||||||
|
|
||||||
const player = new Player(center);
|
const player = new Player(center);
|
||||||
game.addEntity(player);
|
game.addEntity(player);
|
||||||
|
|
||||||
Array.from({ length: dimensions.width })
|
|
||||||
.fill(0)
|
|
||||||
.map(() => {
|
|
||||||
// random grass
|
|
||||||
return new Grass({
|
|
||||||
x: Math.floor(
|
|
||||||
normalRandom(dimensions.width / 2, dimensions.width / 4, 1.5),
|
|
||||||
),
|
|
||||||
y: Math.floor(
|
|
||||||
normalRandom(dimensions.height / 2, dimensions.height / 4, 1.5),
|
|
||||||
),
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.forEach((e) => game.addEntity(e));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,7 @@ export class Tutorial extends Level {
|
|||||||
{ x: 4, y: 3 },
|
{ x: 4, y: 3 },
|
||||||
),
|
),
|
||||||
new Sign(
|
new Sign(
|
||||||
"<div>this is a Term Application; interact to view its code<br><br>push the term ➡️ created by the factory any direction into the Application to produce a new one 💭<br><br>note that:<br><br>+ _INPUT is the term replaced by the pushed term<br><br>+ in this case _KEY is applied to the function to make a new KEY! 🔑</div>",
|
"this is a Term Application; interact to view its code<br><br>push the term ➡️ created by the factory any direction into the Application to produce a new one 💭<br><br>note that:<br><br>+ _INPUT is the term replaced by the pushed term<br><br>+ in this case _KEY is applied to the function to make a new KEY! 🔑",
|
||||||
{ x: 4, y: 6 },
|
{ x: 4, y: 6 },
|
||||||
),
|
),
|
||||||
new Wall({ x: 10, y: 9 }),
|
new Wall({ x: 10, y: 9 }),
|
||||||
|
@ -2,17 +2,11 @@ export * from "./LevelNames";
|
|||||||
export * from "./Level";
|
export * from "./Level";
|
||||||
export * from "./LevelSelection";
|
export * from "./LevelSelection";
|
||||||
export * from "./Tutorial";
|
export * from "./Tutorial";
|
||||||
export * from "./CarCadr";
|
|
||||||
|
|
||||||
import { LevelNames } from ".";
|
import { LevelNames } from ".";
|
||||||
import { CarCadr, LevelSelection, Tutorial, Level } from ".";
|
import { LevelSelection, Tutorial, Level } from ".";
|
||||||
|
|
||||||
export const LEVELS: Level[] = [
|
export const LEVELS: Level[] = [new LevelSelection(), new Tutorial()];
|
||||||
new LevelSelection(),
|
|
||||||
new Tutorial(),
|
|
||||||
new CarCadr(),
|
|
||||||
];
|
|
||||||
export const LEVEL_PROGRESSION: Record<string, string[]> = {
|
export const LEVEL_PROGRESSION: Record<string, string[]> = {
|
||||||
[LevelNames.LevelSelection]: [LevelNames.Tutorial],
|
[LevelNames.LevelSelection]: [LevelNames.Tutorial],
|
||||||
[LevelNames.Tutorial]: [LevelNames.CarCadr],
|
|
||||||
};
|
};
|
||||||
|
@ -27,38 +27,34 @@ export class FacingDirection extends System {
|
|||||||
game.forEachEntityWithComponent(
|
game.forEachEntityWithComponent(
|
||||||
ComponentNames.FacingDirection,
|
ComponentNames.FacingDirection,
|
||||||
(entity) => {
|
(entity) => {
|
||||||
const facingDirection = entity.getComponent<FacingDirectionComponent>(
|
if (!entity.hasComponent(ComponentNames.BoundingBox)) {
|
||||||
ComponentNames.FacingDirection,
|
|
||||||
);
|
|
||||||
if (!entity.hasComponent(ComponentNames.Sprite)) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entity.hasComponent(ComponentNames.Control)) {
|
const boundingBox = entity.getComponent<BoundingBox>(
|
||||||
const boundingBox = entity.getComponent<BoundingBox>(
|
ComponentNames.BoundingBox,
|
||||||
ComponentNames.BoundingBox,
|
)!;
|
||||||
)!;
|
const facingDirection = entity.getComponent<FacingDirectionComponent>(
|
||||||
|
ComponentNames.FacingDirection,
|
||||||
|
);
|
||||||
|
|
||||||
const { center } = boundingBox;
|
const { center } = boundingBox;
|
||||||
const angle = Math.atan2(
|
const angle = Math.atan2(
|
||||||
mousePosition.y - center.y,
|
mousePosition.y - center.y,
|
||||||
mousePosition.x - center.x,
|
mousePosition.x - center.x,
|
||||||
);
|
);
|
||||||
|
|
||||||
const mouseInBoundingBox =
|
const mouseInBoundingBox =
|
||||||
boundingBox.isCollidingWith(mouseBoundingBox);
|
boundingBox.isCollidingWith(mouseBoundingBox);
|
||||||
const direction = mouseInBoundingBox
|
const direction = mouseInBoundingBox
|
||||||
? Direction.NONE
|
? Direction.NONE
|
||||||
: angleToDirection(angle);
|
: angleToDirection(angle);
|
||||||
|
|
||||||
facingDirection.setDirection(direction);
|
facingDirection.setDirection(direction);
|
||||||
entity.addComponent(facingDirection);
|
entity.addComponent(facingDirection);
|
||||||
}
|
|
||||||
|
|
||||||
const oldSprite = entity.getComponent<Sprite>(ComponentNames.Sprite);
|
const oldSprite = entity.getComponent<Sprite>(ComponentNames.Sprite);
|
||||||
const sprite = facingDirection.directionSprites.get(
|
const sprite = facingDirection.directionSprites.get(direction)!;
|
||||||
facingDirection.currentDirection,
|
|
||||||
)!;
|
|
||||||
sprite.fillTimingsFromSprite(oldSprite);
|
sprite.fillTimingsFromSprite(oldSprite);
|
||||||
|
|
||||||
entity.addComponent(sprite);
|
entity.addComponent(sprite);
|
||||||
|
@ -54,9 +54,6 @@ export class Grid extends System {
|
|||||||
if (!entity.hasComponent(ComponentNames.Grid)) {
|
if (!entity.hasComponent(ComponentNames.Grid)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!entity.hasComponent(ComponentNames.Control)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const grid = entity.getComponent<GridComponent>(ComponentNames.Grid)!;
|
const grid = entity.getComponent<GridComponent>(ComponentNames.Grid)!;
|
||||||
const facingDirection = entity.getComponent<FacingDirection>(
|
const facingDirection = entity.getComponent<FacingDirection>(
|
||||||
@ -142,17 +139,11 @@ export class Grid extends System {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (collidingEntities.length > 0) {
|
if (collidingEntities.length > 0) {
|
||||||
// ensure everything that is a "pushable" or "colliding" which will collide with the entity
|
// i.e. key going into a door or function going into an application
|
||||||
// can actually continue moving in the direction
|
|
||||||
const allEntitiesInPreviousCellCanCollide = Array.from(
|
const allEntitiesInPreviousCellCanCollide = Array.from(
|
||||||
this.grid[currentPosition.y][currentPosition.x],
|
this.grid[currentPosition.y][currentPosition.x],
|
||||||
)
|
)
|
||||||
.map((id) => game.getEntity(id)!)
|
.map((id) => game.getEntity(id)!)
|
||||||
.filter(
|
|
||||||
(entity) =>
|
|
||||||
entity.hasComponent(ComponentNames.Colliding) ||
|
|
||||||
entity.hasComponent(ComponentNames.Pushable),
|
|
||||||
)
|
|
||||||
.every((entity) =>
|
.every((entity) =>
|
||||||
collidingEntities.every((collidingEntity) =>
|
collidingEntities.every((collidingEntity) =>
|
||||||
Collision.canCollide(entity.name, collidingEntity.name),
|
Collision.canCollide(entity.name, collidingEntity.name),
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Grid as GridSystem, SystemNames, System, Level } from ".";
|
import { Grid as GridSystem, SystemNames, System } from ".";
|
||||||
import { Game } from "..";
|
import { Game } from "..";
|
||||||
import { ComponentNames, Grid, Interactable } from "../components";
|
import { ComponentNames, Grid, Interactable } from "../components";
|
||||||
import { Control } from "../components/Control";
|
import { Control } from "../components/Control";
|
||||||
@ -31,10 +31,6 @@ export class Input extends System {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public update(_dt: number, game: Game) {
|
public update(_dt: number, game: Game) {
|
||||||
if (this.hasSomeKey(KeyConstants.ActionKeys.get(Action.RESET))) {
|
|
||||||
game.getSystem<Level>(SystemNames.Level).reset(game);
|
|
||||||
}
|
|
||||||
|
|
||||||
game.forEachEntityWithComponent(ComponentNames.Control, (entity) =>
|
game.forEachEntityWithComponent(ComponentNames.Control, (entity) =>
|
||||||
this.handleMovement(entity, game),
|
this.handleMovement(entity, game),
|
||||||
);
|
);
|
||||||
|
@ -3,9 +3,8 @@ import { Game } from "..";
|
|||||||
import { type Level as LevelType, LEVELS, LEVEL_PROGRESSION } from "../levels";
|
import { type Level as LevelType, LEVELS, LEVEL_PROGRESSION } from "../levels";
|
||||||
|
|
||||||
export class Level extends System {
|
export class Level extends System {
|
||||||
// TODO: read from localstorage
|
private unlockedLevels: Set<string>;
|
||||||
private unlockedLevels: Set<string> = new Set();
|
private currentLevel: LevelType | null;
|
||||||
private currentLevel: LevelType | null = null;
|
|
||||||
private moveToLevel: string | null;
|
private moveToLevel: string | null;
|
||||||
private levelMap: Map<string, LevelType>;
|
private levelMap: Map<string, LevelType>;
|
||||||
|
|
||||||
@ -17,18 +16,15 @@ export class Level extends System {
|
|||||||
this.levelMap.set(level.name, level);
|
this.levelMap.set(level.name, level);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.currentLevel = null;
|
||||||
this.moveToLevel = initialLevel;
|
this.moveToLevel = initialLevel;
|
||||||
|
this.unlockedLevels = new Set();
|
||||||
}
|
}
|
||||||
|
|
||||||
public setLevel(level: string) {
|
public setLevel(level: string) {
|
||||||
this.moveToLevel = level;
|
this.moveToLevel = level;
|
||||||
}
|
}
|
||||||
|
|
||||||
public reset(game: Game) {
|
|
||||||
game.resetState();
|
|
||||||
this.currentLevel?.init(game);
|
|
||||||
}
|
|
||||||
|
|
||||||
public update(_dt: number, game: Game) {
|
public update(_dt: number, game: Game) {
|
||||||
if (this.moveToLevel === this.currentLevel?.name || !this.moveToLevel) {
|
if (this.moveToLevel === this.currentLevel?.name || !this.moveToLevel) {
|
||||||
return;
|
return;
|
||||||
|
@ -1,93 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,65 +0,0 @@
|
|||||||
import { System, SystemNames } from ".";
|
|
||||||
import {
|
|
||||||
ComponentNames,
|
|
||||||
Grid,
|
|
||||||
RadialObserve as RadialObserveComponent,
|
|
||||||
} from "../components";
|
|
||||||
import { Entity, EntityNames } from "../entities";
|
|
||||||
import { Game } from "../Game";
|
|
||||||
import { cartesianDistance } from "../interfaces";
|
|
||||||
|
|
||||||
const radialObservations: Record<string, Set<string>> = {
|
|
||||||
[EntityNames.Piston]: new Set([EntityNames.FunctionBox]),
|
|
||||||
};
|
|
||||||
|
|
||||||
export class RadialObserve extends System {
|
|
||||||
constructor() {
|
|
||||||
super(SystemNames.RadialObserve);
|
|
||||||
}
|
|
||||||
|
|
||||||
public update(_dt: number, game: Game) {
|
|
||||||
game.forEachEntityWithComponent(ComponentNames.RadialObserve, (entity) => {
|
|
||||||
if (!(entity.name in radialObservations)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const observable = radialObservations[entity.name];
|
|
||||||
|
|
||||||
const entityObserve = entity.getComponent<RadialObserveComponent>(
|
|
||||||
ComponentNames.RadialObserve,
|
|
||||||
);
|
|
||||||
if (!entityObserve.onObservation) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const entityPosition = entity.getComponent<Grid>(
|
|
||||||
ComponentNames.Grid,
|
|
||||||
).gridPosition;
|
|
||||||
|
|
||||||
const observations: Entity[] = [];
|
|
||||||
game.forEachEntityWithComponent(ComponentNames.RadialObserve, (other) => {
|
|
||||||
if (entity === other) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!observable.has(other.name)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const otherPosition = other.getComponent<Grid>(
|
|
||||||
ComponentNames.Grid,
|
|
||||||
).gridPosition;
|
|
||||||
if (
|
|
||||||
cartesianDistance(entityPosition, otherPosition) >
|
|
||||||
entityObserve.radius
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
observations.push(other);
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const observation of observations) {
|
|
||||||
entityObserve.onObservation!(game, observation);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -8,6 +8,4 @@ export namespace SystemNames {
|
|||||||
export const Life = "Life";
|
export const Life = "Life";
|
||||||
export const Music = "Music";
|
export const Music = "Music";
|
||||||
export const Level = "Level";
|
export const Level = "Level";
|
||||||
export const Modal = "Modal";
|
|
||||||
export const RadialObserve = "RadialObserve";
|
|
||||||
}
|
}
|
||||||
|
@ -9,5 +9,3 @@ export * from "./Collision";
|
|||||||
export * from "./Life";
|
export * from "./Life";
|
||||||
export * from "./Music";
|
export * from "./Music";
|
||||||
export * from "./Level";
|
export * from "./Level";
|
||||||
export * from "./Modal";
|
|
||||||
export * from "./RadialObserve";
|
|
||||||
|
@ -1,202 +0,0 @@
|
|||||||
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, 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();
|
|
@ -1,45 +0,0 @@
|
|||||||
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,8 +1,6 @@
|
|||||||
export * from "./clamp";
|
export * from "./clamp";
|
||||||
export * from "./dotProduct";
|
export * from "./dotProduct";
|
||||||
export * from "./rotateVector";
|
export * from "./rotateVector";
|
||||||
|
export * from "./modal";
|
||||||
export * from "./colors";
|
export * from "./colors";
|
||||||
export * from "./random";
|
export * from "./random";
|
||||||
export * from "./tryWrap";
|
|
||||||
export * from "./Modal";
|
|
||||||
export * from "./CodeEditor";
|
|
||||||
|
41
src/engine/utils/modal.ts
Normal file
41
src/engine/utils/modal.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
@ -1,7 +0,0 @@
|
|||||||
export const tryWrap = <T>(supplier: () => T): { data?: T; error?: any } => {
|
|
||||||
try {
|
|
||||||
return { data: supplier() };
|
|
||||||
} catch (error) {
|
|
||||||
return { error: error as any };
|
|
||||||
}
|
|
||||||
};
|
|
@ -9,8 +9,6 @@ import {
|
|||||||
|
|
||||||
export class InvalidLambdaTermError extends Error {}
|
export class InvalidLambdaTermError extends Error {}
|
||||||
|
|
||||||
export class MaxRecursionDepthError extends Error {}
|
|
||||||
|
|
||||||
export type DebrujinAbstraction = {
|
export type DebrujinAbstraction = {
|
||||||
abstraction: {
|
abstraction: {
|
||||||
param: string;
|
param: string;
|
||||||
@ -154,11 +152,7 @@ export const adjustIndices = (
|
|||||||
|
|
||||||
export const betaReduce = (
|
export const betaReduce = (
|
||||||
term: DebrujinifiedLambdaTerm,
|
term: DebrujinifiedLambdaTerm,
|
||||||
maxDepth: number,
|
|
||||||
): DebrujinifiedLambdaTerm => {
|
): DebrujinifiedLambdaTerm => {
|
||||||
if (maxDepth === 0) {
|
|
||||||
throw new MaxRecursionDepthError("max recursion depth identified");
|
|
||||||
}
|
|
||||||
if ("index" in term) {
|
if ("index" in term) {
|
||||||
return term;
|
return term;
|
||||||
}
|
}
|
||||||
@ -168,7 +162,7 @@ export const betaReduce = (
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
abstraction: {
|
abstraction: {
|
||||||
body: betaReduce(body, maxDepth - 1),
|
body: betaReduce(body),
|
||||||
param,
|
param,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -176,9 +170,7 @@ export const betaReduce = (
|
|||||||
|
|
||||||
if ("application" in term) {
|
if ("application" in term) {
|
||||||
const { left } = term.application;
|
const { left } = term.application;
|
||||||
const args = term.application.args.map((term) =>
|
const args = term.application.args.map(betaReduce);
|
||||||
betaReduce(term, maxDepth - 1),
|
|
||||||
);
|
|
||||||
|
|
||||||
return args.reduce((acc: DebrujinifiedLambdaTerm, x) => {
|
return args.reduce((acc: DebrujinifiedLambdaTerm, x) => {
|
||||||
if ("abstraction" in acc) {
|
if ("abstraction" in acc) {
|
||||||
@ -249,19 +241,18 @@ export const emitNamed = (term: DebrujinifiedLambdaTerm): string => {
|
|||||||
export const interpret = (
|
export const interpret = (
|
||||||
term: string,
|
term: string,
|
||||||
symbolTable = new SymbolTable(),
|
symbolTable = new SymbolTable(),
|
||||||
allowUnderscores = false, // in our world, underscores should be internal to the game.
|
allowUnderscores = false,
|
||||||
maxDepth = 15,
|
|
||||||
): DebrujinifiedLambdaTerm => {
|
): DebrujinifiedLambdaTerm => {
|
||||||
const ast = parse(term, allowUnderscores);
|
const ast = parse(term, allowUnderscores);
|
||||||
const debrujined = debrujinify(ast, symbolTable);
|
const debrujined = debrujinify(ast, symbolTable);
|
||||||
|
|
||||||
let prev = debrujined;
|
let prev = debrujined;
|
||||||
let next = betaReduce(prev, maxDepth);
|
let next = betaReduce(prev);
|
||||||
|
|
||||||
while (emitDebrujin(prev) !== emitDebrujin(next)) {
|
while (emitDebrujin(prev) !== emitDebrujin(next)) {
|
||||||
// alpha equivalence
|
// alpha equivalence
|
||||||
prev = next;
|
prev = next;
|
||||||
next = betaReduce(prev, maxDepth);
|
next = betaReduce(prev);
|
||||||
}
|
}
|
||||||
return next;
|
return next;
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user