diff --git a/src/css/modal.css b/src/css/modal.css index bd6f2a3..a609ef9 100644 --- a/src/css/modal.css +++ b/src/css/modal.css @@ -1,32 +1,30 @@ -/* The Modal (background) */ .modal { - display: none; /* Hidden by default */ - position: fixed; /* Stay in place */ - z-index: 1; /* Sit on top */ + display: none; + position: fixed; + z-index: 1; left: 0; top: 0; - width: 100%; /* Full width */ - height: 100%; /* Full height */ - justify-content: center; /* Center horizontally */ - align-items: center; /* Center vertically */ - background-color: rgba(0, 0, 0, 0.4); /* Black w/ opacity */ - overflow: auto; /* Enable scroll if needed */ - animation: fadeIn 0.25s; /* Fade in the background */ + width: 100%; + height: 100%; + justify-content: center; + align-items: center; + background-color: rgba(0, 0, 0, 0.4); + overflow: auto; + animation: fadeIn 0.25s; } -/* Modal Content */ .modal-content { display: flex; - background-color: #282828; /* Gruvbox background */ - color: #ebdbb2; /* Gruvbox foreground */ + background-color: #282828; + color: #ebdbb2; margin: auto; padding: 20px; - border: 1px solid #928374; /* Gruvbox grey */ - width: 40%; /* Adjust as needed */ + border: 1px solid #928374; + width: 40%; box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2); - animation: scaleUp 0.25s; /* Scale animation */ - border-radius: 8px; /* Rounded corners */ - justify-content: center; /* Center horizontally */ + animation: scaleUp 0.25s; + border-radius: 8px; + justify-content: center; } /* Animations */ diff --git a/src/engine/TheAbstractionEngine.ts b/src/engine/TheAbstractionEngine.ts index 15ef011..2db599b 100644 --- a/src/engine/TheAbstractionEngine.ts +++ b/src/engine/TheAbstractionEngine.ts @@ -1,8 +1,7 @@ import { Game } from "."; import { Miscellaneous, loadAssets } from "./config"; -import { Player, FunctionBox } from "./entities"; -import { FacingDirection, Input, Render } from "./systems"; -import { Grid } from "./systems/Grid"; +import { Player, FunctionBox, Wall } from "./entities"; +import { Grid, FacingDirection, Input, Render } from "./systems"; export class TheAbstractionEngine { private game: Game; @@ -24,7 +23,6 @@ export class TheAbstractionEngine { const facingDirectionSystem = new FacingDirection(inputSystem); [ - new Render(this.ctx), inputSystem, facingDirectionSystem, new Grid( @@ -34,6 +32,7 @@ export class TheAbstractionEngine { height: Miscellaneous.GRID_CELL_HEIGHT, }, ), + new Render(this.ctx), ].forEach((system) => this.game.addSystem(system)); const player = new Player(); @@ -43,6 +42,9 @@ export class TheAbstractionEngine { this.game.addEntity(box); const box2 = new FunctionBox({ x: 4, y: 1 }, "λ x . (x)"); this.game.addEntity(box2); + + const wall = new Wall({ x: 5, y: 3 }); + this.game.addEntity(wall); } public play() { diff --git a/src/engine/components/Colliding.ts b/src/engine/components/Colliding.ts new file mode 100644 index 0000000..4027c3d --- /dev/null +++ b/src/engine/components/Colliding.ts @@ -0,0 +1,7 @@ +import { Component, ComponentNames } from "."; + +export class Colliding extends Component { + constructor() { + super(ComponentNames.Colliding); + } +} diff --git a/src/engine/components/ComponentNames.ts b/src/engine/components/ComponentNames.ts index da6c37a..5e9b589 100644 --- a/src/engine/components/ComponentNames.ts +++ b/src/engine/components/ComponentNames.ts @@ -6,4 +6,6 @@ export namespace ComponentNames { export const Control = "Control"; export const Highlight = "Highlight"; export const Interactable = "Interactable"; + export const Pushable = "Pushable"; + export const Colliding = "Colliding"; } diff --git a/src/engine/components/Grid.ts b/src/engine/components/Grid.ts index 0c18a65..a62cc7b 100644 --- a/src/engine/components/Grid.ts +++ b/src/engine/components/Grid.ts @@ -6,15 +6,12 @@ export class Grid extends Component { public gridPosition: Coord2D; public movingDirection: Direction; - public pushable: boolean = false; - constructor(pushable: boolean = false, position: Coord2D = { x: 0, y: 0 }) { + constructor(position: Coord2D = { x: 0, y: 0 }) { super(ComponentNames.Grid); this.initialized = false; - this.gridPosition = position; this.movingDirection = Direction.NONE; - this.pushable = pushable; } } diff --git a/src/engine/components/Highlight.ts b/src/engine/components/Highlight.ts index 49d9f96..5875057 100644 --- a/src/engine/components/Highlight.ts +++ b/src/engine/components/Highlight.ts @@ -1,7 +1,33 @@ import { Component, ComponentNames } from "."; export class Highlight extends Component { - constructor() { + public isHighlighted: boolean; + private onHighlight: Function; + private onUnhighlight: Function; + + constructor( + onHighlight: Function, + onUnhighlight: Function, + isHighlighted: boolean = false, + ) { super(ComponentNames.Highlight); + + this.isHighlighted = isHighlighted; + this.onHighlight = onHighlight; + this.onUnhighlight = onUnhighlight; + } + + public highlight() { + if (!this.isHighlighted) { + this.isHighlighted = true; + this.onHighlight(); + } + } + + public unhighlight() { + if (this.isHighlighted) { + this.isHighlighted = false; + this.onUnhighlight(); + } } } diff --git a/src/engine/components/Pushable.ts b/src/engine/components/Pushable.ts new file mode 100644 index 0000000..b3d0dc4 --- /dev/null +++ b/src/engine/components/Pushable.ts @@ -0,0 +1,11 @@ +import { Component, ComponentNames } from "."; + +export class Pushable extends Component { + public pushable: boolean; + + constructor(pushable = false) { + super(ComponentNames.Pushable); + + this.pushable = pushable; + } +} diff --git a/src/engine/components/index.ts b/src/engine/components/index.ts index c470eff..4ae886a 100644 --- a/src/engine/components/index.ts +++ b/src/engine/components/index.ts @@ -7,3 +7,5 @@ export * from "./BoundingBox"; export * from "./Control"; export * from "./Highlight"; export * from "./Interactable"; +export * from "./Pushable"; +export * from "./Colliding"; diff --git a/src/engine/config/sprites.ts b/src/engine/config/sprites.ts index eab65fd..7cb8adf 100644 --- a/src/engine/config/sprites.ts +++ b/src/engine/config/sprites.ts @@ -3,6 +3,7 @@ import { Direction } from "../interfaces/Direction"; export enum Sprites { PLAYER, FUNCTION_BOX, + WALL, } export interface SpriteSpec { @@ -46,3 +47,12 @@ const functionBoxSpriteSpec = { sheet: "/assets/function_block.png", }; SPRITE_SPECS.set(Sprites.FUNCTION_BOX, functionBoxSpriteSpec); + +const wallSpriteSpec = { + msPerFrame: 200, + width: 64, + height: 64, + frames: 3, + sheet: "/assets/wall.png", +}; +SPRITE_SPECS.set(Sprites.WALL, wallSpriteSpec); diff --git a/src/engine/entities/EntityNames.ts b/src/engine/entities/EntityNames.ts index 1a4c1ed..ffcc937 100644 --- a/src/engine/entities/EntityNames.ts +++ b/src/engine/entities/EntityNames.ts @@ -1,4 +1,5 @@ export namespace EntityNames { export const Player = "Player"; export const FunctionBox = "FunctionBox"; + export const Wall = "Wall"; } diff --git a/src/engine/entities/FunctionBox.ts b/src/engine/entities/FunctionBox.ts index 57eeedb..e5d031a 100644 --- a/src/engine/entities/FunctionBox.ts +++ b/src/engine/entities/FunctionBox.ts @@ -4,7 +4,9 @@ import { BoundingBox, ComponentNames, Grid, + Highlight, Interactable, + Pushable, Sprite, } from "../components"; import { Coord2D } from "../interfaces"; @@ -36,7 +38,9 @@ export class FunctionBox extends Entity { ), ); - this.addComponent(new Grid(true, gridPosition)); + this.addComponent(new Pushable()); + + this.addComponent(new Grid(gridPosition)); this.addComponent( new Sprite( @@ -51,25 +55,31 @@ export class FunctionBox extends Entity { ), ); - this.hooks.set(ComponentNames.Highlight, { - add: () => { - let modalOpen = false; - const interaction = () => { - if (modalOpen) { - modalOpen = false; - closeModal(); - return; - } - modalOpen = true; - openModal(this.code); - }; + this.addComponent( + new Highlight( + () => this.onHighlight(), + () => this.onUnhighlight(), + ), + ); + } - this.addComponent(new Interactable(interaction)); - }, - remove: () => { + private onUnhighlight() { + closeModal(); + this.removeComponent(ComponentNames.Interactable); + } + + private onHighlight() { + let modalOpen = false; + const interaction = () => { + if (modalOpen) { + modalOpen = false; closeModal(); - this.removeComponent(ComponentNames.Interactable); - }, - }); + return; + } + modalOpen = true; + openModal(this.code); + }; + + this.addComponent(new Interactable(interaction)); } } diff --git a/src/engine/entities/Wall.ts b/src/engine/entities/Wall.ts new file mode 100644 index 0000000..621a569 --- /dev/null +++ b/src/engine/entities/Wall.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 Wall extends Entity { + private static spriteSpec: SpriteSpec = SPRITE_SPECS.get( + Sprites.WALL, + ) as SpriteSpec; + + constructor(gridPosition: Coord2D) { + super(EntityNames.Wall); + + this.addComponent(new Grid(gridPosition)); + + this.addComponent(new Colliding()); + + this.addComponent( + new BoundingBox( + { + x: 0, + y: 0, + }, + { + width: Wall.spriteSpec.width, + height: Wall.spriteSpec.height, + }, + 0, + ), + ); + + this.addComponent( + new Sprite( + IMAGES.get(Wall.spriteSpec.sheet)!, + { x: 0, y: 0 }, + { + width: Wall.spriteSpec.width, + height: Wall.spriteSpec.height, + }, + Wall.spriteSpec.msPerFrame, + Wall.spriteSpec.frames, + ), + ); + } +} diff --git a/src/engine/entities/index.ts b/src/engine/entities/index.ts index d6a8aed..e63b272 100644 --- a/src/engine/entities/index.ts +++ b/src/engine/entities/index.ts @@ -2,3 +2,4 @@ export * from "./Entity"; export * from "./EntityNames"; export * from "./Player"; export * from "./FunctionBox"; +export * from "./Wall"; diff --git a/src/engine/systems/Grid.ts b/src/engine/systems/Grid.ts index c9cab6b..28ca5ea 100644 --- a/src/engine/systems/Grid.ts +++ b/src/engine/systems/Grid.ts @@ -71,14 +71,20 @@ export class Grid extends System { highlightableEntities.forEach((id) => { const entity = game.getEntity(id)!; - if (!entity.hasComponent(ComponentNames.Highlight)) { - entity.addComponent(new Highlight()); + if (entity.hasComponent(ComponentNames.Highlight)) { + const highlight = entity.getComponent( + ComponentNames.Highlight, + )!; + highlight.highlight(); } }); game.forEachEntityWithComponent(ComponentNames.Highlight, (entity) => { if (!highlightableEntities.has(entity.id)) { - entity.removeComponent(ComponentNames.Highlight); + const highlight = entity.getComponent( + ComponentNames.Highlight, + )!; + highlight.unhighlight(); } }); } @@ -97,21 +103,41 @@ export class Grid extends System { // continue until no more pushable entities are found for (const entity of movingEntities) { const grid = entity.getComponent(ComponentNames.Grid)!; + const { gridPosition, movingDirection } = grid; + + grid.movingDirection = Direction.NONE; + entity.addComponent(grid); // default to not moving + let nextGridPosition = this.getNewGridPosition( - grid.gridPosition, - grid.movingDirection, + gridPosition, + movingDirection, ); + + const moving = new Set(); + moving.add(entity.id); + while (!this.isOutOfBounds(nextGridPosition)) { const { x, y } = nextGridPosition; const entities = Array.from(this.grid[y][x]).map( (id) => game.getEntity(id)!, ); + if ( + entities.some((entity) => + entity.hasComponent(ComponentNames.Colliding), + ) + ) { + moving.clear(); + break; + } + const pushableEntities = entities.filter((entity) => { if (!entity.hasComponent(ComponentNames.Grid)) return false; - const { pushable, movingDirection } = - entity.getComponent(ComponentNames.Grid)!; + const { movingDirection } = entity.getComponent( + ComponentNames.Grid, + )!; + const pushable = entity.hasComponent(ComponentNames.Pushable); return movingDirection === Direction.NONE && pushable; }); if (pushableEntities.length === 0) { @@ -119,18 +145,21 @@ export class Grid extends System { } for (const pushableEntity of pushableEntities) { - const pushableGrid = pushableEntity.getComponent( - ComponentNames.Grid, - )!; - pushableGrid.movingDirection = grid.movingDirection; - pushableEntity.addComponent(pushableEntity); + moving.add(pushableEntity.id); } nextGridPosition = this.getNewGridPosition( nextGridPosition, - grid.movingDirection, + movingDirection, ); } + + for (const id of moving) { + const entity = game.getEntity(id)!; + const grid = entity.getComponent(ComponentNames.Grid)!; + grid.movingDirection = movingDirection; + entity.addComponent(grid); + } } } diff --git a/src/engine/systems/Render.ts b/src/engine/systems/Render.ts index 7cb5d81..83daa52 100644 --- a/src/engine/systems/Render.ts +++ b/src/engine/systems/Render.ts @@ -1,5 +1,5 @@ import { System, SystemNames } from "."; -import { BoundingBox, ComponentNames, Sprite } from "../components"; +import { BoundingBox, ComponentNames, Highlight, Sprite } from "../components"; import { Game } from ".."; import { clamp } from "../utils"; import { DrawArgs } from "../interfaces"; @@ -43,8 +43,13 @@ export class Render extends System { center: boundingBox.center, dimension: boundingBox.dimension, rotation: boundingBox.rotation, - tint: entity.hasComponent(ComponentNames.Highlight) ? "red" : undefined, }; + if (entity.hasComponent(ComponentNames.Highlight)) { + const highlight = entity.getComponent( + ComponentNames.Highlight, + ); + drawArgs.tint = highlight.isHighlighted ? "red" : undefined; + } sprite.draw(this.ctx, drawArgs); }); diff --git a/src/engine/systems/index.ts b/src/engine/systems/index.ts index 31c98ac..18ffa2e 100644 --- a/src/engine/systems/index.ts +++ b/src/engine/systems/index.ts @@ -3,3 +3,4 @@ export * from "./System"; export * from "./Render"; export * from "./Input"; export * from "./FacingDirection"; +export * from "./Grid";