diff --git a/src/common/utils.ts b/src/common/utils.ts index 9b7dd2f..7bac6a0 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -32,6 +32,20 @@ export const shuffle = (array: T[]): T[] => { return shuffledArray; } +export function zip(a1: Iterable, a2: Iterable, a3: Iterable): Generator<[T1, T2, T3]>; +export function zip(a1: Iterable, a2: Iterable): Generator<[T1, T2]>; +export function zip(a1: Iterable): Generator<[T1]>; +export function* zip(...args: Iterable[]) { + const iterators = args.map(i => i[Symbol.iterator]()); + + while (true) { + const nextValues = iterators.map(i => i.next()); + if (nextValues.some(v => v.done)) return; + + yield nextValues.map(v => v.value); + } +} + export const range = (size: number | string) => Object.keys((new Array(+size)).fill(0)).map(k => +k); export const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value)); diff --git a/src/games/zombies/assets/bg.jpg b/src/games/zombies/assets/bg.jpg new file mode 100644 index 0000000..f2721a9 Binary files /dev/null and b/src/games/zombies/assets/bg.jpg differ diff --git a/src/games/zombies/assets/characters/big.jpg b/src/games/zombies/assets/characters/big.jpg new file mode 100644 index 0000000..8bbefae Binary files /dev/null and b/src/games/zombies/assets/characters/big.jpg differ diff --git a/src/games/zombies/assets/characters/ninja.jpg b/src/games/zombies/assets/characters/ninja.jpg new file mode 100644 index 0000000..b304033 Binary files /dev/null and b/src/games/zombies/assets/characters/ninja.jpg differ diff --git a/src/games/zombies/assets/characters/nurse.jpg b/src/games/zombies/assets/characters/nurse.jpg new file mode 100644 index 0000000..f19a264 Binary files /dev/null and b/src/games/zombies/assets/characters/nurse.jpg differ diff --git a/src/games/zombies/assets/characters/police.jpg b/src/games/zombies/assets/characters/police.jpg new file mode 100644 index 0000000..f63bcd2 Binary files /dev/null and b/src/games/zombies/assets/characters/police.jpg differ diff --git a/src/games/zombies/assets/characters/runner.jpg b/src/games/zombies/assets/characters/runner.jpg new file mode 100644 index 0000000..06c7a6a Binary files /dev/null and b/src/games/zombies/assets/characters/runner.jpg differ diff --git a/src/games/zombies/assets/enemies/boss.jpg b/src/games/zombies/assets/enemies/boss.jpg new file mode 100644 index 0000000..c6736cb Binary files /dev/null and b/src/games/zombies/assets/enemies/boss.jpg differ diff --git a/src/games/zombies/assets/enemies/dog.jpg b/src/games/zombies/assets/enemies/dog.jpg new file mode 100644 index 0000000..da23833 Binary files /dev/null and b/src/games/zombies/assets/enemies/dog.jpg differ diff --git a/src/games/zombies/assets/enemies/spider.jpg b/src/games/zombies/assets/enemies/spider.jpg new file mode 100644 index 0000000..1e66002 Binary files /dev/null and b/src/games/zombies/assets/enemies/spider.jpg differ diff --git a/src/games/zombies/assets/enemies/zombie.jpg b/src/games/zombies/assets/enemies/zombie.jpg new file mode 100644 index 0000000..4557777 Binary files /dev/null and b/src/games/zombies/assets/enemies/zombie.jpg differ diff --git a/src/games/zombies/assets/favicon.ico b/src/games/zombies/assets/favicon.ico new file mode 100644 index 0000000..4fdd0a9 Binary files /dev/null and b/src/games/zombies/assets/favicon.ico differ diff --git a/src/games/zombies/assets/items/fuel.jpg b/src/games/zombies/assets/items/fuel.jpg new file mode 100644 index 0000000..ebf92d4 Binary files /dev/null and b/src/games/zombies/assets/items/fuel.jpg differ diff --git a/src/games/zombies/assets/items/heal.jpg b/src/games/zombies/assets/items/heal.jpg new file mode 100644 index 0000000..c2ee3f4 Binary files /dev/null and b/src/games/zombies/assets/items/heal.jpg differ diff --git a/src/games/zombies/assets/items/keys.jpg b/src/games/zombies/assets/items/keys.jpg new file mode 100644 index 0000000..ca86f38 Binary files /dev/null and b/src/games/zombies/assets/items/keys.jpg differ diff --git a/src/games/zombies/assets/items/planks.jpg b/src/games/zombies/assets/items/planks.jpg new file mode 100644 index 0000000..c428147 Binary files /dev/null and b/src/games/zombies/assets/items/planks.jpg differ diff --git a/src/games/zombies/assets/weapons/assault_rifle.jpg b/src/games/zombies/assets/weapons/assault_rifle.jpg new file mode 100644 index 0000000..c4a705a Binary files /dev/null and b/src/games/zombies/assets/weapons/assault_rifle.jpg differ diff --git a/src/games/zombies/assets/weapons/axe.jpg b/src/games/zombies/assets/weapons/axe.jpg new file mode 100644 index 0000000..f4bcf59 Binary files /dev/null and b/src/games/zombies/assets/weapons/axe.jpg differ diff --git a/src/games/zombies/assets/weapons/crossbow.jpg b/src/games/zombies/assets/weapons/crossbow.jpg new file mode 100644 index 0000000..f42e8f8 Binary files /dev/null and b/src/games/zombies/assets/weapons/crossbow.jpg differ diff --git a/src/games/zombies/assets/weapons/grenade.jpg b/src/games/zombies/assets/weapons/grenade.jpg new file mode 100644 index 0000000..25e4d97 Binary files /dev/null and b/src/games/zombies/assets/weapons/grenade.jpg differ diff --git a/src/games/zombies/assets/weapons/knife.jpg b/src/games/zombies/assets/weapons/knife.jpg new file mode 100644 index 0000000..5803093 Binary files /dev/null and b/src/games/zombies/assets/weapons/knife.jpg differ diff --git a/src/games/zombies/assets/weapons/pistol.jpg b/src/games/zombies/assets/weapons/pistol.jpg new file mode 100644 index 0000000..6303f8e Binary files /dev/null and b/src/games/zombies/assets/weapons/pistol.jpg differ diff --git a/src/games/zombies/assets/weapons/rocket_launcher.jpg b/src/games/zombies/assets/weapons/rocket_launcher.jpg new file mode 100644 index 0000000..491c06f Binary files /dev/null and b/src/games/zombies/assets/weapons/rocket_launcher.jpg differ diff --git a/src/games/zombies/assets/weapons/shotgun.jpg b/src/games/zombies/assets/weapons/shotgun.jpg new file mode 100644 index 0000000..12669b7 Binary files /dev/null and b/src/games/zombies/assets/weapons/shotgun.jpg differ diff --git a/src/games/zombies/index.ts b/src/games/zombies/index.ts index a6df502..540ae9f 100644 --- a/src/games/zombies/index.ts +++ b/src/games/zombies/index.ts @@ -3,11 +3,12 @@ import Spinner from "./spinner"; import type Entity from "./entity"; import { getRealPoint } from "@common/dom"; import { nextFrame } from "@common/utils"; -import { TileMap } from "./tile"; +import bgImg from './assets/bg.jpg'; +import TileMap from "./tilemap"; const MAP_SIZE = 12; const MAP_PIXEL_SIZE = 1000; -const MAP_PADDING = 100; +const MAP_PADDING = 10; const TILE_SIZE = (MAP_PIXEL_SIZE - MAP_PADDING * 2) / MAP_SIZE; const SPINNER_SIZE = 200; const canvas = createCanvas(MAP_PIXEL_SIZE + SPINNER_SIZE, MAP_PIXEL_SIZE); @@ -24,9 +25,14 @@ async function update(dt: number) { } async function render(ctx: CanvasRenderingContext2D) { + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillStyle = 'green'; ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.drawImage(bgImg, 0, 0, MAP_PIXEL_SIZE, MAP_PIXEL_SIZE); + entities.forEach(entity => entity.render(ctx)); } diff --git a/src/games/zombies/item.ts b/src/games/zombies/item.ts new file mode 100644 index 0000000..ce77c54 --- /dev/null +++ b/src/games/zombies/item.ts @@ -0,0 +1,119 @@ +import big from './assets/characters/big.jpg'; +import ninja from './assets/characters/ninja.jpg'; +import nurse from './assets/characters/nurse.jpg'; +import police from './assets/characters/police.jpg'; +import runner from './assets/characters/runner.jpg'; + +import boss from './assets/enemies/boss.jpg'; +import dog from './assets/enemies/dog.jpg'; +import spider from './assets/enemies/spider.jpg'; +import zombie from './assets/enemies/zombie.jpg'; + +import fuel from './assets/items/fuel.jpg'; +import heal from './assets/items/heal.jpg'; +import keys from './assets/items/keys.jpg'; +import planks from './assets/items/planks.jpg'; + +import assaultRifle from './assets/weapons/assault_rifle.jpg'; +import axe from './assets/weapons/axe.jpg'; +import crossbow from './assets/weapons/crossbow.jpg'; +import grenade from './assets/weapons/grenade.jpg'; +import knife from './assets/weapons/knife.jpg'; +import pistol from './assets/weapons/pistol.jpg'; +import rocketLauncher from './assets/weapons/rocket_launcher.jpg'; +import shotgun from './assets/weapons/shotgun.jpg'; + +export const ItemType = { + CHAR_BIG: big, + CHAR_NINJA: ninja, + CHAR_NURSE: nurse, + CHAR_POLICE: police, + CHAR_RUNNER: runner, + + ENEMY_BOSS: boss, + ENEMY_DOG: dog, + ENEMY_SPIDER: spider, + ENEMY_ZOMBIE: zombie, + + ITEM_FUEL: fuel, + ITEM_HEAL: heal, + ITEM_KEYS: keys, + ITEM_PLANKS: planks, + + WEAPON_ASSAULT_RIFLE: assaultRifle, + WEAPON_AXE: axe, + WEAPON_CROSSBOW: crossbow, + WEAPON_GRENADE: grenade, + WEAPON_KNIFE: knife, + WEAPON_PISTOL: pistol, + WEAPON_ROCKET_LAUNCHER: rocketLauncher, + WEAPON_SHOTGUN: shotgun, +} as const; + +type ItemTypeType = typeof ItemType; +type ImageKey = keyof ItemTypeType; +type ItemTypeImage = ItemTypeType[ImageKey]; + +export default class Item { + constructor(public readonly type: ItemTypeImage) { + } + + get isPickable() { + return this.isItem || this.isWeapon; + } + + get isCharacter() { + return [ + ItemType.CHAR_BIG, + ItemType.CHAR_NINJA, + ItemType.CHAR_NURSE, + ItemType.CHAR_POLICE, + ItemType.CHAR_RUNNER, + ].includes(this.type); + } + + get isEnemy() { + return [ + ItemType.ENEMY_BOSS, + ItemType.ENEMY_DOG, + ItemType.ENEMY_SPIDER, + ItemType.ENEMY_ZOMBIE, + ].includes(this.type); + } + + get isItem() { + return [ + ItemType.ITEM_FUEL, + ItemType.ITEM_HEAL, + ItemType.ITEM_KEYS, + ItemType.ITEM_PLANKS, + ].includes(this.type); + } + + get isWeapon() { + return this.isMeleeWeapon || this.isShootingWeapon || [ + ItemType.WEAPON_GRENADE, + ItemType.WEAPON_ROCKET_LAUNCHER, + ].includes(this.type); + } + + get isMeleeWeapon() { + return [ + ItemType.WEAPON_AXE, + ItemType.WEAPON_KNIFE, + ].includes(this.type); + } + + get isShootingWeapon() { + return [ + ItemType.WEAPON_ASSAULT_RIFLE, + ItemType.WEAPON_CROSSBOW, + ItemType.WEAPON_PISTOL, + ItemType.WEAPON_SHOTGUN, + ].includes(this.type); + } + + toString() { + return Object.entries(ItemType).find(t => t[1] === this.type)?.[0]; + } +} diff --git a/src/games/zombies/pathfinding.ts b/src/games/zombies/pathfinding.ts new file mode 100644 index 0000000..b514e3a --- /dev/null +++ b/src/games/zombies/pathfinding.ts @@ -0,0 +1,115 @@ +import type Tile from "./tile"; + +namespace Pathfinding { + /** + * A* search from start to end Tile. + * @returns array of Tiles from start to end (inclusive), or [] if no path. + */ + export function findPath(start: Tile, end: Tile): Tile[] { + // The set of discovered nodes to be evaluated + const openSet: Set = new Set([start]); + + // For each node, the cost of getting from start to that node. + const gScore = new Map(); + gScore.set(start, 0); + + // For each node, the total cost of getting from start to goal + // by passing by that node: gScore + heuristic estimate to goal. + const fScore = new Map(); + fScore.set(start, heuristic(start, end)); + + // For path reconstruction: map each node to the node it can most efficiently be reached from. + const cameFrom = new Map(); + + while (openSet.size > 0) { + // Get the node in openSet having the lowest fScore + let current: Tile | null = null; + let currentF = Infinity; + for (const tile of openSet) { + const f = fScore.get(tile) ?? Infinity; + if (f < currentF) { + currentF = f; + current = tile; + } + } + if (!current) break; + + // If we’ve reached the goal, reconstruct and return the path. + if (current === end) { + return reconstructPath(cameFrom, current); + } + + openSet.delete(current); + + if (current.items.length > 0) continue; + + for (const neighbor of current.connections) { + // tentative gScore is current’s gScore plus cost to move to neighbor + const tentativeG = (gScore.get(current) ?? Infinity) + distance(current, neighbor); + + if (tentativeG < (gScore.get(neighbor) ?? Infinity)) { + // This path to neighbor is better than any previous one. Record it! + cameFrom.set(neighbor, current); + gScore.set(neighbor, tentativeG); + fScore.set(neighbor, tentativeG + heuristic(neighbor, end)); + if (!openSet.has(neighbor)) { + openSet.add(neighbor); + } + } + } + } + + // Open set is empty but goal was never reached + return []; + } + + /** Heuristic: Manhattan distance between tiles. */ + function heuristic(a: Tile, b: Tile): number { + return Math.abs(a.centerX - b.centerX) + Math.abs(a.centerY - b.centerY); + } + + /** Actual cost between two connected tiles (Euclidean). */ + function distance(a: Tile, b: Tile): number { + const dx = a.centerX - b.centerX; + const dy = a.centerY - b.centerY; + return Math.hypot(dx, dy); + } + + /** Reconstructs path from cameFrom map, ending at `current`. */ + function reconstructPath( + cameFrom: Map, + current: Tile + ): Tile[] { + const path: Tile[] = [current]; + while (cameFrom.has(current)) { + current = cameFrom.get(current)!; + path.push(current); + } + return path.reverse(); + } + + export function findPossibleMoves(start: Tile, steps: number): Tile[] { + const result = new Set(); + for (const tile of findPossibleTiles(start, steps)) { + const path = findPath(start, tile); + if (path.length === steps + 1) { + result.add(tile); + } + } + + return Array.from(result); + } + + function findPossibleTiles(start: Tile, steps: number): Set { + const result = new Set([start]); + if (steps > 0) { + for (const tile of start.connections) { + findPossibleTiles(tile, steps - 1).forEach(t => result.add(t)); + } + } + + return result; + } +} + +export default Pathfinding; \ No newline at end of file diff --git a/src/games/zombies/spinner.ts b/src/games/zombies/spinner.ts index 5d36656..e22222c 100644 --- a/src/games/zombies/spinner.ts +++ b/src/games/zombies/spinner.ts @@ -1,10 +1,18 @@ import Entity from "./entity"; -export type SpinnerListener = (angle: number) => void; +export enum SpinnerAction { + RUN = 1, + BITE = 2, + MELEE = 3, + SHOOT = 4, +}; + +export type SpinnerListener = (action: SpinnerAction) => void; 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 symbols = ['🏃‍♂️', '🧟‍♂️', '🔪', '🎯']; private readonly startAngle = -Math.PI / 2 - this.probabilities[0] * 2 * Math.PI; private angle = this.startAngle; @@ -38,6 +46,7 @@ export default class Spinner extends Entity { const center = (angle + nextAngle) / 2; ctx.fillText(`${i + 1}`, 0.5 + Math.cos(center) * 0.4, 0.5 + Math.sin(center) * 0.4); + ctx.fillText(this.symbols[i], 0.5 + Math.cos(center) * 0.18, 0.5 + Math.sin(center) * 0.18); angle = nextAngle; } diff --git a/src/games/zombies/tile.ts b/src/games/zombies/tile.ts index 5e19855..f1a25d3 100644 --- a/src/games/zombies/tile.ts +++ b/src/games/zombies/tile.ts @@ -1,209 +1,73 @@ -import { range } from "@common/utils"; import Entity from "./entity"; +import type Item from "./item"; export enum TileType { - EMPTY, + NORMAL, START, END, } -export namespace AStar { - /** - * A* search from start to end Tile. - * @returns array of Tiles from start to end (inclusive), or [] if no path. - */ - export function findPath(start: Tile, end: Tile): Tile[] { - // The set of discovered nodes to be evaluated - const openSet: Set = new Set([start]); - - // For each node, the cost of getting from start to that node. - const gScore = new Map(); - gScore.set(start, 0); - - // For each node, the total cost of getting from start to goal - // by passing by that node: gScore + heuristic estimate to goal. - const fScore = new Map(); - fScore.set(start, heuristic(start, end)); - - // For path reconstruction: map each node to the node it can most efficiently be reached from. - const cameFrom = new Map(); - - while (openSet.size > 0) { - // Get the node in openSet having the lowest fScore - let current: Tile | null = null; - let currentF = Infinity; - for (const tile of openSet) { - const f = fScore.get(tile) ?? Infinity; - if (f < currentF) { - currentF = f; - current = tile; - } - } - if (!current) break; - - // If we’ve reached the goal, reconstruct and return the path. - if (current === end) { - return reconstructPath(cameFrom, current); - } - - openSet.delete(current); - - for (const neighbor of current.connections) { - // tentative gScore is current’s gScore plus cost to move to neighbor - const tentativeG = (gScore.get(current) ?? Infinity) + distance(current, neighbor); - - if (tentativeG < (gScore.get(neighbor) ?? Infinity)) { - // This path to neighbor is better than any previous one. Record it! - cameFrom.set(neighbor, current); - gScore.set(neighbor, tentativeG); - fScore.set(neighbor, tentativeG + heuristic(neighbor, end)); - if (!openSet.has(neighbor)) { - openSet.add(neighbor); - } - } - } - } - - // Open set is empty but goal was never reached - return []; - } - - /** Heuristic: Manhattan distance between tiles. */ - function heuristic(a: Tile, b: Tile): number { - return Math.abs(a.centerX - b.centerX) + Math.abs(a.centerY - b.centerY); - } - - /** Actual cost between two connected tiles (Euclidean). */ - function distance(a: Tile, b: Tile): number { - const dx = a.centerX - b.centerX; - const dy = a.centerY - b.centerY; - return Math.hypot(dx, dy); - } - - /** Reconstructs path from cameFrom map, ending at `current`. */ - function reconstructPath( - cameFrom: Map, - current: Tile - ): Tile[] { - const path: Tile[] = [current]; - while (cameFrom.has(current)) { - current = cameFrom.get(current)!; - path.push(current); - } - return path.reverse(); - } - -} - -export class TileMap extends Entity { - private tiles: Tile[] = []; - private pathTiles: Tile[] = []; - public startTile: Tile; - - constructor(position: [number, number], private mapSize: number, private tileSize: number) { - super(position, [mapSize * tileSize, mapSize * tileSize]); - this.startTile = this.createMap(); - } - - public createMap() { - const map = range(this.mapSize) - .map(x => - range(this.mapSize) - .map(y => - new Tile([x, y], this.tileSize) - ) - ); - - const startTile = new Tile([0, (this.mapSize / 2) - 1], this.tileSize * 2, TileType.START); - - delete map[0][this.mapSize - 2]; - delete map[1][this.mapSize - 2]; - delete map[1][this.mapSize - 1]; - - map[0][this.mapSize - 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][this.mapSize - 3], - map[1][this.mapSize - 3], - map[2][this.mapSize - 2], - map[2][this.mapSize - 1], - ].filter(t => t); - - startTile.connections.forEach(t => t.connections.push(startTile)); - - this.tiles = map.flat(); - - return startTile; - } - - public override handleMouseMove(x: number, y: number): void { - this.tiles.forEach(tile => tile.handleMouseMove(x - this.left, y - this.top)); - } - - public override handleClick(x: number, y: number): void { - for (const tile of this.tiles) { - if (tile.isPointInBounds(x - this.left, y - this.top)) { - this.pathTiles = AStar.findPath(this.startTile, tile); - break; - } - } - } - - protected draw(ctx: CanvasRenderingContext2D): void { - ctx.scale(1 / this.width, 1 / this.height); - this.tiles.forEach(t => t.render(ctx)); - - if (this.pathTiles.length > 0) { - ctx.lineWidth = 1; - ctx.strokeStyle = 'yellow'; - ctx.beginPath(); - - const [start, ...path] = this.pathTiles; - ctx.moveTo(start.centerX, start.centerY); - path.forEach(t => ctx.lineTo(t.centerX, t.centerY)); - - ctx.stroke(); - } - } - - public update(dt: number): void { - this.tiles.forEach(t => t.update(dt)); - } -} - export default class Tile extends Entity { public connections: Tile[] = []; + public items: Item[] = []; + public isOpen = false; - constructor(position: [number, number], size: number, public type: TileType = TileType.EMPTY) { + constructor(position: [number, number], size: number, public type: TileType = TileType.NORMAL) { super([position[0] * size, position[1] * size], [size, size]); } protected draw(ctx: CanvasRenderingContext2D) { if (this.hovered) { - ctx.fillStyle = `rgba(255, 255, 255, 0.1)`; + ctx.fillStyle = `rgba(255, 255, 255, 0.2)`; 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(); + // } + } + if (this.items.length > 0) { + if (this.isOpen) { + const item = this.items[0]; + ctx.drawImage(item.type, 0.15, 0.15, 0.7, 0.7); + } else { + ctx.fillStyle = 'white'; + ctx.fillRect(0.15, 0.15, 0.7, 0.7); + + ctx.fillStyle = 'black'; + ctx.font = '0.2px Arial'; + ctx.fillText('❓', 0.5, 0.5); + } + } + } + + public open(): Item[] { + if (!this.isOpen) { + this.isOpen = true; + const { pickable = [], notPickable = [] } = Object.groupBy( + this.items, + (i) => i.isPickable ? 'pickable' : 'notPickable', + ); + + this.items = notPickable; + return pickable; } - ctx.lineWidth = 1 / this.width; - ctx.strokeStyle = `rgba(0, 0, 0, 0.5)`; - ctx.strokeRect(0, 0, 1, 1); + return []; + } + + public override onClick(): void { + console.log(...this.open().map(t => t.toString())); } public update(dt: number) { diff --git a/src/games/zombies/tilemap.ts b/src/games/zombies/tilemap.ts new file mode 100644 index 0000000..275329d --- /dev/null +++ b/src/games/zombies/tilemap.ts @@ -0,0 +1,174 @@ +import { range, shuffle, zip } from "@common/utils"; +import Entity from "./entity"; +import Tile, { TileType } from "./tile"; +import Pathfinding from "./pathfinding"; +import Item, { ItemType } from "./item"; + +export default class TileMap extends Entity { + private tiles: Tile[] = []; + private pathTiles: Tile[] = []; + public startTile: Tile; + + constructor(position: [number, number], private mapSize: number, private tileSize: number) { + super(position, [mapSize * tileSize, mapSize * tileSize]); + this.startTile = this.createMap(); + } + + public createMap() { + const map = range(this.mapSize) + .map(x => + range(this.mapSize) + .map(y => + new Tile([x, y], this.tileSize) + ) + ); + + const startTile = new Tile([0, (this.mapSize / 2) - 1], this.tileSize * 2, TileType.START); + + delete map[0][this.mapSize - 2]; + delete map[1][this.mapSize - 2]; + delete map[1][this.mapSize - 1]; + + map[0][this.mapSize - 1] = startTile; + + const verticalWalls: [number, number][] = [ + [1, 3], [3, 3], [6, 3], [10, 3], + [3, 4], + [1, 5], [6, 5], [10, 5], + [1, 6], [6, 6], [10, 6], + [1, 8], [6, 8], [10, 8], + [1, 9], [6, 9], [10, 9], + ]; + const horizontalWalls: [number, number][] = [ + [2, 2], [2, 5], [2, 9], + [3, 2], [3, 5], [3, 9], + [4, 2], [5, 5], + [6, 2], [6, 5], [6, 9], + [7, 2], [7, 5], [7, 9], + [8, 2], [8, 5], + [9, 2], [9, 9], + [10, 2], [10, 5], [10, 9], + ]; + + 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 = [ + verticalWalls.find(w => w[0] === x - 1 && w[1] === y) + ? undefined + : map[x - 1]?.[y], + + verticalWalls.find(w => w[0] === x && w[1] === y) + ? undefined + : map[x + 1]?.[y], + + horizontalWalls.find(w => w[0] === x && w[1] === y - 1) + ? undefined + : map[x]?.[y - 1], + + horizontalWalls.find(w => w[0] === x && w[1] === y) + ? undefined + : map[x]?.[y + 1], + ].filter(t => t != null); + } + } + + startTile.connections = [ + map[0][this.mapSize - 3], + map[1][this.mapSize - 3], + map[2][this.mapSize - 2], + map[2][this.mapSize - 1], + ].filter(t => t); + + startTile.connections.forEach(t => t.connections.push(startTile)); + + this.tiles = map.flat(); + + this.fillItems(); + + return startTile; + } + + public fillItems() { + const itemsMap = new Map([ + [ItemType.ENEMY_BOSS, 1], + [ItemType.ENEMY_DOG, 5], + [ItemType.ENEMY_SPIDER, 5], + [ItemType.ENEMY_ZOMBIE, 17], + + [ItemType.ITEM_FUEL, 1], + [ItemType.ITEM_HEAL, 6], + [ItemType.ITEM_KEYS, 1], + [ItemType.ITEM_PLANKS, 8], + + [ItemType.WEAPON_ASSAULT_RIFLE, 1], + [ItemType.WEAPON_AXE, 1], + [ItemType.WEAPON_CROSSBOW, 1], + [ItemType.WEAPON_GRENADE, 4], + [ItemType.WEAPON_KNIFE, 1], + [ItemType.WEAPON_PISTOL, 1], + [ItemType.WEAPON_ROCKET_LAUNCHER, 1], + [ItemType.WEAPON_SHOTGUN, 1], + ]); + + const items: Item[] = []; + + for (const [type, amount] of itemsMap.entries()) { + for (const _ in range(amount)) { + const item = new Item(type); + items.push(item); + } + } + + const fillableTiles = this.tiles.filter(t => t.type === TileType.NORMAL); + for (const [tile, item] of zip(shuffle(fillableTiles), items)) { + tile.items.push(item); + }; + } + + public override handleMouseMove(x: number, y: number): void { + this.tiles.forEach(tile => tile.handleMouseMove(x - this.left, y - this.top)); + } + + public override handleClick(x: number, y: number): void { + for (const tile of this.tiles) { + if (tile.isPointInBounds(x - this.left, y - this.top)) { + this.pathTiles = Pathfinding.findPath(this.startTile, tile); + if (this.pathTiles.length > 0) { + this.startTile = this.pathTiles.at(-1); + tile.handleClick(x - this.left, y - this.top); + } + break; + } + } + } + + protected draw(ctx: CanvasRenderingContext2D): void { + ctx.scale(1 / this.width, 1 / this.height); + this.tiles.forEach(t => t.render(ctx)); + + ctx.lineWidth = 2; + ctx.strokeStyle = 'yellow'; + + if (this.pathTiles.length > 0) { + ctx.beginPath(); + + const [start, ...path] = this.pathTiles; + ctx.moveTo(start.centerX, start.centerY); + path.forEach(t => ctx.lineTo(t.centerX, t.centerY)); + + ctx.stroke(); + } + + ctx.beginPath(); + ctx.arc(this.startTile.centerX, this.startTile.centerY, this.startTile.width * 0.4, 0, 7); + ctx.stroke(); + } + + public update(dt: number): void { + this.tiles.forEach(t => t.update(dt)); + } +} \ No newline at end of file