diff --git a/src/common/utils.ts b/src/common/utils.ts index 3b9dd08..ca4051a 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -46,6 +46,13 @@ export function* zip(...args: Iterable[]) { } } +export function* enumerate(iterable: Iterable): Generator<[number, T]> { + let i = 0; + for (const item of iterable) { + yield [i++, item]; + } +} + 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)); export const lerp = (start: number, end: number, t: number) => (start + (end - start) * t); diff --git a/src/games/zombies/character.ts b/src/games/zombies/character.ts index b84deae..2129e13 100644 --- a/src/games/zombies/character.ts +++ b/src/games/zombies/character.ts @@ -1,5 +1,5 @@ import { lerp } from "@common/utils"; -import Item from "./item"; +import Item, { ItemType } from "./item"; import Tile from "./tile"; const MOVE_DURATION = .1; @@ -8,6 +8,13 @@ export default class Character extends Item { public tile = new Tile([0, 0], 1); private path: Tile[] = []; private pathProgress = 0; + + get moveBonus() { + if (this.type === ItemType.CHAR_RUNNER || this.type === ItemType.ENEMY_DOG) return 1; + if (this.type === ItemType.ENEMY_ZOMBIE) return -1; + + return 0; + } public get displayPosition(): [number, number] { if (this.path.length > 1) { @@ -16,6 +23,11 @@ export default class Character extends Item { const currentTileIdx = Math.floor(progress); const currentTile = this.path[currentTileIdx]; + + if (!currentTile) { + return [this.tile.centerX, this.tile.centerY]; + } + const nextTile = this.path[currentTileIdx + 1]; if (!nextTile) { diff --git a/src/games/zombies/pathfinding.ts b/src/games/zombies/pathfinding.ts index 4cdbbe8..f840747 100644 --- a/src/games/zombies/pathfinding.ts +++ b/src/games/zombies/pathfinding.ts @@ -5,7 +5,7 @@ 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[] { + export function findPath(start: Tile, end: Tile, forEnemy = false): Tile[] { // The set of discovered nodes to be evaluated const openSet: Set = new Set([start]); @@ -21,6 +21,9 @@ namespace Pathfinding { // For path reconstruction: map each node to the node it can most efficiently be reached from. const cameFrom = new Map(); + let closestTile: Tile = start; + let closestDistance = heuristic(closestTile, end); + while (openSet.size > 0) { // Get the node in openSet having the lowest fScore let current: Tile | null = null; @@ -34,6 +37,12 @@ namespace Pathfinding { } if (!current) break; + const dist = heuristic(current, end); + if (closestDistance < dist) { + closestTile = current; + closestDistance = dist; + } + // If we’ve reached the goal, reconstruct and return the path. if (current === end) { return reconstructPath(cameFrom, current); @@ -41,7 +50,7 @@ namespace Pathfinding { openSet.delete(current); - if (current !== start && current.items.length > 0) continue; + if (current !== start && !forEnemy && current.items.length > 0) continue; for (const neighbor of current.activeConnections) { // tentative gScore is current’s gScore plus cost to move to neighbor @@ -60,6 +69,10 @@ namespace Pathfinding { } // Open set is empty but goal was never reached + if (forEnemy) { + return reconstructPath(cameFrom, closestTile); + } + return []; } diff --git a/src/games/zombies/player.ts b/src/games/zombies/player.ts index 3229fc4..5318da8 100644 --- a/src/games/zombies/player.ts +++ b/src/games/zombies/player.ts @@ -35,10 +35,6 @@ export default class Player extends Character { return this.type === ItemType.CHAR_NURSE ? 2 : 1; } - get moveBonus() { - return this.type === ItemType.CHAR_RUNNER ? 1 : 0; - } - get isDead() { return this.health <= 0; } diff --git a/src/games/zombies/tile.ts b/src/games/zombies/tile.ts index 7566591..ecaa4eb 100644 --- a/src/games/zombies/tile.ts +++ b/src/games/zombies/tile.ts @@ -1,3 +1,4 @@ +import type Character from "./character"; import Entity from "./entity"; import type Item from "./item"; import { ItemType } from "./item"; @@ -45,7 +46,17 @@ export default class Tile extends Entity { if (this.items.length > 0) { if (this.isOpen) { const item = this.enemy ?? this.items[0]; - ctx.drawImage(item.type, 0.1, 0.1, 0.8, 0.8); + + if ('displayPosition' in item) { + const character = item as Character; + const [x, y] = character.displayPosition; + const offsetX = (x - this.centerX) / this.width; + const offsetY = (y - this.centerY) / this.height; + + ctx.drawImage(character.type, offsetX + 0.1, offsetY + 0.1, 0.8, 0.8); + } else { + ctx.drawImage(item.type, 0.1, 0.1, 0.8, 0.8); + } if (this.items.length > 1) { ctx.fillStyle = 'black'; @@ -72,8 +83,8 @@ export default class Tile extends Entity { } } - get enemy() { - return this.items.find(i => i.isEnemy); + get enemy(): Character | undefined { + return this.items.find(i => i.isEnemy) as Character; } get activeConnections() { diff --git a/src/games/zombies/tilemap.ts b/src/games/zombies/tilemap.ts index c653c32..500927f 100644 --- a/src/games/zombies/tilemap.ts +++ b/src/games/zombies/tilemap.ts @@ -1,10 +1,11 @@ -import { range, shuffle, zip } from "@common/utils"; +import { enumerate, randInt, range, shuffle, zip } from "@common/utils"; import Entity from "./entity"; import Tile, { TileType } from "./tile"; import Pathfinding from "./pathfinding"; import Item, { ItemType } from "./item"; import Player, { Players } from "./player"; import Spinner, { SpinnerAction } from "./spinner"; +import Character from "./character"; enum GameState { NORMAL, @@ -171,7 +172,14 @@ export default class TileMap extends Entity { for (const [type, amount] of itemsMap.entries()) { for (const _ in range(amount)) { - const item = new Item(type); + const Constructor = [ + ItemType.ENEMY_BOSS, + ItemType.ENEMY_DOG, + ItemType.ENEMY_SPIDER, + ItemType.ENEMY_ZOMBIE, + ].includes(type) ? Character : Item; + + const item = new Constructor(type); items.push(item); } } @@ -199,10 +207,16 @@ export default class TileMap extends Entity { for (const [tile, item] of zip(fillableTiles, shuffle(items))) { tile.items.push(item); - if (item instanceof Player) { + if (item instanceof Character) { item.tile = tile; } - }; + } + + for (const tile of this.tiles) { + if (tile.items.length === 0) { + tile.isOpen = true; + } + } } get player() { @@ -218,6 +232,15 @@ export default class TileMap extends Entity { )); } + private get enemies() { + return this.tiles.filter( + (tile) => + tile.isOpen + && tile.enemy + && tile.items.length === 1 + ) + } + public override handleMouseMove(x: number, y: number): void { this.activeTiles.forEach(tile => tile.handleMouseMove(x - this.left, y - this.top)); } @@ -350,8 +373,42 @@ export default class TileMap extends Entity { } } setTimeout(alert, 2000, "πŸŽ‰πŸŽ‰πŸŽ‰ ΠŸΠžΠ‘Π•Π”Π! πŸš— πŸŽ‰πŸŽ‰πŸŽ‰"); - } else if (this.currentPlayerIdx === 0) { - // TODO zombies turn + } else if (this.currentPlayerIdx === 0 && this.players.length < this.foundPlayers.size) { // if someone is dead + const enemies = this.enemies; + + loop: + for (const enemyTile of shuffle(enemies)) { + const moves = Pathfinding.findPossibleMoves(enemyTile, 5); + + for (const move of shuffle(moves)) { + if (move.items.length > 0) continue; + + for (const [playerIndex, player] of enumerate(this.players)) { + if (player.tile === move) { + const enemy = enemyTile.enemy!; + const allowedSteps = randInt(1, 5) + enemy.moveBonus; + + console.log({enemy, allowedSteps}); + if (allowedSteps <= 0) continue; + + const path = Pathfinding.findPath(enemyTile, player.tile, true); + + const targetTile = path[allowedSteps] ?? player.tile; + + enemyTile.removeItem(enemy); + targetTile.items.push(enemy); + enemy.moveTo(targetTile, path.slice(0, allowedSteps + 1)); + + if (targetTile === player.tile) { + this.state = GameState.FIGHT; + this.currentPlayerIdx = playerIndex; + } + + break loop; + } + } + } + } } } @@ -457,5 +514,6 @@ export default class TileMap extends Entity { public update(dt: number) { this.players.forEach((player) => player.update(dt)); + this.enemies.forEach((tile) => tile.enemy!.update(dt)); } } \ No newline at end of file