audio, finalize gameplay, wrap around map, stuff
This commit is contained in:
parent
0281233cbd
commit
f5fcce110a
19 changed files with 743 additions and 120 deletions
|
@ -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 {
|
||||||
|
|
|
@ -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 |
|
@ -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>
|
||||||
|
|
|
@ -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
BIN
client/sfx/death.mp3
Normal file
Binary file not shown.
BIN
client/sfx/ghost.wav
Normal file
BIN
client/sfx/ghost.wav
Normal file
Binary file not shown.
BIN
client/sfx/intro.mp3
Normal file
BIN
client/sfx/intro.mp3
Normal file
Binary file not shown.
BIN
client/sfx/move.wav
Normal file
BIN
client/sfx/move.wav
Normal file
Binary file not shown.
|
@ -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'
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue