From 217d587863ef581dac7369db5b65b31b406c589b Mon Sep 17 00:00:00 2001 From: Pabloader Date: Wed, 25 Jun 2025 16:10:03 +0000 Subject: [PATCH] Tile map for zombies --- src/common/dom.ts | 23 +++++++++ src/games/zombies/entity.ts | 51 +++++++++++++++++++ src/games/zombies/index.ts | 95 +++++++++++++++++++++++++++++------- src/games/zombies/spinner.ts | 21 +++++--- src/games/zombies/tile.ts | 42 ++++++++++++++++ 5 files changed, 207 insertions(+), 25 deletions(-) create mode 100644 src/common/dom.ts create mode 100644 src/games/zombies/entity.ts create mode 100644 src/games/zombies/tile.ts diff --git a/src/common/dom.ts b/src/common/dom.ts new file mode 100644 index 0000000..323d107 --- /dev/null +++ b/src/common/dom.ts @@ -0,0 +1,23 @@ +export interface EventWithPoint { + readonly clientX: number; + readonly clientY: number; +} + +export const getRealPoint = (canvas: HTMLCanvasElement, e: EventWithPoint): DOMPoint => { + const matrix = new DOMMatrix(); + const scale = Math.min(canvas.clientWidth / canvas.width, canvas.clientHeight / canvas.height); + + const realWidth = canvas.width * scale; + const realHeight = canvas.height * scale; + + const offsetLeft = (canvas.clientWidth - realWidth) / 2; + const offsetTop = (canvas.clientHeight - realHeight) / 2; + + matrix.translateSelf(offsetLeft, offsetTop); + matrix.scaleSelf(scale); + matrix.invertSelf(); + + const point = new DOMPoint(e.clientX, e.clientY); + + return point.matrixTransform(matrix); +} diff --git a/src/games/zombies/entity.ts b/src/games/zombies/entity.ts new file mode 100644 index 0000000..0a8d9c9 --- /dev/null +++ b/src/games/zombies/entity.ts @@ -0,0 +1,51 @@ +export default abstract class Entity { + protected hovered = false; + + constructor(public position: [number, number], public size: [number, number]) { + + } + + public render(ctx: CanvasRenderingContext2D) { + ctx.save(); + + ctx.translate(...this.position); + ctx.scale(...this.size); + + this.draw(ctx); + + ctx.restore(); + } + + public isPointInBounds(x: number, y: number) { + return ( + this.left <= x && x <= this.right && + this.top <= y && y <= this.bottom + ); + } + + get centerX() { return this.position[0] + this.size[0] / 2; } + get centerY() { return this.position[1] + this.size[0] / 2; } + + get left() { return this.position[0]; } + get top() { return this.position[1]; } + + get right() { return this.position[0] + this.size[0]; } + get bottom() { return this.position[1] + this.size[1]; } + + get width() { return this.size[0]; } + get height() { return this.size[1]; } + + public handleClick(x: number, y: number) { + if (this.isPointInBounds(x, y)) { + this.onClick(); + } + } + + public handleMouseMove(x: number, y: number) { + this.hovered = this.isPointInBounds(x, y); + } + + protected abstract draw(ctx: CanvasRenderingContext2D): void; + protected onClick() {} + public abstract update(dt: number): void; +} \ No newline at end of file diff --git a/src/games/zombies/index.ts b/src/games/zombies/index.ts index d64ef7d..cd93ddb 100644 --- a/src/games/zombies/index.ts +++ b/src/games/zombies/index.ts @@ -1,49 +1,110 @@ import { createCanvas } from "@common/display/canvas"; import Spinner from "./spinner"; +import type Entity from "./entity"; +import { getRealPoint } from "@common/dom"; +import { nextFrame, range } from "@common/utils"; +import Tile from "./tile"; -const canvas = createCanvas(1024, 1024); -const spinner = new Spinner(); +const MAP_SIZE = 12; +const MAP_PIXEL_SIZE = 1000; +const TILE_SIZE = MAP_PIXEL_SIZE / (MAP_SIZE + 4); +const SPINNER_SIZE = 200; +const canvas = createCanvas(MAP_PIXEL_SIZE + SPINNER_SIZE, MAP_PIXEL_SIZE); +const spinner = new Spinner([MAP_PIXEL_SIZE, 0], [SPINNER_SIZE, SPINNER_SIZE]); +const map = createMap(); + +const entities: Entity[] = [ + spinner, + ...map.flat(), +]; async function update(dt: number) { - spinner.update(dt); + entities.forEach(entity => entity.update(dt)); } async function render(ctx: CanvasRenderingContext2D) { ctx.fillStyle = 'green'; ctx.fillRect(0, 0, canvas.width, canvas.height); - ctx.save(); + entities.forEach(entity => entity.render(ctx)); +} - ctx.translate(100, 100); - ctx.scale(200, 200); +async function onClick(e: MouseEvent) { + const point = getRealPoint(canvas, e); - spinner.render(ctx); + entities.forEach(entity => entity.handleClick(point.x, point.y)); +} - ctx.restore(); +async function onMouseMove(e: MouseEvent) { + const point = getRealPoint(canvas, e); + + entities.forEach(entity => entity.handleMouseMove(point.x, point.y)); +} + +function createMap() { + const map = range(MAP_SIZE) + .map(x => + range(MAP_SIZE) + .map(y => + new Tile([x + 2, y + 2], TILE_SIZE) + ) + ); + + const startTile = new Tile([1, (MAP_SIZE / 2)], TILE_SIZE * 2); + + delete map[0][MAP_SIZE - 2]; + delete map[1][MAP_SIZE - 2]; + delete map[1][MAP_SIZE - 1]; + + map[0][MAP_SIZE - 1] = startTile; + + // TODO walls + + for (let x = 0; x < map.length; x++) { + const column = map[x]; + if (!column) continue; + for (let y = 0; y < column.length; y++) { + const tile = column[y]; + if (!tile) continue; + tile.connections = [ + map[x - 1]?.[y], + map[x + 1]?.[y], + map[x]?.[y - 1], + map[x]?.[y + 1], + ].filter(t => t); + } + } + + startTile.connections = [ + map[0][MAP_SIZE - 3], + map[1][MAP_SIZE - 3], + map[2][MAP_SIZE - 2], + map[2][MAP_SIZE - 1], + ].filter(t => t); + + startTile.connections.forEach(t => t.connections.push(startTile)); + + return map; } export default async function main() { - document.body.style.background = 'green'; canvas.style.imageRendering = 'auto'; const ctx = canvas.getContext('2d'); spinner.addListener(console.log); - canvas.addEventListener('click', () => { - spinner.spin(); - }) + canvas.addEventListener('click', onClick); + canvas.addEventListener('mousemove', onMouseMove); if (ctx) { let prevFrame = performance.now(); - const loop = () => { - const now = performance.now() + while (true) { + const now = await nextFrame(); const dt = (now - prevFrame) / 1000; prevFrame = now; update(dt); render(ctx); - requestAnimationFrame(loop); - }; - loop(); + } } } \ No newline at end of file diff --git a/src/games/zombies/spinner.ts b/src/games/zombies/spinner.ts index 2938792..5d36656 100644 --- a/src/games/zombies/spinner.ts +++ b/src/games/zombies/spinner.ts @@ -1,6 +1,8 @@ +import Entity from "./entity"; + export type SpinnerListener = (angle: number) => void; -export default class Spinner { +export default class Spinner extends Entity { private readonly probabilities = [0.3, 0.3, 0.2, 0.2]; private readonly colors = ['yellow', 'green', 'blue', 'red']; private readonly startAngle = -Math.PI / 2 - this.probabilities[0] * 2 * Math.PI; @@ -11,7 +13,7 @@ export default class Spinner { private fired = true; private listeners = new Set(); - public render(ctx: CanvasRenderingContext2D) { + protected override draw(ctx: CanvasRenderingContext2D) { ctx.fillStyle = 'white'; ctx.beginPath(); @@ -50,21 +52,24 @@ export default class Spinner { ctx.beginPath(); ctx.moveTo(0.5, 0.5); - ctx.lineTo(0.5 + Math.cos(angle) * 0.5, 0.5 + Math.sin(angle) * 0.5) + ctx.lineTo(0.5 + Math.cos(angle) * 0.49, 0.5 + Math.sin(angle) * 0.49); ctx.stroke(); angle = nextAngle; } - - ctx.lineWidth = 0.02; ctx.beginPath(); - ctx.arc(0.5, 0.5, 0.5, 0, Math.PI * 2); + ctx.arc(0.5, 0.5, 0.3, 0, Math.PI * 2); + ctx.stroke(); + + ctx.lineWidth = 0.03; + ctx.beginPath(); + ctx.arc(0.5, 0.5, 0.49, 0, Math.PI * 2); ctx.moveTo(0.5, 0.5); ctx.lineTo(0.5 + Math.cos(this.angle) * 0.4, 0.5 + Math.sin(this.angle) * 0.4); ctx.stroke(); } - public update(dt: number) { + public override update(dt: number) { if (this.fired) return; if (this.speed < 0.1) { this.fire(); @@ -76,7 +81,7 @@ export default class Spinner { this.friction += 0.7 * dt; } - public spin() { + public override onClick() { if (!this.fired) return; this.fired = false; diff --git a/src/games/zombies/tile.ts b/src/games/zombies/tile.ts new file mode 100644 index 0000000..5cb6ce4 --- /dev/null +++ b/src/games/zombies/tile.ts @@ -0,0 +1,42 @@ +import Entity from "./entity"; + +export default class Tile extends Entity { + public connections: Tile[] = []; + + constructor(position: [number, number], size: number, public image: HTMLImageElement | null = null) { + super([position[0] * size, position[1] * size], [size, size]); + } + + protected draw(ctx: CanvasRenderingContext2D) { + if (this.image) { + ctx.drawImage(this.image, 0, 0, 1, 1); + } + + if (this.hovered) { + ctx.fillStyle = `rgba(255, 255, 255, 0.1)`; + ctx.fillRect(0, 0, 1, 1); + + ctx.lineWidth = 1 / this.width; + ctx.strokeStyle = 'red'; + + for (const tile of this.connections) { + const center = [ + 0.5 + (tile.centerX - this.centerX) / this.width, + 0.5 + (tile.centerY - this.centerY) / this.height, + ]; + + ctx.beginPath(); + ctx.moveTo(0.5, 0.5); + ctx.lineTo(center[0], center[1]); + ctx.stroke(); + } + } + + ctx.lineWidth = 1 / this.width; + ctx.strokeStyle = `rgba(0, 0, 0, 0.5)`; + ctx.strokeRect(0, 0, 1, 1); + } + + public update(dt: number) { + } +}