diff --git a/src/App.tsx b/src/App.tsx index 1ed1ffd..add7c08 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,5 @@ import { GameCanvas } from "./components/GameCanvas"; - -const WIDTH = 800; -const HEIGHT = 800; +import { Miscellaneous } from "./engine/config"; export const App = () => { return ( @@ -12,7 +10,7 @@ export const App = () => {
- +
diff --git a/src/engine/TheAbstractionEngine.ts b/src/engine/TheAbstractionEngine.ts index 76ca7e9..094555b 100644 --- a/src/engine/TheAbstractionEngine.ts +++ b/src/engine/TheAbstractionEngine.ts @@ -1,7 +1,8 @@ import { Game } from "."; -import { loadAssets } from "./config"; +import { Miscellaneous, loadAssets } from "./config"; import { Player } from "./entities"; import { FacingDirection, Input, Render } from "./systems"; +import { Grid } from "./systems/Grid"; export class TheAbstractionEngine { private game: Game; @@ -22,9 +23,18 @@ export class TheAbstractionEngine { const facingDirectionSystem = new FacingDirection(inputSystem); - [new Render(this.ctx), inputSystem, facingDirectionSystem].forEach( - (system) => this.game.addSystem(system), - ); + [ + new Render(this.ctx), + inputSystem, + facingDirectionSystem, + new Grid( + { width: Miscellaneous.GRID_COLUMNS, height: Miscellaneous.GRID_ROWS }, + { + width: Miscellaneous.GRID_CELL_WIDTH, + height: Miscellaneous.GRID_CELL_HEIGHT, + }, + ), + ].forEach((system) => this.game.addSystem(system)); const player = new Player(); this.game.addEntity(player); diff --git a/src/engine/components/ComponentNames.ts b/src/engine/components/ComponentNames.ts index bfeb101..032d08a 100644 --- a/src/engine/components/ComponentNames.ts +++ b/src/engine/components/ComponentNames.ts @@ -1,7 +1,7 @@ export namespace ComponentNames { export const Sprite = "Sprite"; export const FacingDirection = "FacingDirection"; - export const GridPosition = "GridPosition"; + export const Grid = "Grid"; export const BoundingBox = "BoundingBox"; export const Control = "Control"; } diff --git a/src/engine/components/Grid.ts b/src/engine/components/Grid.ts new file mode 100644 index 0000000..0c18a65 --- /dev/null +++ b/src/engine/components/Grid.ts @@ -0,0 +1,20 @@ +import { Component, ComponentNames } from "."; +import { Coord2D, Direction } from "../interfaces"; + +export class Grid extends Component { + public initialized: boolean; + + public gridPosition: Coord2D; + public movingDirection: Direction; + public pushable: boolean = false; + + constructor(pushable: boolean = false, 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/GridPosition.ts b/src/engine/components/GridPosition.ts deleted file mode 100644 index b5acf3b..0000000 --- a/src/engine/components/GridPosition.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Component, ComponentNames } from "."; - -export class GridPosition extends Component { - public x: number; - public y: number; - - constructor(x: number, y: number) { - super(ComponentNames.GridPosition); - - this.x = x; - this.y = y; - } -} diff --git a/src/engine/components/index.ts b/src/engine/components/index.ts index d3a32ad..e9f8de9 100644 --- a/src/engine/components/index.ts +++ b/src/engine/components/index.ts @@ -2,6 +2,6 @@ export * from "./Component"; export * from "./ComponentNames"; export * from "./Sprite"; export * from "./FacingDirection"; -export * from "./GridPosition"; +export * from "./Grid"; export * from "./BoundingBox"; export * from "./Control"; diff --git a/src/engine/config/constants.ts b/src/engine/config/constants.ts index c2cbc76..0b07108 100644 --- a/src/engine/config/constants.ts +++ b/src/engine/config/constants.ts @@ -41,10 +41,17 @@ export namespace KeyConstants { }, new Map()); } +export namespace PhysicsConstants { + export const GRID_MOVEMENT_VELOCITY = 2; +} + export namespace Miscellaneous { export const WIDTH = 800; export const HEIGHT = 800; - export const DEFAULT_GRID_WIDTH = 30; - export const DEFAULT_GRID_HEIGHT = 30; + export const GRID_ROWS = 15; + export const GRID_COLUMNS = 15; + + export const GRID_CELL_WIDTH = Math.floor(WIDTH / GRID_COLUMNS); + export const GRID_CELL_HEIGHT = Math.floor(HEIGHT / GRID_ROWS); } diff --git a/src/engine/entities/Player.ts b/src/engine/entities/Player.ts index 2f616c4..cb9161b 100644 --- a/src/engine/entities/Player.ts +++ b/src/engine/entities/Player.ts @@ -3,7 +3,7 @@ import { IMAGES, SPRITE_SPECS, Sprites, type SpriteSpec } from "../config"; import { FacingDirection, Sprite, - GridPosition, + Grid, BoundingBox, Control, } from "../components"; @@ -20,8 +20,8 @@ export class Player extends Entity { this.addComponent( new BoundingBox( { - x: 200, - y: 200, + x: 0, + y: 0, }, { width: Player.spriteSpec.width, height: Player.spriteSpec.height }, 0, @@ -30,7 +30,7 @@ export class Player extends Entity { this.addComponent(new Control()); - this.addComponent(new GridPosition(0, 0)); + this.addComponent(new Grid()); this.addFacingDirectionComponents(); } diff --git a/src/engine/systems/Grid.ts b/src/engine/systems/Grid.ts new file mode 100644 index 0000000..e4e4bb8 --- /dev/null +++ b/src/engine/systems/Grid.ts @@ -0,0 +1,199 @@ +import { System, SystemNames } from "."; +import { Game } from ".."; +import { PhysicsConstants } from "../config"; +import { + BoundingBox, + ComponentNames, + Grid as GridComponent, +} from "../components"; +import { Coord2D, Direction, Dimension2D } from "../interfaces"; +import { clamp } from "../utils"; + +export class Grid extends System { + private dimension: Dimension2D; + private grid: Set[][] = []; + + constructor( + { width: columns, height: rows }: Dimension2D, + dimension: Dimension2D, + ) { + super(SystemNames.Grid); + + this.dimension = dimension; + this.grid = new Array(rows) + .fill(null) + .map(() => new Array(columns).fill(null).map(() => new Set())); + } + + public update(dt: number, game: Game): void { + this.putUninitializedEntitiesInGrid(game); + this.rebuildGrid(game); + this.updateMovingEntities(dt, game); + } + + private putUninitializedEntitiesInGrid(game: Game) { + game.forEachEntityWithComponent(ComponentNames.Grid, (entity) => { + const grid = entity.getComponent(ComponentNames.Grid)!; + + if (grid.initialized) { + return; + } + + const hasBoundingBox = entity.hasComponent(ComponentNames.BoundingBox); + if (!hasBoundingBox) { + return; + } + + const boundingBox = entity.getComponent( + ComponentNames.BoundingBox, + )!; + boundingBox.center = this.gridToScreenPosition(grid.gridPosition); + entity.addComponent(boundingBox); + + grid.initialized = true; + entity.addComponent(grid); + }); + } + + private updateMovingEntities( + dt: number, + game: Game, + velocity = PhysicsConstants.GRID_MOVEMENT_VELOCITY, + ) { + game.forEachEntityWithComponent(ComponentNames.Grid, (entity) => { + const grid = entity.getComponent(ComponentNames.Grid)!; + if (grid.movingDirection === Direction.NONE) { + return; + } + + const boundingBox = entity.getComponent( + ComponentNames.BoundingBox, + )!; + + let { x: newX, y: newY } = grid.gridPosition; + switch (grid.movingDirection) { + case Direction.LEFT: + newX -= 1; + break; + case Direction.UP: + newY -= 1; + break; + case Direction.DOWN: + newY += 1; + break; + case Direction.RIGHT: + newX += 1; + break; + } + + const newGridPosition = { x: newX, y: newY }; + if (this.isOutOfBounds(newGridPosition)) { + grid.movingDirection = Direction.NONE; + entity.addComponent(grid); + return; + } + + let { dx, dy } = { dx: 0, dy: 0 }; + switch (grid.movingDirection) { + case Direction.LEFT: + dx = -velocity * dt; + break; + case Direction.RIGHT: + dx = velocity * dt; + break; + case Direction.UP: + dy = -velocity * dt; + break; + case Direction.DOWN: + dy = velocity * dt; + break; + } + + const { x, y } = boundingBox.center; + const nextPosition = { x: x + dx, y: y + dy }; + const passedCenter = this.isEntityPastCenterWhenMoving( + grid.movingDirection, + newGridPosition, + nextPosition, + ); + + if (passedCenter) { + // re-align the entity to its new grid position + this.grid[grid.gridPosition.y][grid.gridPosition.x].delete(entity.id); + + grid.gridPosition = newGridPosition; + grid.movingDirection = Direction.NONE; + this.grid[grid.gridPosition.y][grid.gridPosition.x].add(entity.id); + entity.addComponent(grid); + + boundingBox.center = this.gridToScreenPosition(grid.gridPosition); + entity.addComponent(boundingBox); + return; + } + + boundingBox.center = nextPosition; + entity.addComponent(boundingBox); + }); + } + + private isEntityPastCenterWhenMoving( + direction: Direction, + gridPosition: Coord2D, + entityPosition: Coord2D, + ) { + const { x, y } = this.gridToScreenPosition(gridPosition); + switch (direction) { + case Direction.LEFT: + return entityPosition.x <= x; + case Direction.RIGHT: + return entityPosition.x >= x; + case Direction.UP: + return entityPosition.y <= y; + case Direction.DOWN: + return entityPosition.y >= y; + } + return false; + } + + private gridToScreenPosition(gridPosition: Coord2D) { + const { width, height } = this.dimension; + return { + x: gridPosition.x * width + width / 2, + y: gridPosition.y * height + height / 2, + }; + } + + private isOutOfBounds(position: Coord2D) { + const isOutOfBoundsX = + clamp(position.x, 0, this.grid[0].length - 1) !== position.x; + const isOutOfBoundsY = + clamp(position.y, 0, this.grid.length - 1) !== position.y; + return isOutOfBoundsX || isOutOfBoundsY; + } + + private rebuildGrid(game: Game) { + const movedEntities = new Set(); + game.forEachEntityWithComponent(ComponentNames.Grid, (entity) => { + const grid = entity.getComponent(ComponentNames.Grid)!; + const { x, y } = grid.gridPosition; + if (!this.grid[y][x].has(entity.id)) { + movedEntities.add(entity.id); + this.grid[y][x].add(entity.id); + } + }); + this.grid.forEach((row) => + row.forEach((cell) => { + for (const id of cell) { + if (!movedEntities.has(id)) { + cell.delete(id); + } + } + }), + ); + movedEntities.forEach((id) => { + const entity = game.getEntity(id)!; + const grid = entity.getComponent(ComponentNames.Grid)!; + this.grid[grid.gridPosition.y][grid.gridPosition.x].add(id); + }); + } +} diff --git a/src/engine/systems/Input.ts b/src/engine/systems/Input.ts index 9b88378..e9691e0 100644 --- a/src/engine/systems/Input.ts +++ b/src/engine/systems/Input.ts @@ -4,7 +4,7 @@ import { ComponentNames } from "../components"; import { Control } from "../components/Control"; import { Action, KeyConstants } from "../config"; import { Entity } from "../entities"; -import { Coord2D } from "../interfaces"; +import { Coord2D, Direction } from "../interfaces"; export class Input extends System { private keys: Set; @@ -41,8 +41,44 @@ export class Input extends System { ); if (!controlComponent.isControllable) return; - if (this.hasSomeKey(KeyConstants.ActionKeys.get(Action.INTERACT))) { - console.log("interact"); + const hasGrid = entity.hasComponent(ComponentNames.Grid); + + // TODO: check grid via controlComponent to notify entity of interaction + const [moveUp, moveLeft, moveRight, moveDown] = [ + Action.MOVE_UP, + Action.MOVE_LEFT, + Action.MOVE_RIGHT, + Action.MOVE_DOWN, + ].map((action) => this.hasSomeKey(KeyConstants.ActionKeys.get(action))); + if (hasGrid) { + const gridComponent = entity.getComponent(ComponentNames.Grid); + if (gridComponent.movingDirection !== Direction.NONE) { + return; + } + + if (moveUp) { + gridComponent.movingDirection = Direction.UP; + KeyConstants.ActionKeys.get(Action.MOVE_UP)!.forEach((key) => + this.keyReleased(key), + ); + } else if (moveLeft) { + gridComponent.movingDirection = Direction.LEFT; + KeyConstants.ActionKeys.get(Action.MOVE_LEFT)!.forEach((key) => + this.keyReleased(key), + ); + } else if (moveRight) { + gridComponent.movingDirection = Direction.RIGHT; + KeyConstants.ActionKeys.get(Action.MOVE_RIGHT)!.forEach((key) => + this.keyReleased(key), + ); + } else if (moveDown) { + gridComponent.movingDirection = Direction.DOWN; + KeyConstants.ActionKeys.get(Action.MOVE_DOWN)!.forEach((key) => + this.keyReleased(key), + ); + } + + entity.addComponent(gridComponent); } } diff --git a/src/engine/systems/SystemNames.ts b/src/engine/systems/SystemNames.ts index 1ed9894..0de5857 100644 --- a/src/engine/systems/SystemNames.ts +++ b/src/engine/systems/SystemNames.ts @@ -1,8 +1,6 @@ export namespace SystemNames { export const Render = "Render"; - export const Physics = "Physics"; export const Input = "Input"; - export const Collision = "Collision"; - export const WallBounds = "WallBounds"; export const FacingDirection = "FacingDirection"; + export const Grid = "Grid"; }