summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/engine/TheAbstractionEngine.ts2
-rw-r--r--src/engine/components/Colliding.ts8
-rw-r--r--src/engine/components/ComponentNames.ts1
-rw-r--r--src/engine/components/Life.ts11
-rw-r--r--src/engine/components/Sprite.ts7
-rw-r--r--src/engine/components/index.ts1
-rw-r--r--src/engine/entities/Curry.ts10
-rw-r--r--src/engine/entities/Entity.ts2
-rw-r--r--src/engine/entities/EntityNames.ts1
-rw-r--r--src/engine/entities/LockedDoor.ts47
-rw-r--r--src/engine/entities/Particles.ts194
-rw-r--r--src/engine/entities/index.ts2
-rw-r--r--src/engine/systems/Collision.ts40
-rw-r--r--src/engine/systems/Grid.ts2
-rw-r--r--src/engine/systems/Input.ts28
-rw-r--r--src/engine/systems/Life.ts18
-rw-r--r--src/engine/systems/Render.ts4
-rw-r--r--src/engine/systems/SystemNames.ts1
-rw-r--r--src/engine/systems/index.ts1
-rw-r--r--src/engine/utils/colors.ts22
-rw-r--r--src/engine/utils/index.ts2
-rw-r--r--src/engine/utils/random.ts9
22 files changed, 369 insertions, 44 deletions
diff --git a/src/engine/TheAbstractionEngine.ts b/src/engine/TheAbstractionEngine.ts
index d7dd28b..20ff6cc 100644
--- a/src/engine/TheAbstractionEngine.ts
+++ b/src/engine/TheAbstractionEngine.ts
@@ -15,6 +15,7 @@ import {
Render,
Collision,
GridSpawner,
+ Life,
} from "./systems";
export class TheAbstractionEngine {
@@ -49,6 +50,7 @@ export class TheAbstractionEngine {
new GridSpawner(),
new Collision(),
new Render(this.ctx),
+ new Life(),
].forEach((system) => this.game.addSystem(system));
const player = new Player();
diff --git a/src/engine/components/Colliding.ts b/src/engine/components/Colliding.ts
index 4027c3d..fe782df 100644
--- a/src/engine/components/Colliding.ts
+++ b/src/engine/components/Colliding.ts
@@ -1,7 +1,13 @@
import { Component, ComponentNames } from ".";
+import { Game } from "..";
+import { Entity } from "../entities";
export class Colliding extends Component {
- constructor() {
+ public onCollision?: (game: Game, entity: Entity) => void;
+
+ constructor(onCollision?: (game: Game, entity: Entity) => void) {
super(ComponentNames.Colliding);
+
+ this.onCollision = onCollision;
}
}
diff --git a/src/engine/components/ComponentNames.ts b/src/engine/components/ComponentNames.ts
index a9f0c15..dd50fb3 100644
--- a/src/engine/components/ComponentNames.ts
+++ b/src/engine/components/ComponentNames.ts
@@ -11,4 +11,5 @@ export namespace ComponentNames {
export const GridSpawn = "GridSpawn";
export const Text = "Text";
export const LambdaTerm = "LambdaTerm";
+ export const Life = "Life";
}
diff --git a/src/engine/components/Life.ts b/src/engine/components/Life.ts
new file mode 100644
index 0000000..6e6d278
--- /dev/null
+++ b/src/engine/components/Life.ts
@@ -0,0 +1,11 @@
+import { Component, ComponentNames } from ".";
+
+export class Life extends Component {
+ public alive: boolean = true;
+
+ constructor(alive: boolean) {
+ super(ComponentNames.Life);
+
+ this.alive = alive;
+ }
+}
diff --git a/src/engine/components/Sprite.ts b/src/engine/components/Sprite.ts
index c623bac..fdf9675 100644
--- a/src/engine/components/Sprite.ts
+++ b/src/engine/components/Sprite.ts
@@ -2,7 +2,12 @@ import { Component, ComponentNames } from ".";
import type { Dimension2D, DrawArgs, Coord2D } from "../interfaces";
import { clamp } from "../utils";
-export class Sprite extends Component {
+export interface Renderable {
+ update(dt: number): void;
+ draw(ctx: CanvasRenderingContext2D, drawArgs: DrawArgs): void;
+}
+
+export class Sprite extends Component implements Renderable {
private sheet: HTMLImageElement;
private spriteImgPos: Coord2D;
diff --git a/src/engine/components/index.ts b/src/engine/components/index.ts
index a7a3cf1..023e73d 100644
--- a/src/engine/components/index.ts
+++ b/src/engine/components/index.ts
@@ -12,3 +12,4 @@ export * from "./Colliding";
export * from "./GridSpawn";
export * from "./Text";
export * from "./LambdaTerm";
+export * from "./Life";
diff --git a/src/engine/entities/Curry.ts b/src/engine/entities/Curry.ts
index 85bc7ef..bd57e19 100644
--- a/src/engine/entities/Curry.ts
+++ b/src/engine/entities/Curry.ts
@@ -1,4 +1,5 @@
import { Entity, EntityNames } from ".";
+import { Game } from "..";
import { BoundingBox, Colliding, Grid, Sprite } from "../components";
import { IMAGES, SPRITE_SPECS, SpriteSpec, Sprites } from "../config";
import { Coord2D } from "../interfaces";
@@ -13,7 +14,7 @@ export class Curry extends Entity {
this.addComponent(new Grid(gridPosition));
- this.addComponent(new Colliding());
+ this.addComponent(new Colliding(this.collisionHandler));
this.addComponent(
new BoundingBox(
@@ -42,4 +43,11 @@ export class Curry extends Entity {
),
);
}
+
+ private collisionHandler(game: Game, entity: Entity) {
+ if (entity.name === EntityNames.Player) {
+ game.removeEntity(this.id);
+ game.stop();
+ }
+ }
}
diff --git a/src/engine/entities/Entity.ts b/src/engine/entities/Entity.ts
index d5a8e6e..a9c0d4b 100644
--- a/src/engine/entities/Entity.ts
+++ b/src/engine/entities/Entity.ts
@@ -29,7 +29,7 @@ export abstract class Entity {
this.hooks.get(name)?.remove();
}
- public getComponent<T extends Component>(name: string): T {
+ public getComponent<T>(name: string): T {
if (!this.hasComponent(name)) {
throw new Error("Entity does not have component " + name);
}
diff --git a/src/engine/entities/EntityNames.ts b/src/engine/entities/EntityNames.ts
index 3f6d26f..056db9a 100644
--- a/src/engine/entities/EntityNames.ts
+++ b/src/engine/entities/EntityNames.ts
@@ -7,4 +7,5 @@ export namespace EntityNames {
export const LockedDoor = "LockedDoor";
export const Curry = "Curry";
export const FunctionApplication = "FunctionApplication";
+ export const Particles = "Particles";
}
diff --git a/src/engine/entities/LockedDoor.ts b/src/engine/entities/LockedDoor.ts
index 5e364b8..b4887d6 100644
--- a/src/engine/entities/LockedDoor.ts
+++ b/src/engine/entities/LockedDoor.ts
@@ -1,7 +1,16 @@
-import { Entity, EntityNames } from ".";
-import { BoundingBox, Colliding, Grid, Sprite } from "../components";
+import { Entity, EntityNames, Particles } from ".";
+import { Game } from "..";
+import {
+ BoundingBox,
+ Colliding,
+ Grid,
+ Sprite,
+ ComponentNames,
+} from "../components";
import { IMAGES, SPRITE_SPECS, SpriteSpec, Sprites } from "../config";
import { Coord2D } from "../interfaces";
+import { Grid as GridSystem, SystemNames } from "../systems";
+import { colors } from "../utils";
export class LockedDoor extends Entity {
private static spriteSpec: SpriteSpec = SPRITE_SPECS.get(
@@ -13,7 +22,7 @@ export class LockedDoor extends Entity {
this.addComponent(new Grid(gridPosition));
- this.addComponent(new Colliding());
+ this.addComponent(new Colliding(this.handleCollision.bind(this)));
this.addComponent(
new BoundingBox(
@@ -42,4 +51,36 @@ export class LockedDoor extends Entity {
),
);
}
+
+ private handleCollision(game: Game, entity: Entity) {
+ if (entity.name !== EntityNames.Key) {
+ return;
+ }
+
+ game.removeEntity(this.id);
+ game.removeEntity(entity.id);
+
+ const grid = this.getComponent<Grid>(ComponentNames.Grid);
+ const gridSystem = game.getSystem<GridSystem>(SystemNames.Grid);
+ const { dimension } = gridSystem;
+ const particles = new Particles({
+ center: gridSystem.gridToScreenPosition(grid.gridPosition),
+ spawnerDimensions: {
+ width: dimension.width / 2,
+ height: dimension.height / 2,
+ },
+ particleCount: 20,
+ spawnerShape: "rectangle",
+ particleShape: "rectangle",
+ particleMeanSpeed: 0.35,
+ particleSpeedVariance: 0.15,
+ particleMeanLife: 80,
+ particleMeanSize: 3,
+ particleSizeVariance: 1,
+ particleLifeVariance: 20,
+ particleColors: [colors.yellow, colors.lightYellow],
+ });
+
+ game.addEntity(particles);
+ }
}
diff --git a/src/engine/entities/Particles.ts b/src/engine/entities/Particles.ts
new file mode 100644
index 0000000..34b475c
--- /dev/null
+++ b/src/engine/entities/Particles.ts
@@ -0,0 +1,194 @@
+import { Entity, EntityNames } from ".";
+import {
+ BoundingBox,
+ Component,
+ ComponentNames,
+ Life,
+ Renderable,
+} from "../components";
+import { Coord2D, Dimension2D, DrawArgs } from "../interfaces";
+import { colors } from "../utils";
+import { normalRandom } from "../utils/random";
+
+export interface ParticleSpawnOptions {
+ spawnerDimensions: Dimension2D;
+ center: Coord2D;
+ spawnerShape: "circle" | "rectangle";
+ particleShape: "circle" | "rectangle";
+ particleCount: number;
+ particleMeanLife: number;
+ particleLifeVariance: number;
+ particleMeanSize: number;
+ particleSizeVariance: number;
+ particleMeanSpeed: number;
+ particleSpeedVariance: number;
+ particleColors: Array<string>;
+}
+
+const DEFAULT_PARTICLE_SPAWN_OPTIONS: ParticleSpawnOptions = {
+ spawnerDimensions: { width: 0, height: 0 },
+ center: { x: 0, y: 0 },
+ spawnerShape: "circle",
+ particleShape: "circle",
+ particleCount: 50,
+ particleMeanLife: 200,
+ particleLifeVariance: 50,
+ particleMeanSize: 12,
+ particleSizeVariance: 1,
+ particleMeanSpeed: 2,
+ particleSpeedVariance: 1,
+ particleColors: [colors.gray, colors.aqua, colors.lightAqua],
+};
+
+interface Particle {
+ position: Coord2D;
+ velocity: Coord2D;
+ dimension: Dimension2D;
+ color: string;
+ life: number;
+ shape: "circle" | "rectangle";
+}
+
+class ParticleRenderer extends Component implements Renderable {
+ private particles: Array<Particle>;
+ private onDeath?: () => void;
+
+ constructor(particles: Array<Particle> = [], onDeath?: () => void) {
+ super(ComponentNames.Sprite);
+
+ this.particles = particles;
+ this.onDeath = onDeath;
+ }
+
+ public update(dt: number) {
+ this.particles = this.particles.filter((particle) => {
+ particle.position.x += particle.velocity.x * dt;
+ particle.position.y += particle.velocity.y * dt;
+ particle.life -= dt;
+ return particle.life > 0;
+ });
+
+ if (this.particles.length === 0 && this.onDeath) {
+ this.onDeath();
+ }
+ }
+
+ public draw(ctx: CanvasRenderingContext2D, _drawArgs: DrawArgs) {
+ for (const particle of this.particles) {
+ ctx.fillStyle = particle.color;
+ if (particle.shape === "circle") {
+ ctx.beginPath();
+
+ ctx.ellipse(
+ particle.position.x,
+ particle.position.y,
+ particle.dimension.width / 2,
+ particle.dimension.height / 2,
+ 0,
+ 0,
+ Math.PI * 2,
+ );
+ ctx.fill();
+ } else {
+ ctx.fillRect(
+ particle.position.x - particle.dimension.width / 2,
+ particle.position.y - particle.dimension.height / 2,
+ particle.dimension.width,
+ particle.dimension.height,
+ );
+ }
+ }
+ }
+}
+
+export class Particles extends Entity {
+ constructor(options: Partial<ParticleSpawnOptions>) {
+ super(EntityNames.Particles);
+
+ const spawnOptions = {
+ ...DEFAULT_PARTICLE_SPAWN_OPTIONS,
+ ...options,
+ };
+ const particles = Array(options.particleCount)
+ .fill(0)
+ .map(() => Particles.spawnParticle(spawnOptions));
+
+ this.addComponent(new Life(true));
+ this.addComponent(
+ new ParticleRenderer(particles, () => {
+ const life = this.getComponent<Life>(ComponentNames.Life);
+ life.alive = false;
+ this.addComponent(life);
+ }),
+ );
+
+ this.addComponent(
+ new BoundingBox(
+ {
+ x: 0,
+ y: 0,
+ },
+ {
+ width: spawnOptions.spawnerDimensions.width,
+ height: spawnOptions.spawnerDimensions.height,
+ },
+ 0,
+ ),
+ );
+ }
+
+ static spawnParticle(options: ParticleSpawnOptions) {
+ const angle = Math.random() * Math.PI * 2;
+ const speed = normalRandom(
+ options.particleMeanSpeed,
+ options.particleSpeedVariance,
+ );
+ const life = normalRandom(
+ options.particleMeanLife,
+ options.particleLifeVariance,
+ );
+ const size = normalRandom(
+ options.particleMeanSize,
+ options.particleSizeVariance,
+ );
+ const color =
+ options.particleColors[
+ 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,
+ };
+ if (options.spawnerShape === "rectangle") {
+ // determine a random position on the edge of the spawner based on the angle
+ const halfWidth = options.spawnerDimensions.width / 2;
+ const halfHeight = options.spawnerDimensions.height / 2;
+
+ if (angle < Math.PI / 4 || angle > (Math.PI * 7) / 4) {
+ position.x += halfWidth;
+ position.y += Math.tan(angle) * halfWidth;
+ } else if (angle < (Math.PI * 3) / 4) {
+ position.y += halfHeight;
+ position.x += halfHeight / Math.tan(angle);
+ } else if (angle < (Math.PI * 5) / 4) {
+ position.x -= halfWidth;
+ position.y -= Math.tan(angle) * halfWidth;
+ } else {
+ position.y -= halfHeight;
+ position.x -= halfHeight / Math.tan(angle);
+ }
+ }
+
+ return {
+ position,
+ velocity: {
+ x: Math.cos(angle) * speed,
+ y: Math.sin(angle) * speed,
+ },
+ color,
+ life,
+ dimension: { width: size, height: size },
+ shape: options.particleShape,
+ };
+ }
+}
diff --git a/src/engine/entities/index.ts b/src/engine/entities/index.ts
index 45c95c0..cb256ec 100644
--- a/src/engine/entities/index.ts
+++ b/src/engine/entities/index.ts
@@ -7,3 +7,5 @@ export * from "./LambdaFactory";
export * from "./Key";
export * from "./LockedDoor";
export * from "./Curry";
+export * from "./FunctionApplication";
+export * from "./Particles";
diff --git a/src/engine/systems/Collision.ts b/src/engine/systems/Collision.ts
index 1912fb6..8ef8215 100644
--- a/src/engine/systems/Collision.ts
+++ b/src/engine/systems/Collision.ts
@@ -1,7 +1,7 @@
import { System, SystemNames } from ".";
import { Game } from "..";
import { Entity, EntityNames } from "../entities";
-import { BoundingBox, ComponentNames, Grid } from "../components";
+import { BoundingBox, Colliding, ComponentNames, Grid } from "../components";
const collisionMap: Record<string, Set<string>> = {
[EntityNames.Key]: new Set([EntityNames.LockedDoor]),
@@ -70,34 +70,14 @@ export class Collision extends System {
return;
}
- const keyDoorPair = [EntityNames.Key, EntityNames.LockedDoor].map((x) =>
- [entity, otherEntity].find((y) => y.name === x),
- );
- const [key, door] = keyDoorPair;
- if (key && door) {
- this.handleKeyDoorCollision(key, door, game);
- }
-
- const curryPlayerPair = [EntityNames.Curry, EntityNames.Player].map((x) =>
- [entity, otherEntity].find((y) => y.name === x),
- );
- const [curry, player] = curryPlayerPair;
- if (curry && player) {
- this.handleCurryPlayerCollision(curry, player, game);
- }
- }
-
- private handleKeyDoorCollision(key: Entity, door: Entity, game: Game) {
- game.removeEntity(key.id);
- game.removeEntity(door.id);
- }
-
- private handleCurryPlayerCollision(
- curry: Entity,
- _player: Entity,
- game: Game,
- ) {
- game.removeEntity(curry.id);
- game.stop();
+ [entity, otherEntity].forEach((e) => {
+ if (!e.hasComponent(ComponentNames.Colliding)) {
+ return;
+ }
+ const colliding = e.getComponent<Colliding>(ComponentNames.Colliding);
+ if (colliding?.onCollision) {
+ colliding.onCollision(game, e === entity ? otherEntity : entity);
+ }
+ });
}
}
diff --git a/src/engine/systems/Grid.ts b/src/engine/systems/Grid.ts
index 915335b..1d4a623 100644
--- a/src/engine/systems/Grid.ts
+++ b/src/engine/systems/Grid.ts
@@ -309,7 +309,7 @@ export class Grid extends System {
return false;
}
- private gridToScreenPosition(gridPosition: Coord2D) {
+ public gridToScreenPosition(gridPosition: Coord2D) {
const { width, height } = this.dimension;
return {
x: gridPosition.x * width + width / 2,
diff --git a/src/engine/systems/Input.ts b/src/engine/systems/Input.ts
index 3da018d..8900f4e 100644
--- a/src/engine/systems/Input.ts
+++ b/src/engine/systems/Input.ts
@@ -1,10 +1,11 @@
-import { SystemNames, System } from ".";
+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 { Entity } from "../entities";
+import { Entity, Particles } from "../entities";
import { Coord2D, Direction } from "../interfaces";
+import { colors } from "../utils";
export class Input extends System {
private keys: Set<string>;
@@ -31,7 +32,7 @@ export class Input extends System {
public update(_dt: number, game: Game) {
game.forEachEntityWithComponent(ComponentNames.Control, (entity) =>
- this.handleMovement(entity),
+ this.handleMovement(entity, game),
);
game.forEachEntityWithComponent(ComponentNames.Interactable, (entity) =>
this.handleInteraction(entity),
@@ -57,7 +58,7 @@ export class Input extends System {
);
}
- public handleMovement(entity: Entity) {
+ public handleMovement(entity: Entity, game: Game) {
const controlComponent = entity.getComponent<Control>(
ComponentNames.Control,
);
@@ -103,6 +104,25 @@ 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);
+ }
+
entity.addComponent(gridComponent);
}
diff --git a/src/engine/systems/Life.ts b/src/engine/systems/Life.ts
new file mode 100644
index 0000000..f454437
--- /dev/null
+++ b/src/engine/systems/Life.ts
@@ -0,0 +1,18 @@
+import { System, SystemNames } from ".";
+import { Game } from "..";
+import { Life as LifeComponent, ComponentNames } from "../components";
+
+export class Life extends System {
+ constructor() {
+ super(SystemNames.Life);
+ }
+
+ public update(_dt: number, game: Game) {
+ game.forEachEntityWithComponent(ComponentNames.Life, (entity) => {
+ const life = entity.getComponent<LifeComponent>(ComponentNames.Life);
+ if (!life.alive) {
+ game.removeEntity(entity.id);
+ }
+ });
+ }
+}
diff --git a/src/engine/systems/Render.ts b/src/engine/systems/Render.ts
index f273deb..2dd35e2 100644
--- a/src/engine/systems/Render.ts
+++ b/src/engine/systems/Render.ts
@@ -4,7 +4,7 @@ import {
Text,
ComponentNames,
Highlight,
- Sprite,
+ Renderable,
} from "../components";
import { Game } from "..";
import { clamp } from "../utils";
@@ -22,7 +22,7 @@ export class Render extends System {
this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
game.forEachEntityWithComponent(ComponentNames.Sprite, (entity) => {
- const sprite = entity.getComponent<Sprite>(ComponentNames.Sprite);
+ const sprite = entity.getComponent<Renderable>(ComponentNames.Sprite);
sprite.update(dt);
const boundingBox = entity.getComponent<BoundingBox>(
diff --git a/src/engine/systems/SystemNames.ts b/src/engine/systems/SystemNames.ts
index 555746c..738dfba 100644
--- a/src/engine/systems/SystemNames.ts
+++ b/src/engine/systems/SystemNames.ts
@@ -5,4 +5,5 @@ export namespace SystemNames {
export const Grid = "Grid";
export const GridSpawner = "GridSpawner";
export const Collision = "Collision";
+ export const Life = "Life";
}
diff --git a/src/engine/systems/index.ts b/src/engine/systems/index.ts
index 34b369c..a420216 100644
--- a/src/engine/systems/index.ts
+++ b/src/engine/systems/index.ts
@@ -6,3 +6,4 @@ export * from "./FacingDirection";
export * from "./Grid";
export * from "./GridSpawner";
export * from "./Collision";
+export * from "./Life";
diff --git a/src/engine/utils/colors.ts b/src/engine/utils/colors.ts
new file mode 100644
index 0000000..199180c
--- /dev/null
+++ b/src/engine/utils/colors.ts
@@ -0,0 +1,22 @@
+// gruvbox dark
+export const colors = {
+ bg: "#282828",
+ fg: "#ebdbb2",
+ red: "#cc241d",
+ green: "#98971a",
+ yellow: "#d79921",
+ blue: "#458588",
+ purple: "#b16286",
+ aqua: "#689d6a",
+ orange: "#d65d0e",
+ gray: "#a89984",
+ lightGray: "#928374",
+ darkGray: "#3c3836",
+ lightRed: "#fb4934",
+ lightGreen: "#b8bb26",
+ lightYellow: "#fabd2f",
+ lightBlue: "#83a598",
+ lightPurple: "#d3869b",
+ lightAqua: "#8ec07c",
+ lightOrange: "#fe8019",
+};
diff --git a/src/engine/utils/index.ts b/src/engine/utils/index.ts
index 78b600e..48b94b8 100644
--- a/src/engine/utils/index.ts
+++ b/src/engine/utils/index.ts
@@ -2,3 +2,5 @@ export * from "./clamp";
export * from "./dotProduct";
export * from "./rotateVector";
export * from "./modal";
+export * from "./colors";
+export * from "./random";
diff --git a/src/engine/utils/random.ts b/src/engine/utils/random.ts
new file mode 100644
index 0000000..3d9d8b8
--- /dev/null
+++ b/src/engine/utils/random.ts
@@ -0,0 +1,9 @@
+export const normalRandom = (mean: number, stdDev: number, maxStdDevs = 2) => {
+ const [u, v] = [0, 0].map(() => Math.random() + 1e-12);
+ const normal =
+ mean + stdDev * Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
+ return Math.min(
+ mean + maxStdDevs * stdDev,
+ Math.max(mean - maxStdDevs * stdDev, normal),
+ );
+};