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;
}
#mapgen {
#mapgen, #mapload {
display: flex;
flex-direction: column;
position: absolute;
}
#mapload {
margin-top: 3rem;
}
#export {

View file

@ -19,10 +19,16 @@ body {
#center {
width: 100vw;
height: 100vh;
display: flex;
position: absolute;
}
#center-inner {
width: 100%;
height: 100%;
position: relative;
align-items: center;
justify-content: center;
position: absolute;
display: flex;
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>
<style id="style"></style>
<div id="center">
<div id="center-inner">
<form id="join" autocomplete="off">
<input type="text" id="room_code" name="room_code" placeholder="Room Code">
<input type="text" id="player_name" name="name" placeholder="Player Name">
@ -20,6 +21,7 @@
<input type="button" id="start" value="Start Game"/>
</div>
</div>
</div>
<script src="js/main.js" type="module"></script>
</body>
</html>

View file

@ -10,20 +10,27 @@
<canvas id="canvas" style="display: none;"></canvas>
<style id="style"></style>
<div id="center">
<div id="center-inner">
<form id="mapgen" autocomplete="off">
<input type="text" id="width" name="width" placeholder="Map Width">
<input type="text" id="height" name="height" placeholder="Map Height">
<input type="submit" value="Create"/>
</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">
<input type="button" id="close" value="x" onclick="document.getElementById('popup').style.display = 'none'">
<textarea id="textarea" disabled='true'></textarea>
<input type="button" id="copy" value="Copy to clipboard">
</div>
</div>
</div>
<div id="sidebar" style="display: none;">
<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>F: Place Food</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 { 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'

View file

@ -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 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 newPath = getGhostPath(ghost, map)
if (newPath) {
ghost.targetQueue = newPath
}
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
}
@ -184,6 +369,8 @@ const updateGhost = (state: GameState, map: Map, type: GhostType) => {
}
incrementPos(ghost.pos, ghost.currentDirection, MOVE_SPEED)
wrapPos(ghost.pos, map)
updateKilled(ghost, state)
}
export const updateGhosts = (state: GameState) => {

View file

@ -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
@ -13,9 +13,23 @@ 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]
}
}

View file

@ -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) {
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)
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
}
let map = getMap(mapId)
// if (!map) {
// let {width, height, data} = decompressMap(maps[mapId])
// map = genMap(width, height, data, mapId)
// }
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 { 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,10 +218,16 @@ 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
}

View file

@ -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
};
}

View file

@ -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) {

View file

@ -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
}
}

View file

@ -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],
[0, 3],
[2, 2],
[1, 2],
[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]
break
case Tile.PLAYER_SPAWN_1:
atlasIndex = [3, 1]
break
case Tile.PLAYER_SPAWN_2:
atlasIndex = [4, 1]
break
case Tile.PLAYER_SPAWN_1:
atlasIndex = [5, 1]
break
case Tile.PLAYER_SPAWN_2:
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,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
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,
@ -509,6 +605,17 @@ export const startGraphicsUpdater = () => {
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)
if (!map) return
@ -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
}

View file

@ -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
}