import { getMap } from "../map.js"; import { Map, Vec2, GhostType, GameState, SpawnIndex, Player, Rotation, GhostState, Tile, Ghost } from "../types.js"; import { random } from "./logic.js"; import { MOVE_SPEED, roundPos, isStablePos, getTile, getTileFrontWithRot, incrementPos, wrapPos } from "./movement.js"; const vec2eq = (a: Vec2, b: Vec2): boolean => { return a.x == b.x && a.y == b.y } const canPath = (tile: Tile): boolean => tile != Tile.WALL const canCollide = (tile: Tile, isPathing: boolean): boolean => tile != Tile.WALL && (isPathing ? true : tile != Tile.GHOST_EXIT) const path = (start: Vec2, end: Vec2, map: Map) => { let mask: Vec2[] = new Array(map.width * map.height) let queue: Vec2[] = [] queue.push(start) while (true) { let next: Vec2 = queue.shift() if (!next) { console.log('failed') return undefined } if (vec2eq(next, end)) break let north = canPath(getTile(map, next, 0, -1)) let south = canPath(getTile(map, next, 0, 1)) let east = canPath(getTile(map, next, 1, 0)) let west = canPath(getTile(map, next, -1, 0)) if (north && !mask[(next.y - 1) * map.width + (next.x)]) { queue.push({x: next.x, y: next.y - 1}) mask[(next.y - 1) * map.width + (next.x)] = next } if (south && !mask[(next.y + 1) * map.width + (next.x)]) { queue.push({x: next.x, y: next.y + 1}) mask[(next.y + 1) * map.width + (next.x)] = next } if (east && !mask[(next.y) * map.width + (next.x + 1)]) { queue.push({x: next.x + 1, y: next.y}) mask[(next.y) * map.width + (next.x + 1)] = next } if (west && !mask[(next.y) * map.width + (next.x - 1)]) { queue.push({x: next.x - 1, y: next.y}) mask[(next.y) * map.width + (next.x - 1)] = next } } let solution = [] let next = end while (true) { solution.push(structuredClone(next)) if (vec2eq(next, start)) { break } else { next = mask[next.y * map.width + next.x] } } solution = solution.reverse() solution.shift() return solution } const diff = (a: Vec2, b:Vec2): Vec2 => { return {x: a.x - b.x, y: a.y - b.y} } const dist = (a: Vec2, b: Vec2): number => { return Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2)) } const trans = (pos: Vec2, rot: Rotation, dist: number): Vec2 => { switch (rot) { case Rotation.NORTH: case Rotation.NOTHING: return {x: pos.x - dist, y: pos.y - dist} case Rotation.EAST: return {x: pos.x + dist, y: pos.y} case Rotation.SOUTH: return {x: pos.x, y: pos.y + dist} case Rotation.WEST: return {x: pos.x - dist, y: pos.y} } } const getNearestPlayer = (state: GameState, pos: Vec2): Player => { let min = undefined; let nearest = undefined; for (let id in state.players) { let player = state.players[id]; if (!player || player.dead) { continue } let d = dist(player.pos, pos) if (!min || min > d) { min = d nearest = player } } return nearest } const pickTargetScatter = (state: GameState, type: GhostType): Vec2 => { let map = getMap(state.mapId) switch (type) { case GhostType.BLINKY: return {x: 0, y: -1} case GhostType.PINKY: return {x: map.width - 1, y: -1} case GhostType.INKY: return {x: map.width - 1, y: map.height} case GhostType.CLYDE: return {x: 0, y: map.height} } } const pickTargetChase = (state: GameState, type: GhostType): Vec2 => { let ghost = state.ghosts[type] let player = getNearestPlayer(state, ghost.pos) switch (type) { case GhostType.BLINKY: return {x: player.pos.x, y: player.pos.y} case GhostType.PINKY: return trans(player.pos, player.moveRotation, 2) case GhostType.INKY: let target = trans(player.pos, player.moveRotation, 1) let vec = diff(target, state.ghosts[GhostType.BLINKY].pos) return {x: target.x + vec.x, y: target.y + vec.y} case GhostType.CLYDE: if (dist(ghost.pos, player.pos) > 8) return {x: player.pos.x, y: player.pos.y} else return pickTargetScatter(state, type) } } const pickTarget = (state: GameState, map: Map, type: GhostType): Vec2 => { let ghost: Ghost = state.ghosts[type] let target = ghost.targetQueue.shift() if (target) { return target } switch (ghost.state) { case GhostState.SCATTER: return pickTargetScatter(state, type) case GhostState.CHASE: return pickTargetChase(state, type) case GhostState.EATEN: return structuredClone(map.spawns[SpawnIndex.GHOST_SPAWN]) case GhostState.SCARED: return { x: random(state) % map.width, y: random(state) % map.height } } } const checkIfEaten = (ghost: Ghost, state: GameState): boolean => { if (ghost.state != GhostState.SCARED) { return false } for (let id in state.players) { let player = state.players[id] if (!player || player.dead) { continue } if (player.thiccLeft > 0 && dist(player.pos, ghost.pos) <= 1) { return true } } return false } const updateKilled = (ghost: Ghost, state: GameState) => { if (ghost.state == GhostState.EATEN) { return } for (let id in state.players) { let player = state.players[id] if (!player || player.dead) { continue } if (dist(player.pos, ghost.pos) > 1) { continue } if (ghost.state != GhostState.SCARED || player.thiccLeft < 1) { player.dead = true player.framesDead = 0 } } } const getGhostState = (ghost: Ghost, state: GameState): GhostState => { if (ghost.state == GhostState.EATEN || checkIfEaten(ghost, state)) { ghost.hasRespawned = false return GhostState.EATEN } if (Object.values(state.players).filter((p: Player) => p.thiccLeft > 0).length > 0) { if (!ghost.hasRespawned) { return GhostState.SCARED } } else { ghost.hasRespawned = false } return (Object.keys(state.items).length % 100 < 10 ? GhostState.SCATTER : GhostState.CHASE) } const getGhostPath = (ghost: Ghost, map: Map): Vec2[] => { if (ghost.targetQueue.length > 0) { // already pathing return undefined } if (ghost.state == GhostState.EATEN) { // dead go back to spawn if (vec2eq(ghost.pos, map.spawns[SpawnIndex.GHOST_SPAWN])) { // returned to spawn, exit the box ghost.state = GhostState.CHASE ghost.hasRespawned = true return path(ghost.pos, map.spawns[SpawnIndex.GHOST_EXIT], map) } else { // go to the box still dead return path(ghost.pos, map.spawns[SpawnIndex.GHOST_SPAWN], map) } } let tile = getTile(map, ghost.pos, 0, 0) if (tile != Tile.GHOST_WALL && tile != Tile.GHOST_SPAWN) { // not in the box return undefined } let threshold = 0 switch (ghost.type) { case GhostType.BLINKY: threshold = 60 * 5 break case GhostType.PINKY: threshold = 60 * 10 break case GhostType.INKY: threshold = 60 * 15 break case GhostType.CLYDE: threshold = 60 * 20 break } if (ghost.framesInBox < threshold) { return undefined } return path(ghost.pos, map.spawns[SpawnIndex.GHOST_EXIT], map) } const updateGhost = (state: GameState, map: Map, type: GhostType) => { let ghost: Ghost = state.ghosts[type] if (!ghost) { ghost = { pos: structuredClone(map.spawns[SpawnIndex.GHOST_SPAWN]), target: structuredClone(map.spawns[SpawnIndex.GHOST_SPAWN]), targetQueue: [], type, state: GhostState.SCATTER, currentDirection: Rotation.EAST, hasRespawned: false, framesInBox: 0, } state.ghosts[type] = ghost } let tile = getTile(map, ghost.pos, 0, 0) if (tile == Tile.GHOST_WALL || tile == Tile.GHOST_SPAWN) { ghost.framesInBox++ } ghost.state = getGhostState(ghost, state) if (isStablePos(ghost.pos)) { ghost.pos = roundPos(ghost.pos) let newPath = getGhostPath(ghost, map) if (newPath) { ghost.targetQueue = newPath } let frontTile = getTileFrontWithRot(map, ghost.pos, ghost.currentDirection) let northTile = getTile(map, ghost.pos, 0, -1) let eastTile = getTile(map, ghost.pos, 1, 0) let southTile = getTile(map, ghost.pos, 0, 1) let westTile = getTile(map, ghost.pos, -1, 0) let isPathing = ghost.targetQueue.length > 0 let front = canCollide(frontTile, isPathing) let north = canCollide(northTile, isPathing) let east = canCollide(eastTile, isPathing) let south = canCollide(southTile, isPathing) let west = canCollide(westTile, isPathing) let isIntersection = (north && east) || (east && south) || (south && west) || (west && north) if (isIntersection || !front || (isPathing && vec2eq(ghost.pos, ghost.target))) { let target = pickTarget(state, map, type) ghost.target = target if ((!isIntersection && !front) || isPathing) { ghost.currentDirection = undefined } let newRot = ghost.currentDirection let min = undefined if (north && ghost.currentDirection !== Rotation.SOUTH) { let d = dist({x: ghost.pos.x, y: ghost.pos.y - 1}, target) if (min === undefined || min > d) { min = d newRot = Rotation.NORTH } } if (east && ghost.currentDirection !== Rotation.WEST) { let d = dist({x: ghost.pos.x + 1, y: ghost.pos.y}, target) if (min === undefined || min > d) { min = d newRot = Rotation.EAST } } if (south && ghost.currentDirection !== Rotation.NORTH) { let d = dist({x: ghost.pos.x, y: ghost.pos.y + 1}, target) if (min === undefined || min > d) { min = d newRot = Rotation.SOUTH } } if (west && ghost.currentDirection !== Rotation.EAST) { let d = dist({x: ghost.pos.x - 1, y: ghost.pos.y}, target) if (min === undefined || min > d) { min = d newRot = Rotation.WEST } } ghost.currentDirection = newRot } } incrementPos(ghost.pos, ghost.currentDirection, MOVE_SPEED) wrapPos(ghost.pos, map) updateKilled(ghost, state) } export const updateGhosts = (state: GameState) => { let map = getMap(state.mapId) if (!map) return updateGhost(state, map, GhostType.BLINKY) updateGhost(state, map, GhostType.PINKY) updateGhost(state, map, GhostType.INKY) updateGhost(state, map, GhostType.CLYDE) }