import { Collision, System, SystemNames } from "."; import { Game } from ".."; import { Entity } from "../entities"; import { PhysicsConstants } from "../config"; import { BoundingBox, ComponentNames, FacingDirection, Highlight, Grid as GridComponent, } from "../components"; import { Coord2D, Direction, Dimension2D } from "../interfaces"; import { clamp } from "../utils"; export class Grid extends System { public dimension: Dimension2D; public 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.highlightEntitiesLookedAt(game); this.propogateEntityMovements(game); this.updateMovingEntities(dt, game); } private highlightEntitiesLookedAt(game: Game) { const highlightableEntities: [string, Direction][] = []; game.forEachEntityWithComponent( ComponentNames.FacingDirection, (entity) => { if (!entity.hasComponent(ComponentNames.Grid)) { return; } const grid = entity.getComponent(ComponentNames.Grid)!; const facingDirection = entity.getComponent( ComponentNames.FacingDirection, )!; const lookingAt = this.getNewGridPosition( grid.gridPosition, facingDirection.currentDirection, ); if ( facingDirection.currentDirection === Direction.NONE || this.isOutOfBounds(lookingAt) ) { return; } this.grid[lookingAt.y][lookingAt.x].forEach((id) => { highlightableEntities.push([id, facingDirection.currentDirection]); }); }, ); highlightableEntities.forEach(([id, direction]) => { const entity = game.getEntity(id)!; if (entity.hasComponent(ComponentNames.Highlight)) { const highlight = entity.getComponent( ComponentNames.Highlight, )!; highlight.highlight(direction); } }); game.forEachEntityWithComponent(ComponentNames.Highlight, (entity) => { if (!highlightableEntities.find(([id]) => id === entity.id)) { const highlight = entity.getComponent( ComponentNames.Highlight, )!; highlight.unhighlight(); } }); } private propogateEntityMovements(game: Game) { const movingEntities: Entity[] = []; game.forEachEntityWithComponent(ComponentNames.Grid, (entity) => { const grid = entity.getComponent(ComponentNames.Grid)!; if (grid.movingDirection !== Direction.NONE) { movingEntities.push(entity); } }); // for each moving entity, check the entities in the grid cell it's moving to // if they are pushable, move them in the same direction // 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 [currentPosition, nextGridPosition] = [ gridPosition, this.getNewGridPosition(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)!, ); 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; } const pushableEntities = entities.filter((entity) => { if (!entity.hasComponent(ComponentNames.Grid)) return false; const { movingDirection } = entity.getComponent( ComponentNames.Grid, )!; const pushable = entity.hasComponent(ComponentNames.Pushable); return movingDirection === Direction.NONE && pushable; }); if (pushableEntities.length === 0) { break; } for (const pushableEntity of pushableEntities) { moving.add(pushableEntity.id); } currentPosition = nextGridPosition; nextGridPosition = this.getNewGridPosition( nextGridPosition, movingDirection, ); } for (const id of moving) { const entity = game.getEntity(id)!; const grid = entity.getComponent(ComponentNames.Grid)!; grid.movingDirection = movingDirection; entity.addComponent(grid); } } } 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, )!; const newGridPosition = this.getNewGridPosition( grid.gridPosition, grid.movingDirection, ); 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 getNewGridPosition(prev: Coord2D, direction: Direction) { let { x: newX, y: newY } = prev; switch (direction) { case Direction.LEFT: newX -= 1; break; case Direction.UP: newY -= 1; break; case Direction.DOWN: newY += 1; break; case Direction.RIGHT: newX += 1; break; } return { x: newX, y: newY }; } 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) || !game.getEntity(id)) { cell.delete(id); } } }), ); movedEntities.forEach((id) => { const entity = game.getEntity(id)!; const grid = entity.getComponent(ComponentNames.Grid)!; const { x, y } = grid.gridPosition; this.grid[y][x].add(id); }); } }