Skip to content

Commit

Permalink
Animate zombies walking
Browse files Browse the repository at this point in the history
  • Loading branch information
delasy committed Oct 26, 2024
1 parent 852e977 commit 5a5737c
Show file tree
Hide file tree
Showing 15 changed files with 180 additions and 47 deletions.
4 changes: 1 addition & 3 deletions components/Visualizer.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
2 changes: 2 additions & 0 deletions constants/visualizer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const AUTO_REPLAY_SPEED = 1_500;
export const REPLAY_SPEED = 600;
110 changes: 83 additions & 27 deletions renderer/index.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -60,27 +71,6 @@ async function loadImage(src: string): Promise<HTMLImageElement> {
});
}

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;
Expand All @@ -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,
Expand Down Expand Up @@ -131,14 +123,35 @@ 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();

for (const entity of entities) {
this.drawEntity(entity);
}

if (this.hasEntitiesToAnimate()) {
this.req = window.requestAnimationFrame(() => {
this.req = null;
this.draw();
});
}
}

private drawBg() {
Expand Down Expand Up @@ -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)";
Expand All @@ -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());
}
}
22 changes: 21 additions & 1 deletion simulators/zombie-survival/Change.ts
Original file line number Diff line number Diff line change
@@ -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;
}
14 changes: 14 additions & 0 deletions simulators/zombie-survival/Direction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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;
Expand All @@ -22,7 +25,6 @@ export class Entity {
position: Position,
) {
this.destructible = destructible;
this.changes = [];
this.health = health;
this.position = position;
this.type = type;
Expand All @@ -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 = [];
}
Expand All @@ -40,6 +51,20 @@ export class Entity {
return this.health === 0;
}

public eq(entity: Entity): boolean {
return this.id === entity.id;
}

public getChange<T extends ChangeType>(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<Change, { type: T }>;
}

public getChanges(): Change[] {
return this.changes;
}
Expand All @@ -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;
Expand Down
19 changes: 14 additions & 5 deletions simulators/zombie-survival/ZombieSurvival.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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,
});
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion simulators/zombie-survival/entities/Box.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
2 changes: 1 addition & 1 deletion simulators/zombie-survival/entities/Player.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
2 changes: 1 addition & 1 deletion simulators/zombie-survival/entities/Rock.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
2 changes: 1 addition & 1 deletion simulators/zombie-survival/entities/Zombie.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
2 changes: 1 addition & 1 deletion simulators/zombie-survival/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
2 changes: 1 addition & 1 deletion simulators/zombie-survival/lib/closestEntity.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Entity } from "../entities/Entity";
import { Entity } from "../Entity";

export interface ClosestEntityScore {
distance: number;
Expand Down
2 changes: 1 addition & 1 deletion simulators/zombie-survival/lib/entityAt.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Entity } from "../Entity";
import { Position } from "../Position";
import { Entity } from "../entities/Entity";

export function entityAt(
entities: Entity[],
Expand Down
Loading

0 comments on commit 5a5737c

Please sign in to comment.