diff options
Diffstat (limited to 'client/src/logic/ai.ts')
-rw-r--r-- | client/src/logic/ai.ts | 197 |
1 files changed, 197 insertions, 0 deletions
diff --git a/client/src/logic/ai.ts b/client/src/logic/ai.ts new file mode 100644 index 0000000..875621a --- /dev/null +++ b/client/src/logic/ai.ts @@ -0,0 +1,197 @@ +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"; + +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 (!id) 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] + 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 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 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]), + type, + state: GhostState.SCARED, + currentDirection: Rotation.EAST, + } + state.ghosts[type] = ghost + } + + if (isStablePos(ghost.pos)) { + + ghost.pos = roundPos(ghost.pos) + + 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 isIntersection = + (north && east) || + (east && south) || + (south && west) || + (west && north) + + if (!isIntersection && front) { + ghost.currentDirection = flipRot(ghost.currentDirection) + } else if (isIntersection) { + let target = pickTarget(state, map, type) + ghost.target = target + + 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) { + 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 || 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 || 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 || min > d) { + min = d + newRot = Rotation.WEST + } + } + + ghost.currentDirection = newRot + } + } + + incrementPos(ghost.pos, ghost.currentDirection, MOVE_SPEED) +} + +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) +} |