diff --git a/public/assets/curry.png b/public/assets/curry.png new file mode 100644 index 0000000..348063c Binary files /dev/null and b/public/assets/curry.png differ diff --git a/src/engine/Game.ts b/src/engine/Game.ts index 2df9f17..9fe9e87 100644 --- a/src/engine/Game.ts +++ b/src/engine/Game.ts @@ -25,6 +25,10 @@ export class Game { this.running = true; } + public stop() { + this.running = false; + } + public addEntity(entity: Entity) { this.entities.set(entity.id, entity); } diff --git a/src/engine/TheAbstractionEngine.ts b/src/engine/TheAbstractionEngine.ts index 7f71fd5..e84093f 100644 --- a/src/engine/TheAbstractionEngine.ts +++ b/src/engine/TheAbstractionEngine.ts @@ -1,12 +1,21 @@ import { Game } from "."; import { Miscellaneous, loadAssets } from "./config"; -import { Player, FunctionBox, Wall, LambdaFactory } from "./entities"; +import { + Player, + FunctionBox, + Wall, + LambdaFactory, + Key, + LockedDoor, + Curry, +} from "./entities"; import { Grid, FacingDirection, Input, Render, LambdaFactory as LambdaFactorySpawnSystem, + Collision, } from "./systems"; export class TheAbstractionEngine { @@ -39,6 +48,7 @@ export class TheAbstractionEngine { }, ), new LambdaFactorySpawnSystem(), + new Collision(), new Render(this.ctx), ].forEach((system) => this.game.addSystem(system)); @@ -55,6 +65,15 @@ export class TheAbstractionEngine { const factory = new LambdaFactory({ x: 6, y: 6 }, "λ x . (x)", 10); this.game.addEntity(factory); + + const lockedDoor = new LockedDoor({ x: 8, y: 8 }); + this.game.addEntity(lockedDoor); + + const key = new Key({ x: 7, y: 7 }); + this.game.addEntity(key); + + const curry = new Curry({ x: 9, y: 8 }); + this.game.addEntity(curry); } public play() { diff --git a/src/engine/config/sprites.ts b/src/engine/config/sprites.ts index 39ad260..cca5961 100644 --- a/src/engine/config/sprites.ts +++ b/src/engine/config/sprites.ts @@ -5,6 +5,9 @@ export enum Sprites { FUNCTION_BOX, WALL, LAMBDA_FACTORY, + KEY, + LOCKED_DOOR, + CURRY, } export interface SpriteSpec { @@ -66,3 +69,30 @@ const lambdaFactorySpriteSpec = { sheet: "/assets/function_factory.png", }; SPRITE_SPECS.set(Sprites.LAMBDA_FACTORY, lambdaFactorySpriteSpec); + +const keySpriteSpec = { + msPerFrame: 200, + width: 64, + height: 64, + frames: 3, + sheet: "/assets/key.png", +}; +SPRITE_SPECS.set(Sprites.KEY, keySpriteSpec); + +const lockedDoorSpriteSpec = { + msPerFrame: 200, + width: 64, + height: 64, + frames: 3, + sheet: "/assets/locked_door.png", +}; +SPRITE_SPECS.set(Sprites.LOCKED_DOOR, lockedDoorSpriteSpec); + +const currySpriteSpec = { + msPerFrame: 200, + width: 64, + height: 64, + frames: 3, + sheet: "/assets/curry.png", +}; +SPRITE_SPECS.set(Sprites.CURRY, currySpriteSpec); diff --git a/src/engine/entities/Curry.ts b/src/engine/entities/Curry.ts new file mode 100644 index 0000000..85bc7ef --- /dev/null +++ b/src/engine/entities/Curry.ts @@ -0,0 +1,45 @@ +import { Entity, EntityNames } from "."; +import { BoundingBox, Colliding, Grid, Sprite } from "../components"; +import { IMAGES, SPRITE_SPECS, SpriteSpec, Sprites } from "../config"; +import { Coord2D } from "../interfaces"; + +export class Curry extends Entity { + private static spriteSpec: SpriteSpec = SPRITE_SPECS.get( + Sprites.CURRY, + ) as SpriteSpec; + + constructor(gridPosition: Coord2D) { + super(EntityNames.Curry); + + this.addComponent(new Grid(gridPosition)); + + this.addComponent(new Colliding()); + + this.addComponent( + new BoundingBox( + { + x: 0, + y: 0, + }, + { + width: Curry.spriteSpec.width, + height: Curry.spriteSpec.height, + }, + 0, + ), + ); + + this.addComponent( + new Sprite( + IMAGES.get(Curry.spriteSpec.sheet)!, + { x: 0, y: 0 }, + { + width: Curry.spriteSpec.width, + height: Curry.spriteSpec.height, + }, + Curry.spriteSpec.msPerFrame, + Curry.spriteSpec.frames, + ), + ); + } +} diff --git a/src/engine/entities/EntityNames.ts b/src/engine/entities/EntityNames.ts index 3ad31d0..7fc69c6 100644 --- a/src/engine/entities/EntityNames.ts +++ b/src/engine/entities/EntityNames.ts @@ -3,4 +3,7 @@ export namespace EntityNames { export const FunctionBox = "FunctionBox"; export const Wall = "Wall"; export const LambdaFactory = "LambdaFactory"; + export const Key = "Key"; + export const LockedDoor = "LockedDoor"; + export const Curry = "Curry"; } diff --git a/src/engine/entities/Key.ts b/src/engine/entities/Key.ts new file mode 100644 index 0000000..7168ee8 --- /dev/null +++ b/src/engine/entities/Key.ts @@ -0,0 +1,45 @@ +import { Entity, EntityNames } from "."; +import { BoundingBox, Grid, Pushable, Sprite } from "../components"; +import { IMAGES, SPRITE_SPECS, SpriteSpec, Sprites } from "../config"; +import { Coord2D } from "../interfaces"; + +export class Key extends Entity { + private static spriteSpec: SpriteSpec = SPRITE_SPECS.get( + Sprites.KEY, + ) as SpriteSpec; + + constructor(gridPosition: Coord2D) { + super(EntityNames.Key); + + this.addComponent(new Grid(gridPosition)); + + this.addComponent(new Pushable()); + + this.addComponent( + new BoundingBox( + { + x: 0, + y: 0, + }, + { + width: Key.spriteSpec.width, + height: Key.spriteSpec.height, + }, + 0, + ), + ); + + this.addComponent( + new Sprite( + IMAGES.get(Key.spriteSpec.sheet)!, + { x: 0, y: 0 }, + { + width: Key.spriteSpec.width, + height: Key.spriteSpec.height, + }, + Key.spriteSpec.msPerFrame, + Key.spriteSpec.frames, + ), + ); + } +} diff --git a/src/engine/entities/LockedDoor.ts b/src/engine/entities/LockedDoor.ts new file mode 100644 index 0000000..5e364b8 --- /dev/null +++ b/src/engine/entities/LockedDoor.ts @@ -0,0 +1,45 @@ +import { Entity, EntityNames } from "."; +import { BoundingBox, Colliding, Grid, Sprite } from "../components"; +import { IMAGES, SPRITE_SPECS, SpriteSpec, Sprites } from "../config"; +import { Coord2D } from "../interfaces"; + +export class LockedDoor extends Entity { + private static spriteSpec: SpriteSpec = SPRITE_SPECS.get( + Sprites.LOCKED_DOOR, + ) as SpriteSpec; + + constructor(gridPosition: Coord2D) { + super(EntityNames.LockedDoor); + + this.addComponent(new Grid(gridPosition)); + + this.addComponent(new Colliding()); + + this.addComponent( + new BoundingBox( + { + x: 0, + y: 0, + }, + { + width: LockedDoor.spriteSpec.width, + height: LockedDoor.spriteSpec.height, + }, + 0, + ), + ); + + this.addComponent( + new Sprite( + IMAGES.get(LockedDoor.spriteSpec.sheet)!, + { x: 0, y: 0 }, + { + width: LockedDoor.spriteSpec.width, + height: LockedDoor.spriteSpec.height, + }, + LockedDoor.spriteSpec.msPerFrame, + LockedDoor.spriteSpec.frames, + ), + ); + } +} diff --git a/src/engine/entities/index.ts b/src/engine/entities/index.ts index a049350..45c95c0 100644 --- a/src/engine/entities/index.ts +++ b/src/engine/entities/index.ts @@ -4,3 +4,6 @@ export * from "./Player"; export * from "./FunctionBox"; export * from "./Wall"; export * from "./LambdaFactory"; +export * from "./Key"; +export * from "./LockedDoor"; +export * from "./Curry"; diff --git a/src/engine/systems/Collision.ts b/src/engine/systems/Collision.ts new file mode 100644 index 0000000..7b1b963 --- /dev/null +++ b/src/engine/systems/Collision.ts @@ -0,0 +1,103 @@ +import { System, SystemNames } from "."; +import { Game } from ".."; +import { Entity, EntityNames } from "../entities"; +import { BoundingBox, Colliding, ComponentNames, Grid } from "../components"; + +const collisionMap: Record> = { + [EntityNames.Key]: new Set([EntityNames.LockedDoor]), + [EntityNames.Curry]: new Set([EntityNames.Player]), +}; + +export class Collision extends System { + static canCollide(entityName: string, otherEntityName: string) { + if (collisionMap[entityName]) { + return collisionMap[entityName].has(otherEntityName); + } + return collisionMap[otherEntityName]?.has(entityName) ?? false; + } + + constructor() { + super(SystemNames.Collision); + } + + public update(_dt: number, game: Game) { + game.forEachEntityWithComponent(ComponentNames.Colliding, (entity) => { + if (!entity.hasComponent(ComponentNames.BoundingBox)) { + return; + } + const collidingBox = entity.getComponent( + ComponentNames.BoundingBox, + ); + let collidingGrid = entity.hasComponent(ComponentNames.Grid) + ? entity.getComponent(ComponentNames.Grid) + : null; + + const collidingWith: Entity[] = []; + game.forEachEntityWithComponent( + ComponentNames.BoundingBox, + (otherEntity) => { + const otherBoundingBox = otherEntity.getComponent( + ComponentNames.BoundingBox, + ); + let otherGrid = otherEntity.hasComponent(ComponentNames.Grid) + ? otherEntity.getComponent(ComponentNames.Grid) + : null; + + if (collidingGrid && otherGrid) { + if ( + collidingGrid.gridPosition.x === otherGrid.gridPosition.x && + collidingGrid.gridPosition.y === otherGrid.gridPosition.y + ) { + collidingWith.push(otherEntity); + } + return; + } + + if (collidingBox.isCollidingWith(otherBoundingBox)) { + collidingWith.push(otherEntity); + } + }, + ); + + for (const collision of collidingWith) { + this.handleCollision(entity, collision, game); + } + }); + } + + private handleCollision(entity: Entity, otherEntity: Entity, game: Game) { + if (!Collision.canCollide(entity.name, otherEntity.name)) { + 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(); + } +} diff --git a/src/engine/systems/Grid.ts b/src/engine/systems/Grid.ts index 8756320..915335b 100644 --- a/src/engine/systems/Grid.ts +++ b/src/engine/systems/Grid.ts @@ -1,4 +1,4 @@ -import { System, SystemNames } from "."; +import { Collision, System, SystemNames } from "."; import { Game } from ".."; import { Entity } from "../entities"; import { PhysicsConstants } from "../config"; @@ -13,8 +13,8 @@ import { Coord2D, Direction, Dimension2D } from "../interfaces"; import { clamp } from "../utils"; export class Grid extends System { - private dimension: Dimension2D; - private grid: Set[][] = []; + public dimension: Dimension2D; + public grid: Set[][] = []; constructor( { width: columns, height: rows }: Dimension2D, @@ -108,10 +108,10 @@ export class Grid extends System { grid.movingDirection = Direction.NONE; entity.addComponent(grid); // default to not moving - let nextGridPosition = this.getNewGridPosition( + let [currentPosition, nextGridPosition] = [ gridPosition, - movingDirection, - ); + this.getNewGridPosition(gridPosition, movingDirection), + ]; const moving = new Set(); moving.add(entity.id); @@ -122,11 +122,24 @@ export class Grid extends System { (id) => game.getEntity(id)!, ); - if ( - entities.some((entity) => - entity.hasComponent(ComponentNames.Colliding), + const collidingEntities = entities.filter((entity) => + entity.hasComponent(ComponentNames.Colliding), + ); + + if (collidingEntities.length > 0) { + // i.e. key going into a door or function going into an application + const allEntitiesInPreviousCellCanCollide = Array.from( + this.grid[currentPosition.y][currentPosition.x], ) - ) { + .map((id) => game.getEntity(id)!) + .every((entity) => + collidingEntities.every((collidingEntity) => + Collision.canCollide(entity.name, collidingEntity.name), + ), + ); + if (allEntitiesInPreviousCellCanCollide) { + break; + } moving.clear(); break; } @@ -148,6 +161,7 @@ export class Grid extends System { moving.add(pushableEntity.id); } + currentPosition = nextGridPosition; nextGridPosition = this.getNewGridPosition( nextGridPosition, movingDirection, @@ -324,7 +338,7 @@ export class Grid extends System { this.grid.forEach((row) => row.forEach((cell) => { for (const id of cell) { - if (movedEntities.has(id)) { + if (movedEntities.has(id) || !game.getEntity(id)) { cell.delete(id); } } diff --git a/src/engine/systems/SystemNames.ts b/src/engine/systems/SystemNames.ts index 85d1539..c96dc48 100644 --- a/src/engine/systems/SystemNames.ts +++ b/src/engine/systems/SystemNames.ts @@ -4,4 +4,5 @@ export namespace SystemNames { export const FacingDirection = "FacingDirection"; export const Grid = "Grid"; export const LambdaFactory = "LambdaFactory"; + export const Collision = "Collision"; } diff --git a/src/engine/systems/index.ts b/src/engine/systems/index.ts index 4490ee2..6ee5392 100644 --- a/src/engine/systems/index.ts +++ b/src/engine/systems/index.ts @@ -5,3 +5,4 @@ export * from "./Input"; export * from "./FacingDirection"; export * from "./Grid"; export * from "./LambdaFactory"; +export * from "./Collision";