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 ebb467e..135b960 100644 Binary files a/client/img/atlas.png and b/client/img/atlas.png differ 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 }