diff --git a/components/Visualizer.tsx b/components/Visualizer.tsx index daebf20..0c9a8e0 100644 --- a/components/Visualizer.tsx +++ b/components/Visualizer.tsx @@ -1,11 +1,9 @@ import * as React from "react"; import { Button } from "@/components/ui/button"; +import { AUTO_REPLAY_SPEED, REPLAY_SPEED } from "@/constants/visualizer"; import { Renderer } from "@/renderer"; import { ZombieSurvival } from "@/simulators/zombie-survival"; -const AUTO_REPLAY_SPEED = 1_500; -const REPLAY_SPEED = 600; - export function Visualizer({ autoReplay = false, autoStart = false, diff --git a/constants/visualizer.ts b/constants/visualizer.ts new file mode 100644 index 0000000..c5083b4 --- /dev/null +++ b/constants/visualizer.ts @@ -0,0 +1,2 @@ +export const AUTO_REPLAY_SPEED = 1_500; +export const REPLAY_SPEED = 600; diff --git a/renderer/index.ts b/renderer/index.ts index b25e4cc..8748556 100644 --- a/renderer/index.ts +++ b/renderer/index.ts @@ -1,9 +1,20 @@ +import { REPLAY_SPEED } from "@/constants/visualizer"; import { type Entity, EntityType, + Position, ZombieSurvival, + move, } from "@/simulators/zombie-survival"; -import { Change } from "@/simulators/zombie-survival/Change"; +import { ChangeType } from "@/simulators/zombie-survival/Change"; + +export interface AnimatedEntity { + entity: Entity; + duration: number; + startedAt: number; + from: Position; + to: Position; +} export interface RendererAssets { loading: boolean; @@ -60,27 +71,6 @@ async function loadImage(src: string): Promise { }); } -function getEntityImage(entity: Entity): HTMLImageElement | null { - switch (entity.getType()) { - case EntityType.Box: { - return assets.box; - } - case EntityType.Player: { - return assets.player; - } - case EntityType.Rock: { - return assets.rock; - } - case EntityType.Zombie: { - if (entity.getChanges().includes(Change.Walking)) { - return assets.zombieWalking; - } else { - return assets.zombie; - } - } - } -} - export class Renderer { private readonly assets: RendererAssets; private readonly cellSize: number; @@ -90,6 +80,8 @@ export class Renderer { private canvas2: HTMLCanvasElement; private ctx: CanvasRenderingContext2D; private ctx2: CanvasRenderingContext2D; + private req: number | null = null; + private simulator: ZombieSurvival | null = null; public constructor( boardHeight: number, @@ -131,7 +123,21 @@ export class Renderer { } public render(simulator: ZombieSurvival) { - const entities = simulator.getAllEntities(); + if (this.req !== null) { + window.cancelAnimationFrame(this.req); + this.req = null; + } + + this.simulator = simulator; + this.draw(); + } + + private draw() { + if (this.simulator === null) { + return; + } + + const entities = this.simulator.getAllEntities(); this.ctx.clearRect(0, 0, this.w, this.h); this.drawBg(); @@ -139,6 +145,13 @@ export class Renderer { for (const entity of entities) { this.drawEntity(entity); } + + if (this.hasEntitiesToAnimate()) { + this.req = window.requestAnimationFrame(() => { + this.req = null; + this.draw(); + }); + } } private drawBg() { @@ -173,17 +186,30 @@ export class Renderer { return; } - const entityImage = getEntityImage(entity); + const entityImage = this.getEntityImage(entity); if (entityImage === null) { return; } const entityPosition = entity.getPosition(); - const x = entityPosition.x * this.cellSize; - const y = entityPosition.y * this.cellSize; + let x = entityPosition.x; + let y = entityPosition.y; + + if (entity.hasChange(ChangeType.Walking)) { + const change = entity.getChange(ChangeType.Walking); + const timePassed = Date.now() - change.startedAt; + const delta = timePassed / change.duration; + const { to, from } = change; + + x = from.x + (to.x - from.x) * delta; + y = from.y + (to.y - from.y) * delta; + } - if (entity.getChanges().includes(Change.Hit)) { + x *= this.cellSize; + y *= this.cellSize; + + if (entity.hasChange(ChangeType.Hit)) { this.ctx2.clearRect(0, 0, this.cellSize, this.cellSize); this.ctx2.filter = "hue-rotate(300deg)"; @@ -200,4 +226,34 @@ export class Renderer { this.ctx.drawImage(entityImage, x, y, this.cellSize, this.cellSize); } + + private getEntityImage(entity: Entity): HTMLImageElement | null { + switch (entity.getType()) { + case EntityType.Box: { + return this.assets.box; + } + case EntityType.Player: { + return this.assets.player; + } + case EntityType.Rock: { + return this.assets.rock; + } + case EntityType.Zombie: { + if (entity.hasChange(ChangeType.Walking)) { + return this.assets.zombieWalking; + } else { + return this.assets.zombie; + } + } + } + } + + private hasEntitiesToAnimate(): boolean { + if (this.simulator === null) { + return false; + } + + const entities = this.simulator.getAllEntities(); + return entities.some((entity) => entity.animating()); + } } diff --git a/simulators/zombie-survival/Change.ts b/simulators/zombie-survival/Change.ts index 755c9d7..c7993a6 100644 --- a/simulators/zombie-survival/Change.ts +++ b/simulators/zombie-survival/Change.ts @@ -1,5 +1,25 @@ -export enum Change { +import { Position } from "./Position"; + +export enum ChangeType { Hit, Killed, Walking, } + +export type Change = HitChange | KilledChange | WalkingChange; + +export interface HitChange { + type: ChangeType.Hit; +} + +export interface KilledChange { + type: ChangeType.Killed; +} + +export interface WalkingChange { + type: ChangeType.Walking; + duration: number; + startedAt: number; + from: Position; + to: Position; +} diff --git a/simulators/zombie-survival/Direction.ts b/simulators/zombie-survival/Direction.ts index 49177b0..1894376 100644 --- a/simulators/zombie-survival/Direction.ts +++ b/simulators/zombie-survival/Direction.ts @@ -51,6 +51,20 @@ export function directionFromString(val: string): Direction { } } +export function determine(p1: Position, p2: Position): Direction { + if (p1.x > p2.x) { + return Direction.Left; + } else if (p1.x < p2.x) { + return Direction.Right; + } else if (p1.y > p2.y) { + return Direction.Up; + } else if (p1.y < p2.y) { + return Direction.Down; + } + + throw new Error("Unable to determine direction"); +} + export function move(position: Position, direction: Direction): Position { switch (direction) { case Direction.Down: { diff --git a/simulators/zombie-survival/entities/Entity.ts b/simulators/zombie-survival/Entity.ts similarity index 63% rename from simulators/zombie-survival/entities/Entity.ts rename to simulators/zombie-survival/Entity.ts index 7ebee97..ca28308 100644 --- a/simulators/zombie-survival/entities/Entity.ts +++ b/simulators/zombie-survival/Entity.ts @@ -1,5 +1,6 @@ -import { type Change } from "../Change"; -import { Position } from "../Position"; +import { type Change, ChangeType } from "./Change"; +import { Position } from "./Position"; +import { randomId } from "./lib/randomId"; export enum EntityType { Box, @@ -9,8 +10,10 @@ export enum EntityType { } export class Entity { + private id: string = randomId(); + protected destructible: boolean; - protected changes: Change[]; + protected changes: Change[] = []; protected health: number; protected position: Position; protected type: EntityType; @@ -22,7 +25,6 @@ export class Entity { position: Position, ) { this.destructible = destructible; - this.changes = []; this.health = health; this.position = position; this.type = type; @@ -32,6 +34,15 @@ export class Entity { this.changes.push(change); } + public animating(): boolean { + if (this.hasChange(ChangeType.Walking)) { + const change = this.getChange(ChangeType.Walking); + return Date.now() < change.startedAt + change.duration; + } + + return false; + } + public clearChanges(): void { this.changes = []; } @@ -40,6 +51,20 @@ export class Entity { return this.health === 0; } + public eq(entity: Entity): boolean { + return this.id === entity.id; + } + + public getChange(type: T) { + const change = this.changes.find((change) => change.type === type); + + if (change === undefined) { + throw new Error("Unable to find change of this type"); + } + + return change as Extract; + } + public getChanges(): Change[] { return this.changes; } @@ -60,6 +85,10 @@ export class Entity { return this.type; } + public hasChange(type: ChangeType): boolean { + return this.changes.some((change) => change.type === type); + } + public hit() { if (!this.destructible) { return; diff --git a/simulators/zombie-survival/ZombieSurvival.ts b/simulators/zombie-survival/ZombieSurvival.ts index 263e5cd..d951c29 100644 --- a/simulators/zombie-survival/ZombieSurvival.ts +++ b/simulators/zombie-survival/ZombieSurvival.ts @@ -1,11 +1,12 @@ -import { Change } from "./Change"; +import { ChangeType } from "./Change"; +import { Entity } from "./Entity"; import { Position, samePosition } from "./Position"; import { Box } from "./entities/Box"; -import { Entity } from "./entities/Entity"; import { Player } from "./entities/Player"; import { Rock } from "./entities/Rock"; import { Zombie } from "./entities/Zombie"; import { entityAt } from "./lib/entityAt"; +import { REPLAY_SPEED } from "@/constants/visualizer"; export class ZombieSurvival { public readonly boardHeight: number; @@ -212,11 +213,19 @@ export class ZombieSurvival { zombie.walk(); if (initialHealth[i] !== zombie.getHealth()) { - zombie.addChange(Change.Hit); + zombie.addChange({ type: ChangeType.Hit }); } - if (!samePosition(initialPosition, zombie.getPosition())) { - zombie.addChange(Change.Walking); + const currentPosition = zombie.getPosition(); + + if (!samePosition(initialPosition, currentPosition)) { + zombie.addChange({ + type: ChangeType.Walking, + duration: REPLAY_SPEED, + startedAt: Date.now(), + from: initialPosition, + to: currentPosition, + }); } } } diff --git a/simulators/zombie-survival/entities/Box.ts b/simulators/zombie-survival/entities/Box.ts index ca2f027..9cd339e 100644 --- a/simulators/zombie-survival/entities/Box.ts +++ b/simulators/zombie-survival/entities/Box.ts @@ -1,5 +1,5 @@ +import { Entity, EntityType } from "../Entity"; import { Position } from "../Position"; -import { Entity, EntityType } from "./Entity"; export class Box extends Entity { public static Destructible = true; diff --git a/simulators/zombie-survival/entities/Player.ts b/simulators/zombie-survival/entities/Player.ts index 586a21f..b21c222 100644 --- a/simulators/zombie-survival/entities/Player.ts +++ b/simulators/zombie-survival/entities/Player.ts @@ -1,7 +1,7 @@ +import { Entity, EntityType } from "../Entity"; import { Position } from "../Position"; import { ZombieSurvival } from "../ZombieSurvival"; import { closestEntity } from "../lib/closestEntity"; -import { Entity, EntityType } from "./Entity"; export class Player extends Entity { public static Destructible = true; diff --git a/simulators/zombie-survival/entities/Rock.ts b/simulators/zombie-survival/entities/Rock.ts index a6d8fa1..bf14e4f 100644 --- a/simulators/zombie-survival/entities/Rock.ts +++ b/simulators/zombie-survival/entities/Rock.ts @@ -1,5 +1,5 @@ +import { Entity, EntityType } from "../Entity"; import { Position } from "../Position"; -import { Entity, EntityType } from "./Entity"; export class Rock extends Entity { public static Destructible = false; diff --git a/simulators/zombie-survival/entities/Zombie.ts b/simulators/zombie-survival/entities/Zombie.ts index 1b5888a..94270b7 100644 --- a/simulators/zombie-survival/entities/Zombie.ts +++ b/simulators/zombie-survival/entities/Zombie.ts @@ -1,9 +1,9 @@ import { Direction, allDirections, move } from "../Direction"; +import { Entity, EntityType } from "../Entity"; import { Position } from "../Position"; import { ZombieSurvival } from "../ZombieSurvival"; import { entityAt } from "../lib/entityAt"; import { pathfinder } from "../lib/pathfinder"; -import { Entity, EntityType } from "./Entity"; export class Zombie extends Entity { public static Destructible = true; diff --git a/simulators/zombie-survival/index.ts b/simulators/zombie-survival/index.ts index 63ab792..3d0b989 100644 --- a/simulators/zombie-survival/index.ts +++ b/simulators/zombie-survival/index.ts @@ -1,5 +1,5 @@ export * from "./entities/Box"; -export * from "./entities/Entity"; +export * from "./Entity"; export * from "./entities/Player"; export * from "./entities/Rock"; export * from "./entities/Zombie"; diff --git a/simulators/zombie-survival/lib/closestEntity.ts b/simulators/zombie-survival/lib/closestEntity.ts index 82d2267..87687bd 100644 --- a/simulators/zombie-survival/lib/closestEntity.ts +++ b/simulators/zombie-survival/lib/closestEntity.ts @@ -1,4 +1,4 @@ -import { Entity } from "../entities/Entity"; +import { Entity } from "../Entity"; export interface ClosestEntityScore { distance: number; diff --git a/simulators/zombie-survival/lib/entityAt.ts b/simulators/zombie-survival/lib/entityAt.ts index 76ea62a..d59eb04 100644 --- a/simulators/zombie-survival/lib/entityAt.ts +++ b/simulators/zombie-survival/lib/entityAt.ts @@ -1,5 +1,5 @@ +import { Entity } from "../Entity"; import { Position } from "../Position"; -import { Entity } from "../entities/Entity"; export function entityAt( entities: Entity[], diff --git a/simulators/zombie-survival/lib/randomId.ts b/simulators/zombie-survival/lib/randomId.ts new file mode 100644 index 0000000..00e3d5c --- /dev/null +++ b/simulators/zombie-survival/lib/randomId.ts @@ -0,0 +1,5 @@ +export function randomId(): string { + const buf = new Uint8Array(8); + window.crypto.getRandomValues(buf); + return Buffer.from(buf).toString("hex"); +}