341 lines
9.6 KiB
TypeScript
Raw Normal View History

2024-03-01 21:29:40 -07:00
import { System, SystemNames } from ".";
import { Game } from "..";
2024-03-02 01:07:55 -07:00
import { Entity } from "../entities";
2024-03-01 21:29:40 -07:00
import { PhysicsConstants } from "../config";
import {
BoundingBox,
ComponentNames,
2024-03-02 01:07:55 -07:00
FacingDirection,
Highlight,
2024-03-01 21:29:40 -07:00
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<string>[][] = [];
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()));
}
2024-03-01 22:04:57 -07:00
public update(dt: number, game: Game) {
2024-03-01 21:29:40 -07:00
this.putUninitializedEntitiesInGrid(game);
this.rebuildGrid(game);
2024-03-02 01:07:55 -07:00
this.highlightEntitiesLookedAt(game);
this.propogateEntityMovements(game);
2024-03-01 21:29:40 -07:00
this.updateMovingEntities(dt, game);
}
2024-03-02 01:07:55 -07:00
private highlightEntitiesLookedAt(game: Game) {
const highlightableEntities = new Set<string>();
game.forEachEntityWithComponent(
ComponentNames.FacingDirection,
(entity) => {
if (!entity.hasComponent(ComponentNames.Grid)) {
return;
}
const grid = entity.getComponent<GridComponent>(ComponentNames.Grid)!;
const facingDirection = entity.getComponent<FacingDirection>(
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.add(id);
});
},
);
highlightableEntities.forEach((id) => {
const entity = game.getEntity(id)!;
2024-03-02 02:22:46 -07:00
if (entity.hasComponent(ComponentNames.Highlight)) {
const highlight = entity.getComponent<Highlight>(
ComponentNames.Highlight,
)!;
highlight.highlight();
2024-03-02 01:07:55 -07:00
}
});
game.forEachEntityWithComponent(ComponentNames.Highlight, (entity) => {
if (!highlightableEntities.has(entity.id)) {
2024-03-02 02:22:46 -07:00
const highlight = entity.getComponent<Highlight>(
ComponentNames.Highlight,
)!;
highlight.unhighlight();
2024-03-02 01:07:55 -07:00
}
});
}
private propogateEntityMovements(game: Game) {
const movingEntities: Entity[] = [];
game.forEachEntityWithComponent(ComponentNames.Grid, (entity) => {
const grid = entity.getComponent<GridComponent>(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<GridComponent>(ComponentNames.Grid)!;
2024-03-02 02:22:46 -07:00
const { gridPosition, movingDirection } = grid;
grid.movingDirection = Direction.NONE;
entity.addComponent(grid); // default to not moving
2024-03-02 01:07:55 -07:00
let nextGridPosition = this.getNewGridPosition(
2024-03-02 02:22:46 -07:00
gridPosition,
movingDirection,
2024-03-02 01:07:55 -07:00
);
2024-03-02 02:22:46 -07:00
const moving = new Set<string>();
moving.add(entity.id);
2024-03-02 01:07:55 -07:00
while (!this.isOutOfBounds(nextGridPosition)) {
const { x, y } = nextGridPosition;
const entities = Array.from(this.grid[y][x]).map(
(id) => game.getEntity(id)!,
);
2024-03-02 02:22:46 -07:00
if (
entities.some((entity) =>
entity.hasComponent(ComponentNames.Colliding),
)
) {
moving.clear();
break;
}
2024-03-02 01:07:55 -07:00
const pushableEntities = entities.filter((entity) => {
if (!entity.hasComponent(ComponentNames.Grid)) return false;
2024-03-02 02:22:46 -07:00
const { movingDirection } = entity.getComponent<GridComponent>(
ComponentNames.Grid,
)!;
const pushable = entity.hasComponent(ComponentNames.Pushable);
2024-03-02 01:07:55 -07:00
return movingDirection === Direction.NONE && pushable;
});
if (pushableEntities.length === 0) {
break;
}
for (const pushableEntity of pushableEntities) {
2024-03-02 02:22:46 -07:00
moving.add(pushableEntity.id);
2024-03-02 01:07:55 -07:00
}
nextGridPosition = this.getNewGridPosition(
nextGridPosition,
2024-03-02 02:22:46 -07:00
movingDirection,
2024-03-02 01:07:55 -07:00
);
}
2024-03-02 02:22:46 -07:00
for (const id of moving) {
const entity = game.getEntity(id)!;
const grid = entity.getComponent<GridComponent>(ComponentNames.Grid)!;
grid.movingDirection = movingDirection;
entity.addComponent(grid);
}
2024-03-02 01:07:55 -07:00
}
}
2024-03-01 21:29:40 -07:00
private putUninitializedEntitiesInGrid(game: Game) {
game.forEachEntityWithComponent(ComponentNames.Grid, (entity) => {
const grid = entity.getComponent<GridComponent>(ComponentNames.Grid)!;
if (grid.initialized) {
return;
}
const hasBoundingBox = entity.hasComponent(ComponentNames.BoundingBox);
if (!hasBoundingBox) {
return;
}
const boundingBox = entity.getComponent<BoundingBox>(
ComponentNames.BoundingBox,
)!;
boundingBox.center = this.gridToScreenPosition(grid.gridPosition);
2024-03-01 22:04:57 -07:00
boundingBox.dimension = this.dimension;
2024-03-01 21:29:40 -07:00
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<GridComponent>(ComponentNames.Grid)!;
if (grid.movingDirection === Direction.NONE) {
return;
}
const boundingBox = entity.getComponent<BoundingBox>(
ComponentNames.BoundingBox,
)!;
2024-03-02 01:07:55 -07:00
const newGridPosition = this.getNewGridPosition(
grid.gridPosition,
grid.movingDirection,
);
2024-03-01 21:29:40 -07:00
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);
});
}
2024-03-02 01:07:55 -07:00
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 };
}
2024-03-01 21:29:40 -07:00
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<string>();
game.forEachEntityWithComponent(ComponentNames.Grid, (entity) => {
const grid = entity.getComponent<GridComponent>(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) {
2024-03-02 01:07:55 -07:00
if (movedEntities.has(id)) {
2024-03-01 21:29:40 -07:00
cell.delete(id);
}
}
}),
);
movedEntities.forEach((id) => {
const entity = game.getEntity(id)!;
const grid = entity.getComponent<GridComponent>(ComponentNames.Grid)!;
2024-03-02 01:07:55 -07:00
const { x, y } = grid.gridPosition;
this.grid[y][x].add(id);
2024-03-01 21:29:40 -07:00
});
}
}