import { getMap } from "./map.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) => { let style = document.getElementById("style") const css = ` * { --scale: 100; --aspect: ${width/height}; --scaleX: calc(var(--scale) * 1vw); --scaleY: calc(var(--scale) * 1vh); } #canvas { width: calc(var(--scaleY) * var(--aspect)); height: var(--scaleY); margin-top: calc((100vh - var(--scaleY))/2); margin-left: calc(50vw - var(--scaleY)*var(--aspect)/2); position: relative; vertical-align: top; line-height: 0; } @media (max-aspect-ratio: ${width}/${height}) { #canvas { width: var(--scaleX); height: calc(var(--scaleX) / var(--aspect)); margin-left: calc((100vw - var(--scaleX))/2); margin-top: calc(50vh - var(--scaleX)/var(--aspect)/2); } }`; style.innerHTML = css } const drawSprite = ( ctx: CanvasRenderingContext2D, x: number, y: number, width: number, atlas: CanvasImageSource, atlasIndex: [number, number], atlasTileWidth: number, rotation: Rotation ) => { ctx.save() ctx.translate( (x + 0.5) * atlasTileWidth, (y + 0.5) * atlasTileWidth ) ctx.rotate(rotation * Math.PI / 180) ctx.drawImage( atlas, atlasIndex[0] * atlasTileWidth, atlasIndex[1] * atlasTileWidth, atlasTileWidth, atlasTileWidth, -width * atlasTileWidth / 2, -width * atlasTileWidth / 2, width * atlasTileWidth, width * atlasTileWidth ) ctx.restore() } 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, frame: number, map: Map, drawBig: boolean ) => { const defaultFrame: [number, number] = [0, 3] const movingFrames: [number, number][] = [ [0, 3], [1, 3], [2, 3], [3, 3], [4, 3], [3, 3], [2, 3], [1, 3], ] const deathFrames: [number, number][] = [ [0, 3], [4, 2], [3, 2], [2, 2], [1, 2], [0, 2], ] const playerHues: string[] = [ '#d93030', '#e6c147', '#eb42d4', '#425eeb' ] let hueIndex = 0 for (let id in players) { let player = players[id] if (!player) continue if ( drawBig && player.thiccLeft == 0 || !drawBig && player.thiccLeft > 0 ) { hueIndex++ continue } let atlasIndex = defaultFrame if (player.dead) { atlasIndex = deathFrames[Math.floor(player.framesDead / 20)] } else if (player.moving) { atlasIndex = movingFrames[Math.floor(frame / 2) % movingFrames.length] } if (!atlasIndex) { hueIndex++ continue } let rot = player.moveRotation if (rot == Rotation.NOTHING) { if (player.pos.x < map.width / 2) { rot = Rotation.EAST } else { rot = Rotation.WEST } } let rotation: number switch (rot) { case Rotation.NORTH: rotation = 270 break case Rotation.SOUTH: rotation = 90 break case Rotation.WEST: rotation = 180 break case Rotation.EAST: rotation = 0 break } drawSpriteHue ( ctx, player.pos.x, player.pos.y, player.thiccLeft > 0 ? 2 : 1, atlas, atlasIndex, ATLAS_TILE_WIDTH, rotation, playerHues[hueIndex] ) hueIndex++ } } 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, [3, 1], 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 ) => { for (let item_key in items) { let item = items[item_key] if (!item) continue let width: number, atlasIndex: [number, number] switch (item.type) { case ItemType.DOT: width = .2 atlasIndex = [4, 3] break case ItemType.THICC_DOT: width = .4 atlasIndex = [4, 3] break case ItemType.FOOD: width = 1 atlasIndex = [4, 1] break default: continue } drawSprite ( ctx, item.pos.x, item.pos.y, width, atlas, atlasIndex, ATLAS_TILE_WIDTH, 0 ) } } const drawMapCanvas = ( ctx: CanvasRenderingContext2D, atlas: CanvasImageSource, map: Map ) => { for (let y = 0; y < map.height; y++) { for (let x = 0; x < map.width; x++) { let wall_type = map.walls[y * map.width + x] let atlasIndex: [number, number], rotation: number; switch(wall_type) { case Wall.EMPTY: continue case Wall.WALL_HZ: atlasIndex = [1, 1] rotation = 0 break case Wall.WALL_VT: atlasIndex = [1, 1] rotation = 90 break case Wall.TURN_Q1: atlasIndex = [2, 0] rotation = 0 break case Wall.TURN_Q2: atlasIndex = [2, 0] rotation = 270 break case Wall.TURN_Q3: atlasIndex = [2, 0] rotation = 180 break case Wall.TURN_Q4: atlasIndex = [2, 0] rotation = 90 break case Wall.TEE_NORTH: atlasIndex = [1, 0] rotation = 180 break case Wall.TEE_EAST: atlasIndex = [1, 0] rotation = 270 break case Wall.TEE_SOUTH: atlasIndex = [1, 0] rotation = 0 break case Wall.TEE_WEST: atlasIndex = [1, 0] rotation = 90 break case Wall.CROSS: atlasIndex = [0, 0] rotation = 0 break case Wall.DOT: atlasIndex = [2, 1] rotation = 0 break case Wall.WALL_END_NORTH: atlasIndex = [0, 1] rotation = 0 break; case Wall.WALL_END_EAST: atlasIndex = [0, 1] rotation = 90 break; case Wall.WALL_END_SOUTH: atlasIndex = [0, 1] rotation = 180 break; case Wall.WALL_END_WEST: atlasIndex = [0, 1] rotation = 270 break; } drawSprite ( ctx, x, y, 1, atlas, atlasIndex, ATLAS_TILE_WIDTH, rotation ) } } } const drawDebugSprites = ( ctx: CanvasRenderingContext2D, atlas: CanvasImageSource, map: Map ) => { for (let y = 0; y < map.height; y++) { for (let x = 0; x < map.width; x++) { let tile_type = map.data[y * map.width + x] let size = 1 let atlasIndex: [number, number]; switch (tile_type) { case Tile.EMPTY: case Tile.WALL: continue case Tile.GHOST_WALL: atlasIndex = [4, 0] break case Tile.GHOST_SPAWN: atlasIndex = [3, 0] break case Tile.GHOST_EXIT: atlasIndex = [5, 0] break case Tile.FOOD: atlasIndex = [4, 1] break case Tile.PLAYER_SPAWN_1: atlasIndex = [5, 1] break case Tile.PLAYER_SPAWN_2: atlasIndex = [5, 2] break case Tile.PLAYER_SPAWN_3: atlasIndex = [5, 3] break case Tile.PLAYER_SPAWN_4: atlasIndex = [5, 4] break case Tile.THICC_DOT: atlasIndex = [4, 3] size = .4 break case Tile.INITIAL_DOT: atlasIndex = [4, 3] size = .2 break } drawSprite ( ctx, x, y, size, atlas, atlasIndex, ATLAS_TILE_WIDTH, 0 ) } } } let mapCanvas = document.createElement("canvas") const drawMap = ( ctx: CanvasRenderingContext2D, atlas: CanvasImageSource, map: Map, last: number | undefined, editor: boolean ) => { if (map.id !== last || editor) { mapCanvas.width = map.width * ATLAS_TILE_WIDTH mapCanvas.height = map.height * ATLAS_TILE_WIDTH let map_ctx = mapCanvas.getContext("2d") map_ctx.imageSmoothingEnabled = false drawMapCanvas(map_ctx, atlas, map) if (editor) { drawDebugSprites(map_ctx, atlas, map) } } ctx.drawImage ( mapCanvas, 0, 0 ) } const updateAudio = (state: GameState) => { if (state.roundTimer < 60 * 5) { if (!INTRO_AUDIO.getPlaying()) { INTRO_AUDIO.play(false) } } else { if (INTRO_AUDIO.getPlaying()) { INTRO_AUDIO.stop() } } let moving = false for (let id in state.players) { let player = state.players[id] if (player.dead && player.framesDead == 0) { DEATH_AUDIO.play(false) } moving ||= player.atePellets > 0 } if (moving && state.endRoundTimer === undefined) { if (!MOVE_AUDIO.getPlaying()) { MOVE_AUDIO.play(true) } } else { if (MOVE_AUDIO.getPlaying()) { MOVE_AUDIO.stop() } } if (state.roundTimer > 60 * 5 && state.endRoundTimer === undefined) { if (!GHOST_AUDIO.getPlaying()) { GHOST_AUDIO.play(true) } } else { if (GHOST_AUDIO.getPlaying()) { GHOST_AUDIO.stop() } } } let lastMapDrawn: number | undefined export const startGraphicsUpdater = () => { let canvas = document.getElementById("canvas") as HTMLCanvasElement let atlas = document.getElementById("atlas") as HTMLImageElement let lobby = document.getElementById("lobby") return ( data: GameState, frame: number, editor: boolean = false ) => { if (!data.started) { canvas.style.display = 'none' if (lobby) lobby.style.display = '' return } else { canvas.style.display = '' if (lobby) lobby.style.display = 'none' } let map = getMap(data.mapId) if (!map) return if (map.id !== lastMapDrawn) { canvas.style.display = "" canvas.width = map.width * ATLAS_TILE_WIDTH canvas.height = map.height * ATLAS_TILE_WIDTH } let ctx = canvas.getContext("2d") ctx.imageSmoothingEnabled = false ctx.clearRect(0, 0, canvas.width, canvas.height) drawMap(ctx, atlas, map, lastMapDrawn, editor) drawItems(ctx, atlas, data.items) drawGhosts(ctx, atlas, data.ghosts) drawPlayers(ctx, atlas, data.players, frame, map, false) drawPlayers(ctx, atlas, data.players, frame, map, true) updateStyle(map.width, map.height) if (!editor) { updateAudio(data) } lastMapDrawn = map.id } }