diff options
Diffstat (limited to 'client/src')
-rw-r--r-- | client/src/editor.ts | 37 | ||||
-rw-r--r-- | client/src/logic/ai.ts | 237 | ||||
-rw-r--r-- | client/src/logic/items.ts | 16 | ||||
-rw-r--r-- | client/src/logic/logic.ts | 83 | ||||
-rw-r--r-- | client/src/logic/movement.ts | 113 | ||||
-rw-r--r-- | client/src/logic/players.ts | 5 | ||||
-rw-r--r-- | client/src/main.ts | 2 | ||||
-rw-r--r-- | client/src/map.ts | 17 | ||||
-rw-r--r-- | client/src/renderer.ts | 169 | ||||
-rw-r--r-- | client/src/types.ts | 106 |
10 files changed, 695 insertions, 90 deletions
diff --git a/client/src/editor.ts b/client/src/editor.ts index 0be5c68..0f72545 100644 --- a/client/src/editor.ts +++ b/client/src/editor.ts @@ -1,9 +1,10 @@ import { InitialState } from "./logic/logic.js" -import { genMap, compressMap, decompressMap } from "./map.js" +import { genMap, compressMap, decompressMap, checkMap } from "./map.js" import { startGraphicsUpdater } from "./renderer.js" import { GameState, Vec2, Tile } from "./types.js" const mapgen = document.getElementById("mapgen") +const mapload = document.getElementById("mapload") const sidebar = document.getElementById("sidebar") mapgen.onsubmit = async (event) => { @@ -21,10 +22,30 @@ mapgen.onsubmit = async (event) => { } mapgen.style.display = "none" + mapload.style.display = "none" runMapEditor(width, height) } +mapload.onsubmit = async (event) => { + event.preventDefault() + + const data_str = (<HTMLInputElement>document.getElementById("data")).value + const {width, height, data} = decompressMap(data_str) + const map = genMap(width, height, data, 0) + const [success, result] = checkMap(map) + + if (!success) { + alert(result as String) + return + } + + mapgen.style.display = "none" + mapload.style.display = "none" + + runMapEditor(width, height, data) +} + const startKeyListener = () => { let keys = {} @@ -93,6 +114,8 @@ const checkInputs = (pressed: {[key: string]: boolean}): [Tile, boolean] => { return [Tile.WALL, false] } else if (pressed["KeyG"]) { return [Tile.GHOST_WALL, false] + } else if (pressed["KeyR"]) { + return [Tile.GHOST_EXIT, true] } else if (pressed["KeyH"]) { return [Tile.GHOST_SPAWN, true] } else if (pressed["KeyF"]) { @@ -121,12 +144,11 @@ const checkBounds = (tilePos: Vec2, width: number, height: number) => { return true } -const runMapEditor = (width: number, height: number) => { +const runMapEditor = (width: number, height: number, map_data: number[] = undefined) => { sidebar.style.display = "" - let data: number[] = new Array(width * height).fill(0) - + let data: number[] = map_data ? map_data : new Array(width * height).fill(0) let map = genMap(width, height, data, Tile.EMPTY) let state: GameState = structuredClone(InitialState); @@ -168,6 +190,13 @@ const runMapEditor = (width: number, height: number) => { requestAnimationFrame(loop) document.getElementById("export").onclick = () => { + + let [success, status] = checkMap(map) + if (!success) { + alert(status) + return + } + let encoded = compressMap(map) document.getElementById("textarea").textContent = encoded document.getElementById("popup").style.display = 'flex' 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) => { diff --git a/client/src/logic/items.ts b/client/src/logic/items.ts index 5f8a38e..79624d3 100644 --- a/client/src/logic/items.ts +++ b/client/src/logic/items.ts @@ -1,5 +1,5 @@ import { getMap, getItemKey } from "../map.js" -import { GameState, Map, Player } from "../types.js" +import { GameState, ItemType, Map, Player } from "../types.js" const ceilHalf = (n: number): number => { return Math.ceil(n*2)/2 @@ -12,10 +12,24 @@ const floorHalf = (n: number): number => { const eatItems = (data: GameState, map: Map, player: Player) => { let pos = player.pos + + player.atePellets = Math.max(player.atePellets - 1, 0) for (let x = ceilHalf(pos.x-.5); x <= floorHalf(pos.x+.5); x += .5) { for (let y = ceilHalf(pos.y-.5); y <= floorHalf(pos.y+.5); y += .5) { let item_key = getItemKey(x, y, map.width) + + let item = data.items[item_key] + if (!item) { + continue + } + + player.atePellets = 30 + + if (item.type == ItemType.THICC_DOT) { + player.thiccLeft += 60 * 10 + } + delete data.items[item_key] } } diff --git a/client/src/logic/logic.ts b/client/src/logic/logic.ts index d9ac6c8..1c3ec1a 100644 --- a/client/src/logic/logic.ts +++ b/client/src/logic/logic.ts @@ -3,7 +3,7 @@ import { updatePlayers } from "./players.js" import { updateUI } from "./ui.js" import { updateMovement } from "./movement.js" import { updateItems } from "./items.js" -import { GameState, Input } from "../types.js"; +import { GameState, Input, Rotation, SpawnIndex } from "../types.js"; import { updateGhosts } from "./ai.js"; export const InitialState: GameState = { @@ -14,7 +14,9 @@ export const InitialState: GameState = { items: {}, mapId: undefined, frame: 0, - rng: 0 + rng: 0, + roundTimer: 0, + endRoundTimer: undefined } export const random = (state: GameState): number => { @@ -32,36 +34,85 @@ export const onLogic = ( random(data) let startPressed = updatePlayers(data, input); + let playersLeft = 0 + for (let id in data.players) { + let player = data.players[id] + if (player.dead) { + player.framesDead++ + } else { + playersLeft++ + } + } if (data.started) { - updateMovement(data) - updateItems(data) - updateGhosts(data) + data.roundTimer++ + if (data.roundTimer < 60 * 5) { + // uh do nothing just draw shit + } else if (playersLeft > 1) { + updateMovement(data) + updateItems(data) + updateGhosts(data) + } else { + if (data.endRoundTimer === undefined) { + data.endRoundTimer = 60 * 5 + } + data.endRoundTimer-- + if (data.endRoundTimer < 1) { + nextMap(data) + } + } } else { updateUI(data) } - if (startPressed && !data.started) { - initMap(data, 0) - data.started = true; + if (startPressed && !data.started && Object.values(data.players).length > 1) { + nextMap(data) + data.started = true; + } + + let map = getMap(data.mapId) + if (map && Object.keys(data.items).length < 1) { + data.items = genItems(map, false) } return data } -const initMap = (gameData: GameState, mapId: number) => { +const nextMap = (gameData: GameState) => { - document.getElementById("lobby").style.display = "none" + if (gameData.mapId === undefined) { + gameData.mapId = 0 + } else { + gameData.mapId++ + } - gameData.mapId = mapId + if (gameData.mapId > 3) { + gameData.mapId = undefined + gameData.started = false + return + } + + gameData.ghosts = [undefined, undefined, undefined, undefined] - let map = getMap(mapId) - // if (!map) { - // let {width, height, data} = decompressMap(maps[mapId]) - // map = genMap(width, height, data, mapId) - // } + let map = getMap(gameData.mapId) + let index = SpawnIndex.PAC_SPAWN_1 + for (let id in gameData.players) { + let player = gameData.players[id] + player.pos = structuredClone(map.spawns[index++]) + player.dead = false + player.framesDead = 0 + player.thiccLeft = 0 + player.atePellets = 0 + player.moving = false + player.moveRotation = Rotation.NOTHING + player.inputRotation = Rotation.NOTHING + player.velocityRotation = Rotation.NOTHING + } + gameData.items = genItems(map) + gameData.roundTimer = 0 + gameData.endRoundTimer = undefined } diff --git a/client/src/logic/movement.ts b/client/src/logic/movement.ts index f2a06e7..726f87a 100644 --- a/client/src/logic/movement.ts +++ b/client/src/logic/movement.ts @@ -1,5 +1,5 @@ import { getMap } from "../map.js" -import { Vec2, Map, Rotation, Key, Player, GameState, Tile } from "../types.js" +import { Vec2, Map, Rotation, Key, Player, GameState, Tile, BoundingBox } from "../types.js" export const MOVE_SPEED = .08333 @@ -20,10 +20,18 @@ export const getTile = ( ): number => { let x = Math.round(pos.x + ox) let y = Math.round(pos.y + oy) - if (x < 0 || x >= map.width || y < 0 || y >= map.height) return Tile.WALL + + x = (x + map.width) % map.width + y = (y + map.height) % map.height + return map.data[y * map.width + x] } +export const wrapPos = (pos: Vec2, map: Map) => { + pos.x = (pos.x + map.width) % map.width + pos.y = (pos.y + map.height) % map.height +} + export const getTileFrontWithRot = ( map: Map, pos: Vec2, @@ -86,10 +94,11 @@ const updateMovementForPlayer = ( let inputRot = getRot(inputKey) let moveRot = player.moveRotation + let speed = MOVE_SPEED let currentPosition = player.pos let turningFrontTile = getTileFrontWithRot(map, currentPosition, inputRot) - if (turningFrontTile == Tile.WALL || turningFrontTile == Tile.GHOST_WALL) { + if (turningFrontTile == Tile.WALL || turningFrontTile == Tile.GHOST_WALL || turningFrontTile == Tile.GHOST_EXIT) { inputRot = Rotation.NOTHING } @@ -97,24 +106,110 @@ const updateMovementForPlayer = ( player.inputRotation = inputRot - if (turning && isStablePos(currentPosition)) { + if (player.velocityRotation != Rotation.NOTHING) { + moveRot = player.velocityRotation + speed *= 1.5 + } else if (turning && isStablePos(currentPosition)) { currentPosition = roundPos(currentPosition) player.moveRotation = inputRot moveRot = inputRot } let movePos = structuredClone(currentPosition) - incrementPos(movePos, moveRot, MOVE_SPEED) + incrementPos(movePos, moveRot, speed) + wrapPos(movePos, map) let frontTile = getTileFrontWithRot(map, currentPosition, moveRot) - if (frontTile != Tile.WALL && frontTile != Tile.GHOST_WALL) { + if (frontTile != Tile.WALL && frontTile != Tile.GHOST_WALL && frontTile != Tile.GHOST_EXIT) { player.pos = movePos player.moving = true } else { player.pos = roundPos(currentPosition) player.moving = false + player.velocityRotation = Rotation.NOTHING + } + +} + +const createBoundingBox = (player: Player): BoundingBox => { + let pos = player.pos + let width = player.thiccLeft > 0 ? 2 : 1 + return { + x: pos.x - width/2, + y: pos.y - width/2, + w: width, + h: width + } +} + +const checkBoundingBox = (aBB: BoundingBox, bBB: BoundingBox): Rotation => { + if ( + aBB.x < bBB.x + bBB.w && + aBB.x + aBB.w > bBB.x && + aBB.y < bBB.y + bBB.h && + aBB.y + aBB.h > bBB.y + ) { + let diff = {x: aBB.x - bBB.x, y: aBB.y - bBB.y} + + if (Math.abs(diff.x) > Math.abs(diff.y)) { + if (diff.x > 0) { + return Rotation.EAST + } else { + return Rotation.WEST + } + } else { + if (diff.y < 0) { + return Rotation.NORTH + } else { + return Rotation.SOUTH + } + } + + } else { + return Rotation.NOTHING + } +} + +const flipRotation = (rot: Rotation): Rotation => { + switch (rot) { + case Rotation.NOTHING: + return Rotation.NOTHING + 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 updateCollision = (data: GameState) => { + + let players: Player[] = Object.values(data.players).filter(p => p != null && p !== undefined) + let bb = structuredClone(players).map(p => createBoundingBox(p)) + let num = players.length + + for (let i = 0; i < num - 1; i++) { + for (let j = i + 1; j < num; j++) { + let rot = checkBoundingBox(bb[i], bb[j]) + if (rot == Rotation.NOTHING) { + continue + } + + if (players[i].thiccLeft > 0 && players[j].thiccLeft == 0) { + players[j].dead = true + players[i].framesDead = 0 + } else if (players[j].thiccLeft > 0 && players[i].thiccLeft == 0) { + players[i].dead = true + players[i].framesDead = 0 + } else { + players[i].velocityRotation = rot + players[j].velocityRotation = flipRotation(rot) + } + } + } } @@ -123,9 +218,15 @@ export const updateMovement = (data: GameState) => { let map = getMap(data.mapId) if (!map) return + updateCollision(data) + for (const id in data.players) { const player = data.players[id] + + if (player.thiccLeft > 0) { + player.thiccLeft-- + } if(!player) { continue diff --git a/client/src/logic/players.ts b/client/src/logic/players.ts index 96779fe..96337ea 100644 --- a/client/src/logic/players.ts +++ b/client/src/logic/players.ts @@ -34,7 +34,12 @@ export const updatePlayers = (data: GameState, input: Input) => { pos: {x: 1, y: 1}, inputRotation: Rotation.EAST, moveRotation: Rotation.NOTHING, + velocityRotation: Rotation.NOTHING, moving: false, + thiccLeft: 0, + dead: false, + framesDead: 0, + atePellets: 0 }; } diff --git a/client/src/main.ts b/client/src/main.ts index 9913d8b..af985db 100644 --- a/client/src/main.ts +++ b/client/src/main.ts @@ -9,7 +9,7 @@ const lobby = document.getElementById("lobby") const mapeditor = document.getElementById("mapeditor") const maps = { - [0]: 'EwRgPqYgNDew+TEuW6AGT2u59mPI/PeLZclSys1AhSgThJcJb2bdq7p5rupV2DYaVFCKI9HwFDiWACyxyK5WpCqArOPSCeuqfUnzDwaGbMyT3FAGZT0ABznD9ybWv0LLq61kz4S0M9WRMANnVVDUi1AHYdQxt+dyNEhJNiNk8A3npkiVSmcUEM6E5C/wL86rlivLqxaVymltggA=' + [0]: 'MwRgPgTOIDS/dEOU1L1sxgDDX+9CDijSTyzLcFrVabN7EnYXCUnaBOK31xCMyEUSPfg3FZ20trTlTJnPpX4iV6tRLpCOC7bslbVvNQBY41SxesBWE/bYHpTnFPkZjFResG/hpb003RGBEMR0YQQAOSIjYcNDDRntAgNi/Txl/L2CkozgXfWdgtTUANmsQKyrrAHYHFLiI2T1870KgkkzS7N6CLUdmpvbipyV4hv7pQVSe8PGittHllo7J9Z7N5Y9W3Z39uCA' } join.onsubmit = async function(event) { diff --git a/client/src/map.ts b/client/src/map.ts index 70de0b8..a7bf004 100644 --- a/client/src/map.ts +++ b/client/src/map.ts @@ -91,9 +91,9 @@ const genWalls = ( return walls } -const canItem = (tile: Tile): boolean => tile != Tile.WALL && tile != Tile.GHOST_WALL && tile != Tile.GHOST_SPAWN +const canItem = (initial: boolean, tile: Tile): boolean => tile != Tile.WALL && tile != Tile.GHOST_WALL && tile != Tile.GHOST_SPAWN && tile != Tile.GHOST_EXIT && (initial ? tile == Tile.INITIAL_DOT : true) -export const genItems = (map: Map): Items => { +export const genItems = (map: Map, initial: boolean = true): Items => { let width = map.width let height = map.height @@ -104,7 +104,7 @@ export const genItems = (map: Map): Items => { for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { let tile = getPoint(width, height, data, x, y) - if (!canItem(tile)) continue + if (!canItem(initial, tile)) continue let item_key = getItemKey(x, y, width) @@ -123,13 +123,13 @@ export const genItems = (map: Map): Items => { type = ItemType.DOT let tile_south = getPoint(width, height, data, x, y + 1) - if (canItem(tile_south) && tile_south != Tile.FOOD) { + if (canItem(initial, tile_south) && tile_south != Tile.FOOD) { item_key = getItemKey(x, y + .5, width) items[item_key] = {type, pos: {x, y: y + .5}} } let tile_east = getPoint(width, height, data, x + 1, y) - if (canItem(tile_east) && tile_east != Tile.FOOD) { + if (canItem(initial, tile_east) && tile_east != Tile.FOOD) { item_key = getItemKey(x + .5, y, width) items[item_key] = {type, pos: {x: x + .5, y}} } @@ -166,7 +166,7 @@ export const getMap = (mapId: number): Map | undefined => { } export const checkMap = (map: Map): [boolean, string | Vec2[]] => { - let spawns = new Array(5).fill(undefined) + let spawns = new Array(6).fill(undefined) let hasFood = false let hasThicc = false let hasInitial = false @@ -215,6 +215,11 @@ export const checkMap = (map: Map): [boolean, string | Vec2[]] => { return [false, "Map cannot have duplicate spawns"] spawns[SpawnIndex.GHOST_SPAWN] = {x, y} break + case Tile.GHOST_EXIT: + if (spawns[SpawnIndex.GHOST_EXIT]) + return [false, "Map cannot have duplicate spawns"] + spawns[SpawnIndex.GHOST_EXIT] = {x, y} + break } } diff --git a/client/src/renderer.ts b/client/src/renderer.ts index cf3189a..3c27297 100644 --- a/client/src/renderer.ts +++ b/client/src/renderer.ts @@ -1,5 +1,5 @@ import { getMap } from "./map.js"; -import { Items, Players, Rotation, ItemType, Map, Wall, GameState, Tile, ATLAS_TILE_WIDTH, Ghosts, Ghost, GhostType, GhostState } from "./types.js"; +import { Items, Players, Rotation, ItemType, Map, Wall, GameState, Tile, ATLAS_TILE_WIDTH, Ghosts, Ghost, GhostType, GhostState, INTRO_AUDIO, DEATH_AUDIO, MOVE_AUDIO, GHOST_AUDIO } from "./types.js"; const updateStyle = (width: number, height: number) => { @@ -114,32 +114,77 @@ const drawPlayers = ( ctx: CanvasRenderingContext2D, atlas: CanvasImageSource, players: Players, - frame: number + frame: number, + map: Map, + drawBig: boolean ) => { - let atlas_frames: [number, number][] = [ - [0, 2], - [1, 2], - [2, 2], + const defaultFrame: [number, number] = [0, 3] + + const movingFrames: [number, number][] = [ [0, 3], [1, 3], + [2, 3], + [3, 3], + [4, 3], + [3, 3], + [2, 3], + [1, 3], + ] + + const deathFrames: [number, number][] = [ [0, 3], + [4, 2], + [3, 2], [2, 2], [1, 2], + [0, 2], + ] + + const playerHues: string[] = [ + '#d93030', + '#e6c147', + '#eb42d4', + '#425eeb' ] + let hueIndex = 0 for (let id in players) { let player = players[id] if (!player) continue - let atlasIndex = atlas_frames[0] - if (player.moving) { - atlasIndex = atlas_frames[Math.floor(frame / 2) % atlas_frames.length] + if ( + drawBig && player.thiccLeft == 0 || + !drawBig && player.thiccLeft > 0 + ) { + hueIndex++ + continue + } + + let atlasIndex = defaultFrame + if (player.dead) { + atlasIndex = deathFrames[Math.floor(player.framesDead / 20)] + } else if (player.moving) { + atlasIndex = movingFrames[Math.floor(frame / 2) % movingFrames.length] + } + + if (!atlasIndex) { + hueIndex++ + continue + } + + let rot = player.moveRotation + if (rot == Rotation.NOTHING) { + if (player.pos.x < map.width / 2) { + rot = Rotation.EAST + } else { + rot = Rotation.WEST + } } let rotation: number - switch (player.moveRotation) { + switch (rot) { case Rotation.NORTH: rotation = 270 break @@ -150,21 +195,23 @@ const drawPlayers = ( rotation = 180 break case Rotation.EAST: - default: rotation = 0 break } - drawSprite ( + drawSpriteHue ( ctx, player.pos.x, player.pos.y, - 1, + player.thiccLeft > 0 ? 2 : 1, atlas, atlasIndex, ATLAS_TILE_WIDTH, - rotation + rotation, + playerHues[hueIndex] ) + + hueIndex++ } } @@ -244,7 +291,7 @@ const drawGhosts = ( ghost.pos.y, 1, atlas, - [4, 3], + [3, 1], ATLAS_TILE_WIDTH, 0 ) @@ -280,15 +327,15 @@ const drawItems = ( switch (item.type) { case ItemType.DOT: width = .2 - atlasIndex = [2, 3] + atlasIndex = [4, 3] break case ItemType.THICC_DOT: width = .4 - atlasIndex = [2, 3] + atlasIndex = [4, 3] break case ItemType.FOOD: width = 1 - atlasIndex = [3, 3] + atlasIndex = [4, 1] break default: continue @@ -405,7 +452,7 @@ const drawMapCanvas = ( } -const draw_debug_sprites = ( +const drawDebugSprites = ( ctx: CanvasRenderingContext2D, atlas: CanvasImageSource, map: Map @@ -429,27 +476,30 @@ const draw_debug_sprites = ( case Tile.GHOST_SPAWN: atlasIndex = [3, 0] break + case Tile.GHOST_EXIT: + atlasIndex = [5, 0] + break case Tile.FOOD: - atlasIndex = [3, 3] + atlasIndex = [4, 1] break case Tile.PLAYER_SPAWN_1: - atlasIndex = [3, 1] + atlasIndex = [5, 1] break case Tile.PLAYER_SPAWN_2: - atlasIndex = [4, 1] + atlasIndex = [5, 2] break case Tile.PLAYER_SPAWN_3: - atlasIndex = [3, 2] + atlasIndex = [5, 3] break case Tile.PLAYER_SPAWN_4: - atlasIndex = [4, 2] + atlasIndex = [5, 4] break case Tile.THICC_DOT: - atlasIndex = [2, 3] + atlasIndex = [4, 3] size = .4 break case Tile.INITIAL_DOT: - atlasIndex = [2, 3] + atlasIndex = [4, 3] size = .2 break } @@ -483,9 +533,10 @@ const drawMap = ( mapCanvas.height = map.height * ATLAS_TILE_WIDTH let map_ctx = mapCanvas.getContext("2d") + map_ctx.imageSmoothingEnabled = false drawMapCanvas(map_ctx, atlas, map) if (editor) { - draw_debug_sprites(map_ctx, atlas, map) + drawDebugSprites(map_ctx, atlas, map) } } @@ -497,17 +548,73 @@ const drawMap = ( } +const updateAudio = (state: GameState) => { + if (state.roundTimer < 60 * 5) { + if (!INTRO_AUDIO.getPlaying()) { + INTRO_AUDIO.play(false) + } + } else { + if (INTRO_AUDIO.getPlaying()) { + INTRO_AUDIO.stop() + } + } + + let moving = false + for (let id in state.players) { + let player = state.players[id] + + if (player.dead && player.framesDead == 0) { + DEATH_AUDIO.play(false) + } + + moving ||= player.atePellets > 0 + + } + + if (moving && state.endRoundTimer === undefined) { + if (!MOVE_AUDIO.getPlaying()) { + MOVE_AUDIO.play(true) + } + } else { + if (MOVE_AUDIO.getPlaying()) { + MOVE_AUDIO.stop() + } + } + + if (state.roundTimer > 60 * 5 && state.endRoundTimer === undefined) { + if (!GHOST_AUDIO.getPlaying()) { + GHOST_AUDIO.play(true) + } + } else { + if (GHOST_AUDIO.getPlaying()) { + GHOST_AUDIO.stop() + } + } +} + let lastMapDrawn: number | undefined export const startGraphicsUpdater = () => { let canvas = document.getElementById("canvas") as HTMLCanvasElement let atlas = document.getElementById("atlas") as HTMLImageElement + let lobby = document.getElementById("lobby") return ( data: GameState, frame: number, editor: boolean = false ) => { + + if (!data.started) { + canvas.style.display = 'none' + if (lobby) + lobby.style.display = '' + return + } else { + canvas.style.display = '' + if (lobby) + lobby.style.display = 'none' + } let map = getMap(data.mapId) @@ -520,14 +627,20 @@ export const startGraphicsUpdater = () => { } let ctx = canvas.getContext("2d") + ctx.imageSmoothingEnabled = false ctx.clearRect(0, 0, canvas.width, canvas.height) drawMap(ctx, atlas, map, lastMapDrawn, editor) drawItems(ctx, atlas, data.items) drawGhosts(ctx, atlas, data.ghosts) - drawPlayers(ctx, atlas, data.players, frame) + drawPlayers(ctx, atlas, data.players, frame, map, false) + drawPlayers(ctx, atlas, data.players, frame, map, true) updateStyle(map.width, map.height) + if (!editor) { + updateAudio(data) + } + lastMapDrawn = map.id } diff --git a/client/src/types.ts b/client/src/types.ts index 76f5116..01f4e3a 100644 --- a/client/src/types.ts +++ b/client/src/types.ts @@ -2,10 +2,84 @@ export const ATLAS_TILE_WIDTH = 32 export const GAME_MAP_COUNT = 4 +const loadAudio = (src: string, callback: (node: GameAudio) => void) => { + + let actx = new AudioContext() + + fetch(src, {mode: "cors"}) + .then((resp) => { + return resp.arrayBuffer() + }) + .then((ebuf) => { + actx.decodeAudioData(ebuf, (abuf) => { + + var sourceNode = null, + startedAt = 0, + pausedAt = 0, + playing = false + + var play = (loop: boolean) => { + var offset = pausedAt + + sourceNode = actx.createBufferSource() + sourceNode.connect(actx.destination) + sourceNode.buffer = abuf + sourceNode.start(0, offset) + sourceNode.loop = loop + + startedAt = actx.currentTime - offset + pausedAt = 0 + playing = true + } + + var pause = () => { + var elapsed = actx.currentTime - startedAt + stop() + pausedAt = elapsed + }; + + var stop = () => { + if (sourceNode) { + sourceNode.disconnect() + sourceNode.stop(0) + sourceNode = null + } + pausedAt = 0 + startedAt = 0 + playing = false + } + + var getPlaying = (): boolean => { + return playing + } + + callback({play, pause, stop, getPlaying}) + }) + }) +} + +let EMPTY_AUDIO: GameAudio = { + play: (_loop: boolean) => {}, + pause: () => {}, + stop: () => {}, + getPlaying: () => true, +} + +export var INTRO_AUDIO = EMPTY_AUDIO +export var DEATH_AUDIO = EMPTY_AUDIO +export var MOVE_AUDIO = EMPTY_AUDIO +export var GHOST_AUDIO = EMPTY_AUDIO + +loadAudio('sfx/intro.mp3', (node) => INTRO_AUDIO = node) +loadAudio('sfx/death.mp3', (node) => DEATH_AUDIO = node) +loadAudio('sfx/move.wav', (node) => MOVE_AUDIO = node) +loadAudio('sfx/ghost.wav', (node) => GHOST_AUDIO = node) + export enum Tile { EMPTY = 0, WALL = 1, GHOST_WALL = 2, + GHOST_EXIT = 11, FOOD = 3, PLAYER_SPAWN_1 = 4, PLAYER_SPAWN_2 = 5, @@ -68,8 +142,11 @@ export type Ghost = { pos: Vec2, type: GhostType, target: Vec2, + targetQueue: Vec2[], state: GhostState, currentDirection: Rotation, + hasRespawned: boolean, + framesInBox: number, } export type KeyMap = { @@ -104,8 +181,14 @@ export type Player = { pos: Vec2, moveRotation: Rotation, inputRotation: Rotation, + velocityRotation: Rotation, moving: boolean, - name?: string + thiccLeft: number, + name?: string, + dead: boolean, + framesDead: number, + atePellets: number, + } export type PlayerInput = { @@ -151,7 +234,8 @@ export enum SpawnIndex { PAC_SPAWN_2 = 2, PAC_SPAWN_3 = 3, PAC_SPAWN_4 = 4, - GHOST_SPAWN = 0 + GHOST_SPAWN = 0, + GHOST_EXIT = 5 } export type Map = { @@ -177,10 +261,26 @@ export type GameState = { items: Items, frame: number, rng: number, - mapId: number | undefined + mapId: number | undefined, + roundTimer: number, + endRoundTimer: number } export type Frame = { data: GameState, input: Input } + +export type GameAudio = { + getPlaying: () => boolean, + play: (loop: boolean) => void, + pause: () => void, + stop: () => void +} + +export type BoundingBox = { + x: number, + y: number, + w: number, + h: number +} |