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) { 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); boundingBox.dimension = this.dimension; 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); }); } }