audio, finalize gameplay, wrap around map, stuff

This commit is contained in:
Freya Murphy 2023-06-29 11:40:46 -04:00
parent 0281233cbd
commit f5fcce110a
19 changed files with 743 additions and 120 deletions

View file

@ -11,10 +11,13 @@ canvas {
padding: 1rem; padding: 1rem;
} }
#mapgen { #mapgen, #mapload {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: absolute; }
#mapload {
margin-top: 3rem;
} }
#export { #export {

View file

@ -19,10 +19,16 @@ body {
#center { #center {
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
display: flex; position: absolute;
}
#center-inner {
width: 100%;
height: 100%;
position: relative;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
position: absolute; display: flex;
flex-direction: column; flex-direction: column;
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View file

@ -8,6 +8,7 @@
<canvas id="canvas" style="display: none;"></canvas> <canvas id="canvas" style="display: none;"></canvas>
<style id="style"></style> <style id="style"></style>
<div id="center"> <div id="center">
<div id="center-inner">
<form id="join" autocomplete="off"> <form id="join" autocomplete="off">
<input type="text" id="room_code" name="room_code" placeholder="Room Code"> <input type="text" id="room_code" name="room_code" placeholder="Room Code">
<input type="text" id="player_name" name="name" placeholder="Player Name"> <input type="text" id="player_name" name="name" placeholder="Player Name">
@ -20,6 +21,7 @@
<input type="button" id="start" value="Start Game"/> <input type="button" id="start" value="Start Game"/>
</div> </div>
</div> </div>
</div>
<script src="js/main.js" type="module"></script> <script src="js/main.js" type="module"></script>
</body> </body>
</html> </html>

View file

@ -10,20 +10,27 @@
<canvas id="canvas" style="display: none;"></canvas> <canvas id="canvas" style="display: none;"></canvas>
<style id="style"></style> <style id="style"></style>
<div id="center"> <div id="center">
<div id="center-inner">
<form id="mapgen" autocomplete="off"> <form id="mapgen" autocomplete="off">
<input type="text" id="width" name="width" placeholder="Map Width"> <input type="text" id="width" name="width" placeholder="Map Width">
<input type="text" id="height" name="height" placeholder="Map Height"> <input type="text" id="height" name="height" placeholder="Map Height">
<input type="submit" value="Create"/> <input type="submit" value="Create"/>
</form> </form>
<form id="mapload" autocomplete="off">
<input type="text" id="data" name="data" placeholder="Map Data">
<input type="submit" value="Load"/>
</form>
<div id="popup" style="display: none"> <div id="popup" style="display: none">
<input type="button" id="close" value="x" onclick="document.getElementById('popup').style.display = 'none'"> <input type="button" id="close" value="x" onclick="document.getElementById('popup').style.display = 'none'">
<textarea id="textarea" disabled='true'></textarea> <textarea id="textarea" disabled='true'></textarea>
<input type="button" id="copy" value="Copy to clipboard"> <input type="button" id="copy" value="Copy to clipboard">
</div> </div>
</div> </div>
</div>
<div id="sidebar" style="display: none;"> <div id="sidebar" style="display: none;">
<p>W: Place Wall</p> <p>W: Place Wall</p>
<p>G: Place Ghost Wall</p> <p>G: Place Ghost Only Wall</p>
<p>R: Place Ghost Exit</p>
<p>H: Place Ghost Spawn</p> <p>H: Place Ghost Spawn</p>
<p>F: Place Food</p> <p>F: Place Food</p>
<p>1: Place Pac Spawn 1</p> <p>1: Place Pac Spawn 1</p>

BIN
client/sfx/death.mp3 Normal file

Binary file not shown.

BIN
client/sfx/ghost.wav Normal file

Binary file not shown.

BIN
client/sfx/intro.mp3 Normal file

Binary file not shown.

BIN
client/sfx/move.wav Normal file

Binary file not shown.

View file

@ -1,9 +1,10 @@
import { InitialState } from "./logic/logic.js" 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 { startGraphicsUpdater } from "./renderer.js"
import { GameState, Vec2, Tile } from "./types.js" import { GameState, Vec2, Tile } from "./types.js"
const mapgen = document.getElementById("mapgen") const mapgen = document.getElementById("mapgen")
const mapload = document.getElementById("mapload")
const sidebar = document.getElementById("sidebar") const sidebar = document.getElementById("sidebar")
mapgen.onsubmit = async (event) => { mapgen.onsubmit = async (event) => {
@ -21,10 +22,30 @@ mapgen.onsubmit = async (event) => {
} }
mapgen.style.display = "none" mapgen.style.display = "none"
mapload.style.display = "none"
runMapEditor(width, height) 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 = () => { const startKeyListener = () => {
let keys = {} let keys = {}
@ -93,6 +114,8 @@ const checkInputs = (pressed: {[key: string]: boolean}): [Tile, boolean] => {
return [Tile.WALL, false] return [Tile.WALL, false]
} else if (pressed["KeyG"]) { } else if (pressed["KeyG"]) {
return [Tile.GHOST_WALL, false] return [Tile.GHOST_WALL, false]
} else if (pressed["KeyR"]) {
return [Tile.GHOST_EXIT, true]
} else if (pressed["KeyH"]) { } else if (pressed["KeyH"]) {
return [Tile.GHOST_SPAWN, true] return [Tile.GHOST_SPAWN, true]
} else if (pressed["KeyF"]) { } else if (pressed["KeyF"]) {
@ -121,12 +144,11 @@ const checkBounds = (tilePos: Vec2, width: number, height: number) => {
return true return true
} }
const runMapEditor = (width: number, height: number) => { const runMapEditor = (width: number, height: number, map_data: number[] = undefined) => {
sidebar.style.display = "" 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 map = genMap(width, height, data, Tile.EMPTY)
let state: GameState = structuredClone(InitialState); let state: GameState = structuredClone(InitialState);
@ -168,6 +190,13 @@ const runMapEditor = (width: number, height: number) => {
requestAnimationFrame(loop) requestAnimationFrame(loop)
document.getElementById("export").onclick = () => { document.getElementById("export").onclick = () => {
let [success, status] = checkMap(map)
if (!success) {
alert(status)
return
}
let encoded = compressMap(map) let encoded = compressMap(map)
document.getElementById("textarea").textContent = encoded document.getElementById("textarea").textContent = encoded
document.getElementById("popup").style.display = 'flex' document.getElementById("popup").style.display = 'flex'

View file

@ -1,7 +1,73 @@
import { getMap } from "../map.js"; import { getMap } from "../map.js";
import { Map, Vec2, GhostType, GameState, SpawnIndex, Player, Rotation, GhostState, Tile, Ghost } from "../types.js"; import { Map, Vec2, GhostType, GameState, SpawnIndex, Player, Rotation, GhostState, Tile, Ghost } from "../types.js";
import { random } from "./logic.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 => { const diff = (a: Vec2, b:Vec2): Vec2 => {
return {x: a.x - b.x, y: a.y - b.y} 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 => { const pickTarget = (state: GameState, map: Map, type: GhostType): Vec2 => {
let ghost: Ghost = state.ghosts[type] let ghost: Ghost = state.ghosts[type]
let target = ghost.targetQueue.shift()
if (target) {
return target
}
switch (ghost.state) { switch (ghost.state) {
case GhostState.SCATTER: case GhostState.SCATTER:
return pickTargetScatter(state, type) return pickTargetScatter(state, type)
@ -95,17 +167,105 @@ const pickTarget = (state: GameState, map: Map, type: GhostType): Vec2 => {
} }
} }
const flipRot = (rot: Rotation) => { const checkIfEaten = (ghost: Ghost, state: GameState): boolean => {
switch (rot) {
case Rotation.NORTH: if (ghost.state != GhostState.SCARED) {
return Rotation.SOUTH return false
case Rotation.SOUTH:
return Rotation.NORTH
case Rotation.EAST:
return Rotation.WEST
case Rotation.WEST:
return Rotation.EAST
} }
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) => { const updateGhost = (state: GameState, map: Map, type: GhostType) => {
@ -115,22 +275,45 @@ const updateGhost = (state: GameState, map: Map, type: GhostType) => {
ghost = { ghost = {
pos: structuredClone(map.spawns[SpawnIndex.GHOST_SPAWN]), pos: structuredClone(map.spawns[SpawnIndex.GHOST_SPAWN]),
target: structuredClone(map.spawns[SpawnIndex.GHOST_SPAWN]), target: structuredClone(map.spawns[SpawnIndex.GHOST_SPAWN]),
targetQueue: [],
type, type,
state: GhostState.SCARED, state: GhostState.SCATTER,
currentDirection: Rotation.EAST, currentDirection: Rotation.EAST,
hasRespawned: false,
framesInBox: 0,
} }
state.ghosts[type] = ghost 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)) { if (isStablePos(ghost.pos)) {
ghost.pos = roundPos(ghost.pos) ghost.pos = roundPos(ghost.pos)
let front = getTileFrontWithRot(map, ghost.pos, ghost.currentDirection) == Tile.WALL let newPath = getGhostPath(ghost, map)
let north = getTile(map, ghost.pos, 0, -1) != Tile.WALL if (newPath) {
let east = getTile(map, ghost.pos, 1, 0) != Tile.WALL ghost.targetQueue = newPath
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 = let isIntersection =
(north && east) || (north && east) ||
@ -138,18 +321,20 @@ const updateGhost = (state: GameState, map: Map, type: GhostType) => {
(south && west) || (south && west) ||
(west && north) (west && north)
if (!isIntersection && front) { if (isIntersection || !front || (isPathing && vec2eq(ghost.pos, ghost.target))) {
ghost.currentDirection = flipRot(ghost.currentDirection)
} else if (isIntersection) {
let target = pickTarget(state, map, type) let target = pickTarget(state, map, type)
ghost.target = target ghost.target = target
if ((!isIntersection && !front) || isPathing) {
ghost.currentDirection = undefined
}
let newRot = ghost.currentDirection let newRot = ghost.currentDirection
let min = undefined let min = undefined
if (north && ghost.currentDirection !== Rotation.SOUTH) { if (north && ghost.currentDirection !== Rotation.SOUTH) {
let d = dist({x: ghost.pos.x, y: ghost.pos.y - 1}, target) let d = dist({x: ghost.pos.x, y: ghost.pos.y - 1}, target)
if (!min || min > d) { if (min === undefined || min > d) {
min = d min = d
newRot = Rotation.NORTH newRot = Rotation.NORTH
} }
@ -157,7 +342,7 @@ const updateGhost = (state: GameState, map: Map, type: GhostType) => {
if (east && ghost.currentDirection !== Rotation.WEST) { if (east && ghost.currentDirection !== Rotation.WEST) {
let d = dist({x: ghost.pos.x + 1, y: ghost.pos.y}, target) let d = dist({x: ghost.pos.x + 1, y: ghost.pos.y}, target)
if (!min || min > d) { if (min === undefined || min > d) {
min = d min = d
newRot = Rotation.EAST newRot = Rotation.EAST
} }
@ -165,7 +350,7 @@ const updateGhost = (state: GameState, map: Map, type: GhostType) => {
if (south && ghost.currentDirection !== Rotation.NORTH) { if (south && ghost.currentDirection !== Rotation.NORTH) {
let d = dist({x: ghost.pos.x, y: ghost.pos.y + 1}, target) let d = dist({x: ghost.pos.x, y: ghost.pos.y + 1}, target)
if (!min || min > d) { if (min === undefined || min > d) {
min = d min = d
newRot = Rotation.SOUTH newRot = Rotation.SOUTH
} }
@ -173,7 +358,7 @@ const updateGhost = (state: GameState, map: Map, type: GhostType) => {
if (west && ghost.currentDirection !== Rotation.EAST) { if (west && ghost.currentDirection !== Rotation.EAST) {
let d = dist({x: ghost.pos.x - 1, y: ghost.pos.y}, target) let d = dist({x: ghost.pos.x - 1, y: ghost.pos.y}, target)
if (!min || min > d) { if (min === undefined || min > d) {
min = d min = d
newRot = Rotation.WEST newRot = Rotation.WEST
} }
@ -184,6 +369,8 @@ 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) => { export const updateGhosts = (state: GameState) => {

View file

@ -1,5 +1,5 @@
import { getMap, getItemKey } from "../map.js" 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 => { const ceilHalf = (n: number): number => {
return Math.ceil(n*2)/2 return Math.ceil(n*2)/2
@ -13,9 +13,23 @@ const eatItems = (data: GameState, map: Map, player: Player) => {
let pos = player.pos 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 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) { for (let y = ceilHalf(pos.y-.5); y <= floorHalf(pos.y+.5); y += .5) {
let item_key = getItemKey(x, y, map.width) 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] delete data.items[item_key]
} }
} }

View file

@ -3,7 +3,7 @@ import { updatePlayers } from "./players.js"
import { updateUI } from "./ui.js" import { updateUI } from "./ui.js"
import { updateMovement } from "./movement.js" import { updateMovement } from "./movement.js"
import { updateItems } from "./items.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"; import { updateGhosts } from "./ai.js";
export const InitialState: GameState = { export const InitialState: GameState = {
@ -14,7 +14,9 @@ export const InitialState: GameState = {
items: {}, items: {},
mapId: undefined, mapId: undefined,
frame: 0, frame: 0,
rng: 0 rng: 0,
roundTimer: 0,
endRoundTimer: undefined
} }
export const random = (state: GameState): number => { export const random = (state: GameState): number => {
@ -32,36 +34,85 @@ export const onLogic = (
random(data) random(data)
let startPressed = updatePlayers(data, input); 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) { if (data.started) {
data.roundTimer++
if (data.roundTimer < 60 * 5) {
// uh do nothing just draw shit
} else if (playersLeft > 1) {
updateMovement(data) updateMovement(data)
updateItems(data) updateItems(data)
updateGhosts(data) updateGhosts(data)
} else {
if (data.endRoundTimer === undefined) {
data.endRoundTimer = 60 * 5
}
data.endRoundTimer--
if (data.endRoundTimer < 1) {
nextMap(data)
}
}
} else { } else {
updateUI(data) updateUI(data)
} }
if (startPressed && !data.started) { if (startPressed && !data.started && Object.values(data.players).length > 1) {
initMap(data, 0) nextMap(data)
data.started = true; data.started = true;
} }
let map = getMap(data.mapId)
if (map && Object.keys(data.items).length < 1) {
data.items = genItems(map, false)
}
return data 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
gameData.mapId = mapId } else {
gameData.mapId++
let map = getMap(mapId) }
// if (!map) {
// let {width, height, data} = decompressMap(maps[mapId]) if (gameData.mapId > 3) {
// map = genMap(width, height, data, mapId) gameData.mapId = undefined
// } gameData.started = false
return
gameData.items = genItems(map) }
gameData.ghosts = [undefined, undefined, undefined, undefined]
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
} }

View file

@ -1,5 +1,5 @@
import { getMap } from "../map.js" 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 export const MOVE_SPEED = .08333
@ -20,10 +20,18 @@ export const getTile = (
): number => { ): number => {
let x = Math.round(pos.x + ox) let x = Math.round(pos.x + ox)
let y = Math.round(pos.y + oy) 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] 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 = ( export const getTileFrontWithRot = (
map: Map, map: Map,
pos: Vec2, pos: Vec2,
@ -86,10 +94,11 @@ const updateMovementForPlayer = (
let inputRot = getRot(inputKey) let inputRot = getRot(inputKey)
let moveRot = player.moveRotation let moveRot = player.moveRotation
let speed = MOVE_SPEED
let currentPosition = player.pos let currentPosition = player.pos
let turningFrontTile = getTileFrontWithRot(map, currentPosition, inputRot) 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 inputRot = Rotation.NOTHING
} }
@ -97,24 +106,110 @@ const updateMovementForPlayer = (
player.inputRotation = inputRot 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) currentPosition = roundPos(currentPosition)
player.moveRotation = inputRot player.moveRotation = inputRot
moveRot = inputRot moveRot = inputRot
} }
let movePos = structuredClone(currentPosition) let movePos = structuredClone(currentPosition)
incrementPos(movePos, moveRot, MOVE_SPEED) incrementPos(movePos, moveRot, speed)
wrapPos(movePos, map)
let frontTile = getTileFrontWithRot(map, currentPosition, moveRot) 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.pos = movePos
player.moving = true player.moving = true
} else { } else {
player.pos = roundPos(currentPosition) player.pos = roundPos(currentPosition)
player.moving = false 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,10 +218,16 @@ export const updateMovement = (data: GameState) => {
let map = getMap(data.mapId) let map = getMap(data.mapId)
if (!map) return if (!map) return
updateCollision(data)
for (const id in data.players) { for (const id in data.players) {
const player = data.players[id] const player = data.players[id]
if (player.thiccLeft > 0) {
player.thiccLeft--
}
if(!player) { if(!player) {
continue continue
} }

View file

@ -34,7 +34,12 @@ export const updatePlayers = (data: GameState, input: Input) => {
pos: {x: 1, y: 1}, pos: {x: 1, y: 1},
inputRotation: Rotation.EAST, inputRotation: Rotation.EAST,
moveRotation: Rotation.NOTHING, moveRotation: Rotation.NOTHING,
velocityRotation: Rotation.NOTHING,
moving: false, moving: false,
thiccLeft: 0,
dead: false,
framesDead: 0,
atePellets: 0
}; };
} }

View file

@ -9,7 +9,7 @@ const lobby = document.getElementById("lobby")
const mapeditor = document.getElementById("mapeditor") const mapeditor = document.getElementById("mapeditor")
const maps = { const maps = {
[0]: 'EwRgPqYgNDew+TEuW6AGT2u59mPI/PeLZclSys1AhSgThJcJb2bdq7p5rupV2DYaVFCKI9HwFDiWACyxyK5WpCqArOPSCeuqfUnzDwaGbMyT3FAGZT0ABznD9ybWv0LLq61kz4S0M9WRMANnVVDUi1AHYdQxt+dyNEhJNiNk8A3npkiVSmcUEM6E5C/wL86rlivLqxaVymltggA=' [0]: 'MwRgPgTOIDS/dEOU1L1sxgDDX+9CDijSTyzLcFrVabN7EnYXCUnaBOK31xCMyEUSPfg3FZ20trTlTJnPpX4iV6tRLpCOC7bslbVvNQBY41SxesBWE/bYHpTnFPkZjFResG/hpb003RGBEMR0YQQAOSIjYcNDDRntAgNi/Txl/L2CkozgXfWdgtTUANmsQKyrrAHYHFLiI2T1870KgkkzS7N6CLUdmpvbipyV4hv7pQVSe8PGittHllo7J9Z7N5Y9W3Z39uCA'
} }
join.onsubmit = async function(event) { join.onsubmit = async function(event) {

View file

@ -91,9 +91,9 @@ const genWalls = (
return walls 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 width = map.width
let height = map.height let height = map.height
@ -104,7 +104,7 @@ export const genItems = (map: Map): Items => {
for (let y = 0; y < height; y++) { for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) { for (let x = 0; x < width; x++) {
let tile = getPoint(width, height, data, x, y) let tile = getPoint(width, height, data, x, y)
if (!canItem(tile)) continue if (!canItem(initial, tile)) continue
let item_key = getItemKey(x, y, width) let item_key = getItemKey(x, y, width)
@ -123,13 +123,13 @@ export const genItems = (map: Map): Items => {
type = ItemType.DOT type = ItemType.DOT
let tile_south = getPoint(width, height, data, x, y + 1) 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) item_key = getItemKey(x, y + .5, width)
items[item_key] = {type, pos: {x, y: y + .5}} items[item_key] = {type, pos: {x, y: y + .5}}
} }
let tile_east = getPoint(width, height, data, x + 1, y) 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) item_key = getItemKey(x + .5, y, width)
items[item_key] = {type, pos: {x: x + .5, y}} 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[]] => { 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 hasFood = false
let hasThicc = false let hasThicc = false
let hasInitial = false let hasInitial = false
@ -215,6 +215,11 @@ export const checkMap = (map: Map): [boolean, string | Vec2[]] => {
return [false, "Map cannot have duplicate spawns"] return [false, "Map cannot have duplicate spawns"]
spawns[SpawnIndex.GHOST_SPAWN] = {x, y} spawns[SpawnIndex.GHOST_SPAWN] = {x, y}
break break
case Tile.GHOST_EXIT:
if (spawns[SpawnIndex.GHOST_EXIT])
return [false, "Map cannot have duplicate spawns"]
spawns[SpawnIndex.GHOST_EXIT] = {x, y}
break
} }
} }

View file

@ -1,5 +1,5 @@
import { getMap } from "./map.js"; 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) => { const updateStyle = (width: number, height: number) => {
@ -114,32 +114,77 @@ const drawPlayers = (
ctx: CanvasRenderingContext2D, ctx: CanvasRenderingContext2D,
atlas: CanvasImageSource, atlas: CanvasImageSource,
players: Players, players: Players,
frame: number frame: number,
map: Map,
drawBig: boolean
) => { ) => {
let atlas_frames: [number, number][] = [ const defaultFrame: [number, number] = [0, 3]
[0, 2],
[1, 2], const movingFrames: [number, number][] = [
[2, 2],
[0, 3], [0, 3],
[1, 3], [1, 3],
[0, 3], [2, 3],
[2, 2], [3, 3],
[1, 2], [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) { for (let id in players) {
let player = players[id] let player = players[id]
if (!player) continue if (!player) continue
let atlasIndex = atlas_frames[0] if (
if (player.moving) { drawBig && player.thiccLeft == 0 ||
atlasIndex = atlas_frames[Math.floor(frame / 2) % atlas_frames.length] !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 let rotation: number
switch (player.moveRotation) { switch (rot) {
case Rotation.NORTH: case Rotation.NORTH:
rotation = 270 rotation = 270
break break
@ -150,21 +195,23 @@ const drawPlayers = (
rotation = 180 rotation = 180
break break
case Rotation.EAST: case Rotation.EAST:
default:
rotation = 0 rotation = 0
break break
} }
drawSprite ( drawSpriteHue (
ctx, ctx,
player.pos.x, player.pos.x,
player.pos.y, player.pos.y,
1, player.thiccLeft > 0 ? 2 : 1,
atlas, atlas,
atlasIndex, atlasIndex,
ATLAS_TILE_WIDTH, ATLAS_TILE_WIDTH,
rotation rotation,
playerHues[hueIndex]
) )
hueIndex++
} }
} }
@ -244,7 +291,7 @@ const drawGhosts = (
ghost.pos.y, ghost.pos.y,
1, 1,
atlas, atlas,
[4, 3], [3, 1],
ATLAS_TILE_WIDTH, ATLAS_TILE_WIDTH,
0 0
) )
@ -280,15 +327,15 @@ const drawItems = (
switch (item.type) { switch (item.type) {
case ItemType.DOT: case ItemType.DOT:
width = .2 width = .2
atlasIndex = [2, 3] atlasIndex = [4, 3]
break break
case ItemType.THICC_DOT: case ItemType.THICC_DOT:
width = .4 width = .4
atlasIndex = [2, 3] atlasIndex = [4, 3]
break break
case ItemType.FOOD: case ItemType.FOOD:
width = 1 width = 1
atlasIndex = [3, 3] atlasIndex = [4, 1]
break break
default: default:
continue continue
@ -405,7 +452,7 @@ const drawMapCanvas = (
} }
const draw_debug_sprites = ( const drawDebugSprites = (
ctx: CanvasRenderingContext2D, ctx: CanvasRenderingContext2D,
atlas: CanvasImageSource, atlas: CanvasImageSource,
map: Map map: Map
@ -429,27 +476,30 @@ const draw_debug_sprites = (
case Tile.GHOST_SPAWN: case Tile.GHOST_SPAWN:
atlasIndex = [3, 0] atlasIndex = [3, 0]
break break
case Tile.GHOST_EXIT:
atlasIndex = [5, 0]
break
case Tile.FOOD: case Tile.FOOD:
atlasIndex = [3, 3]
break
case Tile.PLAYER_SPAWN_1:
atlasIndex = [3, 1]
break
case Tile.PLAYER_SPAWN_2:
atlasIndex = [4, 1] atlasIndex = [4, 1]
break break
case Tile.PLAYER_SPAWN_1:
atlasIndex = [5, 1]
break
case Tile.PLAYER_SPAWN_2:
atlasIndex = [5, 2]
break
case Tile.PLAYER_SPAWN_3: case Tile.PLAYER_SPAWN_3:
atlasIndex = [3, 2] atlasIndex = [5, 3]
break break
case Tile.PLAYER_SPAWN_4: case Tile.PLAYER_SPAWN_4:
atlasIndex = [4, 2] atlasIndex = [5, 4]
break break
case Tile.THICC_DOT: case Tile.THICC_DOT:
atlasIndex = [2, 3] atlasIndex = [4, 3]
size = .4 size = .4
break break
case Tile.INITIAL_DOT: case Tile.INITIAL_DOT:
atlasIndex = [2, 3] atlasIndex = [4, 3]
size = .2 size = .2
break break
} }
@ -483,9 +533,10 @@ const drawMap = (
mapCanvas.height = map.height * ATLAS_TILE_WIDTH mapCanvas.height = map.height * ATLAS_TILE_WIDTH
let map_ctx = mapCanvas.getContext("2d") let map_ctx = mapCanvas.getContext("2d")
map_ctx.imageSmoothingEnabled = false
drawMapCanvas(map_ctx, atlas, map) drawMapCanvas(map_ctx, atlas, map)
if (editor) { if (editor) {
draw_debug_sprites(map_ctx, atlas, map) drawDebugSprites(map_ctx, atlas, map)
} }
} }
@ -497,11 +548,56 @@ 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 let lastMapDrawn: number | undefined
export const startGraphicsUpdater = () => { export const startGraphicsUpdater = () => {
let canvas = document.getElementById("canvas") as HTMLCanvasElement let canvas = document.getElementById("canvas") as HTMLCanvasElement
let atlas = document.getElementById("atlas") as HTMLImageElement let atlas = document.getElementById("atlas") as HTMLImageElement
let lobby = document.getElementById("lobby")
return ( return (
data: GameState, data: GameState,
@ -509,6 +605,17 @@ export const startGraphicsUpdater = () => {
editor: boolean = false 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) let map = getMap(data.mapId)
if (!map) return if (!map) return
@ -520,14 +627,20 @@ export const startGraphicsUpdater = () => {
} }
let ctx = canvas.getContext("2d") let ctx = canvas.getContext("2d")
ctx.imageSmoothingEnabled = false
ctx.clearRect(0, 0, canvas.width, canvas.height) ctx.clearRect(0, 0, canvas.width, canvas.height)
drawMap(ctx, atlas, map, lastMapDrawn, editor) drawMap(ctx, atlas, map, lastMapDrawn, editor)
drawItems(ctx, atlas, data.items) drawItems(ctx, atlas, data.items)
drawGhosts(ctx, atlas, data.ghosts) 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) updateStyle(map.width, map.height)
if (!editor) {
updateAudio(data)
}
lastMapDrawn = map.id lastMapDrawn = map.id
} }

View file

@ -2,10 +2,84 @@
export const ATLAS_TILE_WIDTH = 32 export const ATLAS_TILE_WIDTH = 32
export const GAME_MAP_COUNT = 4 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 { export enum Tile {
EMPTY = 0, EMPTY = 0,
WALL = 1, WALL = 1,
GHOST_WALL = 2, GHOST_WALL = 2,
GHOST_EXIT = 11,
FOOD = 3, FOOD = 3,
PLAYER_SPAWN_1 = 4, PLAYER_SPAWN_1 = 4,
PLAYER_SPAWN_2 = 5, PLAYER_SPAWN_2 = 5,
@ -68,8 +142,11 @@ export type Ghost = {
pos: Vec2, pos: Vec2,
type: GhostType, type: GhostType,
target: Vec2, target: Vec2,
targetQueue: Vec2[],
state: GhostState, state: GhostState,
currentDirection: Rotation, currentDirection: Rotation,
hasRespawned: boolean,
framesInBox: number,
} }
export type KeyMap = { export type KeyMap = {
@ -104,8 +181,14 @@ export type Player = {
pos: Vec2, pos: Vec2,
moveRotation: Rotation, moveRotation: Rotation,
inputRotation: Rotation, inputRotation: Rotation,
velocityRotation: Rotation,
moving: boolean, moving: boolean,
name?: string thiccLeft: number,
name?: string,
dead: boolean,
framesDead: number,
atePellets: number,
} }
export type PlayerInput = { export type PlayerInput = {
@ -151,7 +234,8 @@ export enum SpawnIndex {
PAC_SPAWN_2 = 2, PAC_SPAWN_2 = 2,
PAC_SPAWN_3 = 3, PAC_SPAWN_3 = 3,
PAC_SPAWN_4 = 4, PAC_SPAWN_4 = 4,
GHOST_SPAWN = 0 GHOST_SPAWN = 0,
GHOST_EXIT = 5
} }
export type Map = { export type Map = {
@ -177,10 +261,26 @@ export type GameState = {
items: Items, items: Items,
frame: number, frame: number,
rng: number, rng: number,
mapId: number | undefined mapId: number | undefined,
roundTimer: number,
endRoundTimer: number
} }
export type Frame = { export type Frame = {
data: GameState, data: GameState,
input: Input 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
}