summaryrefslogtreecommitdiff
path: root/client/src
diff options
context:
space:
mode:
Diffstat (limited to 'client/src')
-rw-r--r--client/src/editor.ts37
-rw-r--r--client/src/logic/ai.ts237
-rw-r--r--client/src/logic/items.ts16
-rw-r--r--client/src/logic/logic.ts83
-rw-r--r--client/src/logic/movement.ts113
-rw-r--r--client/src/logic/players.ts5
-rw-r--r--client/src/main.ts2
-rw-r--r--client/src/map.ts17
-rw-r--r--client/src/renderer.ts169
-rw-r--r--client/src/types.ts106
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
+}