From 0281233cbdc76e065a812780de0325fcfbd4e660 Mon Sep 17 00:00:00 2001 From: Tyler Murphy Date: Sun, 25 Jun 2023 18:19:26 -0400 Subject: [PATCH] ghost --- .gitignore | 3 + client/img/atlas.png | Bin 8495 -> 9345 bytes client/src/editor.ts | 11 +- client/src/logic/ai.ts | 197 +++++++++++++++++++++++ client/src/logic/logic.ts | 31 ++-- client/src/logic/movement.ts | 12 +- client/src/main.ts | 24 ++- client/src/map.ts | 72 ++++++++- client/src/renderer.ts | 292 ++++++++++++++++++++++++++--------- client/src/types.ts | 39 ++++- 10 files changed, 576 insertions(+), 105 deletions(-) create mode 100644 client/src/logic/ai.ts diff --git a/.gitignore b/.gitignore index f4c9b61..ba11228 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ server/target client/js +client/package.json +client/package-lock.json +client/node_modules diff --git a/client/img/atlas.png b/client/img/atlas.png index ebb467efb8b0a908fdc9ef29ef809c971cff8ad4..135b9601eb473433c24df0989df721f6a7424181 100644 GIT binary patch delta 4458 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 delta 3974 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 diff --git a/client/src/editor.ts b/client/src/editor.ts index 1abaad4..0be5c68 100644 --- a/client/src/editor.ts +++ b/client/src/editor.ts @@ -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() diff --git a/client/src/logic/ai.ts b/client/src/logic/ai.ts new file mode 100644 index 0000000..875621a --- /dev/null +++ b/client/src/logic/ai.ts @@ -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) +} diff --git a/client/src/logic/logic.ts b/client/src/logic/logic.ts index dd6e21d..d9ac6c8 100644 --- a/client/src/logic/logic.ts +++ b/client/src/logic/logic.ts @@ -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) } diff --git a/client/src/logic/movement.ts b/client/src/logic/movement.ts index f03008b..f2a06e7 100644 --- a/client/src/logic/movement.ts +++ b/client/src/logic/movement.ts @@ -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 diff --git a/client/src/main.ts b/client/src/main.ts index b5ea424..9913d8b 100644 --- a/client/src/main.ts +++ b/client/src/main.ts @@ -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 diff --git a/client/src/map.ts b/client/src/map.ts index 1ad6d63..70de0b8 100644 --- a/client/src/map.ts +++ b/client/src/map.ts @@ -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) diff --git a/client/src/renderer.ts b/client/src/renderer.ts index bc6cf83..cf3189a 100644 --- a/client/src/renderer.ts +++ b/client/src/renderer.ts @@ -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 } diff --git a/client/src/types.ts b/client/src/types.ts index c130980..76f5116 100644 --- a/client/src/types.ts +++ b/client/src/types.ts @@ -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 }