diff --git a/public/assets/bubble.png b/public/assets/bubble.png index 62744b3..9c8b758 100644 Binary files a/public/assets/bubble.png and b/public/assets/bubble.png differ diff --git a/public/assets/key.png b/public/assets/key.png index c6d07a4..23bada6 100644 Binary files a/public/assets/key.png and b/public/assets/key.png differ diff --git a/public/assets/locked_door.png b/public/assets/locked_door.png index 4a90d84..7175ae4 100644 Binary files a/public/assets/locked_door.png and b/public/assets/locked_door.png differ diff --git a/public/assets/sound/failure.wav b/public/assets/sound/failure.wav new file mode 100644 index 0000000..c7dbddc Binary files /dev/null and b/public/assets/sound/failure.wav differ diff --git a/public/assets/sound/keyopen.wav b/public/assets/sound/keyopen.wav new file mode 100644 index 0000000..bdc2d18 Binary files /dev/null and b/public/assets/sound/keyopen.wav differ diff --git a/public/assets/sound/lambda_save.wav b/public/assets/sound/lambda_save.wav new file mode 100644 index 0000000..b08c015 Binary files /dev/null and b/public/assets/sound/lambda_save.wav differ diff --git a/public/assets/sound/lambda_transform.wav b/public/assets/sound/lambda_transform.wav new file mode 100644 index 0000000..03a180a Binary files /dev/null and b/public/assets/sound/lambda_transform.wav differ diff --git a/public/assets/sound/modal_close.wav b/public/assets/sound/modal_close.wav new file mode 100644 index 0000000..fe978d2 Binary files /dev/null and b/public/assets/sound/modal_close.wav differ diff --git a/public/assets/sound/modal_open.wav b/public/assets/sound/modal_open.wav new file mode 100644 index 0000000..f28b1c7 Binary files /dev/null and b/public/assets/sound/modal_open.wav differ diff --git a/public/assets/sound/move_1.wav b/public/assets/sound/move_1.wav new file mode 100644 index 0000000..76949dd Binary files /dev/null and b/public/assets/sound/move_1.wav differ diff --git a/public/assets/sound/move_2.wav b/public/assets/sound/move_2.wav new file mode 100644 index 0000000..d69e5e5 Binary files /dev/null and b/public/assets/sound/move_2.wav differ diff --git a/public/assets/sound/move_3.wav b/public/assets/sound/move_3.wav new file mode 100644 index 0000000..3ade3bc Binary files /dev/null and b/public/assets/sound/move_3.wav differ diff --git a/public/assets/sound/move_4.wav b/public/assets/sound/move_4.wav new file mode 100644 index 0000000..0407d8a Binary files /dev/null and b/public/assets/sound/move_4.wav differ diff --git a/public/assets/sound/music/credits.txt b/public/assets/sound/music/credits.txt new file mode 100644 index 0000000..0ac1440 --- /dev/null +++ b/public/assets/sound/music/credits.txt @@ -0,0 +1,20 @@ +Reverie by +Music promoted by https://www.chosic.com/free-music/all/ +Creative Commons CC BY 4.0 +https://creativecommons.org/licenses/by/4.0/ + +Moonlight by Scott Buckley | www.scottbuckley.com.au +Music promoted by https://www.chosic.com/free-music/all/ +Creative Commons CC BY 4.0 +https://creativecommons.org/licenses/by/4.0/ + +Jul by Scott Buckley | www.scottbuckley.com.au +Music promoted by https://www.chosic.com/free-music/all/ +Creative Commons Attribution 4.0 International (CC BY 4.0) +https://creativecommons.org/licenses/by/4.0/ + +A Kind Of Hope by Scott Buckley | www.scottbuckley.com.au +Music promoted by https://www.chosic.com/free-music/all/ +Creative Commons CC BY 4.0 +https://creativecommons.org/licenses/by/4.0/ + diff --git a/public/assets/sound/music/hope.mp3 b/public/assets/sound/music/hope.mp3 new file mode 100644 index 0000000..2a3501a Binary files /dev/null and b/public/assets/sound/music/hope.mp3 differ diff --git a/public/assets/sound/music/jul.mp3 b/public/assets/sound/music/jul.mp3 new file mode 100644 index 0000000..c87218c Binary files /dev/null and b/public/assets/sound/music/jul.mp3 differ diff --git a/public/assets/sound/music/moonlight.mp3 b/public/assets/sound/music/moonlight.mp3 new file mode 100644 index 0000000..f1c0294 Binary files /dev/null and b/public/assets/sound/music/moonlight.mp3 differ diff --git a/public/assets/sound/music/reverie.mp3 b/public/assets/sound/music/reverie.mp3 new file mode 100644 index 0000000..e77af11 Binary files /dev/null and b/public/assets/sound/music/reverie.mp3 differ diff --git a/src/App.tsx b/src/App.tsx index 3f3f67d..295b01b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -32,6 +32,10 @@ export const App = () => { | inspired by{" "} baba is you + {" "} + | music by{" "} + + scott buckley diff --git a/src/components/GameCanvas.tsx b/src/components/GameCanvas.tsx index 09351e3..b6c585d 100644 --- a/src/components/GameCanvas.tsx +++ b/src/components/GameCanvas.tsx @@ -1,6 +1,7 @@ import { useState, useEffect, useRef } from "react"; import { TheAbstractionEngine, Game } from "../engine"; import { Miscellaneous } from "../engine/config"; +import { Title } from "./Title"; export interface GameCanvasProps { width: number; @@ -10,6 +11,8 @@ export interface GameCanvasProps { export const GameCanvas = ({ width, height }: GameCanvasProps) => { const canvasRef = useRef(null); const [game, setGame] = useState(); + const [ready, setReady] = useState(false); + const [loading, setLoading] = useState(true); useEffect(() => { if (canvasRef.current && !game) { @@ -21,25 +24,35 @@ export const GameCanvas = ({ width, height }: GameCanvasProps) => { theAbstractionEngine.init().then(() => { theAbstractionEngine.play(); - setGame(theAbstractionEngine); - canvas.focus(); + + setGame(theAbstractionEngine); + setLoading(false); }); return () => theAbstractionEngine.stop(); } } - }, [canvasRef]); + }, [canvasRef, ready]); + + if (ready) { + return ( +
+ + {loading && Loading...} +
+ ); + } return (
- + </div> ); }; diff --git a/src/components/Title.tsx b/src/components/Title.tsx new file mode 100644 index 0000000..99c7584 --- /dev/null +++ b/src/components/Title.tsx @@ -0,0 +1,17 @@ +export interface TitleProps { + setReady: (ready: boolean) => void; +} + +export const Title = ({ setReady }: TitleProps) => { + return ( + <div style={{ textAlign: "center" }}> + <h1>the abstraction engine</h1> + <p>a game based on the lambda calculus</p> + + <br /> + <hr /> + <br /> + <button onClick={() => setReady(true)}>ready</button> + </div> + ); +}; diff --git a/src/css/style.css b/src/css/style.css index 35fdc31..1b98555 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -12,7 +12,6 @@ padding: 0; margin: 0; font-family: "scientifica", monospace; - transition: background 0.2s ease-in-out; font-smooth: never; } @@ -21,10 +20,13 @@ body { margin: 0; width: 100%; height: 100%; + background: radial-gradient(ellipse at top, var(--bg), transparent), + radial-gradient(ellipse at left, var(--blue), transparent), + radial-gradient(ellipse at right, var(--purple), transparent), + radial-gradient(ellipse at bottom, var(--bg), transparent); } body { - background-color: var(--bg); color: var(--text); } @@ -51,17 +53,20 @@ a:visited { justify-content: space-around; padding-top: 1rem; padding-bottom: 1rem; + border-bottom: 2px solid var(--yellow); + border-radius: 0.5rem; } .content { - border-top: 1px solid var(--yellow); - border-bottom: 1px solid var(--yellow); max-height: 90vh; } .footer { + border-top: 2px solid var(--yellow); + border-radius: 0.5rem; padding-top: 1rem; padding-bottom: 1rem; + text-align: center; } .nav { @@ -88,12 +93,13 @@ a:visited { .centered-game canvas { display: block; - max-height: 90%; + max-height: 100%; width: auto; max-width: 100%; border: 1px solid var(--yellow); border-radius: 0.5rem; - margin: 0; + margin-bottom: 2rem; + background-color: var(--bg); } button { diff --git a/src/engine/TheAbstractionEngine.ts b/src/engine/TheAbstractionEngine.ts index 20ff6cc..93684ef 100644 --- a/src/engine/TheAbstractionEngine.ts +++ b/src/engine/TheAbstractionEngine.ts @@ -7,6 +7,7 @@ import { Key, LockedDoor, Curry, + FunctionApplication, } from "./entities"; import { Grid, @@ -16,6 +17,7 @@ import { Collision, GridSpawner, Life, + Music, } from "./systems"; export class TheAbstractionEngine { @@ -49,8 +51,9 @@ export class TheAbstractionEngine { ), new GridSpawner(), new Collision(), - new Render(this.ctx), new Life(), + new Music(), + new Render(this.ctx), ].forEach((system) => this.game.addSystem(system)); const player = new Player(); @@ -70,6 +73,9 @@ export class TheAbstractionEngine { const curry = new Curry({ x: 9, y: 8 }); this.game.addEntity(curry); + + const application = new FunctionApplication({ x: 5, y: 5 }, "(_INPUT key)"); + this.game.addEntity(application); } public play() { diff --git a/src/engine/components/Grid.ts b/src/engine/components/Grid.ts index a62cc7b..9fad5ce 100644 --- a/src/engine/components/Grid.ts +++ b/src/engine/components/Grid.ts @@ -6,6 +6,7 @@ export class Grid extends Component { public gridPosition: Coord2D; public movingDirection: Direction; + public previousDirection: Direction; constructor(position: Coord2D = { x: 0, y: 0 }) { super(ComponentNames.Grid); @@ -13,5 +14,6 @@ export class Grid extends Component { this.initialized = false; this.gridPosition = position; this.movingDirection = Direction.NONE; + this.previousDirection = this.movingDirection; } } diff --git a/src/engine/config/assets.ts b/src/engine/config/assets.ts index 5ce13e8..fbfab2f 100644 --- a/src/engine/config/assets.ts +++ b/src/engine/config/assets.ts @@ -1,4 +1,5 @@ import { type SpriteSpec, SPRITE_SPECS } from "."; +import { SOUND_SPECS, SoundSpec } from "./sounds"; export const FONT = new FontFace("scientifica", "url(/fonts/scientifica.ttf)"); FONT.load().then((font) => { @@ -6,6 +7,7 @@ FONT.load().then((font) => { }); export const IMAGES = new Map<string, HTMLImageElement>(); +export const SOUNDS = new Map<string, HTMLAudioElement>(); export const loadSpritesIntoImageElements = ( spriteSpecs: Partial<SpriteSpec>[], @@ -35,6 +37,45 @@ export const loadSpritesIntoImageElements = ( return spritePromises; }; +export const loadSoundsIntoAudioElements = ( + soundSpecs: SoundSpec[], +): Promise<void>[] => { + const soundPromises: Promise<void>[] = []; + + for (const soundSpec of soundSpecs) { + if (soundSpec.url) { + const promise = fetch(soundSpec.url) + .then((response) => response.blob()) + .then((blob) => { + const audio = new Audio(); + audio.src = URL.createObjectURL(blob); + audio.volume = soundSpec.volume ?? 1; + + SOUNDS.set(soundSpec.name, audio); + return new Promise<void>((resolve, rej) => { + audio.oncanplaythrough = () => { + resolve(); + }; + + audio.onerror = (e) => { + console.error(soundSpec); + rej(e); + }; + }); + }); + soundPromises.push(promise); + } + + if (soundSpec.states) { + soundPromises.push( + ...loadSoundsIntoAudioElements(Array.from(soundSpec.states.values())), + ); + } + } + + return soundPromises; +}; + export const loadAssets = () => Promise.all([ ...loadSpritesIntoImageElements( @@ -43,5 +84,5 @@ export const loadAssets = () => ), ), FONT.load(), - // TODO: Sound + ...loadSoundsIntoAudioElements(Array.from(SOUND_SPECS.values())), ]); diff --git a/src/engine/config/index.ts b/src/engine/config/index.ts index a574965..9b69740 100644 --- a/src/engine/config/index.ts +++ b/src/engine/config/index.ts @@ -1,3 +1,4 @@ export * from "./constants"; export * from "./assets"; export * from "./sprites"; +export * from "./sounds"; diff --git a/src/engine/config/sounds.ts b/src/engine/config/sounds.ts new file mode 100644 index 0000000..b182c86 --- /dev/null +++ b/src/engine/config/sounds.ts @@ -0,0 +1,84 @@ +export type SoundSpec = { + name: string; + url?: string; + volume?: number; + states?: Map<string | number, SoundSpec>; +}; + +export const MovingSound: SoundSpec = { + name: "moving", + states: new Map([ + [1, { name: "moving_1", url: "/assets/sound/move_1.wav" }], + // [2, { name: "moving_2", url: "/assets/sound/move_2.wav" }], + // [3, { name: "moving_3", url: "/assets/sound/move_3.wav" }], + [4, { name: "moving_4", url: "/assets/sound/move_4.wav" }], + ]), +}; + +export const LambdaTransformSound: SoundSpec = { + name: "lambdaTransform", + url: "/assets/sound/lambda_transform.wav", + volume: 0.3, +}; + +export const LambdaSave: SoundSpec = { + name: "lambdaSave", + url: "/assets/sound/lambda_save.wav", +}; + +export const Failure: SoundSpec = { + name: "failure", + url: "/assets/sound/failure.wav", + volume: 0.5, +}; + +export const ModalOpen: SoundSpec = { + name: "modalOpen", + url: "/assets/sound/modal_open.wav", + volume: 0.5, +}; + +export const ModalClose: SoundSpec = { + name: "modalClose", + url: "/assets/sound/modal_close.wav", + volume: 0.5, +}; + +export const KeyOpen: SoundSpec = { + name: "keyOpen", + url: "/assets/sound/keyopen.wav", +}; + +export const Music: SoundSpec = { + name: "music", + states: new Map([ + [ + "hope", + { name: "hope", url: "/assets/sound/music/hope.mp3", volume: 0.5 }, + ], + ["jul", { name: "jul", url: "/assets/sound/music/jul.mp3", volume: 0.5 }], + [ + "reverie", + { name: "reverie", url: "/assets/sound/music/reverie.mp3", volume: 0.5 }, + ], + [ + "moonlight", + { + name: "moonlight", + url: "/assets/sound/music/moonlight.mp3", + volume: 0.5, + }, + ], + ]), +}; + +export const SOUND_SPECS: SoundSpec[] = [ + MovingSound, + LambdaTransformSound, + LambdaSave, + Failure, + ModalOpen, + ModalClose, + KeyOpen, + Music, +]; diff --git a/src/engine/config/sprites.ts b/src/engine/config/sprites.ts index cca5961..83bf0a0 100644 --- a/src/engine/config/sprites.ts +++ b/src/engine/config/sprites.ts @@ -8,6 +8,7 @@ export enum Sprites { KEY, LOCKED_DOOR, CURRY, + BUBBLE, } export interface SpriteSpec { @@ -96,3 +97,12 @@ const currySpriteSpec = { sheet: "/assets/curry.png", }; SPRITE_SPECS.set(Sprites.CURRY, currySpriteSpec); + +const bubbleSpriteSpec = { + msPerFrame: 200, + width: 64, + height: 64, + frames: 3, + sheet: "/assets/bubble.png", +}; +SPRITE_SPECS.set(Sprites.BUBBLE, bubbleSpriteSpec); diff --git a/src/engine/entities/FunctionApplication.ts b/src/engine/entities/FunctionApplication.ts index 31e3490..24e4eec 100644 --- a/src/engine/entities/FunctionApplication.ts +++ b/src/engine/entities/FunctionApplication.ts @@ -1,7 +1,183 @@ -import { Entity, EntityNames } from "."; +import { + Entity, + EntityNames, + FunctionBox, + Key, + Particles, + makeLambdaTermHighlightComponent, +} from "."; +import { + BoundingBox, + Colliding, + ComponentNames, + Grid, + LambdaTerm, + Sprite, +} from "../components"; +import { + Failure, + IMAGES, + LambdaTransformSound, + SOUNDS, + SPRITE_SPECS, + SpriteSpec, + Sprites, +} from "../config"; +import { Coord2D, Direction } from "../interfaces"; +import { Game } from ".."; +import { Grid as GridSystem, SystemNames } from "../systems"; +import { colors } from "../utils"; +import { + DebrujinifiedLambdaTerm, + SymbolTable, + emitNamed, + interpret, +} from "../../interpreter"; export class FunctionApplication extends Entity { - constructor() { + private static spriteSpec = SPRITE_SPECS.get(Sprites.BUBBLE) as SpriteSpec; + + private symbolTable: SymbolTable; + + constructor(gridPosition: Coord2D, lambdaTerm: string) { super(EntityNames.FunctionApplication); + + this.symbolTable = new SymbolTable(); + this.symbolTable.add("key"); + + const dimension = { + width: FunctionApplication.spriteSpec.width, + height: FunctionApplication.spriteSpec.height, + }; + this.addComponent( + new BoundingBox( + { + x: 0, + y: 0, + }, + dimension, + 0, + ), + ); + + this.addComponent(new Grid(gridPosition)); + + this.addComponent(new LambdaTerm(lambdaTerm)); + + this.addComponent( + new Sprite( + IMAGES.get(FunctionApplication.spriteSpec.sheet)!, + { x: 0, y: 0 }, + dimension, + FunctionApplication.spriteSpec.msPerFrame, + FunctionApplication.spriteSpec.frames, + ), + ); + + this.addComponent(new Colliding(this.handleCollision.bind(this))); + + this.addComponent(makeLambdaTermHighlightComponent(this)); + } + + public handleCollision(game: Game, entity: Entity) { + if (entity.name !== EntityNames.FunctionBox) { + return; + } + + const entityGrid = entity.getComponent<Grid>(ComponentNames.Grid); + if (entityGrid.movingDirection !== Direction.NONE) { + // prevent recursive functionBox -> application creation + return; + } + + const grid = this.getComponent<Grid>(ComponentNames.Grid); + const gridSystem = game.getSystem<GridSystem>(SystemNames.Grid); + const fail = () => { + entityGrid.movingDirection = gridSystem.oppositeDirection( + entityGrid.previousDirection, + ); + entity.addComponent(entityGrid); + + const failureSound = SOUNDS.get(Failure.name)!; + failureSound.play(); + }; + + const applicationTerm = this.getComponent<LambdaTerm>( + ComponentNames.LambdaTerm, + ); + const functionTerm = entity.getComponent<LambdaTerm>( + ComponentNames.LambdaTerm, + ); + const newCode = applicationTerm.code.replace("_INPUT", functionTerm.code); + let result: DebrujinifiedLambdaTerm | null = null; + try { + result = interpret(newCode, this.symbolTable); + } catch (e) { + console.error(e); + fail(); + return; + } + + const { dimension } = gridSystem; + const nextPosition = gridSystem.getNewGridPosition( + grid.gridPosition, + entityGrid.previousDirection, + ); + + let applicationResultingEntity: Entity | null = null; // this should be its own function + if ("abstraction" in result) { + const code = emitNamed(result); + + applicationResultingEntity = new FunctionBox(grid.gridPosition, code); + } else if ("name" in result) { + const { name } = result; + if (name === "key") { + applicationResultingEntity = new Key(grid.gridPosition); + } + } else { + fail(); + return; + } + + game.removeEntity(entity.id); + if (applicationResultingEntity) { + const grid = applicationResultingEntity.getComponent<Grid>( + ComponentNames.Grid, + ); + grid.movingDirection = entityGrid.previousDirection; + applicationResultingEntity.addComponent(grid); + + game.addEntity(applicationResultingEntity); + } + + this.playTransformSound(); + const particles = new Particles({ + center: gridSystem.gridToScreenPosition(nextPosition), + spawnerDimensions: { + width: dimension.width / 2, + height: dimension.height / 2, + }, + particleCount: 10, + spawnerShape: "circle", + particleShape: "circle", + particleMeanSpeed: 0.25, + particleSpeedVariance: 0.15, + particleMeanLife: 150, + particleMeanSize: 2, + particleSizeVariance: 1, + particleLifeVariance: 20, + particleColors: [ + colors.lightAqua, + colors.blue, + colors.green, + colors.lightGreen, + ], + }); + game.addEntity(particles); + } + + private playTransformSound() { + const audio = SOUNDS.get(LambdaTransformSound.name)!; + audio.play(); } } diff --git a/src/engine/entities/FunctionBox.ts b/src/engine/entities/FunctionBox.ts index 92f1908..0c9123e 100644 --- a/src/engine/entities/FunctionBox.ts +++ b/src/engine/entities/FunctionBox.ts @@ -1,6 +1,9 @@ import { IMAGES, Miscellaneous, + ModalClose, + ModalOpen, + SOUNDS, SPRITE_SPECS, SpriteSpec, Sprites, @@ -72,10 +75,15 @@ export const makeLambdaTermHighlightComponent = (entity: Entity) => { const onHighlight = () => { let modalOpen = false; + const doModalClose = () => { + SOUNDS.get(ModalClose.name)!.play(); + modalOpen = false; + closeModal(); + }; + const interaction = () => { if (modalOpen) { - modalOpen = false; - closeModal(); + doModalClose(); return; } @@ -86,9 +94,10 @@ export const makeLambdaTermHighlightComponent = (entity: Entity) => { `<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", () => { - closeModal(); + doModalClose(); document.getElementById(Miscellaneous.CANVAS_ID)!.focus(); }); }; diff --git a/src/engine/entities/LambdaFactory.ts b/src/engine/entities/LambdaFactory.ts index a0f5749..9ad1398 100644 --- a/src/engine/entities/LambdaFactory.ts +++ b/src/engine/entities/LambdaFactory.ts @@ -1,6 +1,11 @@ import { + Failure, IMAGES, + LambdaSave, + LambdaTransformSound, Miscellaneous, + ModalOpen, + SOUNDS, SPRITE_SPECS, SpriteSpec, Sprites, @@ -144,6 +149,8 @@ export class LambdaFactory extends Entity { const text = this.getComponent<Text>(ComponentNames.Text); text.text = spawner.spawnsLeft.toString(); this.addComponent(text); + + SOUNDS.get(LambdaTransformSound.name)!.play(); } private openCodeEditor() { @@ -185,6 +192,8 @@ export class LambdaFactory extends Entity { canvas, closeButton, }; + + SOUNDS.get(ModalOpen.name)!.play(); } private refreshCodeEditorText(text: string) { @@ -239,6 +248,7 @@ export class LambdaFactory extends Entity { }); syntaxError.innerText = e.message; + SOUNDS.get(Failure.name)!.play(); return; } @@ -250,6 +260,7 @@ export class LambdaFactory extends Entity { closeModal(); canvas.focus(); + SOUNDS.get(LambdaSave.name)!.play(); } private onHighlight(direction: Direction) { diff --git a/src/engine/entities/LockedDoor.ts b/src/engine/entities/LockedDoor.ts index b4887d6..aa1f328 100644 --- a/src/engine/entities/LockedDoor.ts +++ b/src/engine/entities/LockedDoor.ts @@ -7,7 +7,14 @@ import { Sprite, ComponentNames, } from "../components"; -import { IMAGES, SPRITE_SPECS, SpriteSpec, Sprites } from "../config"; +import { + IMAGES, + KeyOpen, + SOUNDS, + SPRITE_SPECS, + SpriteSpec, + Sprites, +} from "../config"; import { Coord2D } from "../interfaces"; import { Grid as GridSystem, SystemNames } from "../systems"; import { colors } from "../utils"; @@ -82,5 +89,12 @@ export class LockedDoor extends Entity { }); game.addEntity(particles); + + this.playKeySound(); + } + + private playKeySound() { + const audio = SOUNDS.get(KeyOpen.name)!; + audio.play(); } } diff --git a/src/engine/entities/Particles.ts b/src/engine/entities/Particles.ts index 34b475c..5381b23 100644 --- a/src/engine/entities/Particles.ts +++ b/src/engine/entities/Particles.ts @@ -156,8 +156,12 @@ export class Particles extends Entity { Math.floor(Math.random() * options.particleColors.length) ]; const position = { - x: options.center.x + Math.cos(angle) * options.spawnerDimensions.width, - y: options.center.y + Math.sin(angle) * options.spawnerDimensions.height, + x: + options.center.x + + (Math.cos(angle) * options.spawnerDimensions.width) / 2, + y: + options.center.y + + (Math.sin(angle) * options.spawnerDimensions.height) / 2, }; if (options.spawnerShape === "rectangle") { // determine a random position on the edge of the spawner based on the angle diff --git a/src/engine/entities/Player.ts b/src/engine/entities/Player.ts index cb9161b..1b98383 100644 --- a/src/engine/entities/Player.ts +++ b/src/engine/entities/Player.ts @@ -6,6 +6,7 @@ import { Grid, BoundingBox, Control, + Pushable, } from "../components"; import { Direction } from "../interfaces/"; @@ -28,6 +29,8 @@ export class Player extends Entity { ), ); + this.addComponent(new Pushable()); + this.addComponent(new Control()); this.addComponent(new Grid()); diff --git a/src/engine/systems/Collision.ts b/src/engine/systems/Collision.ts index 8ef8215..7d843cc 100644 --- a/src/engine/systems/Collision.ts +++ b/src/engine/systems/Collision.ts @@ -6,6 +6,7 @@ import { BoundingBox, Colliding, ComponentNames, Grid } from "../components"; const collisionMap: Record<string, Set<string>> = { [EntityNames.Key]: new Set([EntityNames.LockedDoor]), [EntityNames.Curry]: new Set([EntityNames.Player]), + [EntityNames.FunctionApplication]: new Set([EntityNames.FunctionBox]), }; export class Collision extends System { diff --git a/src/engine/systems/Grid.ts b/src/engine/systems/Grid.ts index 1d4a623..9ab28e3 100644 --- a/src/engine/systems/Grid.ts +++ b/src/engine/systems/Grid.ts @@ -33,8 +33,8 @@ export class Grid extends System { this.rebuildGrid(game); this.highlightEntitiesLookedAt(game); - this.propogateEntityMovements(game); + this.propogateEntityMovements(game); this.updateMovingEntities(dt, game); } @@ -209,9 +209,11 @@ export class Grid extends System { ) { game.forEachEntityWithComponent(ComponentNames.Grid, (entity) => { const grid = entity.getComponent<GridComponent>(ComponentNames.Grid)!; + if (grid.movingDirection === Direction.NONE) { return; } + grid.previousDirection = grid.movingDirection; const boundingBox = entity.getComponent<BoundingBox>( ComponentNames.BoundingBox, @@ -270,7 +272,7 @@ export class Grid extends System { }); } - private getNewGridPosition(prev: Coord2D, direction: Direction) { + public getNewGridPosition(prev: Coord2D, direction: Direction) { let { x: newX, y: newY } = prev; switch (direction) { case Direction.LEFT: @@ -290,6 +292,25 @@ export class Grid extends System { return { x: newX, y: newY }; } + public oppositeDirection(direction: Direction) { + let opposite = Direction.NONE; + switch (direction) { + case Direction.LEFT: + opposite = Direction.RIGHT; + break; + case Direction.RIGHT: + opposite = Direction.LEFT; + break; + case Direction.UP: + opposite = Direction.DOWN; + break; + case Direction.DOWN: + opposite = Direction.UP; + break; + } + return opposite; + } + private isEntityPastCenterWhenMoving( direction: Direction, gridPosition: Coord2D, diff --git a/src/engine/systems/Input.ts b/src/engine/systems/Input.ts index 8900f4e..c527f29 100644 --- a/src/engine/systems/Input.ts +++ b/src/engine/systems/Input.ts @@ -2,7 +2,7 @@ import { Grid as GridSystem, SystemNames, System } from "."; import { Game } from ".."; import { ComponentNames, Grid, Interactable } from "../components"; import { Control } from "../components/Control"; -import { Action, KeyConstants } from "../config"; +import { Action, KeyConstants, MovingSound, SOUNDS } from "../config"; import { Entity, Particles } from "../entities"; import { Coord2D, Direction } from "../interfaces"; import { colors } from "../utils"; @@ -105,27 +105,44 @@ export class Input extends System { } if (moveUp || moveLeft || moveRight || moveDown) { - const gridPosition = gridComponent.gridPosition; - const gridSystem = game.getSystem<GridSystem>(SystemNames.Grid); - const particles = new Particles({ - center: gridSystem.gridToScreenPosition(gridPosition), - particleCount: 5, - particleShape: "circle", - particleMeanSpeed: 0.05, - particleSpeedVariance: 0.005, - particleMeanLife: 120, - particleMeanSize: 5, - particleSizeVariance: 2, - particleLifeVariance: 30, - particleColors: [colors.gray, colors.darkGray], - }); - - game.addEntity(particles); + this.spawnParticlesAround(entity, game); + this.playMoveSound(); } entity.addComponent(gridComponent); } + private playMoveSound() { + const movingSounds = Array.from(MovingSound.states!.values()); + const soundName = + movingSounds[Math.floor(Math.random() * movingSounds.length)].name; + SOUNDS.get(soundName)!.play(); + } + + private spawnParticlesAround(entity: Entity, game: Game) { + const gridSystem = game.getSystem<GridSystem>(SystemNames.Grid); + const gridComponent = entity.getComponent<Grid>(ComponentNames.Grid)!; + const particles = new Particles({ + center: gridSystem.gridToScreenPosition(gridComponent.gridPosition), + particleCount: 4, + spawnerShape: "circle", + spawnerDimensions: { + width: 10, + height: 10, + }, + particleShape: "rectangle", + particleMeanSpeed: 0.05, + particleSpeedVariance: 0.005, + particleMeanLife: 120, + particleMeanSize: 3, + particleSizeVariance: 1, + particleLifeVariance: 30, + particleColors: [colors.gray, colors.darkGray, colors.lightPurple], + }); + + game.addEntity(particles); + } + private hasSomeKey(keys?: string[]): boolean { if (keys) { return keys.some((key) => this.keys.has(key)); diff --git a/src/engine/systems/Music.ts b/src/engine/systems/Music.ts new file mode 100644 index 0000000..6e2004d --- /dev/null +++ b/src/engine/systems/Music.ts @@ -0,0 +1,40 @@ +import { System, SystemNames } from "."; +import { Music as GameMusic, SOUNDS } from "../config"; + +export class Music extends System { + private songs: string[] = []; + private currentSong?: string; + + constructor() { + super(SystemNames.Music); + + this.songs = Array.from(GameMusic.states!.values()).map( + (state) => state.name, + ); + } + + private chooseRandomSong() { + return this.songs[Math.floor(Math.random() * this.songs.length)]; + } + + public playNext() { + let nextSong = this.chooseRandomSong(); + while (nextSong === this.currentSong) { + nextSong = this.chooseRandomSong(); + } + + this.currentSong = nextSong; + SOUNDS.get(this.currentSong)?.play(); + + // when done, play next song + SOUNDS.get(this.currentSong)?.addEventListener("ended", () => { + this.playNext(); + }); + } + + public update(_dt: number) { + if (!this.currentSong) { + this.playNext(); + } + } +} diff --git a/src/engine/systems/SystemNames.ts b/src/engine/systems/SystemNames.ts index 738dfba..363c72c 100644 --- a/src/engine/systems/SystemNames.ts +++ b/src/engine/systems/SystemNames.ts @@ -6,4 +6,5 @@ export namespace SystemNames { export const GridSpawner = "GridSpawner"; export const Collision = "Collision"; export const Life = "Life"; + export const Music = "Music"; } diff --git a/src/engine/systems/index.ts b/src/engine/systems/index.ts index a420216..46c534d 100644 --- a/src/engine/systems/index.ts +++ b/src/engine/systems/index.ts @@ -7,3 +7,4 @@ export * from "./Grid"; export * from "./GridSpawner"; export * from "./Collision"; export * from "./Life"; +export * from "./Music"; diff --git a/src/engine/utils/modal.ts b/src/engine/utils/modal.ts index 48afae8..a378821 100644 --- a/src/engine/utils/modal.ts +++ b/src/engine/utils/modal.ts @@ -36,6 +36,6 @@ export const closeModal = ( modal.style.display = "none"; modalOpen = false; - }, 250); + }, 200); } }; diff --git a/src/interpreter/PeggyParser.js b/src/interpreter/PeggyParser.js index 5671d91..632f1b7 100644 --- a/src/interpreter/PeggyParser.js +++ b/src/interpreter/PeggyParser.js @@ -201,7 +201,7 @@ export default (function () { ); }; - function peg$parse(input, options) { + function peg$parse(input, options, allowUnderscores = false) { options = options !== undefined ? options : {}; var peg$FAILED = {}; @@ -215,7 +215,7 @@ export default (function () { var peg$c2 = "."; var peg$c3 = "\r\n"; - var peg$r0 = /^[a-zA-Z0-9]/; + var peg$r0 = allowUnderscores ? /^[a-zA-Z0-9]_/ : /^[a-zA-Z0-9]/; var peg$r1 = /^[\\\u03BB]/; var peg$r2 = /^[\t-\n ]/; diff --git a/src/interpreter/interpreter.ts b/src/interpreter/interpreter.ts index 0b3ae34..4599c46 100644 --- a/src/interpreter/interpreter.ts +++ b/src/interpreter/interpreter.ts @@ -198,22 +198,6 @@ export const betaReduce = ( ); }; -export const interpret = (term: string): DebrujinifiedLambdaTerm => { - const ast = parse(term); - const symbolTable = new SymbolTable(); - const debrujined = debrujinify(ast, symbolTable); - - let prev = debrujined; - let next = betaReduce(prev); - - while (emitDebrujin(prev) !== emitDebrujin(next)) { - // alpha equivalence - prev = next; - next = betaReduce(prev); - } - return next; -}; - export const emitDebrujin = (term: DebrujinifiedLambdaTerm): string => { if ("index" in term) { return term.index.toString(); @@ -253,3 +237,22 @@ export const emitNamed = (term: DebrujinifiedLambdaTerm): string => { throw new InvalidLambdaTermError(`Invalid lambda term: ${term}`); }; + +export const interpret = ( + term: string, + symbolTable = new SymbolTable(), + allowUnderscores = false, +): DebrujinifiedLambdaTerm => { + const ast = parse(term, allowUnderscores); + const debrujined = debrujinify(ast, symbolTable); + + let prev = debrujined; + let next = betaReduce(prev); + + while (emitDebrujin(prev) !== emitDebrujin(next)) { + // alpha equivalence + prev = next; + next = betaReduce(prev); + } + return next; +}; diff --git a/src/interpreter/parser.ts b/src/interpreter/parser.ts index 5e3be0f..d288815 100644 --- a/src/interpreter/parser.ts +++ b/src/interpreter/parser.ts @@ -30,6 +30,10 @@ export const isVariable = (term: LambdaTerm): term is Variable => { return typeof term === "string"; }; -export const parse = (term: string, library = false) => { - return peggyParser.parse(term, { peg$library: library }); +export const parse = ( + term: string, + allowUnderscores = false, + library = false, +) => { + return peggyParser.parse(term, { peg$library: library }, allowUnderscores); };