summaryrefslogtreecommitdiff
path: root/src/engine
diff options
context:
space:
mode:
authorElizabeth Hunt <elizabeth.hunt@simponic.xyz>2024-03-07 20:45:47 -0700
committerElizabeth Hunt <elizabeth.hunt@simponic.xyz>2024-03-07 20:45:47 -0700
commite6e29440563e33bb67e0ad51f9fb6c5c2c3fe809 (patch)
tree5deaee322ff1a039dc44a3cb52ecc48a671fda2a /src/engine
parent823620b2a6ebb7ece619991e47a37ad46542b69f (diff)
downloadthe-abstraction-engine-e6e29440563e33bb67e0ad51f9fb6c5c2c3fe809.tar.gz
the-abstraction-engine-e6e29440563e33bb67e0ad51f9fb6c5c2c3fe809.zip
level one (applications prototype finished!)
Diffstat (limited to 'src/engine')
-rw-r--r--src/engine/TheAbstractionEngine.ts8
-rw-r--r--src/engine/components/Grid.ts2
-rw-r--r--src/engine/config/assets.ts43
-rw-r--r--src/engine/config/index.ts1
-rw-r--r--src/engine/config/sounds.ts84
-rw-r--r--src/engine/config/sprites.ts10
-rw-r--r--src/engine/entities/FunctionApplication.ts180
-rw-r--r--src/engine/entities/FunctionBox.ts15
-rw-r--r--src/engine/entities/LambdaFactory.ts11
-rw-r--r--src/engine/entities/LockedDoor.ts16
-rw-r--r--src/engine/entities/Particles.ts8
-rw-r--r--src/engine/entities/Player.ts3
-rw-r--r--src/engine/systems/Collision.ts1
-rw-r--r--src/engine/systems/Grid.ts25
-rw-r--r--src/engine/systems/Input.ts51
-rw-r--r--src/engine/systems/Music.ts40
-rw-r--r--src/engine/systems/SystemNames.ts1
-rw-r--r--src/engine/systems/index.ts1
-rw-r--r--src/engine/utils/modal.ts2
19 files changed, 472 insertions, 30 deletions
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);
}
};