ghost
This commit is contained in:
parent
97c78292fe
commit
0281233cbd
10 changed files with 576 additions and 105 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,2 +1,5 @@
|
|||
server/target
|
||||
client/js
|
||||
client/package.json
|
||||
client/package-lock.json
|
||||
client/node_modules
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 9.1 KiB |
|
@ -1,3 +1,4 @@
|
|||
import { InitialState } from "./logic/logic.js"
|
||||
import { genMap, compressMap, decompressMap } from "./map.js"
|
||||
import { startGraphicsUpdater } from "./renderer.js"
|
||||
import { GameState, Vec2, Tile } from "./types.js"
|
||||
|
@ -128,13 +129,9 @@ const runMapEditor = (width: number, height: number) => {
|
|||
|
||||
let map = genMap(width, height, data, Tile.EMPTY)
|
||||
|
||||
let state: GameState = {
|
||||
started: true,
|
||||
input: {},
|
||||
players: {},
|
||||
items: {},
|
||||
mapId: 0
|
||||
}
|
||||
let state: GameState = structuredClone(InitialState);
|
||||
state.mapId = 0;
|
||||
state.started = true;
|
||||
|
||||
let frame = 0
|
||||
const updateGraphics = startGraphicsUpdater()
|
||||
|
|
197
client/src/logic/ai.ts
Normal file
197
client/src/logic/ai.ts
Normal file
|
@ -0,0 +1,197 @@
|
|||
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";
|
||||
|
||||
const diff = (a: Vec2, b:Vec2): Vec2 => {
|
||||
return {x: a.x - b.x, y: a.y - b.y}
|
||||
}
|
||||
|
||||
const dist = (a: Vec2, b: Vec2): number => {
|
||||
return Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2))
|
||||
}
|
||||
|
||||
const trans = (pos: Vec2, rot: Rotation, dist: number): Vec2 => {
|
||||
switch (rot) {
|
||||
case Rotation.NORTH:
|
||||
case Rotation.NOTHING:
|
||||
return {x: pos.x - dist, y: pos.y - dist}
|
||||
case Rotation.EAST:
|
||||
return {x: pos.x + dist, y: pos.y}
|
||||
case Rotation.SOUTH:
|
||||
return {x: pos.x, y: pos.y + dist}
|
||||
case Rotation.WEST:
|
||||
return {x: pos.x - dist, y: pos.y}
|
||||
}
|
||||
}
|
||||
|
||||
const getNearestPlayer = (state: GameState, pos: Vec2): Player => {
|
||||
let min = undefined;
|
||||
let nearest = undefined;
|
||||
for (let id in state.players) {
|
||||
let player = state.players[id];
|
||||
if (!id) continue;
|
||||
|
||||
let d = dist(player.pos, pos)
|
||||
if (!min || min > d) {
|
||||
min = d
|
||||
nearest = player
|
||||
}
|
||||
}
|
||||
|
||||
return nearest
|
||||
}
|
||||
|
||||
const pickTargetScatter = (state: GameState, type: GhostType): Vec2 => {
|
||||
let map = getMap(state.mapId)
|
||||
switch (type) {
|
||||
case GhostType.BLINKY:
|
||||
return {x: 0, y: -1}
|
||||
case GhostType.PINKY:
|
||||
return {x: map.width - 1, y: -1}
|
||||
case GhostType.INKY:
|
||||
return {x: map.width - 1, y: map.height}
|
||||
case GhostType.CLYDE:
|
||||
return {x: 0, y: map.height}
|
||||
}
|
||||
}
|
||||
|
||||
const pickTargetChase = (state: GameState, type: GhostType): Vec2 => {
|
||||
let ghost = state.ghosts[type]
|
||||
let player = getNearestPlayer(state, ghost.pos)
|
||||
switch (type) {
|
||||
case GhostType.BLINKY:
|
||||
return {x: player.pos.x, y: player.pos.y}
|
||||
case GhostType.PINKY:
|
||||
return trans(player.pos, player.moveRotation, 2)
|
||||
case GhostType.INKY:
|
||||
let target = trans(player.pos, player.moveRotation, 1)
|
||||
let vec = diff(target, state.ghosts[GhostType.BLINKY].pos)
|
||||
return {x: target.x + vec.x, y: target.y + vec.y}
|
||||
case GhostType.CLYDE:
|
||||
if (dist(ghost.pos, player.pos) > 8)
|
||||
return {x: player.pos.x, y: player.pos.y}
|
||||
else
|
||||
return pickTargetScatter(state, type)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
const pickTarget = (state: GameState, map: Map, type: GhostType): Vec2 => {
|
||||
let ghost: Ghost = state.ghosts[type]
|
||||
switch (ghost.state) {
|
||||
case GhostState.SCATTER:
|
||||
return pickTargetScatter(state, type)
|
||||
case GhostState.CHASE:
|
||||
return pickTargetChase(state, type)
|
||||
case GhostState.EATEN:
|
||||
return structuredClone(map.spawns[SpawnIndex.GHOST_SPAWN])
|
||||
case GhostState.SCARED:
|
||||
return {
|
||||
x: random(state) % map.width,
|
||||
y: random(state) % map.height
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 updateGhost = (state: GameState, map: Map, type: GhostType) => {
|
||||
let ghost: Ghost = state.ghosts[type]
|
||||
|
||||
if (!ghost) {
|
||||
ghost = {
|
||||
pos: structuredClone(map.spawns[SpawnIndex.GHOST_SPAWN]),
|
||||
target: structuredClone(map.spawns[SpawnIndex.GHOST_SPAWN]),
|
||||
type,
|
||||
state: GhostState.SCARED,
|
||||
currentDirection: Rotation.EAST,
|
||||
}
|
||||
state.ghosts[type] = ghost
|
||||
}
|
||||
|
||||
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 isIntersection =
|
||||
(north && east) ||
|
||||
(east && south) ||
|
||||
(south && west) ||
|
||||
(west && north)
|
||||
|
||||
if (!isIntersection && front) {
|
||||
ghost.currentDirection = flipRot(ghost.currentDirection)
|
||||
} else if (isIntersection) {
|
||||
let target = pickTarget(state, map, type)
|
||||
ghost.target = target
|
||||
|
||||
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) {
|
||||
min = d
|
||||
newRot = Rotation.NORTH
|
||||
}
|
||||
}
|
||||
|
||||
if (east && ghost.currentDirection !== Rotation.WEST) {
|
||||
let d = dist({x: ghost.pos.x + 1, y: ghost.pos.y}, target)
|
||||
if (!min || min > d) {
|
||||
min = d
|
||||
newRot = Rotation.EAST
|
||||
}
|
||||
}
|
||||
|
||||
if (south && ghost.currentDirection !== Rotation.NORTH) {
|
||||
let d = dist({x: ghost.pos.x, y: ghost.pos.y + 1}, target)
|
||||
if (!min || min > d) {
|
||||
min = d
|
||||
newRot = Rotation.SOUTH
|
||||
}
|
||||
}
|
||||
|
||||
if (west && ghost.currentDirection !== Rotation.EAST) {
|
||||
let d = dist({x: ghost.pos.x - 1, y: ghost.pos.y}, target)
|
||||
if (!min || min > d) {
|
||||
min = d
|
||||
newRot = Rotation.WEST
|
||||
}
|
||||
}
|
||||
|
||||
ghost.currentDirection = newRot
|
||||
}
|
||||
}
|
||||
|
||||
incrementPos(ghost.pos, ghost.currentDirection, MOVE_SPEED)
|
||||
}
|
||||
|
||||
export const updateGhosts = (state: GameState) => {
|
||||
let map = getMap(state.mapId)
|
||||
if (!map) return
|
||||
|
||||
updateGhost(state, map, GhostType.BLINKY)
|
||||
updateGhost(state, map, GhostType.PINKY)
|
||||
updateGhost(state, map, GhostType.INKY)
|
||||
updateGhost(state, map, GhostType.CLYDE)
|
||||
}
|
|
@ -1,35 +1,42 @@
|
|||
import { genItems, genMap, getMap, decompressMap } from "../map.js";
|
||||
import { genItems, getMap } from "../map.js";
|
||||
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";
|
||||
|
||||
const maps = {
|
||||
[0]: 'EwRgPqYgNDew+TEuW6AGT2u59mPI/PeLZclSys1AhSgThJcJb2bdq7p5rupV2DYaVFCKI9HwFDiWACyxyK5WpCqArOPSCeuqfUnzDwaGbMyT3FAGZT0ABznD9ybWv0LLq61kz4S0M9WRMANnVVDUi1AHYdQxt+dyNEhJNiNk8A3npkiVSmcUEM6E5C/wL86rlivLqxaVymltggA='
|
||||
}
|
||||
import { updateGhosts } from "./ai.js";
|
||||
|
||||
export const InitialState: GameState = {
|
||||
started: false,
|
||||
input: {},
|
||||
players: [],
|
||||
ghosts: [undefined, undefined, undefined, undefined],
|
||||
items: {},
|
||||
mapId: undefined
|
||||
mapId: undefined,
|
||||
frame: 0,
|
||||
rng: 0
|
||||
}
|
||||
|
||||
export const random = (state: GameState): number => {
|
||||
return state.rng = (state.rng * 926659 + 4294967291) % 16381
|
||||
}
|
||||
|
||||
export const onLogic = (
|
||||
pastData: GameState = InitialState,
|
||||
input: Input = { players: {} },
|
||||
_frame: number
|
||||
frame: number
|
||||
) => {
|
||||
|
||||
let data = structuredClone(pastData)
|
||||
data.frame = frame
|
||||
random(data)
|
||||
|
||||
let startPressed = updatePlayers(data, input);
|
||||
|
||||
if (data.started) {
|
||||
updateMovement(data)
|
||||
updateItems(data)
|
||||
updateGhosts(data)
|
||||
} else {
|
||||
updateUI(data)
|
||||
}
|
||||
|
@ -45,13 +52,15 @@ export const onLogic = (
|
|||
|
||||
const initMap = (gameData: GameState, mapId: number) => {
|
||||
|
||||
document.getElementById("lobby").style.display = "none"
|
||||
|
||||
gameData.mapId = mapId
|
||||
|
||||
let map = getMap(mapId)
|
||||
if (!map) {
|
||||
let {width, height, data} = decompressMap(maps[mapId])
|
||||
map = genMap(width, height, data, mapId)
|
||||
}
|
||||
// if (!map) {
|
||||
// let {width, height, data} = decompressMap(maps[mapId])
|
||||
// map = genMap(width, height, data, mapId)
|
||||
// }
|
||||
|
||||
gameData.items = genItems(map)
|
||||
}
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
import { getMap } from "../map.js"
|
||||
import { Vec2, Map, Rotation, Key, Player, GameState, Tile } from "../types.js"
|
||||
|
||||
const MOVE_SPEED = .1
|
||||
export const MOVE_SPEED = .08333
|
||||
|
||||
const roundPos = (pos: Vec2): Vec2 => {
|
||||
export const roundPos = (pos: Vec2): Vec2 => {
|
||||
return {x: Math.round(pos.x), y: Math.round(pos.y)}
|
||||
}
|
||||
|
||||
const isStablePos = (pos: Vec2): boolean => {
|
||||
export const isStablePos = (pos: Vec2): boolean => {
|
||||
let rpos = roundPos(pos)
|
||||
return Math.abs(rpos.x - pos.x) < .05 && Math.abs(rpos.y - pos.y) < .05
|
||||
}
|
||||
|
||||
const getTile = (
|
||||
export const getTile = (
|
||||
map: Map,
|
||||
pos: Vec2,
|
||||
ox: number,
|
||||
|
@ -24,7 +24,7 @@ const getTile = (
|
|||
return map.data[y * map.width + x]
|
||||
}
|
||||
|
||||
const getTileFrontWithRot = (
|
||||
export const getTileFrontWithRot = (
|
||||
map: Map,
|
||||
pos: Vec2,
|
||||
rot: Rotation
|
||||
|
@ -57,7 +57,7 @@ const getRot = (key: Key): Rotation => {
|
|||
}
|
||||
}
|
||||
|
||||
const incrementPos = (
|
||||
export const incrementPos = (
|
||||
pos: Vec2,
|
||||
rot: Rotation,
|
||||
speed: number
|
||||
|
|
|
@ -1,12 +1,17 @@
|
|||
import { Game } from "./net/game.js";
|
||||
import { InitialState, onLogic } from "./logic/logic.js";
|
||||
import { startGraphicsUpdater } from "./renderer.js";
|
||||
import { GameKeyMap, Frame, Key, Player } from "./types.js";
|
||||
import { GameKeyMap, Frame, Key, Player, GAME_MAP_COUNT, Vec2 } from "./types.js";
|
||||
import { checkMap, decompressMap, genMap } from "./map.js";
|
||||
|
||||
const join = document.getElementById("join")
|
||||
const lobby = document.getElementById("lobby")
|
||||
const mapeditor = document.getElementById("mapeditor")
|
||||
|
||||
const maps = {
|
||||
[0]: 'EwRgPqYgNDew+TEuW6AGT2u59mPI/PeLZclSys1AhSgThJcJb2bdq7p5rupV2DYaVFCKI9HwFDiWACyxyK5WpCqArOPSCeuqfUnzDwaGbMyT3FAGZT0ABznD9ybWv0LLq61kz4S0M9WRMANnVVDUi1AHYdQxt+dyNEhJNiNk8A3npkiVSmcUEM6E5C/wL86rlivLqxaVymltggA='
|
||||
}
|
||||
|
||||
join.onsubmit = async function(event) {
|
||||
event.preventDefault()
|
||||
|
||||
|
@ -23,8 +28,19 @@ join.onsubmit = async function(event) {
|
|||
return
|
||||
}
|
||||
|
||||
join.style.display = "none"
|
||||
mapeditor.style.display = "none"
|
||||
for (let mapId = 0; mapId < GAME_MAP_COUNT; mapId++) {
|
||||
let {width, height, data} = decompressMap(maps[0]) // for now
|
||||
let map = genMap(width, height, data, mapId)
|
||||
let [success, result] = checkMap(map)
|
||||
|
||||
if (!success) {
|
||||
alert(result)
|
||||
return
|
||||
}
|
||||
|
||||
map.spawns = result as Vec2[]
|
||||
|
||||
}
|
||||
|
||||
startGame(room_code, player_name)
|
||||
}
|
||||
|
@ -48,6 +64,8 @@ const onLoad = (startData: Frame) => {
|
|||
return false
|
||||
}
|
||||
|
||||
join.style.display = "none"
|
||||
mapeditor.style.display = "none"
|
||||
lobby.style.display = ""
|
||||
|
||||
return true
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Wall, ItemType, Map, Maps, Items, Tile } from "./types.js"
|
||||
import { Wall, ItemType, Map, Maps, Items, Tile, SpawnIndex, Vec2 } from "./types.js"
|
||||
import { LZString } from "./lib/lz-string.js"
|
||||
|
||||
export const getItemKey = (
|
||||
|
@ -165,6 +165,76 @@ export const getMap = (mapId: number): Map | undefined => {
|
|||
return mapData[mapId]
|
||||
}
|
||||
|
||||
export const checkMap = (map: Map): [boolean, string | Vec2[]] => {
|
||||
let spawns = new Array(5).fill(undefined)
|
||||
let hasFood = false
|
||||
let hasThicc = false
|
||||
let hasInitial = false
|
||||
|
||||
if (map.width < 5 || map.height < 5 || map.width > 50 || map.height > 50) {
|
||||
return [false, "Map but be between either 5 or 50 on both axies"]
|
||||
}
|
||||
|
||||
for (let y = 0; y < map.height; y++) {
|
||||
for (let x = 0; x < map.width; x++) {
|
||||
|
||||
let type = map.data[y * map.width + x]
|
||||
|
||||
switch (type) {
|
||||
case Tile.FOOD:
|
||||
hasFood = true
|
||||
break
|
||||
case Tile.THICC_DOT:
|
||||
hasThicc = true
|
||||
break
|
||||
case Tile.INITIAL_DOT:
|
||||
hasInitial = true
|
||||
break
|
||||
case Tile.PLAYER_SPAWN_1:
|
||||
if (spawns[SpawnIndex.PAC_SPAWN_1])
|
||||
return [false, "Map cannot have duplicate spawns"]
|
||||
spawns[SpawnIndex.PAC_SPAWN_1] = {x, y}
|
||||
break
|
||||
case Tile.PLAYER_SPAWN_2:
|
||||
if (spawns[SpawnIndex.PAC_SPAWN_2])
|
||||
return [false, "Map cannot have duplicate spawns"]
|
||||
spawns[SpawnIndex.PAC_SPAWN_2] = {x, y}
|
||||
break
|
||||
case Tile.PLAYER_SPAWN_3:
|
||||
if (spawns[SpawnIndex.PAC_SPAWN_3])
|
||||
return [false, "Map cannot have duplicate spawns"]
|
||||
spawns[SpawnIndex.PAC_SPAWN_3] = {x, y}
|
||||
break
|
||||
case Tile.PLAYER_SPAWN_4:
|
||||
if (spawns[SpawnIndex.PAC_SPAWN_4])
|
||||
return [false, "Map cannot have duplicate spawns"]
|
||||
spawns[SpawnIndex.PAC_SPAWN_4] = {x, y}
|
||||
break
|
||||
case Tile.GHOST_SPAWN:
|
||||
if (spawns[SpawnIndex.GHOST_SPAWN])
|
||||
return [false, "Map cannot have duplicate spawns"]
|
||||
spawns[SpawnIndex.GHOST_SPAWN] = {x, y}
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasFood)
|
||||
return [false, "Map must have at least 1 food"]
|
||||
|
||||
if (!hasThicc)
|
||||
return [false, "Map must have at least 1 thicc dot"]
|
||||
|
||||
if (!hasInitial)
|
||||
return [false, "Map must have at least 1 initial dot"]
|
||||
|
||||
if (spawns.filter(s => s === undefined).length > 0)
|
||||
return [false, "Map must have 4 pac spawns and 1 ghost spawn"]
|
||||
|
||||
return [true, spawns]
|
||||
}
|
||||
|
||||
export const compressMap = (map: Map): string => {
|
||||
let encoded = map.width + '|' + map.height + '|' + map.data.map(n => n + ',').join('').slice(0, -1)
|
||||
return LZString.compressToBase64(encoded)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { getMap } from "./map.js";
|
||||
import { Items, Players, Rotation, ItemType, Map, Wall, GameState, Tile, ATLAS_TILE_WIDTH } from "./types.js";
|
||||
import { Items, Players, Rotation, ItemType, Map, Wall, GameState, Tile, ATLAS_TILE_WIDTH, Ghosts, Ghost, GhostType, GhostState } from "./types.js";
|
||||
|
||||
const update_style = (width: number, height: number) => {
|
||||
const updateStyle = (width: number, height: number) => {
|
||||
|
||||
let style = document.getElementById("style")
|
||||
|
||||
|
@ -35,37 +35,82 @@ const update_style = (width: number, height: number) => {
|
|||
style.innerHTML = css
|
||||
}
|
||||
|
||||
const draw_sprite = (
|
||||
const drawSprite = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
atlas: CanvasImageSource,
|
||||
atlas_index: [number, number],
|
||||
atlas_tile_width: number,
|
||||
atlasIndex: [number, number],
|
||||
atlasTileWidth: number,
|
||||
rotation: Rotation
|
||||
) => {
|
||||
ctx.save()
|
||||
ctx.translate(
|
||||
(x + 0.5) * ATLAS_TILE_WIDTH,
|
||||
(y + 0.5) * ATLAS_TILE_WIDTH
|
||||
(x + 0.5) * atlasTileWidth,
|
||||
(y + 0.5) * atlasTileWidth
|
||||
)
|
||||
ctx.rotate(rotation * Math.PI / 180)
|
||||
ctx.drawImage(
|
||||
atlas,
|
||||
atlas_index[0] * atlas_tile_width,
|
||||
atlas_index[1] * atlas_tile_width,
|
||||
atlas_tile_width,
|
||||
atlas_tile_width,
|
||||
-width * ATLAS_TILE_WIDTH / 2,
|
||||
-width * ATLAS_TILE_WIDTH / 2,
|
||||
width * ATLAS_TILE_WIDTH,
|
||||
width * ATLAS_TILE_WIDTH
|
||||
atlasIndex[0] * atlasTileWidth,
|
||||
atlasIndex[1] * atlasTileWidth,
|
||||
atlasTileWidth,
|
||||
atlasTileWidth,
|
||||
-width * atlasTileWidth / 2,
|
||||
-width * atlasTileWidth / 2,
|
||||
width * atlasTileWidth,
|
||||
width * atlasTileWidth
|
||||
)
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
const draw_players = (
|
||||
const hueCanvas = document.createElement("canvas");
|
||||
const drawSpriteHue = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
atlas: CanvasImageSource,
|
||||
atlasIndex: [number, number],
|
||||
atlasTileWidth: number,
|
||||
rotation: Rotation,
|
||||
color: string
|
||||
) => {
|
||||
hueCanvas.width = atlasTileWidth;
|
||||
hueCanvas.height = atlasTileWidth;
|
||||
const hueCtx = hueCanvas.getContext('2d');
|
||||
|
||||
hueCtx.globalCompositeOperation = "copy"
|
||||
hueCtx.fillStyle = color;
|
||||
hueCtx.fillRect(0, 0, atlasTileWidth, atlasTileWidth);
|
||||
|
||||
hueCtx.globalCompositeOperation = "destination-in";
|
||||
hueCtx.drawImage (
|
||||
atlas,
|
||||
atlasIndex[0] * atlasTileWidth,
|
||||
atlasIndex[1] * atlasTileWidth,
|
||||
atlasTileWidth,
|
||||
atlasTileWidth,
|
||||
0,
|
||||
0,
|
||||
atlasTileWidth,
|
||||
atlasTileWidth
|
||||
)
|
||||
|
||||
drawSprite (
|
||||
ctx,
|
||||
x, y,
|
||||
width,
|
||||
hueCanvas,
|
||||
[0, 0],
|
||||
atlasTileWidth,
|
||||
rotation
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
const drawPlayers = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
atlas: CanvasImageSource,
|
||||
players: Players,
|
||||
|
@ -88,9 +133,9 @@ const draw_players = (
|
|||
let player = players[id]
|
||||
if (!player) continue
|
||||
|
||||
let atlas_index = atlas_frames[0]
|
||||
let atlasIndex = atlas_frames[0]
|
||||
if (player.moving) {
|
||||
atlas_index = atlas_frames[Math.floor(frame / 2) % atlas_frames.length]
|
||||
atlasIndex = atlas_frames[Math.floor(frame / 2) % atlas_frames.length]
|
||||
}
|
||||
|
||||
let rotation: number
|
||||
|
@ -110,20 +155,117 @@ const draw_players = (
|
|||
break
|
||||
}
|
||||
|
||||
draw_sprite (
|
||||
drawSprite (
|
||||
ctx,
|
||||
player.pos.x,
|
||||
player.pos.y,
|
||||
1,
|
||||
atlas,
|
||||
atlas_index,
|
||||
atlasIndex,
|
||||
ATLAS_TILE_WIDTH,
|
||||
rotation
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const draw_items = (
|
||||
const drawGhosts = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
atlas: CanvasImageSource,
|
||||
ghosts: Ghosts
|
||||
) => {
|
||||
for (let type in ghosts) {
|
||||
let ghost: Ghost = ghosts[type]
|
||||
if (!ghost) continue
|
||||
|
||||
let color: string
|
||||
switch (ghost.type) {
|
||||
case GhostType.BLINKY:
|
||||
color = '#ed2724'
|
||||
break
|
||||
case GhostType.PINKY:
|
||||
color = '#ffb9de'
|
||||
break
|
||||
case GhostType.INKY:
|
||||
color = '#00ffdf'
|
||||
break
|
||||
case GhostType.CLYDE:
|
||||
color = '#ffb748'
|
||||
break
|
||||
}
|
||||
|
||||
if (
|
||||
ghost.state == GhostState.SCATTER ||
|
||||
ghost.state == GhostState.CHASE
|
||||
) {
|
||||
drawSpriteHue (
|
||||
ctx,
|
||||
ghost.pos.x,
|
||||
ghost.pos.y,
|
||||
1,
|
||||
atlas,
|
||||
[0, 4],
|
||||
ATLAS_TILE_WIDTH,
|
||||
0,
|
||||
color
|
||||
)
|
||||
}
|
||||
|
||||
if (ghost.state != GhostState.SCARED) {
|
||||
let eyes: [number, number]
|
||||
switch (ghost.currentDirection) {
|
||||
case Rotation.EAST:
|
||||
eyes = [1, 4]
|
||||
break
|
||||
case Rotation.WEST:
|
||||
eyes = [2, 4]
|
||||
break
|
||||
case Rotation.NORTH:
|
||||
eyes = [3, 4]
|
||||
break
|
||||
case Rotation.SOUTH:
|
||||
eyes = [4, 4]
|
||||
break
|
||||
}
|
||||
|
||||
drawSprite (
|
||||
ctx,
|
||||
ghost.pos.x,
|
||||
ghost.pos.y,
|
||||
1,
|
||||
atlas,
|
||||
eyes,
|
||||
ATLAS_TILE_WIDTH,
|
||||
0
|
||||
)
|
||||
} else {
|
||||
drawSprite (
|
||||
ctx,
|
||||
ghost.pos.x,
|
||||
ghost.pos.y,
|
||||
1,
|
||||
atlas,
|
||||
[4, 3],
|
||||
ATLAS_TILE_WIDTH,
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
// drawSpriteHue (
|
||||
// ctx,
|
||||
// ghost.target.x,
|
||||
// ghost.target.y,
|
||||
// 1,
|
||||
// atlas,
|
||||
// [3, 0],
|
||||
// ATLAS_TILE_WIDTH,
|
||||
// 0,
|
||||
// color
|
||||
// )
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const drawItems = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
atlas: CanvasImageSource,
|
||||
items: Items
|
||||
|
@ -134,31 +276,31 @@ const draw_items = (
|
|||
let item = items[item_key]
|
||||
if (!item) continue
|
||||
|
||||
let width: number, atlas_index: [number, number]
|
||||
let width: number, atlasIndex: [number, number]
|
||||
switch (item.type) {
|
||||
case ItemType.DOT:
|
||||
width = .2
|
||||
atlas_index = [2, 3]
|
||||
atlasIndex = [2, 3]
|
||||
break
|
||||
case ItemType.THICC_DOT:
|
||||
width = .4
|
||||
atlas_index = [2, 3]
|
||||
atlasIndex = [2, 3]
|
||||
break
|
||||
case ItemType.FOOD:
|
||||
width = 1
|
||||
atlas_index = [3, 3]
|
||||
atlasIndex = [3, 3]
|
||||
break
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
draw_sprite (
|
||||
drawSprite (
|
||||
ctx,
|
||||
item.pos.x,
|
||||
item.pos.y,
|
||||
width,
|
||||
atlas,
|
||||
atlas_index,
|
||||
atlasIndex,
|
||||
ATLAS_TILE_WIDTH,
|
||||
0
|
||||
)
|
||||
|
@ -167,7 +309,7 @@ const draw_items = (
|
|||
|
||||
}
|
||||
|
||||
const draw_map_canvas = (
|
||||
const drawMapCanvas = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
atlas: CanvasImageSource,
|
||||
map: Map
|
||||
|
@ -178,83 +320,83 @@ const draw_map_canvas = (
|
|||
|
||||
let wall_type = map.walls[y * map.width + x]
|
||||
|
||||
let atlas_index: [number, number], rotation: number;
|
||||
let atlasIndex: [number, number], rotation: number;
|
||||
switch(wall_type) {
|
||||
case Wall.EMPTY:
|
||||
continue
|
||||
case Wall.WALL_HZ:
|
||||
atlas_index = [1, 1]
|
||||
atlasIndex = [1, 1]
|
||||
rotation = 0
|
||||
break
|
||||
case Wall.WALL_VT:
|
||||
atlas_index = [1, 1]
|
||||
atlasIndex = [1, 1]
|
||||
rotation = 90
|
||||
break
|
||||
case Wall.TURN_Q1:
|
||||
atlas_index = [2, 0]
|
||||
atlasIndex = [2, 0]
|
||||
rotation = 0
|
||||
break
|
||||
case Wall.TURN_Q2:
|
||||
atlas_index = [2, 0]
|
||||
atlasIndex = [2, 0]
|
||||
rotation = 270
|
||||
break
|
||||
case Wall.TURN_Q3:
|
||||
atlas_index = [2, 0]
|
||||
atlasIndex = [2, 0]
|
||||
rotation = 180
|
||||
break
|
||||
case Wall.TURN_Q4:
|
||||
atlas_index = [2, 0]
|
||||
atlasIndex = [2, 0]
|
||||
rotation = 90
|
||||
break
|
||||
case Wall.TEE_NORTH:
|
||||
atlas_index = [1, 0]
|
||||
atlasIndex = [1, 0]
|
||||
rotation = 180
|
||||
break
|
||||
case Wall.TEE_EAST:
|
||||
atlas_index = [1, 0]
|
||||
atlasIndex = [1, 0]
|
||||
rotation = 270
|
||||
break
|
||||
case Wall.TEE_SOUTH:
|
||||
atlas_index = [1, 0]
|
||||
atlasIndex = [1, 0]
|
||||
rotation = 0
|
||||
break
|
||||
case Wall.TEE_WEST:
|
||||
atlas_index = [1, 0]
|
||||
atlasIndex = [1, 0]
|
||||
rotation = 90
|
||||
break
|
||||
case Wall.CROSS:
|
||||
atlas_index = [0, 0]
|
||||
atlasIndex = [0, 0]
|
||||
rotation = 0
|
||||
break
|
||||
case Wall.DOT:
|
||||
atlas_index = [2, 1]
|
||||
atlasIndex = [2, 1]
|
||||
rotation = 0
|
||||
break
|
||||
case Wall.WALL_END_NORTH:
|
||||
atlas_index = [0, 1]
|
||||
atlasIndex = [0, 1]
|
||||
rotation = 0
|
||||
break;
|
||||
case Wall.WALL_END_EAST:
|
||||
atlas_index = [0, 1]
|
||||
atlasIndex = [0, 1]
|
||||
rotation = 90
|
||||
break;
|
||||
case Wall.WALL_END_SOUTH:
|
||||
atlas_index = [0, 1]
|
||||
atlasIndex = [0, 1]
|
||||
rotation = 180
|
||||
break;
|
||||
case Wall.WALL_END_WEST:
|
||||
atlas_index = [0, 1]
|
||||
atlasIndex = [0, 1]
|
||||
rotation = 270
|
||||
break;
|
||||
}
|
||||
|
||||
draw_sprite (
|
||||
drawSprite (
|
||||
ctx,
|
||||
x,
|
||||
y,
|
||||
1,
|
||||
atlas,
|
||||
atlas_index,
|
||||
atlasIndex,
|
||||
ATLAS_TILE_WIDTH,
|
||||
rotation
|
||||
)
|
||||
|
@ -276,49 +418,49 @@ const draw_debug_sprites = (
|
|||
|
||||
|
||||
let size = 1
|
||||
let atlas_index: [number, number];
|
||||
let atlasIndex: [number, number];
|
||||
switch (tile_type) {
|
||||
case Tile.EMPTY:
|
||||
case Tile.WALL:
|
||||
continue
|
||||
case Tile.GHOST_WALL:
|
||||
atlas_index = [4, 0]
|
||||
atlasIndex = [4, 0]
|
||||
break
|
||||
case Tile.GHOST_SPAWN:
|
||||
atlas_index = [3, 0]
|
||||
atlasIndex = [3, 0]
|
||||
break
|
||||
case Tile.FOOD:
|
||||
atlas_index = [3, 3]
|
||||
atlasIndex = [3, 3]
|
||||
break
|
||||
case Tile.PLAYER_SPAWN_1:
|
||||
atlas_index = [3, 1]
|
||||
atlasIndex = [3, 1]
|
||||
break
|
||||
case Tile.PLAYER_SPAWN_2:
|
||||
atlas_index = [4, 1]
|
||||
atlasIndex = [4, 1]
|
||||
break
|
||||
case Tile.PLAYER_SPAWN_3:
|
||||
atlas_index = [3, 2]
|
||||
atlasIndex = [3, 2]
|
||||
break
|
||||
case Tile.PLAYER_SPAWN_4:
|
||||
atlas_index = [4, 2]
|
||||
atlasIndex = [4, 2]
|
||||
break
|
||||
case Tile.THICC_DOT:
|
||||
atlas_index = [2, 3]
|
||||
atlasIndex = [2, 3]
|
||||
size = .4
|
||||
break
|
||||
case Tile.INITIAL_DOT:
|
||||
atlas_index = [2, 3]
|
||||
atlasIndex = [2, 3]
|
||||
size = .2
|
||||
break
|
||||
}
|
||||
|
||||
draw_sprite (
|
||||
drawSprite (
|
||||
ctx,
|
||||
x,
|
||||
y,
|
||||
size,
|
||||
atlas,
|
||||
atlas_index,
|
||||
atlasIndex,
|
||||
ATLAS_TILE_WIDTH,
|
||||
0
|
||||
)
|
||||
|
@ -327,8 +469,8 @@ const draw_debug_sprites = (
|
|||
}
|
||||
}
|
||||
|
||||
let map_canvas = document.createElement("canvas")
|
||||
const draw_map = (
|
||||
let mapCanvas = document.createElement("canvas")
|
||||
const drawMap = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
atlas: CanvasImageSource,
|
||||
map: Map,
|
||||
|
@ -337,33 +479,30 @@ const draw_map = (
|
|||
) => {
|
||||
|
||||
if (map.id !== last || editor) {
|
||||
map_canvas.width = map.width * ATLAS_TILE_WIDTH
|
||||
map_canvas.height = map.height * ATLAS_TILE_WIDTH
|
||||
mapCanvas.width = map.width * ATLAS_TILE_WIDTH
|
||||
mapCanvas.height = map.height * ATLAS_TILE_WIDTH
|
||||
|
||||
let map_ctx = map_canvas.getContext("2d")
|
||||
draw_map_canvas(map_ctx, atlas, map)
|
||||
let map_ctx = mapCanvas.getContext("2d")
|
||||
drawMapCanvas(map_ctx, atlas, map)
|
||||
if (editor) {
|
||||
draw_debug_sprites(map_ctx, atlas, map)
|
||||
}
|
||||
}
|
||||
|
||||
ctx.drawImage (
|
||||
map_canvas,
|
||||
mapCanvas,
|
||||
0,
|
||||
0
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
let last_map_drawn: number | undefined
|
||||
let lastMapDrawn: number | undefined
|
||||
export const startGraphicsUpdater = () => {
|
||||
|
||||
let canvas = document.getElementById("canvas") as HTMLCanvasElement
|
||||
let atlas = document.getElementById("atlas") as HTMLImageElement
|
||||
|
||||
/**
|
||||
* @param {import("./logic").GameState} data
|
||||
*/
|
||||
return (
|
||||
data: GameState,
|
||||
frame: number,
|
||||
|
@ -374,7 +513,7 @@ export const startGraphicsUpdater = () => {
|
|||
|
||||
if (!map) return
|
||||
|
||||
if (map.id !== last_map_drawn) {
|
||||
if (map.id !== lastMapDrawn) {
|
||||
canvas.style.display = ""
|
||||
canvas.width = map.width * ATLAS_TILE_WIDTH
|
||||
canvas.height = map.height * ATLAS_TILE_WIDTH
|
||||
|
@ -383,12 +522,13 @@ export const startGraphicsUpdater = () => {
|
|||
let ctx = canvas.getContext("2d")
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
draw_map(ctx, atlas, map, last_map_drawn, editor)
|
||||
draw_items(ctx, atlas, data.items)
|
||||
draw_players(ctx, atlas, data.players, frame)
|
||||
update_style(map.width, map.height)
|
||||
drawMap(ctx, atlas, map, lastMapDrawn, editor)
|
||||
drawItems(ctx, atlas, data.items)
|
||||
drawGhosts(ctx, atlas, data.ghosts)
|
||||
drawPlayers(ctx, atlas, data.players, frame)
|
||||
updateStyle(map.width, map.height)
|
||||
|
||||
last_map_drawn = map.id
|
||||
lastMapDrawn = map.id
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
|
||||
export const ATLAS_TILE_WIDTH = 32
|
||||
export const GAME_MAP_COUNT = 4
|
||||
|
||||
export enum Tile {
|
||||
EMPTY = 0,
|
||||
|
@ -49,6 +50,28 @@ export enum Key {
|
|||
RIGHT
|
||||
}
|
||||
|
||||
export enum GhostType {
|
||||
BLINKY = 0,
|
||||
PINKY = 1,
|
||||
INKY = 2,
|
||||
CLYDE = 3
|
||||
}
|
||||
|
||||
export enum GhostState {
|
||||
CHASE,
|
||||
SCATTER,
|
||||
EATEN,
|
||||
SCARED
|
||||
}
|
||||
|
||||
export type Ghost = {
|
||||
pos: Vec2,
|
||||
type: GhostType,
|
||||
target: Vec2,
|
||||
state: GhostState,
|
||||
currentDirection: Rotation,
|
||||
}
|
||||
|
||||
export type KeyMap = {
|
||||
[key: string]: Key
|
||||
}
|
||||
|
@ -123,23 +146,37 @@ export type Items = {
|
|||
[key: number]: Item
|
||||
}
|
||||
|
||||
export enum SpawnIndex {
|
||||
PAC_SPAWN_1 = 1,
|
||||
PAC_SPAWN_2 = 2,
|
||||
PAC_SPAWN_3 = 3,
|
||||
PAC_SPAWN_4 = 4,
|
||||
GHOST_SPAWN = 0
|
||||
}
|
||||
|
||||
export type Map = {
|
||||
data: number[],
|
||||
walls: number[],
|
||||
width: number,
|
||||
height: number,
|
||||
id: number
|
||||
id: number,
|
||||
spawns?: Vec2[]
|
||||
}
|
||||
|
||||
export type Maps = {
|
||||
[key: number]: Map
|
||||
}
|
||||
|
||||
export type Ghosts = [Ghost, Ghost, Ghost, Ghost]
|
||||
|
||||
export type GameState = {
|
||||
started: boolean,
|
||||
input: InputMap,
|
||||
players: Players,
|
||||
ghosts: Ghosts,
|
||||
items: Items,
|
||||
frame: number,
|
||||
rng: number,
|
||||
mapId: number | undefined
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue