diff options
author | Tyler Murphy <tylerm@tylerm.dev> | 2023-06-29 11:40:46 -0400 |
---|---|---|
committer | Tyler Murphy <tylerm@tylerm.dev> | 2023-06-29 11:40:46 -0400 |
commit | f5fcce110a915fca1b114001962170733276e5df (patch) | |
tree | 6ce82c649d7377b42e75c9feb88d73e4aa15d713 /client/src/logic/ai.ts | |
parent | ghost (diff) | |
download | tuxman-f5fcce110a915fca1b114001962170733276e5df.tar.gz tuxman-f5fcce110a915fca1b114001962170733276e5df.tar.bz2 tuxman-f5fcce110a915fca1b114001962170733276e5df.zip |
audio, finalize gameplay, wrap around map, stuff
Diffstat (limited to '')
-rw-r--r-- | client/src/logic/ai.ts | 237 |
1 files changed, 212 insertions, 25 deletions
diff --git a/client/src/logic/ai.ts b/client/src/logic/ai.ts index 875621a..25e5d25 100644 --- a/client/src/logic/ai.ts +++ b/client/src/logic/ai.ts @@ -1,7 +1,73 @@ 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 } from "./movement.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} @@ -80,6 +146,12 @@ const pickTargetChase = (state: GameState, type: GhostType): Vec2 => { 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) @@ -95,17 +167,105 @@ const pickTarget = (state: GameState, map: Map, type: GhostType): Vec2 => { } } -const flipRot = (rot: Rotation) => { - switch (rot) { - case Rotation.NORTH: - return Rotation.SOUTH - case Rotation.SOUTH: - return Rotation.NORTH - case Rotation.EAST: - return Rotation.WEST - case Rotation.WEST: - return Rotation.EAST +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.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 (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) => { @@ -115,22 +275,45 @@ const updateGhost = (state: GameState, map: Map, type: GhostType) => { ghost = { pos: structuredClone(map.spawns[SpawnIndex.GHOST_SPAWN]), target: structuredClone(map.spawns[SpawnIndex.GHOST_SPAWN]), + targetQueue: [], type, - state: GhostState.SCARED, + 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 front = getTileFrontWithRot(map, ghost.pos, ghost.currentDirection) == Tile.WALL - let north = getTile(map, ghost.pos, 0, -1) != Tile.WALL - let east = getTile(map, ghost.pos, 1, 0) != Tile.WALL - let south = getTile(map, ghost.pos, 0, 1) != Tile.WALL - let west = getTile(map, ghost.pos, -1, 0) != Tile.WALL + 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) || @@ -138,18 +321,20 @@ const updateGhost = (state: GameState, map: Map, type: GhostType) => { (south && west) || (west && north) - if (!isIntersection && front) { - ghost.currentDirection = flipRot(ghost.currentDirection) - } else if (isIntersection) { + 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 || min > d) { + if (min === undefined || min > d) { min = d newRot = Rotation.NORTH } @@ -157,7 +342,7 @@ const updateGhost = (state: GameState, map: Map, type: GhostType) => { if (east && ghost.currentDirection !== Rotation.WEST) { let d = dist({x: ghost.pos.x + 1, y: ghost.pos.y}, target) - if (!min || min > d) { + if (min === undefined || min > d) { min = d newRot = Rotation.EAST } @@ -165,7 +350,7 @@ const updateGhost = (state: GameState, map: Map, type: GhostType) => { if (south && ghost.currentDirection !== Rotation.NORTH) { let d = dist({x: ghost.pos.x, y: ghost.pos.y + 1}, target) - if (!min || min > d) { + if (min === undefined || min > d) { min = d newRot = Rotation.SOUTH } @@ -173,7 +358,7 @@ const updateGhost = (state: GameState, map: Map, type: GhostType) => { if (west && ghost.currentDirection !== Rotation.EAST) { let d = dist({x: ghost.pos.x - 1, y: ghost.pos.y}, target) - if (!min || min > d) { + if (min === undefined || min > d) { min = d newRot = Rotation.WEST } @@ -183,7 +368,9 @@ const updateGhost = (state: GameState, map: Map, type: GhostType) => { } } - incrementPos(ghost.pos, ghost.currentDirection, MOVE_SPEED) + incrementPos(ghost.pos, ghost.currentDirection, MOVE_SPEED) + wrapPos(ghost.pos, map) + updateKilled(ghost, state) } export const updateGhosts = (state: GameState) => { |