add interactable component

This commit is contained in:
Elizabeth Hunt 2024-03-02 01:07:55 -07:00
parent a333ce8845
commit 1ec5a8d088
Signed by: simponic
GPG Key ID: 52B3774857EB24B1
20 changed files with 258 additions and 65 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
public/assets/key.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
public/assets/wall.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -8,10 +8,10 @@ export interface GameCanvasProps {
export const GameCanvas = ({ width, height }: GameCanvasProps) => { export const GameCanvas = ({ width, height }: GameCanvasProps) => {
const canvasRef = useRef<HTMLCanvasElement>(null); const canvasRef = useRef<HTMLCanvasElement>(null);
const [_game, setGame] = useState<TheAbstractionEngine>(); const [game, setGame] = useState<TheAbstractionEngine>();
useEffect(() => { useEffect(() => {
if (canvasRef.current) { if (canvasRef.current && !game) {
const canvas = canvasRef.current; const canvas = canvasRef.current;
const ctx = canvas.getContext("2d"); const ctx = canvas.getContext("2d");
if (ctx) { if (ctx) {

View File

@ -39,8 +39,10 @@ export class TheAbstractionEngine {
const player = new Player(); const player = new Player();
this.game.addEntity(player); this.game.addEntity(player);
const box = new FunctionBox({ x: 5, y: 5 }); const box = new FunctionBox({ x: 3, y: 1 });
this.game.addEntity(box); this.game.addEntity(box);
const box2 = new FunctionBox({ x: 4, y: 1 });
this.game.addEntity(box2);
} }
public play() { public play() {

View File

@ -4,4 +4,6 @@ export namespace ComponentNames {
export const Grid = "Grid"; export const Grid = "Grid";
export const BoundingBox = "BoundingBox"; export const BoundingBox = "BoundingBox";
export const Control = "Control"; export const Control = "Control";
export const Highlight = "Highlight";
export const Interactable = "Interactable";
} }

View File

@ -0,0 +1,7 @@
import { Component, ComponentNames } from ".";
export class Highlight extends Component {
constructor() {
super(ComponentNames.Highlight);
}
}

View File

@ -0,0 +1,15 @@
import { Component, ComponentNames } from ".";
export class Interactable extends Component {
private interaction: Function;
constructor(interaction: Function) {
super(ComponentNames.Interactable);
this.interaction = interaction;
}
public interact() {
this.interaction();
}
}

View File

@ -5,3 +5,5 @@ export * from "./FacingDirection";
export * from "./Grid"; export * from "./Grid";
export * from "./BoundingBox"; export * from "./BoundingBox";
export * from "./Control"; export * from "./Control";
export * from "./Highlight";
export * from "./Interactable";

View File

@ -42,7 +42,7 @@ export namespace KeyConstants {
} }
export namespace PhysicsConstants { export namespace PhysicsConstants {
export const GRID_MOVEMENT_VELOCITY = 2; export const GRID_MOVEMENT_VELOCITY = 1;
} }
export namespace Miscellaneous { export namespace Miscellaneous {

View File

@ -43,6 +43,6 @@ const functionBoxSpriteSpec = {
width: 64, width: 64,
height: 64, height: 64,
frames: 3, frames: 3,
sheet: "/assets/border.png", sheet: "/assets/function_block.png",
}; };
SPRITE_SPECS.set(Sprites.FUNCTION_BOX, functionBoxSpriteSpec); SPRITE_SPECS.set(Sprites.FUNCTION_BOX, functionBoxSpriteSpec);

View File

@ -7,14 +7,26 @@ export abstract class Entity {
public components: Map<string, Component>; public components: Map<string, Component>;
public name: string; public name: string;
protected hooks: Map<string, { add: Function; remove: Function }>;
constructor(name: string, id: string = (Entity.Id++).toString()) { constructor(name: string, id: string = (Entity.Id++).toString()) {
this.name = name; this.name = name;
this.id = id; this.id = id;
this.components = new Map(); this.components = new Map();
this.hooks = new Map();
} }
public addComponent(component: Component) { public addComponent(component: Component) {
const hadBeforeSet = this.components.has(component.name);
this.components.set(component.name, component); this.components.set(component.name, component);
if (!hadBeforeSet) {
this.hooks.get(component.name)?.add();
}
}
public removeComponent(name: string) {
this.components.delete(name);
this.hooks.get(name)?.remove();
} }
public getComponent<T extends Component>(name: string): T { public getComponent<T extends Component>(name: string): T {

View File

@ -1,6 +1,12 @@
import { IMAGES, SPRITE_SPECS, SpriteSpec, Sprites } from "../config"; import { IMAGES, SPRITE_SPECS, SpriteSpec, Sprites } from "../config";
import { Entity, EntityNames } from "."; import { Entity, EntityNames } from ".";
import { BoundingBox, Grid, Sprite } from "../components"; import {
BoundingBox,
ComponentNames,
Grid,
Interactable,
Sprite,
} from "../components";
import { Coord2D } from "../interfaces"; import { Coord2D } from "../interfaces";
export class FunctionBox extends Entity { export class FunctionBox extends Entity {
@ -8,9 +14,13 @@ export class FunctionBox extends Entity {
Sprites.FUNCTION_BOX, Sprites.FUNCTION_BOX,
) as SpriteSpec; ) as SpriteSpec;
constructor(gridPosition: Coord2D) { private code: string;
constructor(gridPosition: Coord2D, code: string) {
super(EntityNames.FunctionBox); super(EntityNames.FunctionBox);
this.code = code;
this.addComponent( this.addComponent(
new BoundingBox( new BoundingBox(
{ {
@ -39,5 +49,18 @@ export class FunctionBox extends Entity {
FunctionBox.spriteSpec.frames, FunctionBox.spriteSpec.frames,
), ),
); );
this.hooks.set(ComponentNames.Highlight, {
add: () => {
this.addComponent(new Interactable(() => this.viewInsides()));
},
remove: () => {
this.removeComponent(ComponentNames.Interactable);
},
});
}
public viewInsides() {
console.log("I am a function box!");
} }
} }

View File

@ -51,6 +51,7 @@ export class FacingDirection extends System {
: angleToDirection(angle); : angleToDirection(angle);
facingDirection.setDirection(direction); facingDirection.setDirection(direction);
entity.addComponent(facingDirection);
const oldSprite = entity.getComponent<Sprite>(ComponentNames.Sprite); const oldSprite = entity.getComponent<Sprite>(ComponentNames.Sprite);
const sprite = facingDirection.directionSprites.get(direction)!; const sprite = facingDirection.directionSprites.get(direction)!;

View File

@ -1,9 +1,12 @@
import { System, SystemNames } from "."; import { System, SystemNames } from ".";
import { Game } from ".."; import { Game } from "..";
import { Entity } from "../entities";
import { PhysicsConstants } from "../config"; import { PhysicsConstants } from "../config";
import { import {
BoundingBox, BoundingBox,
ComponentNames, ComponentNames,
FacingDirection,
Highlight,
Grid as GridComponent, Grid as GridComponent,
} from "../components"; } from "../components";
import { Coord2D, Direction, Dimension2D } from "../interfaces"; import { Coord2D, Direction, Dimension2D } from "../interfaces";
@ -28,9 +31,109 @@ export class Grid extends System {
public update(dt: number, game: Game) { public update(dt: number, game: Game) {
this.putUninitializedEntitiesInGrid(game); this.putUninitializedEntitiesInGrid(game);
this.rebuildGrid(game); this.rebuildGrid(game);
this.highlightEntitiesLookedAt(game);
this.propogateEntityMovements(game);
this.updateMovingEntities(dt, game); this.updateMovingEntities(dt, game);
} }
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)!;
if (!entity.hasComponent(ComponentNames.Highlight)) {
entity.addComponent(new Highlight());
}
});
game.forEachEntityWithComponent(ComponentNames.Highlight, (entity) => {
if (!highlightableEntities.has(entity.id)) {
entity.removeComponent(ComponentNames.Highlight);
}
});
}
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)!;
let nextGridPosition = this.getNewGridPosition(
grid.gridPosition,
grid.movingDirection,
);
while (!this.isOutOfBounds(nextGridPosition)) {
const { x, y } = nextGridPosition;
const entities = Array.from(this.grid[y][x]).map(
(id) => game.getEntity(id)!,
);
const pushableEntities = entities.filter((entity) => {
if (!entity.hasComponent(ComponentNames.Grid)) return false;
const { pushable, movingDirection } =
entity.getComponent<GridComponent>(ComponentNames.Grid)!;
return movingDirection === Direction.NONE && pushable;
});
if (pushableEntities.length === 0) {
break;
}
for (const pushableEntity of pushableEntities) {
const pushableGrid = pushableEntity.getComponent<GridComponent>(
ComponentNames.Grid,
)!;
pushableGrid.movingDirection = grid.movingDirection;
pushableEntity.addComponent(pushableEntity);
}
nextGridPosition = this.getNewGridPosition(
nextGridPosition,
grid.movingDirection,
);
}
}
}
private putUninitializedEntitiesInGrid(game: Game) { private putUninitializedEntitiesInGrid(game: Game) {
game.forEachEntityWithComponent(ComponentNames.Grid, (entity) => { game.forEachEntityWithComponent(ComponentNames.Grid, (entity) => {
const grid = entity.getComponent<GridComponent>(ComponentNames.Grid)!; const grid = entity.getComponent<GridComponent>(ComponentNames.Grid)!;
@ -71,23 +174,10 @@ export class Grid extends System {
ComponentNames.BoundingBox, ComponentNames.BoundingBox,
)!; )!;
let { x: newX, y: newY } = grid.gridPosition; const newGridPosition = this.getNewGridPosition(
switch (grid.movingDirection) { grid.gridPosition,
case Direction.LEFT: grid.movingDirection,
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)) { if (this.isOutOfBounds(newGridPosition)) {
grid.movingDirection = Direction.NONE; grid.movingDirection = Direction.NONE;
entity.addComponent(grid); entity.addComponent(grid);
@ -137,6 +227,26 @@ export class Grid extends System {
}); });
} }
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( private isEntityPastCenterWhenMoving(
direction: Direction, direction: Direction,
gridPosition: Coord2D, gridPosition: Coord2D,
@ -185,7 +295,7 @@ export class Grid extends System {
this.grid.forEach((row) => this.grid.forEach((row) =>
row.forEach((cell) => { row.forEach((cell) => {
for (const id of cell) { for (const id of cell) {
if (!movedEntities.has(id)) { if (movedEntities.has(id)) {
cell.delete(id); cell.delete(id);
} }
} }
@ -194,7 +304,8 @@ export class Grid extends System {
movedEntities.forEach((id) => { movedEntities.forEach((id) => {
const entity = game.getEntity(id)!; const entity = game.getEntity(id)!;
const grid = entity.getComponent<GridComponent>(ComponentNames.Grid)!; const grid = entity.getComponent<GridComponent>(ComponentNames.Grid)!;
this.grid[grid.gridPosition.y][grid.gridPosition.x].add(id); const { x, y } = grid.gridPosition;
this.grid[y][x].add(id);
}); });
} }
} }

View File

@ -1,6 +1,6 @@
import { SystemNames, System } from "."; import { SystemNames, System } from ".";
import { Game } from ".."; import { Game } from "..";
import { ComponentNames } from "../components"; import { ComponentNames, Grid, Interactable } from "../components";
import { Control } from "../components/Control"; import { Control } from "../components/Control";
import { Action, KeyConstants } from "../config"; import { Action, KeyConstants } from "../config";
import { Entity } from "../entities"; import { Entity } from "../entities";
@ -31,11 +31,30 @@ export class Input extends System {
public update(_dt: number, game: Game) { public update(_dt: number, game: Game) {
game.forEachEntityWithComponent(ComponentNames.Control, (entity) => game.forEachEntityWithComponent(ComponentNames.Control, (entity) =>
this.handleInput(entity), this.handleMovement(entity),
);
game.forEachEntityWithComponent(ComponentNames.Interactable, (entity) =>
this.handleInteraction(entity),
); );
} }
public handleInput(entity: Entity) { private handleInteraction(entity: Entity) {
const interactable = entity.getComponent<Interactable>(
ComponentNames.Interactable,
);
const interact = this.hasSomeKey(
KeyConstants.ActionKeys.get(Action.INTERACT),
);
if (!interact) {
return;
}
interactable.interact();
}
public handleMovement(entity: Entity) {
const controlComponent = entity.getComponent<Control>( const controlComponent = entity.getComponent<Control>(
ComponentNames.Control, ComponentNames.Control,
); );
@ -50,8 +69,11 @@ export class Input extends System {
Action.MOVE_RIGHT, Action.MOVE_RIGHT,
Action.MOVE_DOWN, Action.MOVE_DOWN,
].map((action) => this.hasSomeKey(KeyConstants.ActionKeys.get(action))); ].map((action) => this.hasSomeKey(KeyConstants.ActionKeys.get(action)));
if (hasGrid) { if (!hasGrid) {
const gridComponent = entity.getComponent(ComponentNames.Grid); return;
}
const gridComponent = entity.getComponent<Grid>(ComponentNames.Grid)!;
if (gridComponent.movingDirection !== Direction.NONE) { if (gridComponent.movingDirection !== Direction.NONE) {
return; return;
} }
@ -80,7 +102,6 @@ export class Input extends System {
entity.addComponent(gridComponent); entity.addComponent(gridComponent);
} }
}
private hasSomeKey(keys?: string[]): boolean { private hasSomeKey(keys?: string[]): boolean {
if (keys) { if (keys) {

View File

@ -2,6 +2,7 @@ import { System, SystemNames } from ".";
import { BoundingBox, ComponentNames, Sprite } from "../components"; import { BoundingBox, ComponentNames, Sprite } from "../components";
import { Game } from ".."; import { Game } from "..";
import { clamp } from "../utils"; import { clamp } from "../utils";
import { DrawArgs } from "../interfaces";
export class Render extends System { export class Render extends System {
private ctx: CanvasRenderingContext2D; private ctx: CanvasRenderingContext2D;
@ -38,10 +39,11 @@ export class Render extends System {
return; return;
} }
const drawArgs = { const drawArgs: DrawArgs = {
center: boundingBox.center, center: boundingBox.center,
dimension: boundingBox.dimension, dimension: boundingBox.dimension,
rotation: boundingBox.rotation, rotation: boundingBox.rotation,
tint: entity.hasComponent(ComponentNames.Highlight) ? "red" : undefined,
}; };
sprite.draw(this.ctx, drawArgs); sprite.draw(this.ctx, drawArgs);

View File

@ -1,9 +1,4 @@
import React from "react";
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client";
import { App } from "./App.tsx"; import { App } from "./App.tsx";
import "./css/style.css"; import "./css/style.css";
ReactDOM.createRoot(document.getElementById("root")!).render( ReactDOM.createRoot(document.getElementById("root")!).render(<App />);
<React.StrictMode>
<App />
</React.StrictMode>,
);