level one (applications prototype finished!)
This commit is contained in:
parent
823620b2a6
commit
e6e2944056
Binary file not shown.
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 2.4 KiB |
Binary file not shown.
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.6 KiB |
Binary file not shown.
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 2.1 KiB |
BIN
public/assets/sound/failure.wav
Normal file
BIN
public/assets/sound/failure.wav
Normal file
Binary file not shown.
BIN
public/assets/sound/keyopen.wav
Normal file
BIN
public/assets/sound/keyopen.wav
Normal file
Binary file not shown.
BIN
public/assets/sound/lambda_save.wav
Normal file
BIN
public/assets/sound/lambda_save.wav
Normal file
Binary file not shown.
BIN
public/assets/sound/lambda_transform.wav
Normal file
BIN
public/assets/sound/lambda_transform.wav
Normal file
Binary file not shown.
BIN
public/assets/sound/modal_close.wav
Normal file
BIN
public/assets/sound/modal_close.wav
Normal file
Binary file not shown.
BIN
public/assets/sound/modal_open.wav
Normal file
BIN
public/assets/sound/modal_open.wav
Normal file
Binary file not shown.
BIN
public/assets/sound/move_1.wav
Normal file
BIN
public/assets/sound/move_1.wav
Normal file
Binary file not shown.
BIN
public/assets/sound/move_2.wav
Normal file
BIN
public/assets/sound/move_2.wav
Normal file
Binary file not shown.
BIN
public/assets/sound/move_3.wav
Normal file
BIN
public/assets/sound/move_3.wav
Normal file
Binary file not shown.
BIN
public/assets/sound/move_4.wav
Normal file
BIN
public/assets/sound/move_4.wav
Normal file
Binary file not shown.
20
public/assets/sound/music/credits.txt
Normal file
20
public/assets/sound/music/credits.txt
Normal file
@ -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/
|
||||
|
BIN
public/assets/sound/music/hope.mp3
Normal file
BIN
public/assets/sound/music/hope.mp3
Normal file
Binary file not shown.
BIN
public/assets/sound/music/jul.mp3
Normal file
BIN
public/assets/sound/music/jul.mp3
Normal file
Binary file not shown.
BIN
public/assets/sound/music/moonlight.mp3
Normal file
BIN
public/assets/sound/music/moonlight.mp3
Normal file
Binary file not shown.
BIN
public/assets/sound/music/reverie.mp3
Normal file
BIN
public/assets/sound/music/reverie.mp3
Normal file
Binary file not shown.
@ -32,6 +32,10 @@ export const App = () => {
|
||||
| inspired by{" "}
|
||||
<a href="https://hempuli.com/baba/" target="_blank">
|
||||
baba is you
|
||||
</a>{" "}
|
||||
| music by{" "}
|
||||
<a href="https://www.scottbuckley.com.au" target="_blank">
|
||||
scott buckley
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
|
@ -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<HTMLCanvasElement>(null);
|
||||
const [game, setGame] = useState<TheAbstractionEngine>();
|
||||
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 (
|
||||
<div className="centered-game">
|
||||
<canvas
|
||||
id={Miscellaneous.CANVAS_ID}
|
||||
tabIndex={1}
|
||||
ref={canvasRef}
|
||||
width={loading ? 50 : width}
|
||||
height={loading ? 50 : height}
|
||||
></canvas>
|
||||
{loading && <span className="loading">Loading...</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="centered-game">
|
||||
<canvas
|
||||
id={Miscellaneous.CANVAS_ID}
|
||||
tabIndex={1}
|
||||
ref={canvasRef}
|
||||
width={width}
|
||||
height={height}
|
||||
></canvas>
|
||||
<Title setReady={setReady} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
17
src/components/Title.tsx
Normal file
17
src/components/Title.tsx
Normal file
@ -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>
|
||||
);
|
||||
};
|
@ -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 {
|
||||
|
@ -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() {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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())),
|
||||
]);
|
||||
|
@ -1,3 +1,4 @@
|
||||
export * from "./constants";
|
||||
export * from "./assets";
|
||||
export * from "./sprites";
|
||||
export * from "./sounds";
|
||||
|
84
src/engine/config/sounds.ts
Normal file
84
src/engine/config/sounds.ts
Normal file
@ -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,
|
||||
];
|
@ -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);
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
});
|
||||
};
|
||||
|
@ -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) {
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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());
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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));
|
||||
|
40
src/engine/systems/Music.ts
Normal file
40
src/engine/systems/Music.ts
Normal file
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -6,4 +6,5 @@ export namespace SystemNames {
|
||||
export const GridSpawner = "GridSpawner";
|
||||
export const Collision = "Collision";
|
||||
export const Life = "Life";
|
||||
export const Music = "Music";
|
||||
}
|
||||
|
@ -7,3 +7,4 @@ export * from "./Grid";
|
||||
export * from "./GridSpawner";
|
||||
export * from "./Collision";
|
||||
export * from "./Life";
|
||||
export * from "./Music";
|
||||
|
@ -36,6 +36,6 @@ export const closeModal = (
|
||||
modal.style.display = "none";
|
||||
|
||||
modalOpen = false;
|
||||
}, 250);
|
||||
}, 200);
|
||||
}
|
||||
};
|
||||
|
@ -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 ]/;
|
||||
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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);
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user