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
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|