taizo-hori 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +179 -0
- package/assets/sprites/dig_dug_title.png +0 -0
- package/assets/sprites/flower_large_1.png +0 -0
- package/assets/sprites/flower_large_2.png +0 -0
- package/assets/sprites/flower_small.png +0 -0
- package/assets/sprites/fygar_fire_1.png +0 -0
- package/assets/sprites/fygar_fire_2.png +0 -0
- package/assets/sprites/fygar_fire_3.png +0 -0
- package/assets/sprites/fygar_ghosting_1.png +0 -0
- package/assets/sprites/fygar_ghosting_2.png +0 -0
- package/assets/sprites/fygar_inflating_1.png +0 -0
- package/assets/sprites/fygar_inflating_2.png +0 -0
- package/assets/sprites/fygar_inflating_3.png +0 -0
- package/assets/sprites/fygar_popped.png +0 -0
- package/assets/sprites/fygar_smooshed.png +0 -0
- package/assets/sprites/fygar_walking_1.png +0 -0
- package/assets/sprites/fygar_walking_2.png +0 -0
- package/assets/sprites/hose_line_horizontal.png +0 -0
- package/assets/sprites/hose_line_vertical.png +0 -0
- package/assets/sprites/hose_nozzle_horizontal.png +0 -0
- package/assets/sprites/hose_nozzle_vertical.png +0 -0
- package/assets/sprites/player_digging_horizontal_1.png +0 -0
- package/assets/sprites/player_digging_horizontal_2.png +0 -0
- package/assets/sprites/player_digging_vertical_1.png +0 -0
- package/assets/sprites/player_digging_vertical_2.png +0 -0
- package/assets/sprites/player_dying_horizontal_1.png +0 -0
- package/assets/sprites/player_dying_horizontal_2.png +0 -0
- package/assets/sprites/player_dying_horizontal_3.png +0 -0
- package/assets/sprites/player_dying_horizontal_4.png +0 -0
- package/assets/sprites/player_dying_horizontal_5.png +0 -0
- package/assets/sprites/player_dying_vertical_1.png +0 -0
- package/assets/sprites/player_dying_vertical_2.png +0 -0
- package/assets/sprites/player_dying_vertical_3.png +0 -0
- package/assets/sprites/player_dying_vertical_4.png +0 -0
- package/assets/sprites/player_dying_vertical_5.png +0 -0
- package/assets/sprites/player_pumping_horizontal_1.png +0 -0
- package/assets/sprites/player_pumping_horizontal_2.png +0 -0
- package/assets/sprites/player_pumping_vertical_1.png +0 -0
- package/assets/sprites/player_pumping_vertical_2.png +0 -0
- package/assets/sprites/player_shooting_horizontal.png +0 -0
- package/assets/sprites/player_shooting_vertical.png +0 -0
- package/assets/sprites/player_smooshed_horizontal.png +0 -0
- package/assets/sprites/player_smooshed_vertical.png +0 -0
- package/assets/sprites/player_walking_horizontal_1.png +0 -0
- package/assets/sprites/player_walking_horizontal_2.png +0 -0
- package/assets/sprites/player_walking_vertical_1.png +0 -0
- package/assets/sprites/player_walking_vertical_2.png +0 -0
- package/assets/sprites/pooka_ghosting_1.png +0 -0
- package/assets/sprites/pooka_ghosting_2.png +0 -0
- package/assets/sprites/pooka_inflating_1.png +0 -0
- package/assets/sprites/pooka_inflating_2.png +0 -0
- package/assets/sprites/pooka_inflating_3.png +0 -0
- package/assets/sprites/pooka_popped.png +0 -0
- package/assets/sprites/pooka_smooshed.png +0 -0
- package/assets/sprites/pooka_walking_1.png +0 -0
- package/assets/sprites/pooka_walking_2.png +0 -0
- package/assets/sprites/prize_1.png +0 -0
- package/assets/sprites/prize_10.png +0 -0
- package/assets/sprites/prize_11.png +0 -0
- package/assets/sprites/prize_2.png +0 -0
- package/assets/sprites/prize_3.png +0 -0
- package/assets/sprites/prize_4.png +0 -0
- package/assets/sprites/prize_5.png +0 -0
- package/assets/sprites/prize_6.png +0 -0
- package/assets/sprites/prize_7.png +0 -0
- package/assets/sprites/prize_8.png +0 -0
- package/assets/sprites/prize_9.png +0 -0
- package/assets/sprites/rock_1.png +0 -0
- package/assets/sprites/rock_2.png +0 -0
- package/assets/sprites/rock_crumbling_1.png +0 -0
- package/assets/sprites/rock_crumbling_2.png +0 -0
- package/assets/sprites/score_sheet.png +0 -0
- package/dist/assets/style.css +1 -0
- package/dist/digdug.es.js +3197 -0
- package/dist/digdug.es.js.map +1 -0
- package/dist/digdug.umd.js +2 -0
- package/dist/digdug.umd.js.map +1 -0
- package/package.json +61 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"digdug.umd.js","sources":["../src/utils/constants.js","../src/utils/loadImage.js","../src/utils/dirtGradient.js","../src/Renderer.js","../src/utils/Grid.js","../src/systems/InputManager.js","../src/systems/CollisionSystem.js","../src/entities/Enemy.js","../src/entities/Pooka.js","../src/entities/Fygar.js","../src/entities/Rock.js","../src/systems/LevelManager.js","../src/systems/ScoreManager.js","../src/entities/Player.js","../src/Game.js","../src/index.js"],"sourcesContent":["// Game dimensions\nexport const TILE_SIZE = 16;\nexport const GRID_WIDTH = 27;\nexport const GRID_HEIGHT = 18;\nexport const CANVAS_WIDTH = GRID_WIDTH * TILE_SIZE;\nexport const CANVAS_HEIGHT = GRID_HEIGHT * TILE_SIZE;\nexport const HI_SCORE_KEY = 'digdug_highscore';\n\n// Game states\nexport const GAME_STATES = {\n MENU: 'menu',\n INTRO: 'intro', // Starting animation before gameplay\n PLAYING: 'playing',\n PAUSED: 'paused',\n DYING: 'dying',\n RESPAWNING: 'respawning',\n LEVEL_COMPLETE: 'level_complete',\n GAME_OVER: 'game_over',\n};\n\n// Tile types\nexport const TILE_TYPES = {\n EMPTY: 0,\n DIRT: 1,\n ROCK: 2,\n};\n\n// Directions\nexport const DIRECTIONS = {\n UP: 'up',\n DOWN: 'down',\n LEFT: 'left',\n RIGHT: 'right',\n};\n\n// Player settings\nexport const PLAYER = {\n SPEED: 1.2, // Pixels per frame - player should feel responsive\n START_LIVES: 3,\n PUMP_RANGE: TILE_SIZE * 3,\n};\n\n// Enemy settings\nexport const ENEMY_TYPES = {\n POOKA: 'pooka',\n FYGAR: 'fygar',\n};\n\nexport const ENEMY = {\n MIN_GHOST_DURATION: 1200, // Must ghost for at least 1.2 seconds\n POOKA: {\n SPEED: 0.7, // Pixels per frame in tunnels\n POINTS: 200,\n GHOST_SPEED: 0.5, // Speed when moving through dirt\n GHOST_MODE_DELAY: () => 5000 + Math.floor(Math.random() * 3) * 2500, // 5, 7.5, or 10 seconds before entering ghost mode\n },\n FYGAR: {\n SPEED: 0.6, // Pixels per frame in tunnels\n POINTS: 400,\n GHOST_SPEED: 0.4,\n GHOST_MODE_DELAY: 10000, // 10 seconds before entering ghost mode\n FIRE_RANGE: TILE_SIZE * 4,\n FIRE_COOLDOWN: 2500, // 2.5 seconds between fire breaths\n FIRE_CHARGE_TIME: 300, // ms pause before breathing fire\n FIRE_DURATION: 450, // 450ms fire stays visible (150ms per tile extension)\n },\n};\n\n// Scoring\nexport const SCORES = {\n DIG_TILE: 10,\n\n // Depth-based pump kill points\n // Dirt area: rows 2-17 (16 rows), sky: rows 0-1\n // Quarters of DIRT ONLY: rows 2-5 (Q1), 6-9 (Q2), 10-13 (Q3), 14-17 (Q4)\n PUMP_KILL: {\n POOKA: [200, 300, 400, 500],\n FYGAR: [200, 300, 400, 500],\n FYGAR_HORIZONTAL: [400, 600, 800, 1000],\n },\n\n // Rock kills by enemy count (index = count, 8+ capped at index 8)\n ROCK_KILL: [0, 1000, 2500, 4000, 6000, 8000, 10000, 12000, 15000],\n\n // Prize values (prize_1 through prize_11)\n BONUS_ITEMS: [\n 400, 600, 800, 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000,\n ],\n};\n\n// Colors (arcade-accurate)\nexport const COLORS = {\n BACKGROUND: '#000000',\n SKY: 'rgb(0, 0, 145)', // Blue sky for top rows\n TEXT_WHITE: '#ffffff',\n TEXT_RED: '#e33122',\n};\n\n// Animation settings\nexport const ANIMATION = {\n FRAME_DURATION: 150, // ms per frame\n INFLATE_FRAMES: 4,\n WALK_FRAMES: 2,\n};\n\n// Rock physics\nexport const ROCK = {\n FALL_DELAY: 300, // 400ms delay before rock falls after player triggers it\n FALL_SPEED: 3, // Pixels per frame when falling\n SHAKE_DURATION: 200, // ms rock shakes before falling (triggered by player from below)\n};\n\n// Level settings\nexport const LEVEL = {\n START_ENEMIES: 4,\n MAX_ENEMIES: 8,\n ENEMY_INCREMENT: 1, // Additional enemies per level\n ROCKS_PER_LEVEL: 3,\n ROCKS_FOR_BONUS: 2, // Number of rocks dropped to spawn bonus\n};\n\n// Death settings\nexport const DEATH = {\n ANIMATION_DURATION: 1500, // 1.5 second death animation\n RESPAWN_DELAY: 3000, // 3 seconds \"Player 1 Ready\" display\n INVINCIBILITY_TIME: 2000, // 2 seconds invincibility after respawn\n};\n","export const loadImage = (url) => {\n return new Promise((resolve, reject) => {\n const img = new Image();\n img.onload = () => resolve(img);\n img.onerror = (err) => reject(err);\n img.src = url;\n });\n};\n","export const DIRT_COLORS = [\n {\n DIRT_TOP: 'rgb(244, 187, 64)',\n DIRT_MID: 'rgb(207, 111, 41)',\n DIRT_LOW: 'rgb(169, 49, 24)',\n DIRT_LOWEST: 'rgb(138, 26, 16)',\n },\n {\n DIRT_TOP: 'rgb(128, 128, 128)',\n DIRT_MID: 'rgb(162, 130, 74)',\n DIRT_LOW: 'rgb(222, 171, 58)',\n DIRT_LOWEST: 'rgb(187, 102, 37)',\n },\n {\n DIRT_TOP: 'rgb(128, 128, 128)',\n DIRT_MID: 'rgb(104, 104, 69)',\n DIRT_LOW: 'rgb(73, 103, 68)',\n DIRT_LOWEST: 'rgb(64, 64, 64)',\n },\n];\n\nfunction rgbStringToHSL(rgbStr) {\n // Extract numbers\n const [r, g, b] = rgbStr.match(/\\d+/g).map(Number);\n\n // Normalize to 0-1\n const rN = r / 255;\n const gN = g / 255;\n const bN = b / 255;\n\n const max = Math.max(rN, gN, bN);\n const min = Math.min(rN, gN, bN);\n let h, s;\n const l = (max + min) / 2;\n\n if (max === min) {\n h = s = 0; // Achromatic\n } else {\n const d = max - min;\n s = l > 0.5 ? d / (2 - max - min) : d / (max + min);\n switch (max) {\n case rN:\n h = (gN - bN) / d + (gN < bN ? 6 : 0);\n break;\n case gN:\n h = (bN - rN) / d + 2;\n break;\n case bN:\n h = (rN - gN) / d + 4;\n break;\n }\n h /= 6;\n }\n\n return {\n h: Math.round(h * 360),\n s: Math.round(s * 100),\n l: Math.round(l * 100),\n };\n}\n\nexport function getDirtGradient(level) {\n const groupIndex = Math.floor((level - 1) / 4);\n const finalIndex = groupIndex % DIRT_COLORS.length;\n const { DIRT_TOP, DIRT_MID, DIRT_LOW, DIRT_LOWEST } =\n DIRT_COLORS[finalIndex];\n\n return [\n { stop: 0.0, color: rgbStringToHSL(DIRT_TOP) },\n { stop: 0.33, color: rgbStringToHSL(DIRT_MID) },\n { stop: 0.66, color: rgbStringToHSL(DIRT_LOW) },\n { stop: 1.0, color: rgbStringToHSL(DIRT_LOWEST) },\n ];\n}\n","import {\n CANVAS_WIDTH,\n CANVAS_HEIGHT,\n TILE_SIZE,\n COLORS,\n TILE_TYPES,\n DIRECTIONS,\n DEATH,\n ENEMY_TYPES,\n} from './utils/constants.js';\nimport { loadImage } from './utils/loadImage.js';\nimport { getDirtGradient } from './utils/dirtGradient.js';\n\nexport class Renderer {\n constructor(config) {\n this.config = config;\n this.canvas = document.createElement('canvas');\n this.ctx = this.canvas.getContext('2d');\n\n // Set canvas size\n this.canvas.width = config.width;\n this.canvas.height = config.height;\n\n // Apply scaling\n if (config.scale && config.scale !== 1) {\n this.canvas.style.width = `${config.width * config.scale}px`;\n this.canvas.style.height = `${config.height * config.scale}px`;\n }\n\n // Disable image smoothing for pixel-perfect rendering\n this.ctx.imageSmoothingEnabled = false;\n\n // Sprite cache\n this.sprites = {};\n this.spritesLoaded = false;\n this.loadSprites();\n\n // Score sprite sheet (separate from regular sprites)\n this.scoreSheet = null;\n this.scoreSheetLoaded = false;\n this.loadScoreSheet();\n\n // Score sprite coordinates mapping\n // Based on score_sheet.png layout:\n // Row 0: ~, 200, 300, 400, 500, 600, 800, 1000\n // Row 1: 1000, 2000, 2500, 3000, 4000, 5000, 6000\n // Row 2: 7000, 8000, 10000, 12000, 15000\n this.scoreSpriteMap = {\n 200: { x: 23, y: 0, w: 15, h: 7 },\n 300: { x: 42, y: 0, w: 15, h: 7 },\n 400: { x: 61, y: 0, w: 15, h: 7 },\n 500: { x: 81, y: 0, w: 15, h: 7 },\n 600: { x: 100, y: 0, w: 15, h: 7 },\n 800: { x: 119, y: 0, w: 15, h: 7 },\n 1000: { x: 0, y: 15, w: 17, h: 7 },\n 2000: { x: 21, y: 15, w: 20, h: 7 },\n 2500: { x: 45, y: 15, w: 20, h: 7 },\n 3000: { x: 69, y: 15, w: 20, h: 7 },\n 4000: { x: 93, y: 15, w: 20, h: 7 },\n 5000: { x: 117, y: 15, w: 20, h: 7 },\n 6000: { x: 141, y: 15, w: 20, h: 7 },\n 7000: { x: 13, y: 30, w: 20, h: 7 },\n 8000: { x: 40, y: 30, w: 20, h: 7 },\n 10000: { x: 66, y: 30, w: 22, h: 7 },\n 12000: { x: 95, y: 30, w: 22, h: 7 },\n 15000: { x: 124, y: 30, w: 22, h: 7 },\n };\n\n // Background cache - pre-rendered dirt layer for performance\n this.backgroundCanvas = document.createElement('canvas');\n this.backgroundCanvas.width = config.width;\n this.backgroundCanvas.height = config.height;\n this.backgroundCtx = this.backgroundCanvas.getContext('2d');\n this.backgroundCtx.imageSmoothingEnabled = false;\n this.backgroundDirty = true; // Flag to rebuild background when grid changes\n this.lastGridState = null; // Track grid state to detect changes\n }\n\n /**\n * Load sprite images\n */\n loadSprites() {\n const spriteFiles = [\n 'player_walking_horizontal_1.png',\n 'player_walking_horizontal_2.png',\n 'player_walking_vertical_1.png',\n 'player_walking_vertical_2.png',\n 'player_digging_horizontal_1.png',\n 'player_digging_horizontal_2.png',\n 'player_digging_vertical_1.png',\n 'player_digging_vertical_2.png',\n 'player_shooting_horizontal.png',\n 'player_shooting_vertical.png',\n 'player_pumping_horizontal_1.png',\n 'player_pumping_horizontal_2.png',\n 'player_pumping_vertical_1.png',\n 'player_pumping_vertical_2.png',\n 'player_smooshed_horizontal.png',\n 'player_smooshed_vertical.png',\n 'player_dying_horizontal_1.png',\n 'player_dying_horizontal_2.png',\n 'player_dying_horizontal_3.png',\n 'player_dying_horizontal_4.png',\n 'player_dying_horizontal_5.png',\n 'player_dying_vertical_1.png',\n 'player_dying_vertical_2.png',\n 'player_dying_vertical_3.png',\n 'player_dying_vertical_4.png',\n 'player_dying_vertical_5.png',\n 'hose_line_horizontal.png',\n 'hose_line_vertical.png',\n 'hose_nozzle_horizontal.png',\n 'hose_nozzle_vertical.png',\n 'pooka_walking_1.png',\n 'pooka_walking_2.png',\n 'pooka_ghosting_1.png',\n 'pooka_ghosting_2.png',\n 'fygar_walking_1.png',\n 'fygar_walking_2.png',\n 'fygar_ghosting_1.png',\n 'fygar_ghosting_2.png',\n 'fygar_fire_1.png',\n 'fygar_fire_2.png',\n 'fygar_fire_3.png',\n 'pooka_inflating_1.png',\n 'pooka_inflating_2.png',\n 'pooka_inflating_3.png',\n 'pooka_popped.png',\n 'pooka_smooshed.png',\n 'fygar_inflating_1.png',\n 'fygar_inflating_2.png',\n 'fygar_inflating_3.png',\n 'fygar_popped.png',\n 'fygar_smooshed.png',\n 'rock_1.png',\n 'rock_2.png',\n 'rock_crumbling_1.png',\n 'rock_crumbling_2.png',\n 'flower_small.png',\n 'prize_1.png',\n 'prize_2.png',\n 'prize_3.png',\n 'prize_4.png',\n 'prize_5.png',\n 'prize_6.png',\n 'prize_7.png',\n 'prize_8.png',\n 'prize_9.png',\n 'prize_10.png',\n 'prize_11.png',\n ];\n\n let loadedCount = 0;\n const totalSprites = spriteFiles.length;\n\n spriteFiles.forEach((filename) => {\n const img = new Image();\n img.onload = () => {\n loadedCount++;\n if (loadedCount === totalSprites) {\n this.spritesLoaded = true;\n }\n };\n img.onerror = () => {\n console.warn(`Failed to load sprite: ${filename}`);\n loadedCount++;\n if (loadedCount === totalSprites) {\n this.spritesLoaded = true;\n }\n };\n const spriteName = filename.replace('.png', '');\n img.src = `/assets/sprites/${filename}`;\n this.sprites[spriteName] = img;\n });\n }\n\n /**\n * Load the score sprite sheet\n */\n async loadScoreSheet() {\n const img = await loadImage('/assets/sprites/score_sheet.png');\n this.scoreSheet = img;\n this.scoreSheetLoaded = true;\n }\n\n /**\n * Attach canvas to DOM container\n */\n attachTo(container) {\n container.appendChild(this.canvas);\n }\n\n async drawMenu() {\n const img = await loadImage('/assets/sprites/dig_dug_title.png');\n const width = img.naturalWidth * 1.5;\n this.ctx.drawImage(\n img,\n CANVAS_WIDTH / 2 - width / 2,\n TILE_SIZE * 2,\n width,\n img.naturalHeight * 1.5\n );\n\n this.drawText(\n '▶ 1 PLAYER',\n CANVAS_WIDTH / 2,\n CANVAS_HEIGHT / 2 + TILE_SIZE * 6,\n {\n size: 8,\n color: COLORS.TEXT_WHITE,\n align: 'center',\n }\n );\n\n // this.drawText(\n // 'ARROWS -> MOVE // SPACE -> PUMP // ESC -> PAUSE',\n // CANVAS_WIDTH / 2,\n // CANVAS_HEIGHT / 2 + TILE_SIZE * 8,\n // {\n // size: 5,\n // color: COLORS.TEXT_WHITE,\n // align: 'center',\n // }\n // );\n }\n\n /**\n * Clear the canvas\n * Note: With background caching, we don't need to clear to black\n * since drawGrid() will overwrite with the cached background\n */\n clear() {\n // Only clear if background isn't ready yet\n if (this.backgroundDirty || !this.lastGridState) {\n this.ctx.fillStyle = COLORS.BACKGROUND;\n this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);\n }\n }\n\n /**\n * Force clear the canvas to black (for menus, game over screens, etc.)\n */\n forceClear() {\n this.ctx.fillStyle = COLORS.BACKGROUND;\n this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);\n }\n\n /**\n * Mark the background as needing redraw (call when grid changes)\n */\n markBackgroundDirty() {\n this.backgroundDirty = true;\n }\n\n /**\n * Draw the grid using cached background for performance\n * Only redraws when grid actually changes\n */\n drawGrid(grid, currentLevel) {\n // Check if grid state changed (simple hash of tunnel positions)\n const currentGridState = grid.getStateHash ? grid.getStateHash() : null;\n if (currentGridState !== this.lastGridState) {\n this.backgroundDirty = true;\n this.lastGridState = currentGridState;\n }\n\n // Rebuild background cache if dirty\n if (this.backgroundDirty) {\n this.renderBackgroundToCache(grid, currentLevel);\n this.backgroundDirty = false;\n }\n\n // Draw cached background in one operation\n this.ctx.drawImage(this.backgroundCanvas, 0, 0);\n }\n\n getDirtColorHSL(ratio, currentLevel) {\n // Clamp ratio between 0 and 1\n const r = Math.max(0, Math.min(1, ratio));\n const dirtGradient = getDirtGradient(currentLevel);\n\n // Find the two colors we are between (e.g., Light and Mid)\n let lower = dirtGradient[0];\n let upper = dirtGradient[dirtGradient.length - 1];\n\n for (let i = 0; i < dirtGradient.length - 1; i++) {\n if (r >= dirtGradient[i].stop && r <= dirtGradient[i + 1].stop) {\n lower = dirtGradient[i];\n upper = dirtGradient[i + 1];\n break;\n }\n }\n\n // Calculate how far we are between the two stops (0.0 to 1.0)\n // e.g. if Stops are 0.33 and 0.66, and ratio is 0.5, mixPercent is ~0.5\n const range = upper.stop - lower.stop;\n const mixPercent = (r - lower.stop) / range;\n\n // Linear Interpolation (Lerp) the H, S, and L values\n // Note: If Range is 0 (end of array), avoid divide by zero\n if (range === 0) return upper.color;\n\n return {\n h: Math.round(\n lower.color.h + (upper.color.h - lower.color.h) * mixPercent\n ),\n s: Math.round(\n lower.color.s + (upper.color.s - lower.color.s) * mixPercent\n ),\n l: Math.round(\n lower.color.l + (upper.color.l - lower.color.l) * mixPercent\n ),\n };\n }\n\n /**\n * Render the full background to the cache canvas\n * Called only when grid changes\n */\n renderBackgroundToCache(grid, currentLevel) {\n const ctx = this.backgroundCtx;\n\n // Clear with background color\n ctx.fillStyle = COLORS.BACKGROUND;\n ctx.fillRect(\n 0,\n 0,\n this.backgroundCanvas.width,\n this.backgroundCanvas.height\n );\n\n for (let y = 0; y < grid.height; y++) {\n for (let x = 0; x < grid.width; x++) {\n const tile = grid.getTile(x, y);\n const px = x * TILE_SIZE;\n const py = y * TILE_SIZE;\n\n // Top 2 rows are sky\n if (y < 2) {\n ctx.fillStyle = COLORS.SKY;\n ctx.fillRect(px, py, TILE_SIZE, TILE_SIZE);\n continue;\n }\n\n if (tile === TILE_TYPES.DIRT || tile === TILE_TYPES.ROCK) {\n const depthRatio = (y - 2) / (grid.height - 2);\n\n // 1. Get Calculated HSL\n const { h, s, l } = this.getDirtColorHSL(\n depthRatio,\n currentLevel\n );\n\n // 2. Draw Base\n ctx.fillStyle = `hsl(${h}, ${s}%, ${l}%)`;\n ctx.fillRect(px, py, TILE_SIZE, TILE_SIZE);\n\n // 3. Boost Saturation for Specs\n // Shadow: Darker (-15%) and Richer (+20% Saturation)\n const shadowColor = `hsl(${h}, ${Math.min(100, s + 20)}%, ${Math.max(0, l - 15)}%)`;\n\n // Highlight: Brighter (+10%) and Richer (+10% Saturation)\n const lightColor = `hsl(${h}, ${Math.min(100, s + 10)}%, ${Math.min(100, l + 10)}%)`;\n\n // 4. Texture Loop (Spread out 4px grid)\n const STEP = 4;\n const SPEC_SIZE = 2; // 2x2 specs\n\n // --- PASS 1: Saturated Shadows ---\n ctx.fillStyle = shadowColor;\n for (let dy = 0; dy < TILE_SIZE; dy += STEP) {\n for (let dx = 0; dx < TILE_SIZE; dx += STEP) {\n const seedX = x * TILE_SIZE + dx;\n const seedY = y * TILE_SIZE + dy;\n const noise =\n Math.abs(\n Math.sin(seedX * 12.989 + seedY * 78.233) *\n 43758.545\n ) % 1;\n\n if (noise < 0.15) {\n ctx.fillRect(\n px + dx + 1,\n py + dy + 1,\n SPEC_SIZE,\n SPEC_SIZE\n );\n }\n }\n }\n\n // --- PASS 2: Vibrant Highlights ---\n ctx.fillStyle = lightColor;\n for (let dy = 0; dy < TILE_SIZE; dy += STEP) {\n for (let dx = 0; dx < TILE_SIZE; dx += STEP) {\n const seedX = x * TILE_SIZE + dx;\n const seedY = y * TILE_SIZE + dy;\n const noise =\n Math.abs(\n Math.sin(seedX * 90.123 + seedY * 11.456) *\n 12345.678\n ) % 1;\n\n if (noise < 0.08) {\n ctx.fillRect(\n px + dx + 1,\n py + dy + 1,\n SPEC_SIZE,\n SPEC_SIZE\n );\n }\n }\n }\n }\n // Empty tiles are just the background color (already cleared)\n }\n }\n }\n\n /**\n * Get the player's orientation for sprite rendering\n */\n getPlayerOrientation(player) {\n return player.direction === DIRECTIONS.LEFT ||\n player.direction === DIRECTIONS.RIGHT\n ? 'horizontal'\n : 'vertical';\n }\n\n /**\n * Draw the player with walking sprites\n */\n drawPlayer(player) {\n // Handle death animation\n if (player.isDying) {\n this.drawPlayerDeath(player);\n return;\n }\n\n // Flicker during invincibility\n if (player.isInvincible) {\n const flickerVisible =\n Math.floor(player.invincibilityTimer / 100) % 2 === 0;\n if (!flickerVisible) return; // Skip rendering on flicker-off frames\n }\n\n // Draw pump line if pumping\n if (player.pumpLength > 0) {\n this.drawPumpLine(player);\n }\n\n const px = player.x;\n const py = player.y;\n\n // Use sprites if loaded, otherwise fallback to simple square\n if (this.spritesLoaded) {\n // Determine which sprite set to use (horizontal or vertical, walking or digging)\n let spriteAction = 'walking';\n if (player.isDigging) spriteAction = 'digging';\n else if (player.isPumping) spriteAction = 'pumping';\n\n let frameNumber = player.animationFrame === 0 ? '_1' : '_2';\n const orientation = this.getPlayerOrientation(player);\n\n if (player.isShooting) {\n spriteAction = 'shooting';\n frameNumber = '';\n }\n\n const sprite =\n this.sprites[\n `player_${spriteAction}_${orientation}${frameNumber}`\n ];\n\n if (sprite && sprite.complete) {\n // Only use save/restore if we need to flip\n const needsFlip = player.spriteFlipH || player.spriteFlipV;\n\n if (needsFlip) {\n const centerX = px + TILE_SIZE / 2;\n const centerY = py + TILE_SIZE / 2;\n this.drawFlippedSprite(\n centerX,\n centerY,\n player.spriteFlipH,\n player.spriteFlipV,\n sprite\n );\n } else {\n // No flip needed - draw directly without save/restore\n this.ctx.drawImage(sprite, px, py, TILE_SIZE, TILE_SIZE);\n }\n }\n }\n }\n\n /**\n * Draw a horizontally or vertically flipped sprite\n * at TILE_SIZE width and height\n */\n drawFlippedSprite(centerX, centerY, flipH, flipV, sprite) {\n this.ctx.save();\n this.ctx.translate(centerX, centerY);\n this.ctx.scale(flipH ? -1 : 1, flipV ? -1 : 1);\n this.ctx.drawImage(\n sprite,\n -TILE_SIZE / 2,\n -TILE_SIZE / 2,\n TILE_SIZE,\n TILE_SIZE\n );\n this.ctx.restore();\n }\n\n /**\n * Draw player death animation\n */\n drawPlayerDeath(player) {\n const px = player.x;\n const py = player.y;\n const orientation = this.getPlayerOrientation(player);\n\n // If smooshed and still falling with rock OR waiting for delay, show smooshed sprite\n if (\n player.isSmooshed &&\n (player.attachedToRock?.isFalling ||\n player.smooshedDelayTimer < player.SMOOSHED_DELAY)\n ) {\n const spriteKey = `player_smooshed_${orientation}`;\n if (this.spritesLoaded) {\n const sprite = this.sprites[spriteKey];\n if (sprite && sprite.complete) {\n if (player.spriteFlipH) {\n const centerX = px + TILE_SIZE / 2;\n const centerY = py + TILE_SIZE / 2;\n this.drawFlippedSprite(\n centerX,\n centerY,\n true,\n false,\n sprite\n );\n } else {\n this.ctx.drawImage(\n sprite,\n px,\n py,\n TILE_SIZE,\n TILE_SIZE\n );\n }\n return;\n }\n }\n }\n\n // Death animation\n const progress = player.deathTimer / DEATH.ANIMATION_DURATION;\n // Calculate frame number (1-5) based on progress\n const frameNumber = Math.min(5, Math.floor(progress * 5) + 1);\n const spriteKey = `player_dying_${orientation}_${frameNumber}`;\n\n if (this.spritesLoaded) {\n const sprite = this.sprites[spriteKey];\n if (sprite && sprite.complete) {\n // Apply horizontal flip if needed, but never vertical flip\n if (player.spriteFlipH) {\n const centerX = px + TILE_SIZE / 2;\n const centerY = py + TILE_SIZE / 2;\n this.drawFlippedSprite(\n centerX,\n centerY,\n true,\n false,\n sprite\n );\n } else {\n this.ctx.drawImage(sprite, px, py, TILE_SIZE, TILE_SIZE);\n }\n return;\n }\n }\n }\n\n /**\n * Draw pump line extending from player\n */\n drawPumpLine(player) {\n // 1. Early exit if system isn't ready\n if (!this.spritesLoaded) return;\n\n // 2. Cache constants and frequently accessed properties\n const TILE = TILE_SIZE;\n const halfTile = TILE / 2;\n const px = player.x;\n const py = player.y;\n const endPoint = player.getPumpEndPoint();\n const ctx = this.ctx;\n\n // 3. Resolve orientation and sprites immediately\n const orientation = this.getPlayerOrientation(player);\n const nozzleSprite = this.sprites[`hose_nozzle_${orientation}`];\n const lineSprite = this.sprites[`hose_line_${orientation}`];\n\n // Check validity before proceeding with math\n if (\n !nozzleSprite ||\n !nozzleSprite.complete ||\n !lineSprite ||\n !lineSprite.complete\n ) {\n return;\n }\n\n // 4. Calculate Grid Difference\n // startX/Y logic inlined here\n const diffX = Math.round((px + halfTile - endPoint.x) / TILE);\n const diffY = Math.round((py + halfTile - endPoint.y) / TILE);\n\n // Cache Flip states\n const flipH = player.spriteFlipH;\n const flipV = player.spriteFlipV;\n\n // Helper to calculate segments count (exclude the nozzle itself)\n // If diff is -2 (2 tiles away), we need 1 line segment. |diff| - 1.\n\n // --- DOWN (Player looking DOWN) ---\n if (diffY < 0) {\n let segments = Math.abs(diffY) - 1;\n\n if (flipH) {\n const dx = px + halfTile;\n let dy = endPoint.y - TILE * 0.25;\n\n this.drawFlippedSprite(dx, dy, flipH, flipV, nozzleSprite);\n\n while (segments > 0) {\n dy -= TILE;\n this.drawFlippedSprite(dx, dy, flipH, flipV, lineSprite);\n segments--;\n }\n } else {\n const dx = px;\n let dy = endPoint.y - TILE * 0.75;\n\n ctx.drawImage(nozzleSprite, dx, dy, TILE, TILE);\n\n while (segments > 0) {\n dy -= TILE;\n ctx.drawImage(lineSprite, dx, dy, TILE, TILE);\n segments--;\n }\n }\n return;\n }\n\n // --- UP (Player looking UP) ---\n if (diffY > 0) {\n let segments = Math.abs(diffY) - 1;\n\n // Logic: UP always uses drawFlippedSprite\n const dx = px + halfTile;\n let dy = endPoint.y + TILE * 0.25;\n\n this.drawFlippedSprite(dx, dy, flipH, flipV, nozzleSprite);\n\n while (segments > 0) {\n dy += TILE;\n this.drawFlippedSprite(dx, dy, flipH, flipV, lineSprite);\n segments--;\n }\n return;\n }\n\n // --- RIGHT (Player looking RIGHT) ---\n if (diffX < 0) {\n let segments = Math.abs(diffX) - 1;\n\n // Logic: RIGHT always uses drawFlippedSprite\n let dx = endPoint.x - TILE * 0.25;\n const dy = py + halfTile;\n\n this.drawFlippedSprite(dx, dy, flipH, flipV, nozzleSprite);\n\n while (segments > 0) {\n dx -= TILE;\n this.drawFlippedSprite(dx, dy, flipH, flipV, lineSprite);\n segments--;\n }\n return;\n }\n\n // --- LEFT (Player looking LEFT) ---\n if (diffX > 0) {\n let segments = Math.abs(diffX) - 1;\n\n // Logic: LEFT always uses standard drawImage\n let dx = endPoint.x - TILE * 0.25;\n\n ctx.drawImage(nozzleSprite, dx, py, TILE, TILE);\n\n while (segments > 0) {\n dx += TILE;\n ctx.drawImage(lineSprite, dx, py, TILE, TILE);\n segments--;\n }\n }\n }\n\n /**\n * Draw an enemy with inflation, popped, and smooshed states\n */\n drawEnemy(enemy) {\n const px = enemy.x;\n const py = enemy.y;\n const centerX = px + TILE_SIZE / 2;\n const centerY = py + TILE_SIZE / 2;\n\n // Handle smooshed state (crushed by rock)\n if (enemy.isSmooshed) {\n this.drawEnemySmooshed(enemy, centerX, centerY);\n return;\n }\n\n // Handle popped state (after full inflation)\n if (enemy.isPopped) {\n this.drawEnemyPopped(enemy, centerX, centerY);\n return;\n }\n\n // Handle inflating state (being pumped)\n if (enemy.inflateLevel > 1.0) {\n this.drawEnemyInflating(enemy, centerX, centerY);\n return;\n }\n\n // Normal state - use walking/ghosting sprites\n if (this.spritesLoaded) {\n const frameNumber = enemy.animationFrame === 0 ? '1' : '2';\n const state = enemy.isGhosting ? 'ghosting' : 'walking';\n const sprite =\n this.sprites[`${enemy.type}_${state}_${frameNumber}`];\n\n this.drawEnemySprite(sprite, centerX, centerY, enemy.spriteFlipH);\n }\n\n // Draw fire breath for Fygar\n if (enemy.type === ENEMY_TYPES.FYGAR) {\n if (enemy.isCharging && enemy.isCharging()) {\n this.drawFygarCharging(enemy);\n } else if (enemy.isFireActive && enemy.isFireActive()) {\n this.drawFygarFire(enemy);\n }\n }\n }\n\n /**\n * Draw enemy in inflating state using inflation sprites\n * Sprites: inflating_1 (first third), inflating_2 (second third), inflating_3 (final third)\n * Each sprite has its own size (16x16, 20x20, 21x21) - render at natural size\n */\n drawEnemyInflating(enemy, centerX, centerY) {\n // inflateLevel goes from 1.0 to 2.0\n // Map to sprite stages: 1.0-1.33 = stage 1, 1.33-1.66 = stage 2, 1.66-2.0 = stage 3\n const inflateProgress = (enemy.inflateLevel - 1.0) / 1.0; // 0 to 1\n let stage;\n if (inflateProgress < 0.33) {\n stage = 1;\n } else if (inflateProgress < 0.66) {\n stage = 2;\n } else {\n stage = 3;\n }\n\n const spriteName = `${enemy.type}_inflating_${stage}`;\n const sprite = this.sprites[spriteName];\n\n this.drawEnemySprite(sprite, centerX, centerY, enemy.spriteFlipH);\n }\n\n /**\n * Draw enemy in popped state (after full inflation)\n */\n drawEnemyPopped(enemy, centerX, centerY) {\n const sprite = this.sprites[`${enemy.type}_popped`];\n this.drawEnemySprite(sprite, centerX, centerY, enemy.spriteFlipH);\n }\n\n /**\n * Helper to draw enemy sprite with flipping\n * and renders at natural size\n */\n drawEnemySprite(sprite, centerX, centerY, flipH) {\n if (sprite && sprite.complete) {\n const spriteWidth = sprite.naturalWidth;\n const spriteHeight = sprite.naturalHeight;\n\n this.ctx.save();\n this.ctx.translate(centerX, centerY);\n\n if (flipH) this.ctx.scale(-1, 1);\n\n this.ctx.drawImage(\n sprite,\n -spriteWidth / 2,\n -spriteHeight / 2,\n spriteWidth,\n spriteHeight\n );\n\n this.ctx.restore();\n }\n }\n\n /**\n * Draw enemy in smooshed state (crushed by rock)\n */\n drawEnemySmooshed(enemy, centerX, centerY) {\n const sprite = this.sprites[`${enemy.type}_smooshed`];\n this.drawEnemySprite(sprite, centerX, centerY, enemy.spriteFlipH);\n }\n\n /**\n * Draw Fygar charging animation (pulsing/flashing before fire)\n */\n drawFygarCharging(enemy) {\n const centerX = enemy.x + TILE_SIZE / 2;\n const centerY = enemy.y + TILE_SIZE / 2;\n const direction = enemy.getFireDirection();\n\n // Flash orange/red to indicate charging\n const flashRate = Math.floor(Date.now() / 100) % 2;\n this.ctx.fillStyle = flashRate === 0 ? '#ff6600' : '#ff3300';\n\n // Draw small flame particles near mouth\n const offsetX =\n direction === DIRECTIONS.RIGHT ? TILE_SIZE / 2 : -TILE_SIZE / 2;\n\n this.ctx.beginPath();\n this.ctx.arc(centerX + offsetX, centerY, 3, 0, Math.PI * 2);\n this.ctx.fill();\n }\n\n /**\n * Draw Fygar fire breath using sprites\n * Fire extends progressively: 1 tile (fire_1) -> 2 tiles (fire_2) -> 3 tiles (fire_3)\n * Each sprite is already the correct width (1, 2, or 3 tiles), so draw once\n */\n drawFygarFire(enemy) {\n const fireHitbox = enemy.getFireHitbox();\n if (!fireHitbox) return;\n\n const direction = enemy.getFireDirection();\n const facingLeft = direction === DIRECTIONS.LEFT;\n\n // Get current fire length (1-3 tiles based on timer)\n const tileCount = enemy.getFireTileCount ? enemy.getFireTileCount() : 3;\n\n // Use the sprite that matches current fire length\n // fire_1 = 1 tile wide, fire_2 = 2 tiles wide, fire_3 = 3 tiles wide\n const sprite = this.sprites[`fygar_fire_${tileCount}`];\n\n if (sprite && sprite.complete) {\n this.ctx.save();\n\n // Sprite width is tileCount * TILE_SIZE\n const spriteWidth = tileCount * TILE_SIZE;\n\n // Calculate fire position - starts at Fygar's mouth\n let fireX;\n if (facingLeft) {\n // Fire extends to the left from Fygar\n fireX = enemy.x - spriteWidth;\n } else {\n // Fire extends to the right from Fygar\n fireX = enemy.x + TILE_SIZE;\n }\n const fireY = enemy.y;\n\n // Move to center of fire for flipping\n const centerX = fireX + spriteWidth / 2;\n const centerY = fireY + TILE_SIZE / 2;\n\n this.ctx.translate(centerX, centerY);\n\n // Flip horizontally if facing right (sprites are drawn facing left)\n if (!facingLeft) {\n this.ctx.scale(-1, 1);\n }\n\n // Draw sprite centered - use actual sprite dimensions\n this.ctx.drawImage(\n sprite,\n -spriteWidth / 2,\n -TILE_SIZE / 2,\n spriteWidth,\n TILE_SIZE\n );\n\n this.ctx.restore();\n }\n }\n\n /**\n * Draw a rock with shaking and crumbling animations\n */\n drawRock(rock) {\n const px = rock.x;\n const py = rock.y;\n\n // Spawn animation - fade in with flash effect\n if (rock.isSpawning) {\n const progress = rock.spawnTimer / rock.SPAWN_DURATION;\n // Fade in from 0 to 1 over the duration\n const baseAlpha = progress;\n // Flash effect - oscillate rapidly, faster as we progress\n const flashFrequency = 8 + progress * 12; // 8-20 Hz flash\n const flashPhase = Math.sin(\n rock.spawnTimer * flashFrequency * 0.01\n );\n // Flash intensity decreases as spawn completes\n const flashIntensity = (1 - progress) * 0.4;\n const alpha = Math.min(1, baseAlpha + flashPhase * flashIntensity);\n\n this.ctx.save();\n this.ctx.globalAlpha = Math.max(0, alpha);\n\n if (this.spritesLoaded) {\n const sprite = this.sprites['rock_1'];\n if (sprite && sprite.complete) {\n this.ctx.drawImage(sprite, px, py, TILE_SIZE, TILE_SIZE);\n }\n }\n\n this.ctx.restore();\n return;\n }\n\n // Crumble animation - alternate between crumbling sprites\n if (rock.isCrumbling) {\n const progress = rock.crumbleTimer / rock.CRUMBLE_DURATION;\n // First half: crumbling_1, second half: crumbling_2\n const spriteKey =\n progress < 0.5 ? 'rock_crumbling_1' : 'rock_crumbling_2';\n\n if (this.spritesLoaded) {\n const sprite = this.sprites[spriteKey];\n if (sprite && sprite.complete) {\n this.ctx.drawImage(sprite, px, py, TILE_SIZE, TILE_SIZE);\n return;\n }\n }\n }\n\n // Calculate shake offset and determine sprite for shaking\n let spriteKey = 'rock_1';\n\n if (rock.isShaking) {\n // Alternate between rock_1 and rock_2 every 100ms while shaking\n spriteKey =\n Math.floor(rock.shakeTimer / 200) % 2 === 0\n ? 'rock_1'\n : 'rock_2';\n }\n\n if (this.spritesLoaded) {\n const sprite = this.sprites[spriteKey];\n if (sprite && sprite.complete) {\n this.ctx.drawImage(sprite, px, py, TILE_SIZE, TILE_SIZE);\n }\n }\n }\n\n /**\n * Draw bonus item with level-based prize sprites\n * Prizes unlock every 20 levels, spawning in order (0, 1, 2 within available range)\n * Item flashes after 3 seconds and disappears after 5 seconds total\n */\n drawBonusItem(item, level = 1) {\n const px = item.x;\n const py = item.y;\n\n // If flashing, toggle visibility every 100ms\n if (item.isFlashing && item.isFlashing()) {\n const flashVisible = Math.floor(item.elapsedTime / 100) % 2 === 0;\n if (!flashVisible) return; // Skip rendering on flash-off frames\n }\n\n // 1. Calculate the \"Starting Prize\" for this level range.\n // At level 1, start at 0. At level 160, start at 8 (Prize 9).\n // We use Math.min to ensure we don't exceed the index for Prize 9.\n const floorIndex = Math.min(Math.floor((level - 1) / 20), 8);\n\n // 2. Use sequential bonusIndex (cycles through 0, 1, 2) instead of random\n const localIndex = (item.bonusIndex || 0) % 3;\n\n // 3. Final Prize Number\n // This ensures prizes spawn in order within the available range\n const prizeNumber = Math.min(floorIndex + localIndex + 1, 11);\n\n const spriteKey = `prize_${prizeNumber}`;\n\n if (this.spritesLoaded) {\n const sprite = this.sprites[spriteKey];\n\n if (sprite && sprite.complete) {\n this.ctx.drawImage(sprite, px, py, TILE_SIZE, TILE_SIZE);\n }\n }\n }\n\n /**\n * Draw floating score displays\n */\n drawFloatingScores(floatingScores) {\n if (!this.scoreSheetLoaded || !this.scoreSheet) return;\n\n floatingScores.forEach((score) => {\n const spriteInfo = this.scoreSpriteMap[score.points];\n if (!spriteInfo) return;\n\n // Center the score sprite on the position, round to integers for crisp rendering\n const drawX = Math.round(score.x + (TILE_SIZE - spriteInfo.w) / 2);\n const drawY = Math.round(score.y);\n\n this.ctx.drawImage(\n this.scoreSheet,\n spriteInfo.x,\n spriteInfo.y,\n spriteInfo.w,\n spriteInfo.h,\n drawX,\n drawY,\n spriteInfo.w,\n spriteInfo.h\n );\n });\n }\n\n /**\n * Draw UI elements (score, lives, level) with Press Start 2P font\n */\n drawUI(scoreManager, levelManager) {\n // Score (left)\n this.drawText('1UP', 4, 10, {\n size: 6,\n color: COLORS.TEXT_RED,\n align: 'left',\n });\n this.drawText(`${scoreManager.score}`.padStart(2, '0'), 4, 20, {\n size: 8,\n color: COLORS.TEXT_WHITE,\n align: 'left',\n });\n\n // Lives\n if (this.spritesLoaded) {\n const sprite = this.sprites['player_digging_horizontal_1'];\n if (sprite && sprite.complete) {\n for (let i = 1; i < scoreManager.lives; i++) {\n const x = TILE_SIZE * 1.75 + (i - 1) * TILE_SIZE;\n const y = 0;\n this.ctx.drawImage(sprite, x, y, TILE_SIZE, TILE_SIZE);\n }\n }\n }\n\n // Hi-score (center)\n this.drawText('HI-SCORE', CANVAS_WIDTH / 2, 10, {\n size: 6,\n color: COLORS.TEXT_RED,\n align: 'center',\n });\n this.drawText(\n `${scoreManager.highScore}`.padStart(2, '0'),\n CANVAS_WIDTH / 2,\n 20,\n {\n size: 8,\n color: COLORS.TEXT_WHITE,\n align: 'center',\n }\n );\n\n this.drawText(\n `ROUND ${levelManager.currentLevel}`,\n CANVAS_WIDTH - 4,\n 10,\n {\n size: 6,\n color: COLORS.TEXT_WHITE,\n align: 'right',\n }\n );\n\n if (this.spritesLoaded) {\n const sprite = this.sprites['flower_small'];\n if (sprite && sprite.complete) {\n for (let i = 1; i <= levelManager.currentLevel; i++) {\n this.ctx.drawImage(\n sprite,\n CANVAS_WIDTH - TILE_SIZE * i,\n TILE_SIZE,\n TILE_SIZE,\n TILE_SIZE\n );\n }\n }\n }\n }\n\n /**\n * Draw text with Press Start 2P font\n */\n drawText(text, x, y, options = {}) {\n const size = options.size || 16;\n const color = options.color || COLORS.TEXT_WHITE;\n const align = options.align || 'left';\n\n this.ctx.font = `${size}px \"Press Start 2P\", \"Courier New\", monospace`;\n this.ctx.fillStyle = color;\n this.ctx.textAlign = align;\n this.ctx.fillText(text, x, y);\n }\n\n /**\n * Render respawning state with \"Player 1 Ready\" overlay\n */\n renderRespawning() {\n // Note: Game.js should call this.render() first, then this overlay\n // Overlay \"Player 1 Ready\" message\n this.drawText('PLAYER 1', CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2 - 3, {\n size: 10,\n color: COLORS.TEXT_WHITE,\n align: 'center',\n });\n this.drawText(\n 'READY!',\n CANVAS_WIDTH / 2,\n CANVAS_HEIGHT / 2 + TILE_SIZE + 15,\n {\n size: 10,\n color: COLORS.TEXT_WHITE,\n align: 'center',\n }\n );\n }\n\n /**\n * Draw debug information\n */\n drawDebugInfo(player, enemies) {\n // Draw grid lines\n this.ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';\n this.ctx.lineWidth = 1;\n\n for (let x = 0; x <= CANVAS_WIDTH; x += TILE_SIZE) {\n this.ctx.beginPath();\n this.ctx.moveTo(x, 0);\n this.ctx.lineTo(x, CANVAS_HEIGHT);\n this.ctx.stroke();\n }\n\n for (let y = 0; y <= CANVAS_HEIGHT; y += TILE_SIZE) {\n this.ctx.beginPath();\n this.ctx.moveTo(0, y);\n this.ctx.lineTo(CANVAS_WIDTH, y);\n this.ctx.stroke();\n }\n\n // Draw player hitbox\n if (player) {\n this.ctx.strokeStyle = 'rgba(0, 255, 0, 0.5)';\n this.ctx.strokeRect(player.x, player.y, TILE_SIZE, TILE_SIZE);\n }\n\n // Draw enemy hitboxes\n enemies.forEach((enemy) => {\n this.ctx.strokeStyle = 'rgba(255, 0, 0, 0.5)';\n this.ctx.strokeRect(enemy.x, enemy.y, TILE_SIZE, TILE_SIZE);\n });\n }\n}\n","import { GRID_WIDTH, GRID_HEIGHT, TILE_TYPES, TILE_SIZE } from './constants.js';\n\nexport class Grid {\n constructor() {\n this.width = GRID_WIDTH;\n this.height = GRID_HEIGHT;\n this.tiles = [];\n this.stateVersion = 0; // Increments when grid changes\n this.init();\n }\n\n /**\n * Initialize grid with all tiles as dirt\n */\n init() {\n this.tiles = [];\n for (let y = 0; y < this.height; y++) {\n this.tiles[y] = [];\n for (let x = 0; x < this.width; x++) {\n if (y <= 1) this.tiles[y][x] = TILE_TYPES.EMPTY;\n else this.tiles[y][x] = TILE_TYPES.DIRT;\n }\n }\n this.stateVersion++; // Mark as changed\n }\n\n /**\n * Get tile type at grid position\n */\n getTile(x, y) {\n if (x < 0 || x >= this.width || y < 0 || y >= this.height) {\n return TILE_TYPES.DIRT; // Out of bounds treated as dirt\n }\n return this.tiles[y][x];\n }\n\n /**\n * Set tile type at grid position\n */\n setTile(x, y, type) {\n if (x >= 0 && x < this.width && y >= 0 && y < this.height) {\n if (this.tiles[y][x] !== type) {\n this.tiles[y][x] = type;\n this.stateVersion++; // Mark grid as changed\n }\n }\n }\n\n /**\n * Get a simple version number that changes when grid changes\n * Used by renderer for cache invalidation\n */\n getStateHash() {\n return this.stateVersion;\n }\n\n /**\n * Check if tile is empty (tunnel)\n */\n isEmpty(x, y) {\n return this.getTile(x, y) === TILE_TYPES.EMPTY;\n }\n\n /**\n * Check if tile is dirt\n */\n isDirt(x, y) {\n return this.getTile(x, y) === TILE_TYPES.DIRT;\n }\n\n /**\n * Check if tile is a rock\n */\n isRock(x, y) {\n return this.getTile(x, y) === TILE_TYPES.ROCK;\n }\n\n /**\n * Dig (remove dirt) at grid position\n */\n dig(x, y) {\n if (this.isDirt(x, y)) {\n this.setTile(x, y, TILE_TYPES.EMPTY);\n return true;\n }\n return false;\n }\n\n /**\n * Count the number of dirt tiles\n */\n countDirt() {\n let count = 0;\n for (let y = 0; y < this.height; y++) {\n for (let x = 0; x < this.width; x++) {\n if (this.isDirt(x, y)) {\n count++;\n }\n }\n }\n return count;\n }\n\n /**\n * Convert pixel coordinates to grid coordinates\n */\n pixelToGrid(px, py) {\n return {\n x: Math.floor(px / TILE_SIZE),\n y: Math.floor(py / TILE_SIZE),\n };\n }\n\n /**\n * Convert grid coordinates to pixel coordinates (top-left of tile)\n */\n gridToPixel(gx, gy) {\n return {\n x: gx * TILE_SIZE,\n y: gy * TILE_SIZE,\n };\n }\n\n /**\n * Get center pixel position of a grid tile\n */\n gridToPixelCenter(gx, gy) {\n return {\n x: gx * TILE_SIZE + TILE_SIZE / 2,\n y: gy * TILE_SIZE + TILE_SIZE / 2,\n };\n }\n\n /**\n * Check if position (in pixels) is walkable\n */\n isWalkable(px, py) {\n const { x, y } = this.pixelToGrid(px, py);\n return this.isEmpty(x, y);\n }\n\n /**\n * Check if entity can move in a direction\n */\n canMove(px, py, direction, entitySize = TILE_SIZE) {\n let newX = px;\n let newY = py;\n\n switch (direction) {\n case 'up':\n newY -= 1;\n break;\n case 'down':\n newY += 1;\n break;\n case 'left':\n newX -= 1;\n break;\n case 'right':\n newX += 1;\n break;\n }\n\n // Check all corners of the entity\n const corners = [\n { x: newX, y: newY },\n { x: newX + entitySize - 1, y: newY },\n { x: newX, y: newY + entitySize - 1 },\n { x: newX + entitySize - 1, y: newY + entitySize - 1 },\n ];\n\n // All corners must be walkable or allow ghosting through dirt\n return corners.every((corner) => {\n const { x, y } = this.pixelToGrid(corner.x, corner.y);\n return x >= 0 && x < this.width && y >= 0 && y < this.height;\n });\n }\n\n /**\n * Clear a horizontal tunnel\n */\n clearHorizontalTunnel(startX, endX, y) {\n const minX = Math.min(startX, endX);\n const maxX = Math.max(startX, endX);\n for (let x = minX; x <= maxX; x++) {\n this.setTile(x, y, TILE_TYPES.EMPTY);\n }\n }\n\n /**\n * Clear a vertical tunnel\n */\n clearVerticalTunnel(x, startY, endY) {\n const minY = Math.min(startY, endY);\n const maxY = Math.max(startY, endY);\n for (let y = minY; y <= maxY; y++) {\n this.setTile(x, y, TILE_TYPES.EMPTY);\n }\n }\n\n /**\n * Place a rock at grid position\n */\n placeRock(x, y) {\n this.setTile(x, y, TILE_TYPES.ROCK);\n }\n\n /**\n * Remove rock at grid position\n */\n removeRock(x, y) {\n if (this.isRock(x, y)) {\n this.setTile(x, y, TILE_TYPES.EMPTY);\n }\n }\n\n /**\n * Get all rock positions\n */\n getRockPositions() {\n const rocks = [];\n for (let y = 0; y < this.height; y++) {\n for (let x = 0; x < this.width; x++) {\n if (this.isRock(x, y)) {\n rocks.push({ x, y });\n }\n }\n }\n return rocks;\n }\n\n /**\n * Check if there's dirt below a position\n */\n hasDirtBelow(x, y) {\n return this.isDirt(x, y + 1);\n }\n\n /**\n * Reset the grid\n */\n reset() {\n this.init();\n }\n}\n","export class InputManager {\n constructor() {\n this.keys = new Set();\n this.keysPressed = new Set(); // For single-press detection\n this.handleKeyDown = this.handleKeyDown.bind(this);\n this.handleKeyUp = this.handleKeyUp.bind(this);\n }\n\n /**\n * Initialize input listeners\n */\n init() {\n document.addEventListener('keydown', this.handleKeyDown);\n document.addEventListener('keyup', this.handleKeyUp);\n }\n\n /**\n * Handle key down event\n */\n handleKeyDown(e) {\n if (!this.keys.has(e.code)) {\n this.keysPressed.add(e.code);\n }\n this.keys.add(e.code);\n\n // Prevent default behavior for game keys\n if (\n [\n 'ArrowUp',\n 'ArrowDown',\n 'ArrowLeft',\n 'ArrowRight',\n 'Space',\n ].includes(e.code)\n ) {\n e.preventDefault();\n }\n }\n\n /**\n * Handle key up event\n */\n handleKeyUp(e) {\n this.keys.delete(e.code);\n this.keysPressed.delete(e.code);\n }\n\n /**\n * Check if key is currently held down\n */\n isKeyDown(code) {\n return this.keys.has(code);\n }\n\n /**\n * Check if key was just pressed (single press detection)\n */\n isKeyPressed(code) {\n const pressed = this.keysPressed.has(code);\n if (pressed) {\n this.keysPressed.delete(code);\n }\n return pressed;\n }\n\n /**\n * Check if up arrow or W is pressed\n */\n isUpPressed() {\n return this.isKeyDown('ArrowUp') || this.isKeyDown('KeyW');\n }\n\n /**\n * Check if down arrow or S is pressed\n */\n isDownPressed() {\n return this.isKeyDown('ArrowDown') || this.isKeyDown('KeyS');\n }\n\n /**\n * Check if left arrow or A is pressed\n */\n isLeftPressed() {\n return this.isKeyDown('ArrowLeft') || this.isKeyDown('KeyA');\n }\n\n /**\n * Check if right arrow or D is pressed\n */\n isRightPressed() {\n return this.isKeyDown('ArrowRight') || this.isKeyDown('KeyD');\n }\n\n /**\n * Check if space bar is pressed\n */\n isSpacePressed() {\n return this.isKeyDown('Space');\n }\n\n /**\n * Get movement direction from input\n */\n getDirection() {\n if (this.isUpPressed()) return 'up';\n if (this.isDownPressed()) return 'down';\n if (this.isLeftPressed()) return 'left';\n if (this.isRightPressed()) return 'right';\n return null;\n }\n\n /**\n * Clean up listeners\n */\n destroy() {\n document.removeEventListener('keydown', this.handleKeyDown);\n document.removeEventListener('keyup', this.handleKeyUp);\n this.keys.clear();\n this.keysPressed.clear();\n }\n}\n","import { DIRECTIONS, TILE_SIZE } from '../utils/constants.js';\n\nexport class CollisionSystem {\n constructor(grid) {\n this.grid = grid;\n }\n\n /**\n * Check if two rectangles overlap (AABB collision)\n */\n checkAABB(x1, y1, w1, h1, x2, y2, w2, h2) {\n return x1 < x2 + w2 && x1 + w1 > x2 && y1 < y2 + h2 && y1 + h1 > y2;\n }\n\n /**\n * Check collision between player and enemy\n */\n checkPlayerEnemyCollision(player, enemy) {\n const buffer = 6;\n return this.checkAABB(\n player.x,\n player.y,\n TILE_SIZE - buffer,\n TILE_SIZE - buffer,\n enemy.x,\n enemy.y,\n TILE_SIZE - buffer,\n TILE_SIZE - buffer\n );\n }\n\n /**\n * Check collision between rock and entity\n */\n checkRockEntityCollision(rock, entity) {\n return this.checkAABB(\n rock.x,\n rock.y,\n TILE_SIZE,\n TILE_SIZE,\n entity.x,\n entity.y,\n TILE_SIZE,\n TILE_SIZE\n );\n }\n\n /**\n * Check collision between player and bonus item\n */\n checkPlayerBonusCollision(player, bonus) {\n return this.checkAABB(\n player.x,\n player.y,\n TILE_SIZE,\n TILE_SIZE,\n bonus.x,\n bonus.y,\n TILE_SIZE,\n TILE_SIZE\n );\n }\n\n /**\n * Check if pump is in range of enemy\n */\n checkPumpRange(player, enemy, range) {\n const dx = player.x + TILE_SIZE / 2 - (enemy.x + TILE_SIZE / 2);\n const dy = player.y + TILE_SIZE / 2 - (enemy.y + TILE_SIZE / 2);\n const distance = Math.sqrt(dx * dx + dy * dy);\n return distance <= range;\n }\n\n /**\n * Check if entity is facing direction towards target\n */\n isFacing(entity, target, direction) {\n const dx = target.x - entity.x;\n const dy = target.y - entity.y;\n\n switch (direction) {\n case DIRECTIONS.UP:\n return dy < 0 && Math.abs(dy) > Math.abs(dx);\n case DIRECTIONS.DOWN:\n return dy > 0 && Math.abs(dy) > Math.abs(dx);\n case DIRECTIONS.LEFT:\n return dx < 0 && Math.abs(dx) > Math.abs(dy);\n case DIRECTIONS.RIGHT:\n return dx > 0 && Math.abs(dx) > Math.abs(dy);\n default:\n return false;\n }\n }\n\n /**\n * Get distance between two points\n */\n getDistance(x1, y1, x2, y2) {\n const dx = x2 - x1;\n const dy = y2 - y1;\n return Math.sqrt(dx * dx + dy * dy);\n }\n\n /**\n * Check line of sight between two entities\n */\n hasLineOfSight(entity1, entity2) {\n const { x: gx1, y: gy1 } = this.grid.pixelToGrid(entity1.x, entity1.y);\n const { x: gx2, y: gy2 } = this.grid.pixelToGrid(entity2.x, entity2.y);\n\n // Use Bresenham's line algorithm to check tiles between entities\n const dx = Math.abs(gx2 - gx1);\n const dy = Math.abs(gy2 - gy1);\n const sx = gx1 < gx2 ? 1 : -1;\n const sy = gy1 < gy2 ? 1 : -1;\n let err = dx - dy;\n let x = gx1;\n let y = gy1;\n\n while (x !== gx2 || y !== gy2) {\n // If there's dirt in the way, no line of sight\n if (this.grid.isDirt(x, y)) {\n return false;\n }\n\n const e2 = 2 * err;\n if (e2 > -dy) {\n err -= dy;\n x += sx;\n }\n if (e2 < dx) {\n err += dx;\n y += sy;\n }\n }\n\n return true;\n }\n}\n","import {\n TILE_SIZE,\n DIRECTIONS,\n ENEMY,\n ENEMY_TYPES,\n} from '../utils/constants.js';\n\n/**\n * Enemy class - Refactored for cleaner, more maintainable behavior\n * Phase 2: Tunnel movement with player tracking\n */\nexport class Enemy {\n constructor(x, y, type, speed, level = 1) {\n // Position (pixel coordinates)\n this.x = x;\n this.y = y;\n\n // Type and level\n this.type = type;\n this.level = level;\n\n // Speed (increases with level)\n this.baseSpeed = speed;\n this.speed = speed + (level - 1) * 0.1;\n\n // Movement state\n this.direction = DIRECTIONS.DOWN;\n this.isMoving = true;\n this.directionInitialized = false;\n\n // AI state\n this.state = 'roaming'; // 'roaming' or 'chasing'\n this.stateTimer = 0;\n this.directionChangeTimer = 0;\n\n // Track last grid position to detect when we enter a new tile\n this.lastGridX = -1;\n this.lastGridY = -1;\n\n // Track the previous tile to prevent immediately returning to it\n this.prevGridX = -1;\n this.prevGridY = -1;\n\n // Track tiles traveled in current direction to prevent oscillation\n this.tilesTraveledInDirection = 0;\n this.minTilesBeforeReverse = 2; // Must travel at least 2 tiles before reversing\n\n // Animation\n this.animationFrame = 0;\n this.animationTimer = 0;\n this.spriteFlipH = false;\n\n // Inflation (when pumped)\n this.isInflating = false;\n this.inflateLevel = 1.0;\n this.inflateTimer = 0;\n this.INFLATE_DURATION = 1200; // 1.2 seconds to full inflation\n this.deflateTimer = 0;\n\n // Popped state (after full inflation)\n this.isPopped = false;\n this.poppedTimer = 0;\n this.POPPED_DURATION = 400; // Show popped sprite for 1/3 of inflate time\n\n // Smooshed state (crushed by rock)\n this.isSmooshed = false;\n this.smooshedTimer = 0;\n this.SMOOSHED_DURATION = 400;\n\n // Destruction flag\n this.isDestroyed = false;\n\n // Distance from player (for scoring)\n this.distanceFromPlayer = 0;\n\n // Ghost mode state (Phase 4)\n this.ghostModeTimer = 0;\n this.canGhostMode = false;\n this.isGhosting = false;\n this.ghostingDuration = 0; // How long we've been ghosting\n // Ghost mode delay: minimum 5 seconds, plus 0-2 extra seconds in 1-second increments\n this.GHOST_MODE_DELAY =\n type === ENEMY_TYPES.POOKA\n ? ENEMY.POOKA.GHOST_MODE_DELAY()\n : ENEMY.FYGAR.GHOST_MODE_DELAY;\n this.MIN_GHOST_DURATION = ENEMY.MIN_GHOST_DURATION;\n\n // Track previous tile state for dirt-to-tunnel detection\n this.wasInDirt = false;\n\n // Escape state (for last enemy)\n this.isLastEnemy = false;\n this.isEscaping = false;\n this.hasEscaped = false;\n }\n\n /**\n * Update enemy state\n */\n update(deltaTime, player, grid) {\n // Update popped state (dying from inflation)\n if (this.isPopped) {\n this.updatePoppedState(deltaTime);\n return; // Don't update anything else while popped\n }\n\n // Update smooshed state (crushed by rock)\n if (this.isSmooshed) {\n this.updateSmooshedState(deltaTime);\n return; // Don't update anything else while smooshed\n }\n\n // Initialize direction on first update if needed\n if (!this.directionInitialized) {\n this.initializeDirection(grid);\n this.directionInitialized = true;\n // Initialize last grid position\n const { x: gx, y: gy } = grid.pixelToGrid(\n this.x + TILE_SIZE / 2,\n this.y + TILE_SIZE / 2\n );\n this.lastGridX = gx;\n this.lastGridY = gy;\n }\n\n // Update inflation state (handles both inflating and deflating)\n if (this.isInflating || this.inflateLevel > 1.0) {\n this.updateInflation(deltaTime);\n }\n\n // Update AI state based on player distance\n if (player) {\n this.updateAI(deltaTime, player);\n }\n\n // Update ghost mode timer and detect tunnel transitions (after AI so state is current)\n this.updateGhostMode(deltaTime, player, grid);\n\n // Move enemy\n if (this.isMoving && !this.isInflating) {\n this.move(grid, player);\n }\n\n // Check if enemy has escaped (walked fully off-screen)\n if (this.isEscaping && !this.hasEscaped) {\n // Only mark as escaped when fully off the left edge of the screen\n if (this.x + TILE_SIZE <= 0) {\n this.hasEscaped = true;\n }\n }\n\n // Update animation\n this.updateAnimation(deltaTime);\n\n // Calculate distance from player for scoring\n if (player) {\n const dx = this.x - player.x;\n const dy = this.y - player.y;\n this.distanceFromPlayer = Math.sqrt(dx * dx + dy * dy);\n }\n }\n\n /**\n * Update ghost mode state - timer, activation, and deactivation\n */\n updateGhostMode(deltaTime, player, grid) {\n // Get current tile state\n const { x: gx, y: gy } = grid.pixelToGrid(\n this.x + TILE_SIZE / 2,\n this.y + TILE_SIZE / 2\n );\n const currentlyInTunnel = grid.isEmpty(gx, gy);\n const currentlyInDirt = !currentlyInTunnel && !grid.isRock(gx, gy);\n\n // Track how long we've been ghosting\n if (this.isGhosting) {\n this.ghostingDuration += deltaTime;\n }\n\n // Exit ghost mode if in a tunnel and min duration elapsed\n // This handles both dirt-to-tunnel transitions AND ghosts traveling through existing tunnels\n if (\n this.isGhosting &&\n currentlyInTunnel &&\n this.ghostingDuration >= this.MIN_GHOST_DURATION\n ) {\n // Reset ghost mode when in tunnel after min duration\n this.ghostModeTimer = 0;\n this.canGhostMode = false;\n this.isGhosting = false;\n this.ghostingDuration = 0;\n }\n\n // Also check if we're adjacent to a tunnel and should snap to it\n // This prevents enemies from overshooting tunnels while ghosting\n if (\n this.isGhosting &&\n !currentlyInTunnel &&\n this.ghostingDuration >= this.MIN_GHOST_DURATION\n ) {\n const adjacentTunnel = this.findAdjacentTunnel(grid, gx, gy);\n if (adjacentTunnel) {\n // Snap to the adjacent tunnel\n this.x = adjacentTunnel.x * TILE_SIZE;\n this.y = adjacentTunnel.y * TILE_SIZE;\n // Update grid tracking\n this.lastGridX = adjacentTunnel.x;\n this.lastGridY = adjacentTunnel.y;\n // Exit ghost mode\n this.ghostModeTimer = 0;\n this.canGhostMode = false;\n this.isGhosting = false;\n this.ghostingDuration = 0;\n }\n }\n\n // Update previous state for next frame\n this.wasInDirt = currentlyInDirt;\n\n // Increment timer (always, when not currently ghosting)\n if (!this.isGhosting) {\n this.ghostModeTimer += deltaTime;\n\n // After delay, ghost mode becomes available\n if (this.ghostModeTimer >= this.GHOST_MODE_DELAY) {\n this.canGhostMode = true;\n }\n }\n\n // Ghost mode activation - when timer elapsed\n // Don't activate ghost mode if escaping at row 1 (unless chasing)\n const atEscapeRow = gy <= 1;\n const blockGhostForEscape =\n this.isEscaping && this.state === 'escaping' && atEscapeRow;\n\n const canActivateGhost =\n this.canGhostMode && !this.isGhosting && player && !blockGhostForEscape;\n\n if (canActivateGhost) {\n // Activate ghost mode\n this.isGhosting = true;\n this.ghostingDuration = 0;\n }\n }\n\n /**\n * Update AI behavior\n */\n updateAI(deltaTime, player) {\n // If last enemy and not chasing, try to escape\n if (this.isLastEnemy && this.state !== 'chasing') {\n if (!this.isEscaping) {\n this.isEscaping = true;\n this.state = 'escaping';\n this.stateTimer = 0;\n }\n }\n\n // If escaping, don't switch to other states unless player gets very close\n if (this.isEscaping) {\n const dx = this.x - player.x;\n const dy = this.y - player.y;\n const distanceToPlayer = Math.sqrt(dx * dx + dy * dy);\n\n // Only chase if player is very close (4 tiles)\n const ESCAPE_CHASE_DISTANCE = TILE_SIZE * 4;\n if (distanceToPlayer <= ESCAPE_CHASE_DISTANCE) {\n this.state = 'chasing';\n } else {\n this.state = 'escaping';\n }\n\n this.stateTimer += deltaTime;\n this.directionChangeTimer += deltaTime;\n return;\n }\n\n // Calculate distance to player\n const dx = this.x - player.x;\n const dy = this.y - player.y;\n const distanceToPlayer = Math.sqrt(dx * dx + dy * dy);\n\n // State transitions based on distance\n // Chase when player is within 8 tiles (~half the grid width)\n // Roam when player is beyond 10 tiles\n const CHASE_DISTANCE = TILE_SIZE * 8;\n const ROAM_DISTANCE = TILE_SIZE * 10;\n\n if (distanceToPlayer <= CHASE_DISTANCE && this.state !== 'chasing') {\n this.state = 'chasing';\n this.stateTimer = 0;\n } else if (\n distanceToPlayer > ROAM_DISTANCE &&\n this.state !== 'roaming'\n ) {\n this.state = 'roaming';\n this.stateTimer = 0;\n }\n\n // Update timers\n this.stateTimer += deltaTime;\n this.directionChangeTimer += deltaTime;\n }\n\n /**\n * Move enemy through tunnels (or through dirt when ghosting)\n */\n move(grid, player = null) {\n // If ghosting, use ghost movement logic\n if (this.isGhosting && player) {\n this.moveGhost(grid, player);\n return;\n }\n\n // Get current grid position\n const centerX = this.x + TILE_SIZE / 2;\n const centerY = this.y + TILE_SIZE / 2;\n const { x: gx, y: gy } = grid.pixelToGrid(centerX, centerY);\n\n // Check if we've moved to a new grid cell\n const enteredNewTile = gx !== this.lastGridX || gy !== this.lastGridY;\n\n // When entering a new tile, consider changing direction\n if (enteredNewTile) {\n // Remember the tile we came from\n this.prevGridX = this.lastGridX;\n this.prevGridY = this.lastGridY;\n this.lastGridX = gx;\n this.lastGridY = gy;\n this.tilesTraveledInDirection++;\n\n // At a new tile, decide direction based on AI state\n if (player && this.state === 'chasing') {\n this.decideDirectionAtTile(player, grid, gx, gy);\n } else if (this.state === 'escaping') {\n this.escapeAtTile(grid, gx, gy);\n } else if (this.state === 'roaming') {\n this.roamAtTile(grid, gx, gy);\n }\n } else if (player && this.state === 'chasing') {\n // Even if we didn't enter a new tile, check if we should turn\n // This handles the case where we're passing through an intersection\n // and need to turn toward the player\n this.checkForBetterDirection(player, grid, gx, gy);\n } else if (this.state === 'escaping') {\n // Check for better escape direction mid-tile\n this.checkForBetterEscapeDirection(grid, gx, gy);\n }\n\n // Calculate new position based on direction\n let newX = this.x;\n let newY = this.y;\n\n switch (this.direction) {\n case DIRECTIONS.UP:\n newY -= this.speed;\n break;\n case DIRECTIONS.DOWN:\n newY += this.speed;\n break;\n case DIRECTIONS.LEFT:\n newX -= this.speed;\n if (this.spriteFlipH) this.spriteFlipH = false;\n break;\n case DIRECTIONS.RIGHT:\n newX += this.speed;\n if (!this.spriteFlipH) this.spriteFlipH = true;\n break;\n }\n\n // Check if can move to new position (leading edge check)\n const canMove = this.canMoveInDirection(\n newX,\n newY,\n this.direction,\n grid\n );\n\n if (canMove) {\n // Move to new position\n this.x = newX;\n this.y = newY;\n\n // Apply grid snapping for tunnel centering\n this.applyGridSnapping(grid);\n } else {\n // Hit a wall, snap to grid and pick new direction\n this.snapToGrid(grid);\n this.pickValidDirection(grid, player, gx, gy);\n this.tilesTraveledInDirection = 0; // Reset counter when forced to change direction\n }\n }\n\n /**\n * Ghost mode movement - move directly toward target through dirt\n * When escaping, moves toward exit point; otherwise toward player\n * Uses subclass ghostSpeed if defined, otherwise 80% of base speed\n */\n moveGhost(grid, player) {\n // Use subclass-specific ghostSpeed if available (Pooka/Fygar have different speeds)\n const ghostSpeed = this.ghostSpeed || this.speed * 0.8;\n\n // Determine target: when escaping, ghost straight up; otherwise toward player\n let targetX, targetY;\n if (this.isEscaping && this.state === 'escaping') {\n // Ghost straight up - stay at same X, target top of screen\n targetX = this.x;\n targetY = -TILE_SIZE;\n } else {\n targetX = player.x;\n targetY = player.y;\n }\n\n // Calculate direction toward target\n const dx = targetX - this.x;\n const dy = targetY - this.y;\n const distance = Math.sqrt(dx * dx + dy * dy);\n\n // If very close to player, just use cardinal direction\n if (distance < 2) {\n return;\n }\n\n // Normalize direction and calculate movement\n const normalizedDx = dx / distance;\n const normalizedDy = dy / distance;\n\n // Calculate desired movement\n const moveX = normalizedDx * ghostSpeed;\n const moveY = normalizedDy * ghostSpeed;\n\n // Try diagonal movement first (both axes)\n const newX = this.x + moveX;\n const newY = this.y + moveY;\n\n // Determine the visual direction based on primary axis\n let newDirection;\n if (Math.abs(dx) > Math.abs(dy)) {\n newDirection = dx > 0 ? DIRECTIONS.RIGHT : DIRECTIONS.LEFT;\n } else {\n newDirection = dy > 0 ? DIRECTIONS.DOWN : DIRECTIONS.UP;\n }\n\n // Check if we can move diagonally (check both the X and Y destinations)\n const canMoveX = this.canGhostMoveToPosition(\n this.x + moveX,\n this.y,\n grid\n );\n const canMoveY = this.canGhostMoveToPosition(\n this.x,\n this.y + moveY,\n grid\n );\n\n if (canMoveX && canMoveY) {\n // Full diagonal movement\n this.x = newX;\n this.y = newY;\n this.direction = newDirection;\n return;\n }\n\n // Try X movement only\n if (canMoveX) {\n this.x = this.x + moveX;\n this.direction = dx > 0 ? DIRECTIONS.RIGHT : DIRECTIONS.LEFT;\n return;\n }\n\n // Try Y movement only\n if (canMoveY) {\n this.y = this.y + moveY;\n this.direction = dy > 0 ? DIRECTIONS.DOWN : DIRECTIONS.UP;\n return;\n }\n\n // Both blocked by rock - choose perpendicular direction that gets us closer to player\n // Determine which perpendicular directions to try based on player position\n let perpDirs = [];\n if (Math.abs(dx) > Math.abs(dy)) {\n // Primarily blocked horizontally, try vertical movement\n // Prefer the vertical direction toward the player\n if (dy > 0) {\n perpDirs = [DIRECTIONS.DOWN, DIRECTIONS.UP];\n } else {\n perpDirs = [DIRECTIONS.UP, DIRECTIONS.DOWN];\n }\n } else {\n // Primarily blocked vertically, try horizontal movement\n // Prefer the horizontal direction toward the player\n if (dx > 0) {\n perpDirs = [DIRECTIONS.RIGHT, DIRECTIONS.LEFT];\n } else {\n perpDirs = [DIRECTIONS.LEFT, DIRECTIONS.RIGHT];\n }\n }\n\n for (const dir of perpDirs) {\n const newPos = this.getNewPosition(this.x, this.y, dir, ghostSpeed);\n if (this.canGhostMoveToPosition(newPos.x, newPos.y, grid)) {\n this.x = newPos.x;\n this.y = newPos.y;\n this.direction = dir;\n return;\n }\n }\n\n // Still stuck - try all four directions as last resort\n const allDirs = [\n DIRECTIONS.UP,\n DIRECTIONS.DOWN,\n DIRECTIONS.LEFT,\n DIRECTIONS.RIGHT,\n ];\n for (const dir of allDirs) {\n const newPos = this.getNewPosition(this.x, this.y, dir, ghostSpeed);\n if (this.canGhostMoveToPosition(newPos.x, newPos.y, grid)) {\n this.x = newPos.x;\n this.y = newPos.y;\n this.direction = dir;\n return;\n }\n }\n\n // Completely stuck (surrounded by rocks) - stay in place\n }\n\n /**\n * Check if ghost can move to position (only sky blocks)\n */\n canGhostMoveToPosition(x, y, grid) {\n // Check center point\n const { y: gy } = grid.pixelToGrid(\n x + TILE_SIZE / 2,\n y + TILE_SIZE / 2\n );\n // Top row (sky) is always blocked\n if (gy === 0) {\n return false;\n }\n // Ghosts can pass through rocks and dirt\n return true;\n }\n\n /**\n * Get perpendicular direction for rock avoidance\n */\n getPerpendicularDirection(dir, clockwise) {\n if (clockwise) {\n switch (dir) {\n case DIRECTIONS.UP:\n return DIRECTIONS.RIGHT;\n case DIRECTIONS.RIGHT:\n return DIRECTIONS.DOWN;\n case DIRECTIONS.DOWN:\n return DIRECTIONS.LEFT;\n case DIRECTIONS.LEFT:\n return DIRECTIONS.UP;\n }\n } else {\n switch (dir) {\n case DIRECTIONS.UP:\n return DIRECTIONS.LEFT;\n case DIRECTIONS.LEFT:\n return DIRECTIONS.DOWN;\n case DIRECTIONS.DOWN:\n return DIRECTIONS.RIGHT;\n case DIRECTIONS.RIGHT:\n return DIRECTIONS.UP;\n }\n }\n return dir;\n }\n\n /**\n * Calculate new position given direction and speed\n */\n getNewPosition(x, y, direction, speed) {\n let newX = x;\n let newY = y;\n\n switch (direction) {\n case DIRECTIONS.UP:\n newY -= speed;\n break;\n case DIRECTIONS.DOWN:\n newY += speed;\n break;\n case DIRECTIONS.LEFT:\n newX -= speed;\n break;\n case DIRECTIONS.RIGHT:\n newX += speed;\n break;\n }\n\n return { x: newX, y: newY };\n }\n\n /**\n * Check if there's a better direction toward the player while passing through a tile\n * Only triggers at intersections (3+ valid directions)\n */\n checkForBetterDirection(player, grid, gx, gy) {\n // Get valid directions FIRST\n const validDirections = this.getValidDirectionsFromTile(grid, gx, gy);\n\n // Only consider turns at actual intersections (3+ directions)\n // This prevents turning in straight tunnels or corners\n if (validDirections.length < 3) {\n return;\n }\n\n // If current direction is not valid, don't try to change - let canMoveInDirection handle it\n if (!validDirections.includes(this.direction)) {\n return;\n }\n\n const dx = player.x - this.x;\n const dy = player.y - this.y;\n\n // Determine preferred direction toward player\n let preferredDirection;\n if (Math.abs(dx) > Math.abs(dy)) {\n preferredDirection = dx > 0 ? DIRECTIONS.RIGHT : DIRECTIONS.LEFT;\n } else {\n preferredDirection = dy > 0 ? DIRECTIONS.DOWN : DIRECTIONS.UP;\n }\n\n // If we're already going the preferred direction, no change needed\n if (this.direction === preferredDirection) {\n return;\n }\n\n // Check if preferred direction is actually valid (has a tunnel)\n if (!validDirections.includes(preferredDirection)) {\n return; // Can't turn toward player from this intersection\n }\n\n // Check if turning would be a perpendicular turn\n const isCurrentHorizontal =\n this.direction === DIRECTIONS.LEFT ||\n this.direction === DIRECTIONS.RIGHT;\n const isPreferredHorizontal =\n preferredDirection === DIRECTIONS.LEFT ||\n preferredDirection === DIRECTIONS.RIGHT;\n const isPerpendicular = isCurrentHorizontal !== isPreferredHorizontal;\n\n // Calculate alignment - must be very close to tile center\n const tileCenterX = gx * TILE_SIZE;\n const tileCenterY = gy * TILE_SIZE;\n\n if (isPerpendicular) {\n // For perpendicular turns, must be perfectly aligned with tile center\n let alignmentError;\n if (isCurrentHorizontal) {\n alignmentError = Math.abs(this.x - tileCenterX);\n } else {\n alignmentError = Math.abs(this.y - tileCenterY);\n }\n // Very tight tolerance - only turn when essentially at tile center\n const tolerance = this.speed;\n\n if (alignmentError <= tolerance) {\n this.direction = preferredDirection;\n this.tilesTraveledInDirection = 0;\n // Snap BOTH axes to tile position for clean turn\n this.x = tileCenterX;\n this.y = tileCenterY;\n }\n } else {\n // Non-perpendicular (reversal) - only allow if we've traveled enough tiles\n if (this.tilesTraveledInDirection >= this.minTilesBeforeReverse) {\n this.direction = preferredDirection;\n this.tilesTraveledInDirection = 0;\n }\n }\n }\n\n /**\n * Decide which direction to go when entering a new tile (chasing mode)\n */\n decideDirectionAtTile(player, grid, gx, gy) {\n // Get valid tunnel directions from this tile FIRST\n const validDirections = this.getValidDirectionsFromTile(grid, gx, gy);\n\n // If no valid directions, keep current (will be handled by wall collision)\n if (validDirections.length === 0) {\n return;\n }\n\n // If only one valid direction (dead end), don't change direction yet.\n // Let the enemy keep moving until it hits the wall, then it will turn around\n // via pickValidDirection. This makes enemies walk to the end of tunnels.\n if (validDirections.length === 1) {\n return;\n }\n\n // Evaluate direction choices based on tunnel geometry\n const currentIsValid = validDirections.includes(this.direction);\n const oppositeDirection = this.getOppositeDirection(this.direction);\n\n // At a 2-way junction (corner or straight tunnel)\n if (validDirections.length === 2) {\n // If current direction is blocked, we must turn\n if (!currentIsValid) {\n // Pick the direction that isn't going backwards\n const newDir = validDirections.find(\n (d) => d !== oppositeDirection\n );\n if (newDir) {\n this.direction = newDir;\n this.tilesTraveledInDirection = 0;\n }\n return;\n }\n\n // Check if this is a corner (not a straight tunnel)\n // A corner has two perpendicular directions, not opposite ones\n const isCorner = !validDirections.includes(oppositeDirection);\n\n if (isCorner) {\n // At a corner while chasing - consider turning toward player\n const otherDir = validDirections.find(\n (d) => d !== this.direction\n );\n if (otherDir && this.shouldTurnToward(player, otherDir)) {\n this.direction = otherDir;\n this.tilesTraveledInDirection = 0;\n }\n }\n return;\n }\n\n // At an intersection (3+ directions) or current direction invalid\n // Now we can make a smart decision\n\n // Filter out dead-end directions (1-tile stubs)\n const viableDirections = validDirections.filter((d) =>\n this.isViableDirection(grid, gx, gy, d)\n );\n\n // Also filter out the direction that leads back to the previous tile\n // This prevents oscillation between two tiles\n const directionToPrevTile = this.getDirectionToTile(\n gx,\n gy,\n this.prevGridX,\n this.prevGridY\n );\n const nonBacktrackDirections = viableDirections.filter(\n (d) => d !== directionToPrevTile\n );\n\n // Use non-backtracking directions if available, otherwise fall back to viable, then valid\n const directionsToConsider =\n nonBacktrackDirections.length > 0\n ? nonBacktrackDirections\n : viableDirections.length > 0\n ? viableDirections\n : validDirections;\n\n const dx = player.x - this.x;\n const dy = player.y - this.y;\n\n // Determine preferred directions based on player position\n let primaryDirection, secondaryDirection;\n\n if (Math.abs(dx) > Math.abs(dy)) {\n primaryDirection = dx > 0 ? DIRECTIONS.RIGHT : DIRECTIONS.LEFT;\n secondaryDirection = dy > 0 ? DIRECTIONS.DOWN : DIRECTIONS.UP;\n } else {\n primaryDirection = dy > 0 ? DIRECTIONS.DOWN : DIRECTIONS.UP;\n secondaryDirection = dx > 0 ? DIRECTIONS.RIGHT : DIRECTIONS.LEFT;\n }\n\n // Try primary direction first (if viable)\n if (directionsToConsider.includes(primaryDirection)) {\n this.direction = primaryDirection;\n return;\n }\n\n // Try secondary direction (if viable)\n if (directionsToConsider.includes(secondaryDirection)) {\n this.direction = secondaryDirection;\n return;\n }\n\n // Keep current direction if still viable\n if (directionsToConsider.includes(this.direction)) {\n return;\n }\n\n // Pick any direction that isn't reversing\n const nonReverseDirections = directionsToConsider.filter(\n (d) => d !== oppositeDirection\n );\n if (nonReverseDirections.length > 0) {\n this.direction = nonReverseDirections[0];\n } else if (directionsToConsider.length > 0) {\n this.direction = directionsToConsider[0];\n }\n }\n\n /**\n * Get the direction from one tile to an adjacent tile\n * Returns null if tiles are not adjacent or are the same\n */\n getDirectionToTile(fromGx, fromGy, toGx, toGy) {\n const dx = toGx - fromGx;\n const dy = toGy - fromGy;\n\n if (dx === 1 && dy === 0) return DIRECTIONS.RIGHT;\n if (dx === -1 && dy === 0) return DIRECTIONS.LEFT;\n if (dx === 0 && dy === 1) return DIRECTIONS.DOWN;\n if (dx === 0 && dy === -1) return DIRECTIONS.UP;\n return null;\n }\n\n /**\n * Get the opposite direction\n */\n getOppositeDirection(direction) {\n switch (direction) {\n case DIRECTIONS.UP:\n return DIRECTIONS.DOWN;\n case DIRECTIONS.DOWN:\n return DIRECTIONS.UP;\n case DIRECTIONS.LEFT:\n return DIRECTIONS.RIGHT;\n case DIRECTIONS.RIGHT:\n return DIRECTIONS.LEFT;\n default:\n return direction;\n }\n }\n\n /**\n * Check if turning to a direction would move us closer to player\n */\n shouldTurnToward(player, direction) {\n const dx = player.x - this.x;\n const dy = player.y - this.y;\n\n switch (direction) {\n case DIRECTIONS.RIGHT:\n return dx > TILE_SIZE;\n case DIRECTIONS.LEFT:\n return dx < -TILE_SIZE;\n case DIRECTIONS.DOWN:\n return dy > TILE_SIZE;\n case DIRECTIONS.UP:\n return dy < -TILE_SIZE;\n }\n return false;\n }\n\n /**\n * Find an adjacent tunnel tile if enemy is close enough to snap to it\n * Used to exit ghost mode when near a tunnel\n */\n findAdjacentTunnel(grid, gx, gy) {\n // Only snap if we're within a reasonable distance of an adjacent tunnel\n const snapThreshold = TILE_SIZE * 0.6;\n\n // Check each adjacent tile\n const adjacents = [\n { x: gx - 1, y: gy }, // Left\n { x: gx + 1, y: gy }, // Right\n { x: gx, y: gy - 1 }, // Up\n { x: gx, y: gy + 1 }, // Down\n ];\n\n for (const adj of adjacents) {\n // Check if adjacent tile is a tunnel (empty and not a rock)\n if (grid.isEmpty(adj.x, adj.y) && !grid.isRock(adj.x, adj.y)) {\n // Check if we're close enough to this tunnel to snap to it\n const pixelDistX = Math.abs(this.x - adj.x * TILE_SIZE);\n const pixelDistY = Math.abs(this.y - adj.y * TILE_SIZE);\n\n if (pixelDistX < snapThreshold && pixelDistY < snapThreshold) {\n return adj;\n }\n }\n }\n\n return null;\n }\n\n /**\n * Check if a direction leads to a dead end within 5 tiles\n * Returns true if direction is viable (leads somewhere useful)\n */\n isViableDirection(grid, gx, gy, direction) {\n const maxDepth = 5;\n let currentGx = gx;\n let currentGy = gy;\n let currentDir = direction;\n\n for (let depth = 0; depth < maxDepth; depth++) {\n // Move to next tile in current direction\n let nextGx = currentGx;\n let nextGy = currentGy;\n\n switch (currentDir) {\n case DIRECTIONS.UP:\n nextGy--;\n break;\n case DIRECTIONS.DOWN:\n nextGy++;\n break;\n case DIRECTIONS.LEFT:\n nextGx--;\n break;\n case DIRECTIONS.RIGHT:\n nextGx++;\n break;\n }\n\n // Check if next tile is valid\n if (!grid.isEmpty(nextGx, nextGy) || grid.isRock(nextGx, nextGy)) {\n // Hit a wall - this path is a dead end\n return false;\n }\n\n // Check exits from this tile (excluding the way we came)\n const oppositeDir = this.getOppositeDirection(currentDir);\n const tileDirections = this.getValidDirectionsFromTile(\n grid,\n nextGx,\n nextGy\n );\n const exits = tileDirections.filter((d) => d !== oppositeDir);\n\n // If there are multiple exits, this is an intersection - path is viable\n if (exits.length > 1) {\n return true;\n }\n\n // If there's exactly one exit, continue down that path\n if (exits.length === 1) {\n currentGx = nextGx;\n currentGy = nextGy;\n currentDir = exits[0];\n continue;\n }\n\n // No exits (other than back) - dead end\n return false;\n }\n\n // Reached max depth without finding dead end or intersection\n // Consider it viable (path continues beyond our search)\n return true;\n }\n\n /**\n * Roam behavior when entering a new tile\n */\n roamAtTile(grid, gx, gy) {\n const validDirections = this.getValidDirectionsFromTile(grid, gx, gy);\n\n if (validDirections.length === 0) return;\n\n // If only one direction (dead end), don't change direction yet.\n // Let the enemy keep moving until it hits the wall, then it will turn around.\n if (validDirections.length === 1) {\n return;\n }\n\n // Filter out direction back to previous tile to avoid oscillation\n const directionToPrevTile = this.getDirectionToTile(\n gx,\n gy,\n this.prevGridX,\n this.prevGridY\n );\n const forwardDirections = validDirections.filter(\n (d) => d !== directionToPrevTile\n );\n\n // Use forward directions if available\n const directionsToConsider =\n forwardDirections.length > 0 ? forwardDirections : validDirections;\n\n // Only change direction occasionally, unless current direction is invalid\n const currentIsValid = directionsToConsider.includes(this.direction);\n if (\n currentIsValid &&\n this.directionChangeTimer < 1000 + Math.random() * 1000\n ) {\n return;\n }\n this.directionChangeTimer = 0;\n\n if (directionsToConsider.length > 0) {\n this.direction =\n directionsToConsider[\n Math.floor(Math.random() * directionsToConsider.length)\n ];\n }\n }\n\n /**\n * Escape behavior when entering a new tile\n * Try to head up and left to exit at row 1\n * Avoids dead ends to prevent getting stuck\n */\n escapeAtTile(grid, gx, gy) {\n // At row 1 near the left edge, force LEFT to walk off screen\n if (gy === 1 && gx <= 1) {\n this.direction = DIRECTIONS.LEFT;\n return;\n }\n\n const validDirections = this.getValidDirectionsFromTile(grid, gx, gy);\n\n if (validDirections.length === 0) return;\n\n // If only one direction, take it (no choice)\n if (validDirections.length === 1) {\n this.direction = validDirections[0];\n return;\n }\n\n // Get direction to previous tile (to avoid going backwards into dead ends)\n const directionToPrevTile = this.getDirectionToTile(\n gx,\n gy,\n this.prevGridX,\n this.prevGridY\n );\n\n // Filter out dead-end directions to avoid getting stuck\n const viableDirections = validDirections.filter((dir) =>\n this.isViableDirection(grid, gx, gy, dir)\n );\n\n // Also avoid going back the way we came (to prevent oscillation in dead ends)\n // Only apply this if there are other viable options\n const nonReverseViable = viableDirections.filter(\n (dir) => dir !== directionToPrevTile\n );\n const directionsToConsider =\n nonReverseViable.length > 0 ? nonReverseViable : viableDirections;\n\n // If no viable directions, use valid directions (but avoid reverse if possible)\n let finalDirections = directionsToConsider;\n if (finalDirections.length === 0) {\n const nonReverseValid = validDirections.filter(\n (dir) => dir !== directionToPrevTile\n );\n finalDirections =\n nonReverseValid.length > 0 ? nonReverseValid : validDirections;\n }\n\n // Prioritize directions: UP first, then LEFT, to reach exit at top-left\n // The exit is at row 1 (gy === 1), and we want to go left to x=0\n const priorityOrder = [\n DIRECTIONS.UP,\n DIRECTIONS.LEFT,\n DIRECTIONS.DOWN,\n DIRECTIONS.RIGHT,\n ];\n\n // If we're already at row 1, prioritize LEFT to reach the exit\n if (gy <= 1) {\n priorityOrder[0] = DIRECTIONS.LEFT;\n priorityOrder[1] = DIRECTIONS.UP;\n }\n\n // Pick the highest priority viable direction\n for (const dir of priorityOrder) {\n if (finalDirections.includes(dir)) {\n this.direction = dir;\n return;\n }\n }\n\n // Fallback: pick any valid direction\n this.direction = validDirections[0];\n }\n\n /**\n * Check for better escape direction mid-tile (at intersections)\n */\n checkForBetterEscapeDirection(grid, gx, gy) {\n const validDirections = this.getValidDirectionsFromTile(grid, gx, gy);\n\n // Only consider turns at intersections (3+ directions)\n if (validDirections.length < 3) {\n return;\n }\n\n // If current direction is not valid, don't change here\n if (!validDirections.includes(this.direction)) {\n return;\n }\n\n // Determine preferred escape direction\n let preferredDirection;\n if (gy <= 1) {\n // At top row, prefer LEFT\n preferredDirection = DIRECTIONS.LEFT;\n } else {\n // Otherwise prefer UP\n preferredDirection = DIRECTIONS.UP;\n }\n\n // If already going preferred direction, no change needed\n if (this.direction === preferredDirection) {\n return;\n }\n\n // Check if preferred direction is valid\n if (!validDirections.includes(preferredDirection)) {\n return;\n }\n\n // Check alignment for perpendicular turns\n const isCurrentHorizontal =\n this.direction === DIRECTIONS.LEFT ||\n this.direction === DIRECTIONS.RIGHT;\n const isPreferredHorizontal =\n preferredDirection === DIRECTIONS.LEFT ||\n preferredDirection === DIRECTIONS.RIGHT;\n const isPerpendicular = isCurrentHorizontal !== isPreferredHorizontal;\n\n const tileCenterX = gx * TILE_SIZE;\n const tileCenterY = gy * TILE_SIZE;\n\n if (isPerpendicular) {\n let alignmentError;\n if (isCurrentHorizontal) {\n alignmentError = Math.abs(this.x - tileCenterX);\n } else {\n alignmentError = Math.abs(this.y - tileCenterY);\n }\n\n const tolerance = this.speed;\n if (alignmentError <= tolerance) {\n this.direction = preferredDirection;\n this.tilesTraveledInDirection = 0;\n this.x = tileCenterX;\n this.y = tileCenterY;\n }\n } else {\n // Non-perpendicular change\n this.direction = preferredDirection;\n this.tilesTraveledInDirection = 0;\n }\n }\n\n /**\n * Get valid directions (tunnel tiles) from a specific grid position\n */\n getValidDirectionsFromTile(grid, gx, gy) {\n const validDirections = [];\n\n if (grid.isEmpty(gx, gy - 1) && !grid.isRock(gx, gy - 1)) {\n validDirections.push(DIRECTIONS.UP);\n }\n if (grid.isEmpty(gx, gy + 1) && !grid.isRock(gx, gy + 1)) {\n validDirections.push(DIRECTIONS.DOWN);\n }\n if (grid.isEmpty(gx - 1, gy) && !grid.isRock(gx - 1, gy)) {\n validDirections.push(DIRECTIONS.LEFT);\n }\n if (grid.isEmpty(gx + 1, gy) && !grid.isRock(gx + 1, gy)) {\n validDirections.push(DIRECTIONS.RIGHT);\n }\n\n return validDirections;\n }\n\n /**\n * Check if enemy can move in a direction (leading edge collision check)\n */\n canMoveInDirection(x, y, direction, grid) {\n // Check the leading edge based on movement direction\n let checkX, checkY;\n\n switch (direction) {\n case DIRECTIONS.UP:\n checkX = x + TILE_SIZE / 2;\n checkY = y;\n break;\n case DIRECTIONS.DOWN:\n checkX = x + TILE_SIZE / 2;\n checkY = y + TILE_SIZE - 1;\n break;\n case DIRECTIONS.LEFT:\n checkX = x;\n checkY = y + TILE_SIZE / 2;\n break;\n case DIRECTIONS.RIGHT:\n checkX = x + TILE_SIZE - 1;\n checkY = y + TILE_SIZE / 2;\n break;\n default:\n checkX = x + TILE_SIZE / 2;\n checkY = y + TILE_SIZE / 2;\n }\n\n const { x: gx, y: gy } = grid.pixelToGrid(checkX, checkY);\n\n // Allow escaping enemy to walk off the left edge at row 1\n if (\n this.isEscaping &&\n this.state === 'escaping' &&\n direction === DIRECTIONS.LEFT &&\n gy === 1\n ) {\n return true;\n }\n\n // Top row (sky) is always blocked\n if (gy === 0) {\n return false;\n }\n\n // When ghosting, can move through rocks\n if (this.isGhosting) {\n return true;\n }\n\n // Rocks block normal movement\n if (grid.isRock(gx, gy)) {\n return false;\n }\n\n // When ghosting, can move through dirt\n if (this.isGhosting) {\n return true;\n }\n\n // Normal mode: cannot move into dirt\n if (!grid.isEmpty(gx, gy)) {\n return false;\n }\n\n return true;\n }\n\n /**\n * Snap position to align with grid\n */\n snapToGrid(grid) {\n const centerX = this.x + TILE_SIZE / 2;\n const centerY = this.y + TILE_SIZE / 2;\n const { x: gx, y: gy } = grid.pixelToGrid(centerX, centerY);\n\n this.x = gx * TILE_SIZE;\n this.y = gy * TILE_SIZE;\n }\n\n /**\n * Pick a valid direction when hitting a wall\n */\n pickValidDirection(grid, player, gx, gy) {\n const validDirections = this.getValidDirectionsFromTile(grid, gx, gy);\n\n if (validDirections.length === 0) return;\n\n // If escaping, use escape-specific pathfinding\n if (this.isEscaping && this.state === 'escaping') {\n this.pickEscapeDirection(grid, gx, gy, validDirections);\n return;\n }\n\n // If chasing, prefer direction toward player\n if (player && this.state === 'chasing') {\n const dx = player.x - this.x;\n const dy = player.y - this.y;\n\n // Determine primary and secondary preferred directions\n let primaryDirection, secondaryDirection;\n if (Math.abs(dx) > Math.abs(dy)) {\n primaryDirection = dx > 0 ? DIRECTIONS.RIGHT : DIRECTIONS.LEFT;\n secondaryDirection = dy > 0 ? DIRECTIONS.DOWN : DIRECTIONS.UP;\n } else {\n primaryDirection = dy > 0 ? DIRECTIONS.DOWN : DIRECTIONS.UP;\n secondaryDirection =\n dx > 0 ? DIRECTIONS.RIGHT : DIRECTIONS.LEFT;\n }\n\n // Try primary direction first\n if (validDirections.includes(primaryDirection)) {\n this.direction = primaryDirection;\n return;\n }\n\n // Try secondary direction (helps navigate around obstacles)\n if (validDirections.includes(secondaryDirection)) {\n this.direction = secondaryDirection;\n return;\n }\n }\n\n // Pick random valid direction\n this.direction =\n validDirections[Math.floor(Math.random() * validDirections.length)];\n }\n\n /**\n * Pick direction for escaping enemy - navigate tunnels toward exit\n * Avoids dead ends and prefers directions that lead toward row 1, left side\n */\n pickEscapeDirection(grid, gx, gy, validDirections) {\n // At row 1 near exit, always go left\n if (gy === 1 && gx <= 1) {\n this.direction = DIRECTIONS.LEFT;\n return;\n }\n\n // Get direction we just came from (to avoid immediately reversing)\n const oppositeDirection = this.getOppositeDirection(this.direction);\n\n // Filter out dead-end directions using viability check\n const viableDirections = validDirections.filter((dir) =>\n this.isViableDirection(grid, gx, gy, dir)\n );\n\n // Also filter out the direction we came from to avoid oscillation\n // Only do this if there are other viable options\n const nonReverseViable = viableDirections.filter(\n (dir) => dir !== oppositeDirection\n );\n const directionsToConsider =\n nonReverseViable.length > 0 ? nonReverseViable : viableDirections;\n\n // If no viable directions, fall back to valid directions (excluding reverse if possible)\n let finalDirections = directionsToConsider;\n if (finalDirections.length === 0) {\n const nonReverseValid = validDirections.filter(\n (dir) => dir !== oppositeDirection\n );\n finalDirections =\n nonReverseValid.length > 0 ? nonReverseValid : validDirections;\n }\n\n // Prioritize directions toward the exit (row 1, left side)\n // Priority: UP first (to reach row 1), then LEFT, then others\n const priorityOrder =\n gy <= 1\n ? [\n DIRECTIONS.LEFT,\n DIRECTIONS.UP,\n DIRECTIONS.DOWN,\n DIRECTIONS.RIGHT,\n ]\n : [\n DIRECTIONS.UP,\n DIRECTIONS.LEFT,\n DIRECTIONS.DOWN,\n DIRECTIONS.RIGHT,\n ];\n\n // Pick the highest priority viable direction\n for (const dir of priorityOrder) {\n if (finalDirections.includes(dir)) {\n this.direction = dir;\n return;\n }\n }\n\n // Fallback: pick any valid direction\n this.direction = validDirections[0];\n }\n\n /**\n * Apply grid snapping to keep enemy centered in tunnels\n */\n applyGridSnapping(grid) {\n const { x: gx, y: gy } = grid.pixelToGrid(\n this.x + TILE_SIZE / 2,\n this.y + TILE_SIZE / 2\n );\n\n // Only snap if currently in a tunnel\n if (!grid.isEmpty(gx, gy)) {\n return;\n }\n\n // When moving horizontally, align vertically to grid center\n if (\n this.direction === DIRECTIONS.LEFT ||\n this.direction === DIRECTIONS.RIGHT\n ) {\n const targetY = gy * TILE_SIZE;\n const diff = targetY - this.y;\n\n if (Math.abs(diff) > 0) {\n const snapAmount =\n Math.sign(diff) * Math.min(Math.abs(diff), this.speed);\n this.y += snapAmount;\n }\n }\n\n // When moving vertically, align horizontally to grid center\n if (\n this.direction === DIRECTIONS.UP ||\n this.direction === DIRECTIONS.DOWN\n ) {\n const targetX = gx * TILE_SIZE;\n const diff = targetX - this.x;\n\n if (Math.abs(diff) > 0) {\n const snapAmount =\n Math.sign(diff) * Math.min(Math.abs(diff), this.speed);\n this.x += snapAmount;\n }\n }\n }\n\n /**\n * Initialize direction based on tunnel orientation\n */\n initializeDirection(grid) {\n const { x: gx, y: gy } = grid.pixelToGrid(\n this.x + TILE_SIZE / 2,\n this.y + TILE_SIZE / 2\n );\n\n const validDirections = this.getValidDirectionsFromTile(grid, gx, gy);\n\n // Prefer horizontal movement\n if (validDirections.includes(DIRECTIONS.RIGHT)) {\n this.spriteFlipH = true;\n } else if (validDirections.includes(DIRECTIONS.LEFT)) {\n this.direction = DIRECTIONS.LEFT;\n } else if (validDirections.includes(DIRECTIONS.DOWN)) {\n this.direction = DIRECTIONS.DOWN;\n } else if (validDirections.includes(DIRECTIONS.UP)) {\n this.direction = DIRECTIONS.UP;\n }\n }\n\n /**\n * Start or continue inflation (when pumped)\n */\n startInflation() {\n this.isInflating = true;\n this.isMoving = false;\n this.deflateTimer = 0; // Reset deflate countdown\n }\n\n /**\n * Stop being pumped - will start deflating\n */\n stopInflation() {\n // Don't immediately stop - let deflation happen in updateInflation\n }\n\n /**\n * Update inflation state\n * Called every frame - inflates while being pumped, deflates otherwise\n */\n updateInflation(deltaTime) {\n if (this.isInflating) {\n // Being actively pumped - inflate\n this.inflateTimer += deltaTime;\n this.inflateLevel = 1.0 + this.inflateTimer / this.INFLATE_DURATION;\n\n if (this.inflateLevel >= 2.0) {\n this.pop();\n }\n } else if (this.inflateLevel > 1.0) {\n // Not being pumped - deflate slowly\n this.deflateTimer += deltaTime;\n\n // Start deflating after a short pause\n if (this.deflateTimer > 300) {\n this.inflateTimer = Math.max(\n 0,\n this.inflateTimer - deltaTime * 0.5\n );\n this.inflateLevel =\n 1.0 + this.inflateTimer / this.INFLATE_DURATION;\n\n if (this.inflateLevel <= 1.0) {\n // Fully deflated - can move again\n this.inflateLevel = 1.0;\n this.inflateTimer = 0;\n this.deflateTimer = 0;\n this.isMoving = true;\n }\n }\n }\n\n // Reset inflating flag - must be set each frame by pump collision\n this.isInflating = false;\n }\n\n /**\n * Enemy pops (dies from inflation)\n * Shows popped sprite before being destroyed\n */\n pop() {\n this.isPopped = true;\n this.poppedTimer = 0;\n this.isMoving = false;\n }\n\n /**\n * Enemy gets smooshed (crushed by rock)\n * Shows smooshed sprite and falls with rock\n */\n smoosh() {\n this.isSmooshed = true;\n this.smooshedTimer = 0;\n this.isMoving = false;\n }\n\n /**\n * Update popped state timer\n */\n updatePoppedState(deltaTime) {\n if (!this.isPopped) return;\n\n this.poppedTimer += deltaTime;\n if (this.poppedTimer >= this.POPPED_DURATION) {\n this.isDestroyed = true;\n }\n }\n\n /**\n * Update smooshed state timer\n * Only counts down after the rock has stopped falling\n */\n updateSmooshedState(deltaTime) {\n if (!this.isSmooshed) return;\n\n // Only start the destruction timer after the rock has stopped falling\n // This ensures the enemy stays visible while falling with the rock\n if (this.attachedToRock && this.attachedToRock.isFalling) {\n return; // Don't count timer while rock is still falling\n }\n\n this.smooshedTimer += deltaTime;\n if (this.smooshedTimer >= this.SMOOSHED_DURATION) {\n this.isDestroyed = true;\n }\n }\n\n /**\n * Update animation\n */\n updateAnimation(deltaTime) {\n this.animationTimer += deltaTime;\n if (this.animationTimer > 400) {\n this.animationFrame = (this.animationFrame + 1) % 2;\n this.animationTimer = 0;\n }\n }\n\n /**\n * Get center position\n */\n getCenter() {\n return {\n x: this.x + TILE_SIZE / 2,\n y: this.y + TILE_SIZE / 2,\n };\n }\n\n /**\n * Reset all timers - called when game starts or respawns\n */\n resetTimers() {\n this.ghostModeTimer = 0;\n this.canGhostMode = false;\n this.isGhosting = false;\n this.ghostingDuration = 0;\n this.stateTimer = 0;\n this.directionChangeTimer = 0;\n // Reset inflation state\n this.inflateTimer = 0;\n this.inflateLevel = 1.0;\n this.isInflating = false;\n this.deflateTimer = 0;\n this.isMoving = true;\n // Reset escape state\n this.isLastEnemy = false;\n this.isEscaping = false;\n this.hasEscaped = false;\n this.state = 'roaming';\n }\n}\n","import { Enemy } from './Enemy.js';\nimport { ENEMY, ENEMY_TYPES } from '../utils/constants.js';\n\nexport class Pooka extends Enemy {\n constructor(x, y, level = 1) {\n super(x, y, ENEMY_TYPES.POOKA, ENEMY.POOKA.SPEED, level);\n this.ghostSpeed = ENEMY.POOKA.GHOST_SPEED;\n }\n\n // Pooka uses base Enemy movement with ghost mode from timer\n // No override needed - ghost mode is handled by Enemy.updateGhostMode()\n}\n","import { Enemy } from './Enemy.js';\nimport {\n ENEMY,\n DIRECTIONS,\n TILE_SIZE,\n ENEMY_TYPES,\n} from '../utils/constants.js';\n\nexport class Fygar extends Enemy {\n constructor(x, y, level = 1) {\n super(x, y, ENEMY_TYPES.FYGAR, ENEMY.FYGAR.SPEED, level);\n this.ghostSpeed = ENEMY.FYGAR.GHOST_SPEED;\n\n // Fire breathing state machine: 'ready' -> 'charging' -> 'firing' -> 'cooldown' -> 'ready'\n this.fireState = 'ready';\n this.fireStateTimer = 0;\n this.fireCooldownTimer = 0;\n\n // Fire breath direction (locked when charging starts)\n this.fireDirection = null;\n\n // Fire hitbox properties (calculated when firing)\n this.fireHitbox = null;\n }\n\n /**\n * Update Fygar-specific behavior\n */\n update(deltaTime, player, grid) {\n super.update(deltaTime, player, grid);\n\n // Handle fire breathing state machine\n // Only can breathe fire when in a tunnel (not ghosting through dirt)\n if (player && !this.isInflating && !this.isGhosting) {\n this.updateFireBreath(deltaTime, player, grid);\n }\n\n // If being pumped (or already inflated), cancel any fire activity (charging or firing)\n // Check both isInflating and inflateLevel > 1 to catch the pump on first contact\n if (\n (this.isInflating || this.inflateLevel > 1.0) &&\n (this.fireState === 'charging' || this.fireState === 'firing')\n ) {\n this.cancelFire();\n }\n\n // If started ghosting while charging or firing, cancel the fire\n if (\n this.isGhosting &&\n (this.fireState === 'charging' || this.fireState === 'firing')\n ) {\n this.cancelFire();\n }\n }\n\n /**\n * Override ghost mode update to pause timer while charging/firing\n * Fygar should not accumulate ghost mode time while breathing fire\n */\n updateGhostMode(deltaTime, player, grid) {\n // If currently charging or firing, don't update ghost mode at all\n // This pauses the ghost timer while Fygar is breathing fire\n if (this.fireState === 'charging' || this.fireState === 'firing') {\n return;\n }\n\n // Otherwise use parent implementation\n super.updateGhostMode(deltaTime, player, grid);\n }\n\n /**\n * Update fire breath state machine\n */\n updateFireBreath(deltaTime, player, grid) {\n // Update cooldown timer\n if (this.fireState === 'cooldown') {\n this.fireCooldownTimer += deltaTime;\n if (this.fireCooldownTimer >= ENEMY.FYGAR.FIRE_COOLDOWN) {\n this.fireState = 'ready';\n this.fireCooldownTimer = 0;\n }\n return;\n }\n\n // Ready state - check if we should start charging\n if (this.fireState === 'ready') {\n // Only fire when facing horizontally\n if (\n this.direction !== DIRECTIONS.LEFT &&\n this.direction !== DIRECTIONS.RIGHT\n ) {\n return;\n }\n\n // Check if player is in fire range and horizontally aligned\n // Also verify there's no dirt blocking the fire path\n if (\n ((this.direction === DIRECTIONS.RIGHT && this.x < player.x) ||\n (this.direction === DIRECTIONS.LEFT &&\n this.x > player.x)) &&\n this.isPlayerInFireRange(player, grid)\n ) {\n this.startCharging();\n }\n return;\n }\n\n // Charging state - pause before firing\n if (this.fireState === 'charging') {\n this.fireStateTimer += deltaTime;\n if (this.fireStateTimer >= ENEMY.FYGAR.FIRE_CHARGE_TIME) {\n this.startFiring();\n }\n return;\n }\n\n // Firing state - fire is active\n if (this.fireState === 'firing') {\n this.fireStateTimer += deltaTime;\n // Recalculate hitbox each frame as fire extends\n this.calculateFireHitbox();\n if (this.fireStateTimer >= ENEMY.FYGAR.FIRE_DURATION) {\n this.stopFiring();\n }\n return;\n }\n }\n\n /**\n * Check if player is in fire range (horizontally aligned and in front)\n * Also checks that no dirt blocks the fire path\n */\n isPlayerInFireRange(player, grid) {\n const dx = player.x - this.x;\n const dy = player.y - this.y;\n\n // Check if player is horizontally aligned (within same row, with some tolerance)\n if (Math.abs(dy) > TILE_SIZE * 0.75) {\n return false;\n }\n\n // Check if player is in front based on direction and within range\n const inRange =\n (this.direction === DIRECTIONS.RIGHT &&\n dx > 0 &&\n dx < ENEMY.FYGAR.FIRE_RANGE) ||\n (this.direction === DIRECTIONS.LEFT &&\n dx < 0 &&\n dx > -ENEMY.FYGAR.FIRE_RANGE);\n\n if (!inRange) {\n return false;\n }\n\n // Check for dirt blocking the fire path\n const fygarGridX = Math.floor((this.x + TILE_SIZE / 2) / TILE_SIZE);\n const playerGridX = Math.floor((player.x + TILE_SIZE / 2) / TILE_SIZE);\n const gridY = Math.floor((this.y + TILE_SIZE / 2) / TILE_SIZE);\n\n const startX = Math.min(fygarGridX, playerGridX);\n const endX = Math.max(fygarGridX, playerGridX);\n\n // Check all tiles between Fygar and player for dirt\n for (let x = startX + 1; x < endX; x++) {\n if (grid.isDirt(x, gridY)) {\n return false; // Dirt blocks fire\n }\n }\n\n return true;\n }\n\n /**\n * Start charging phase (pause before fire)\n */\n startCharging() {\n this.fireState = 'charging';\n this.fireStateTimer = 0;\n this.fireDirection = this.direction; // Lock fire direction\n this.isMoving = false; // Stop moving while charging/firing\n\n // Snap to grid based on center point (same as movement system uses)\n // Use floor to avoid jumping forward\n const gx = Math.floor((this.x + TILE_SIZE / 2) / TILE_SIZE);\n const gy = Math.floor((this.y + TILE_SIZE / 2) / TILE_SIZE);\n this.x = gx * TILE_SIZE;\n this.y = gy * TILE_SIZE;\n\n // Sync grid tracking\n this.lastGridX = gx;\n this.lastGridY = gy;\n }\n\n /**\n * Start firing phase (fire is now active and can damage player)\n */\n startFiring() {\n this.fireState = 'firing';\n this.fireStateTimer = 0;\n this.calculateFireHitbox();\n }\n\n /**\n * Calculate the fire hitbox based on current position, direction, and tile count\n * Hitbox grows as fire extends: 1 tile -> 2 tiles -> 3 tiles\n */\n calculateFireHitbox() {\n // Get current fire length in tiles (1-3)\n const tileCount = this.getFireTileCount();\n if (tileCount === 0) {\n this.fireHitbox = null;\n return;\n }\n\n const fireWidth = tileCount * TILE_SIZE;\n const centerY = this.y + TILE_SIZE / 2;\n\n if (this.fireDirection === DIRECTIONS.RIGHT) {\n this.fireHitbox = {\n x: this.x + TILE_SIZE, // Start from right edge of Fygar\n y: centerY - TILE_SIZE / 4, // Vertically centered with some height\n width: fireWidth,\n height: TILE_SIZE / 2,\n };\n } else {\n // LEFT\n this.fireHitbox = {\n x: this.x - fireWidth, // Start from current fire length to the left\n y: centerY - TILE_SIZE / 4,\n width: fireWidth,\n height: TILE_SIZE / 2,\n };\n }\n }\n\n /**\n * Stop firing and enter cooldown\n */\n stopFiring() {\n this.fireState = 'cooldown';\n this.fireStateTimer = 0;\n this.fireCooldownTimer = 0;\n this.fireHitbox = null;\n this.fireDirection = null;\n this.isMoving = true; // Resume movement\n\n // Sync grid tracking to current position to prevent \"new tile\" detection glitch\n const gx = Math.floor((this.x + TILE_SIZE / 2) / TILE_SIZE);\n const gy = Math.floor((this.y + TILE_SIZE / 2) / TILE_SIZE);\n this.lastGridX = gx;\n this.lastGridY = gy;\n }\n\n /**\n * Cancel fire (when pumped during charge/fire)\n */\n cancelFire() {\n this.fireState = 'cooldown';\n this.fireStateTimer = 0;\n // Start cooldown partway through so recovery is faster after being interrupted\n this.fireCooldownTimer = ENEMY.FYGAR.FIRE_COOLDOWN * 0.5;\n this.fireHitbox = null;\n this.fireDirection = null;\n // Don't resume movement - inflation handles that\n }\n\n /**\n * Check if fire is currently active and dangerous\n */\n isFireActive() {\n return this.fireState === 'firing' && this.fireHitbox !== null;\n }\n\n /**\n * Check if currently charging (for visual feedback)\n */\n isCharging() {\n return this.fireState === 'charging';\n }\n\n /**\n * Get fire hitbox for collision detection\n */\n getFireHitbox() {\n return this.fireHitbox;\n }\n\n /**\n * Get fire direction for rendering\n */\n getFireDirection() {\n return this.fireDirection;\n }\n\n /**\n * Get current fire length in tiles (1-3) based on fire timer progress\n * Fire extends over the duration: 1 tile -> 2 tiles -> 3 tiles\n */\n getFireTileCount() {\n if (this.fireState !== 'firing') {\n return 0;\n }\n // Divide fire duration into 3 phases\n const progress = this.fireStateTimer / ENEMY.FYGAR.FIRE_DURATION;\n if (progress < 0.33) {\n return 1;\n } else if (progress < 0.66) {\n return 2;\n } else {\n return 3;\n }\n }\n\n /**\n * Reset timers - called on respawn\n */\n resetTimers() {\n super.resetTimers();\n this.fireState = 'ready';\n this.fireStateTimer = 0;\n this.fireCooldownTimer = 0;\n this.fireDirection = null;\n this.fireHitbox = null;\n }\n}\n","import { TILE_SIZE, ROCK } from '../utils/constants.js';\n\nexport class Rock {\n constructor(x, y, grid) {\n this.x = x;\n this.y = y;\n this.grid = grid;\n\n // State\n this.isFalling = false;\n this.isShaking = false;\n this.shakeTimer = 0;\n this.fallDelay = ROCK.FALL_DELAY;\n this.fallSpeed = ROCK.FALL_SPEED;\n\n // Crumbling state (when hits dirt without killing enemy)\n this.isCrumbling = false;\n this.crumbleTimer = 0;\n this.CRUMBLE_DURATION = 800; // ms to show crumble animation\n\n // Delay before crumble when crushing enemy\n this.waitingToCrumble = false;\n this.crumbleDelayTimer = 0;\n this.CRUMBLE_DELAY = 400; // ms delay before crumble after crushing enemy\n\n // Track if rock crushed any enemies\n this.crushedEnemy = false;\n this.enemiesKilled = 0;\n this.killPositions = []; // Store positions for floating score display\n\n // Track player position to wait for them to move\n this.playerStillBelow = false;\n this.playerLastX = 0;\n this.playerLastY = 0;\n\n // Fall delay timer (starts after player clears from underneath)\n this.fallDelayTimer = 0;\n this.waitingToFall = false;\n\n // Grid position\n this.gridX = Math.floor(x / TILE_SIZE);\n this.gridY = Math.floor(y / TILE_SIZE);\n\n // Spawn animation state (for respawned rocks)\n this.isSpawning = false;\n this.spawnTimer = 0;\n this.SPAWN_DURATION = 2000; // 2 seconds fade-in with flash\n }\n\n /**\n * Update rock state\n */\n update(deltaTime, grid, player) {\n // Update spawn animation\n if (this.isSpawning) {\n this.spawnTimer += deltaTime;\n if (this.spawnTimer >= this.SPAWN_DURATION) {\n this.isSpawning = false;\n }\n return; // Don't process anything else while spawning\n }\n\n // Update crumble delay (after crushing enemy)\n if (this.waitingToCrumble) {\n this.crumbleDelayTimer += deltaTime;\n if (this.crumbleDelayTimer >= this.CRUMBLE_DELAY) {\n this.waitingToCrumble = false;\n this.isCrumbling = true;\n this.crumbleTimer = 0;\n }\n return; // Don't process anything else while waiting to crumble\n }\n\n // Update crumbling animation\n if (this.isCrumbling) {\n this.crumbleTimer += deltaTime;\n if (this.crumbleTimer > this.CRUMBLE_DURATION) {\n this.isDestroyed = true; // Mark for removal\n }\n return; // Don't process anything else while crumbling\n }\n\n // Update shaking\n if (this.isShaking) {\n this.shakeTimer += deltaTime;\n\n // After initial shake duration, check if player has cleared\n if (this.shakeTimer > ROCK.SHAKE_DURATION) {\n if (player && this.playerStillBelow) {\n // Check if player has fully cleared from underneath the rock\n const playerCleared = this.hasPlayerCleared(player);\n\n if (playerCleared) {\n this.playerStillBelow = false;\n this.waitingToFall = true;\n this.fallDelayTimer = 0;\n }\n } else if (!this.waitingToFall) {\n // Player wasn't below, start falling immediately\n this.startFalling(grid);\n }\n }\n }\n\n // Update fall delay timer (after player has cleared)\n if (this.waitingToFall) {\n this.fallDelayTimer += deltaTime;\n if (this.fallDelayTimer >= this.fallDelay) {\n this.waitingToFall = false;\n this.startFalling(grid);\n }\n }\n\n // Update falling\n if (this.isFalling) {\n this.fall(grid);\n }\n\n // Check if player is touching rock from below to trigger fall\n if (!this.isFalling && !this.isShaking && player) {\n this.checkPlayerTrigger(player, grid);\n }\n }\n\n /**\n * Check if player is touching rock from below\n */\n checkPlayerTrigger(player, grid) {\n // Check if there's no dirt below the rock\n const hasDirtBelow = grid.isDirt(this.gridX, this.gridY + 1);\n const hasRockBelow = grid.isRock(this.gridX, this.gridY + 1);\n\n if (hasDirtBelow || hasRockBelow || this.gridY >= grid.height - 1) {\n return; // Rock is supported, can't fall\n }\n\n // Check if player is directly below the rock\n const playerGridX = Math.floor(player.x / TILE_SIZE);\n const playerGridY = Math.floor(player.y / TILE_SIZE);\n\n // Player must be in the tile directly below or diagonally adjacent below\n const isBelow =\n playerGridY === this.gridY + 1 &&\n Math.abs(playerGridX - this.gridX) <= 1;\n\n // Also check if player's head is touching rock's bottom\n const playerTop = player.y;\n const rockBottom = this.y + TILE_SIZE;\n const verticalOverlap = Math.abs(playerTop - rockBottom) < 4;\n const horizontalOverlap = Math.abs(player.x - this.x) < TILE_SIZE;\n\n if (\n (isBelow && verticalOverlap && horizontalOverlap) ||\n (verticalOverlap && horizontalOverlap && player.y < this.y)\n ) {\n // Player touched from below, trigger the rock\n this.playerStillBelow = true;\n this.playerLastX = player.x;\n this.playerLastY = player.y;\n this.startShaking();\n }\n }\n\n /**\n * Check if player has fully cleared from underneath the rock\n */\n hasPlayerCleared(player) {\n // Player must be completely outside the rock's horizontal range\n const playerLeft = player.x;\n const playerRight = player.x + TILE_SIZE;\n const rockLeft = this.x;\n const rockRight = this.x + TILE_SIZE;\n\n // Check horizontal clearance (no overlap)\n const horizontalClear =\n playerRight <= rockLeft || playerLeft >= rockRight;\n\n // Also check if player moved to a different row (vertically clear)\n const playerGridY = Math.floor(player.y / TILE_SIZE);\n const verticalClear = playerGridY !== this.gridY + 1;\n\n return horizontalClear || verticalClear;\n }\n\n /**\n * Start spawn animation (for respawned rocks)\n */\n startSpawnAnimation() {\n this.isSpawning = true;\n this.spawnTimer = 0;\n }\n\n /**\n * Start shaking animation\n */\n startShaking() {\n this.isShaking = true;\n this.shakeTimer = 0;\n }\n\n /**\n * Start falling\n */\n startFalling(grid) {\n this.isFalling = true;\n this.isShaking = false;\n\n // Remove rock from grid\n grid.removeRock(this.gridX, this.gridY);\n }\n\n /**\n * Fall downward\n */\n fall(grid) {\n this.y += this.fallSpeed;\n\n // Update grid position\n const newGridY = Math.floor(this.y / TILE_SIZE);\n\n if (newGridY !== this.gridY) {\n this.gridY = newGridY;\n\n // Check what's below: dirt, rock, or bottom\n const hasDirtBelow = grid.isDirt(this.gridX, this.gridY + 1);\n const hasRockBelow = grid.isRock(this.gridX, this.gridY + 1);\n const hitBottom = this.gridY >= grid.height - 1;\n\n if (hasDirtBelow || hasRockBelow || hitBottom) {\n this.stopFalling();\n }\n }\n }\n\n /**\n * Stop falling - crumble after landing\n */\n stopFalling() {\n this.isFalling = false;\n\n // Snap to grid\n this.y = this.gridY * TILE_SIZE;\n\n // Rock crumbles after falling\n this.waitingToCrumble = true;\n this.crumbleDelayTimer = 0;\n }\n\n /**\n * Mark that this rock crushed an enemy\n */\n markEnemyCrushed() {\n this.crushedEnemy = true;\n }\n\n /**\n * Increment the kill count and store position for score display\n */\n incrementKillCount(enemyX, enemyY) {\n this.enemiesKilled++;\n this.killPositions.push({ x: enemyX, y: enemyY });\n return this.enemiesKilled;\n }\n\n /**\n * Get the last kill position for floating score display\n */\n getLastKillPosition() {\n if (this.killPositions.length > 0) {\n return this.killPositions[this.killPositions.length - 1];\n }\n return { x: this.x, y: this.y };\n }\n\n /**\n * Get center position\n */\n getCenter() {\n return {\n x: this.x + TILE_SIZE / 2,\n y: this.y + TILE_SIZE / 2,\n };\n }\n\n /**\n * Reset rock state (called on player respawn)\n */\n reset() {\n this.isShaking = false;\n this.shakeTimer = 0;\n this.playerStillBelow = false;\n this.waitingToFall = false;\n this.fallDelayTimer = 0;\n this.waitingToCrumble = false;\n this.crumbleDelayTimer = 0;\n // Note: don't reset isFalling or position - falling rocks continue falling\n }\n}\n","import {\n GRID_WIDTH,\n GRID_HEIGHT,\n TILE_SIZE,\n LEVEL,\n} from '../utils/constants.js';\nimport { Pooka } from '../entities/Pooka.js';\nimport { Fygar } from '../entities/Fygar.js';\nimport { Rock } from '../entities/Rock.js';\n\nexport class LevelManager {\n constructor(grid) {\n this.grid = grid;\n this.currentLevel = 0;\n this.rocks = [];\n }\n\n /**\n * Generate a procedural level\n */\n generateLevel(levelNumber) {\n this.currentLevel = levelNumber;\n this.grid.reset();\n this.rocks = [];\n this.currentEnemies = []; // Store for rock placement\n\n // Create initial tunnels\n this.generateTunnels();\n }\n\n /**\n * Generate tunnel network\n */\n generateTunnels() {\n // Create a small starting tunnel for the player in the center\n const centerX = Math.floor(GRID_WIDTH / 2);\n const centerY = Math.floor(GRID_HEIGHT / 2);\n\n // Small 3-tile cross pattern for player start\n this.grid.clearHorizontalTunnel(centerX - 1, centerX + 1, centerY);\n if (this.currentLevel > 1)\n this.grid.clearVerticalTunnel(centerX, 2, centerY);\n\n // Store player start tunnel location\n this.playerStartTunnel = { x: centerX, y: centerY };\n }\n\n /**\n * Place rocks strategically in the level (away from paths and enemies)\n * Called after enemies are spawned\n */\n placeRocksAfterEnemies(levelNumber, enemies) {\n // 1. Setup & Pre-calculations\n const extraRocks = Math.floor(levelNumber / 3);\n const targetRocks = Math.min(LEVEL.ROCKS_PER_LEVEL + extraRocks, 6);\n const minRocks = 3;\n\n // Cache grid data\n const gridW = GRID_WIDTH;\n const gridH = GRID_HEIGHT;\n const midX = gridW / 2;\n const noGoMin = midX - 2;\n const noGoMax = midX + 2;\n\n // Pre-calculate enemy positions\n const enemyGridPos = enemies.map((e) => ({\n x: Math.floor(e.x / TILE_SIZE),\n y: Math.floor(e.y / TILE_SIZE),\n }));\n\n this.rocks = [];\n\n // 2. Generate Candidate List (All valid dirt tiles)\n const candidates = [];\n const padX = 4;\n const padY = 5;\n\n for (let y = padY; y < gridH - 5; y++) {\n for (let x = padX; x < gridW - 8; x++) {\n // Skip No-Go Zone (Center)\n if (x >= noGoMin && x <= noGoMax) continue;\n\n // Must be dirt\n if (this.grid.isDirt(x, y)) {\n candidates.push({ x, y });\n }\n }\n }\n\n // 3. Shuffle Candidates (Fisher-Yates)\n for (let i = candidates.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1));\n [candidates[i], candidates[j]] = [candidates[j], candidates[i]];\n }\n\n // 4. Validation Helper\n const isValidSpot = (\n candidate,\n minPathDist,\n minEnemyDist,\n minRockDist\n ) => {\n const { x, y } = candidate;\n\n // A. Check existing rocks (CRITICAL FIX HERE)\n for (let i = 0; i < this.rocks.length; i++) {\n const r = this.rocks[i];\n const dx = Math.abs(r.gridX - x);\n const dy = Math.abs(r.gridY - y);\n\n // STRICT OVERLAP CHECK:\n // If dx < minRockDist AND dy < minRockDist, it's too close.\n // Example: If minRockDist is 2, then dx=0 or dx=1 is REJECTED.\n // This ensures at least 1 tile gap between rocks.\n if (dx < minRockDist && dy < minRockDist) return false;\n }\n\n // B. Check enemies (skip if minEnemyDist is 0)\n if (minEnemyDist > 0) {\n for (let i = 0; i < enemyGridPos.length; i++) {\n const e = enemyGridPos[i];\n const dx = Math.abs(e.x - x);\n const dy = Math.abs(e.y - y);\n if (dx < minEnemyDist && dy < minEnemyDist) return false;\n }\n }\n\n // C. Check Path Proximity\n for (let dy = -minPathDist; dy <= minPathDist; dy++) {\n for (let dx = -minPathDist; dx <= minPathDist; dx++) {\n if (dx === 0 && dy === 0) continue;\n if (this.grid.isEmpty(x + dx, y + dy)) return false;\n }\n }\n\n return true;\n };\n\n // 5. Execution Passes\n const passes = [\n // Pass 1: Strict (Ideal placement)\n // rock: 10 ensures huge gaps\n {\n target: targetRocks,\n path: 3,\n enemy: 6,\n rock: 10,\n },\n // Pass 2: Relaxed (Compromise)\n // rock: 4 ensures reasonable spacing\n {\n target: targetRocks,\n path: 2,\n enemy: 4,\n rock: 4,\n },\n // Pass 3: Desperation (Force Minimum)\n // rock: 2 means dx must be >= 2. (0 and 1 are disallowed).\n // This guarantees rocks never touch, even in desperation mode.\n {\n target: minRocks,\n path: 1,\n enemy: 0,\n rock: 2,\n },\n ];\n\n for (const pass of passes) {\n if (this.rocks.length >= pass.target) continue;\n\n for (const candidate of candidates) {\n if (this.rocks.length >= pass.target) break;\n\n // Prevent placing on a spot that JUST became a rock in this same frame\n if (this.grid.isRock(candidate.x, candidate.y)) continue;\n\n if (isValidSpot(candidate, pass.path, pass.enemy, pass.rock)) {\n this.grid.placeRock(candidate.x, candidate.y);\n this.rocks.push(\n new Rock(\n candidate.x * TILE_SIZE,\n candidate.y * TILE_SIZE,\n this.grid\n )\n );\n }\n }\n }\n }\n\n /**\n * Spawn enemies for the level\n */\n spawnEnemies(levelNumber) {\n const enemies = [];\n\n // Calculate number of enemies based on level\n const numEnemies = Math.min(\n LEVEL.START_ENEMIES + (levelNumber - 1) * LEVEL.ENEMY_INCREMENT,\n LEVEL.MAX_ENEMIES\n );\n\n // Calculate Pooka/Fygar ratio (more Fygars in later levels)\n const fygarRatio = Math.min(0.5, 0.2 + levelNumber * 0.05);\n const numFygars = Math.floor(numEnemies * fygarRatio);\n const numPookas = numEnemies - numFygars;\n\n // Create tunnels for enemies\n this.enemyTunnels = [];\n this.createEnemyTunnels(numEnemies);\n\n // Spawn Pookas\n for (let i = 0; i < numPookas; i++) {\n const pos = this.getEnemySpawnPosition(i);\n enemies.push(new Pooka(pos.x, pos.y, levelNumber));\n }\n\n // Spawn Fygars\n for (let i = 0; i < numFygars; i++) {\n const pos = this.getEnemySpawnPosition(numPookas + i);\n enemies.push(new Fygar(pos.x, pos.y, levelNumber));\n }\n\n return enemies;\n }\n\n /**\n * Create tunnels for enemies to spawn in\n */\n createEnemyTunnels(numEnemies) {\n this.enemyTunnels = []; // Reset\n\n // 1. Setup Constants & Pre-calculations\n const gridW = GRID_WIDTH;\n const gridH = GRID_HEIGHT;\n const playerCX = Math.floor(gridW / 2);\n const playerCY = Math.floor(gridH / 2);\n\n // Squared distances for player proximity (Fast math)\n const STRICT_PLAYER_DIST_SQ = 100; // 10 tiles\n const RELAXED_PLAYER_DIST_SQ = 36; // 6 tiles\n\n // 2. Generate Candidate List\n // We filter out the obvious \"player start\" area early to keep the list small\n const candidates = [];\n const padX = 4;\n const padY = 4;\n\n for (let y = padY; y < gridH - padY; y++) {\n for (let x = padX; x < gridW - padX; x++) {\n // Optimization: Skip player center immediately\n const dx = x - playerCX;\n const dy = y - playerCY;\n if (dx * dx + dy * dy < RELAXED_PLAYER_DIST_SQ) continue;\n\n candidates.push({ x, y });\n }\n }\n\n // 3. Shuffle Candidates (Fisher-Yates)\n for (let i = candidates.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1));\n [candidates[i], candidates[j]] = [candidates[j], candidates[i]];\n }\n\n // 4. Helper: Get Bounding Box for a Tunnel\n // Returns { minX, maxX, minY, maxY }\n const getTunnelBounds = (x, y, horizontal, length) => {\n if (horizontal) {\n // Horizontal: grows to the right\n // Limit length to grid bounds to prevent array out of bounds logic\n const actualLen = Math.min(length, gridW - 2 - x);\n return { minX: x, maxX: x + actualLen, minY: y, maxY: y };\n } else {\n // Vertical: grows down\n const actualLen = Math.min(length, gridH - 2 - y);\n return { minX: x, maxX: x, minY: y, maxY: y + actualLen };\n }\n };\n\n // 5. Helper: Intersection Check (AABB with Buffer)\n // Ensures New Tunnel (A) is at least 'spacing' tiles away from Existing (B)\n const checkOverlap = (rectA, rectB, spacing) => {\n // We expand Rect B by 'spacing' in all directions.\n // If Rect A touches this expanded area, it's too close.\n return (\n rectA.minX <= rectB.maxX + spacing &&\n rectA.maxX >= rectB.minX - spacing &&\n rectA.minY <= rectB.maxY + spacing &&\n rectA.maxY >= rectB.minY - spacing\n );\n };\n\n // 6. Main Placement Logic\n const attemptPlacement = (spacingBuffer, minPlayerDistSq) => {\n // Iterate through shuffled candidates\n // We use a simple index to avoid destroying the array, allowing reuse in Pass 2\n for (let i = 0; i < candidates.length; i++) {\n if (this.enemyTunnels.length >= numEnemies) return;\n\n const { x, y } = candidates[i];\n\n // 1. Check Player Distance (Squared)\n const pdx = x - playerCX;\n const pdy = y - playerCY;\n if (pdx * pdx + pdy * pdy < minPlayerDistSq) continue;\n\n // 2. Generate Random Geometry\n // We must decide this NOW to check valid bounding box\n const horizontal = Math.random() > 0.5;\n const length = Math.floor(Math.random() * 2) + 2; // 3 to 4 tiles total\n\n const newRect = getTunnelBounds(x, y, horizontal, length);\n\n // 3. Check against ALL existing tunnels\n let valid = true;\n for (const existing of this.enemyTunnels) {\n // If checking overlap returns true, it's a collision\n if (checkOverlap(newRect, existing.bounds, spacingBuffer)) {\n valid = false;\n break;\n }\n }\n\n if (valid) {\n // Render Logic\n if (horizontal) {\n this.grid.clearHorizontalTunnel(\n newRect.minX,\n newRect.maxX,\n newRect.minY\n );\n } else {\n this.grid.clearVerticalTunnel(\n newRect.minX,\n newRect.minY,\n newRect.maxY\n );\n }\n\n // Store with pre-calculated bounds for future checks\n this.enemyTunnels.push({\n x,\n y,\n horizontal,\n length,\n bounds: newRect,\n });\n }\n }\n };\n\n // 7. Execution Passes\n\n // Pass 1: Strict Constraints\n // Buffer 6: ensures huge separation between tunnels\n attemptPlacement(6, STRICT_PLAYER_DIST_SQ);\n\n // Pass 2: Relaxed Constraints (if needed)\n // Buffer 2: CRITICAL.\n // A buffer of 1 means they can touch corners or be adjacent.\n // A buffer of 2 guarantees at least 1 empty tile of dirt between them.\n if (this.enemyTunnels.length < numEnemies) {\n attemptPlacement(2, RELAXED_PLAYER_DIST_SQ);\n }\n }\n\n /**\n * Get a valid spawn position for an enemy in their pre-created tunnel\n */\n getEnemySpawnPosition(enemyIndex) {\n // Get the tunnel for this enemy\n if (enemyIndex < this.enemyTunnels.length) {\n const tunnel = this.enemyTunnels[enemyIndex];\n // Spawn in the middle of their tunnel\n const offsetX = tunnel.horizontal\n ? Math.floor(Math.random() * tunnel.length)\n : 0;\n const offsetY = tunnel.horizontal\n ? 0\n : Math.floor(Math.random() * tunnel.length);\n\n const gridX = tunnel.x + offsetX;\n const gridY = tunnel.y + offsetY;\n\n // Verify the position is actually empty (in a tunnel)\n if (this.grid.isEmpty(gridX, gridY)) {\n return {\n x: gridX * TILE_SIZE,\n y: gridY * TILE_SIZE,\n };\n }\n }\n\n // Fallback: find the first empty tunnel position\n for (let y = 0; y < this.grid.height; y++) {\n for (let x = 0; x < this.grid.width; x++) {\n if (this.grid.isEmpty(x, y) && y > 2) {\n return { x: x * TILE_SIZE, y: y * TILE_SIZE };\n }\n }\n }\n\n // Last resort fallback\n return { x: TILE_SIZE * 5, y: TILE_SIZE * 5 };\n }\n\n /**\n * Spawn bonus item in center of screen\n * @param {number} bonusIndex - Sequential index for determining which prize to show (0, 1, 2, ...)\n */\n spawnBonusItem(bonusIndex = 0) {\n const centerX = Math.floor(GRID_WIDTH / 2) * TILE_SIZE;\n const centerY = Math.floor(GRID_HEIGHT / 2) * TILE_SIZE;\n\n return {\n x: centerX,\n y: centerY,\n bonusIndex: bonusIndex, // Store the index for sequential prize selection\n FLASH_START: 3000, // Start flashing after 3 seconds\n FLASH_DURATION: 2000, // Flash for 2 seconds then disappear\n elapsedTime: 0,\n update: function (deltaTime) {\n this.elapsedTime += deltaTime;\n // Check if expired (3s visible + 2s flashing = 5s total)\n if (this.elapsedTime > this.FLASH_START + this.FLASH_DURATION) {\n return false; // Mark for removal\n }\n return true;\n },\n isFlashing: function () {\n return this.elapsedTime >= this.FLASH_START;\n },\n };\n }\n\n /**\n * Spawn a single rock in a dirt tile\n * @returns {Rock|null} The spawned rock, or null if no valid position found\n */\n spawnSingleRock() {\n // Find all dirt tiles that are valid for rock placement\n const validPositions = [];\n\n for (let y = 4; y < GRID_HEIGHT - 2; y++) {\n for (let x = 2; x < GRID_WIDTH - 2; x++) {\n // Must be dirt\n if (!this.grid.isDirt(x, y)) continue;\n\n // Avoid center columns (where player starts)\n if (x >= GRID_WIDTH / 2 - 2 && x <= GRID_WIDTH / 2 + 2)\n continue;\n\n // Check distance from existing rocks\n let tooCloseToRock = false;\n for (const rock of this.rocks) {\n const rockGridX = Math.floor(rock.x / TILE_SIZE);\n const rockGridY = Math.floor(rock.y / TILE_SIZE);\n const dist =\n Math.abs(x - rockGridX) + Math.abs(y - rockGridY);\n if (dist < 4) {\n tooCloseToRock = true;\n break;\n }\n }\n if (tooCloseToRock) continue;\n\n validPositions.push({ x, y });\n }\n }\n\n if (validPositions.length === 0) {\n return null; // No valid position found\n }\n\n // Pick a random valid position\n const pos =\n validPositions[Math.floor(Math.random() * validPositions.length)];\n\n // Place rock in grid\n this.grid.placeRock(pos.x, pos.y);\n\n // Create Rock entity (don't add to this.rocks - Game.js manages that)\n const rock = new Rock(pos.x * TILE_SIZE, pos.y * TILE_SIZE, this.grid);\n\n return rock;\n }\n\n /**\n * Get rocks array\n */\n getRocks() {\n return this.rocks;\n }\n}\n","import {\n ENEMY_TYPES,\n HI_SCORE_KEY,\n PLAYER,\n SCORES,\n TILE_SIZE,\n} from '../utils/constants.js';\n\nexport class ScoreManager {\n constructor() {\n this.score = 0;\n this.lives = PLAYER.START_LIVES;\n this.highScore = this.loadHighScore();\n\n // Extra life thresholds: 20,000, 50,000, 100,000, 150,000, ...\n this.nextExtraLifeThreshold = 20000;\n }\n\n /**\n * Reset score and lives for new game\n */\n reset() {\n this.score = 0;\n this.lives = PLAYER.START_LIVES;\n this.nextExtraLifeThreshold = 20000;\n }\n\n /**\n * Add points for digging a tile\n */\n addDigScore() {\n const points = SCORES.DIG_TILE;\n this.addScore(points);\n return points;\n }\n\n /**\n * Calculate depth quarter based on Y position\n * Sky is rows 0-1, dirt is rows 2-17 (16 rows total)\n * Quarters of dirt: 2-5 (Q0), 6-9 (Q1), 10-13 (Q2), 14-17 (Q3)\n */\n getDepthQuarter(pixelY) {\n const gridY = Math.floor(pixelY / TILE_SIZE);\n const dirtStartRow = 2; // Skip sky rows\n const dirtRows = 16; // 16 rows of dirt\n const quarterSize = dirtRows / 4; // 4 rows per quarter\n\n const relativeRow = Math.max(0, gridY - dirtStartRow);\n return Math.min(3, Math.floor(relativeRow / quarterSize));\n }\n\n /**\n * Add points for enemy kill by pumping\n * Points based on depth and whether Fygar was killed horizontally\n */\n addEnemyKill(enemyType, enemyY, isHorizontalKill = false) {\n const depthQuarter = this.getDepthQuarter(enemyY);\n let points;\n\n if (enemyType === ENEMY_TYPES.FYGAR && isHorizontalKill) {\n points = SCORES.PUMP_KILL.FYGAR_HORIZONTAL[depthQuarter];\n } else if (enemyType === ENEMY_TYPES.FYGAR) {\n points = SCORES.PUMP_KILL.FYGAR[depthQuarter];\n } else {\n points = SCORES.PUMP_KILL.POOKA[depthQuarter];\n }\n\n this.addScore(points);\n return points;\n }\n\n /**\n * Add points for rock kill based on number of enemies killed\n */\n addRockKill(enemyCount) {\n // Cap at index 8 for 8+ enemies\n const index = Math.min(enemyCount, 8);\n const points = SCORES.ROCK_KILL[index];\n this.addScore(points);\n return points;\n }\n\n /**\n * Add points for bonus item based on prize index\n */\n addBonusItem(bonusIndex) {\n // bonusIndex is 0-based, maps to prize_1 through prize_11\n const safeIndex = Math.min(bonusIndex, SCORES.BONUS_ITEMS.length - 1);\n const points = SCORES.BONUS_ITEMS[safeIndex];\n this.addScore(points);\n return points;\n }\n\n /**\n * Add points to score and check for extra life\n * Returns true if an extra life was awarded\n */\n addScore(points) {\n this.score += points;\n\n // Check if we crossed an extra life threshold\n return this.checkExtraLife();\n }\n\n /**\n * Check and award extra life at thresholds:\n * 20,000, 50,000, then every 50,000 (100,000, 150,000, ...)\n */\n checkExtraLife() {\n if (this.score >= this.nextExtraLifeThreshold) {\n this.gainLife();\n\n // Calculate next threshold\n if (this.nextExtraLifeThreshold === 20000) {\n this.nextExtraLifeThreshold = 50000;\n } else {\n this.nextExtraLifeThreshold += 50000;\n }\n\n return true;\n }\n return false;\n }\n\n /**\n * Lose a life\n */\n loseLife() {\n this.lives--;\n }\n\n /**\n * Gain a life\n */\n gainLife() {\n this.lives++;\n }\n\n /**\n * Load high score from localStorage\n */\n loadHighScore() {\n try {\n const saved = localStorage.getItem(HI_SCORE_KEY);\n return saved ? parseInt(saved, 10) : 0;\n } catch (e) {\n return 0;\n }\n }\n\n /**\n * Save high score to localStorage\n */\n saveHighScore() {\n try {\n localStorage.setItem(HI_SCORE_KEY, this.highScore.toString());\n } catch (e) {\n // Ignore localStorage errors\n }\n }\n}\n","import { TILE_SIZE, PLAYER, DIRECTIONS, DEATH } from '../utils/constants.js';\n\nexport class Player {\n constructor(grid) {\n this.grid = grid;\n\n // Position (start at center)\n const centerX = Math.floor(grid.width / 2);\n const centerY = Math.floor(grid.height / 2);\n this.x = TILE_SIZE * centerX;\n this.y = TILE_SIZE * centerY;\n\n // Movement\n this.speed = PLAYER.SPEED;\n this.direction = DIRECTIONS.LEFT;\n this.previousDirection = DIRECTIONS.LEFT;\n this.isMoving = false;\n\n // Pump attack\n this.isShooting = false;\n this.isPumping = false;\n this.pumpTarget = null;\n this.pumpLength = 0; // Current length of pump line in pixels\n this.pumpMaxLength = PLAYER.PUMP_RANGE;\n this.pumpExtendSpeed = 4; // Pixels per frame to extend\n this.pumpRetractSpeed = 8; // Faster retract when releasing\n this.shouldAutoRetract = false; // Auto-retract when max length reached without hitting\n this.pumpUsed = false; // Tracks if pump was used this Space press (requires re-press)\n this.fullLengthTimer = 0; // Timer for delay at full length before auto-retract\n this.fullLengthDelay = 200; // 200ms delay at full length\n\n // Animation\n this.animationFrame = 0;\n this.animationTimer = 0;\n\n // Sprite orientation tracking for UP/DOWN transitions\n this.spriteFlipH = false;\n this.spriteFlipV = false;\n\n // Track if player is digging (in contact with dirt)\n this.isDigging = false;\n\n // Death state\n this.isDying = false;\n this.deathTimer = 0;\n this.deathType = null; // 'enemy' or 'rock'\n\n // Smooshed state (crushed by rock - falls with rock)\n this.isSmooshed = false;\n this.attachedToRock = null;\n this.smooshedDelayTimer = 0;\n this.SMOOSHED_DELAY = 400; // Delay before dying animation starts (matches rock crumble delay)\n\n // Respawn invincibility\n this.isInvincible = false;\n this.invincibilityTimer = 0;\n\n // Pump delay after start/restart\n this.pumpCooldown = 800; // 800ms delay before can pump\n this.pumpCooldownTimer = 0;\n }\n\n /**\n * Start death animation\n */\n startDeath(deathType) {\n this.isDying = true;\n this.deathTimer = 0;\n this.deathType = deathType;\n this.isMoving = false;\n }\n\n /**\n * Player gets smooshed (crushed by rock)\n * Shows smooshed sprite and falls with rock\n */\n smoosh(rock) {\n this.isSmooshed = true;\n this.attachedToRock = rock;\n this.startDeath('rock');\n }\n\n /**\n * Update player state\n */\n update(deltaTime, inputManager, grid) {\n // Update death animation\n if (this.isDying) {\n // If smooshed and attached to a falling rock, don't advance death timer\n if (\n this.isSmooshed &&\n this.attachedToRock &&\n this.attachedToRock.isFalling\n ) {\n return; // Wait for rock to stop falling\n }\n // If smooshed, wait for delay before starting dying animation\n if (\n this.isSmooshed &&\n this.smooshedDelayTimer < this.SMOOSHED_DELAY\n ) {\n this.smooshedDelayTimer += deltaTime;\n return; // Wait for delay to complete\n }\n this.deathTimer += deltaTime;\n return; // Don't process normal updates\n }\n\n // Update invincibility\n if (this.isInvincible) {\n this.invincibilityTimer += deltaTime;\n if (this.invincibilityTimer > DEATH.INVINCIBILITY_TIME) {\n this.isInvincible = false;\n }\n }\n\n // Update pump cooldown\n if (this.pumpCooldownTimer < this.pumpCooldown) {\n this.pumpCooldownTimer += deltaTime;\n }\n\n // Handle pump attack - extend while Space held, retract when released\n const canPump = this.pumpCooldownTimer >= this.pumpCooldown;\n const spacePressed = inputManager.isSpacePressed();\n\n // Reset pumpUsed flag when Space is released (allows next pump)\n if (!spacePressed) {\n this.pumpUsed = false;\n }\n\n if (\n spacePressed &&\n canPump &&\n !this.shouldAutoRetract &&\n !this.pumpUsed\n ) {\n if (!this.isShooting) {\n this.startPump();\n }\n this.extendPump(deltaTime);\n } else if (this.isShooting || this.pumpLength > 0) {\n this.retractPump();\n }\n\n // Get input direction - but don't move or rotate while pumping\n const inputDirection = inputManager.getDirection();\n\n if (inputDirection && !this.isShooting && !this.isPumping) {\n // Track previous direction before changing\n if (inputDirection !== this.direction) {\n this.previousDirection = this.direction;\n this.updateSpriteOrientation(inputDirection);\n }\n this.direction = inputDirection;\n this.isMoving = true;\n this.move(inputDirection, grid);\n } else {\n this.isMoving = false;\n }\n\n // Update animation\n if (this.isMoving || this.isPumping) {\n this.animationTimer += deltaTime;\n if (this.animationTimer > (this.isMoving ? 100 : 200)) {\n this.animationFrame = (this.animationFrame + 1) % 2;\n this.animationTimer = 0;\n }\n }\n\n // Check if player is in contact with dirt\n this.checkDiggingState(grid);\n\n // Dig dirt at current position\n this.dig(grid);\n }\n\n /**\n * Move player in direction\n */\n move(direction, grid) {\n let newX = this.x;\n let newY = this.y;\n\n switch (direction) {\n case DIRECTIONS.UP:\n newY -= this.speed;\n break;\n case DIRECTIONS.DOWN:\n newY += this.speed;\n break;\n case DIRECTIONS.LEFT:\n newX -= this.speed;\n break;\n case DIRECTIONS.RIGHT:\n newX += this.speed;\n break;\n }\n\n // Check boundaries\n if (newX < 0) newX = 0;\n if (newX > grid.width * TILE_SIZE - TILE_SIZE) {\n newX = grid.width * TILE_SIZE - TILE_SIZE;\n }\n if (newY < 0) newY = 0;\n if (newY > grid.height * TILE_SIZE - TILE_SIZE) {\n newY = grid.height * TILE_SIZE - TILE_SIZE;\n }\n\n // Dig the tile we're about to move into BEFORE moving\n const newCenterX = newX + TILE_SIZE / 2;\n const newCenterY = newY + TILE_SIZE / 2;\n const newTile = grid.pixelToGrid(newCenterX, newCenterY);\n grid.dig(newTile.x, newTile.y);\n\n // Can only move through tunnels (empty tiles) or dig through dirt\n // Check all corners of player hitbox\n const canMove = this.canMoveToPosition(newX, newY, grid);\n\n if (canMove) {\n this.x = newX;\n this.y = newY;\n\n // Snap to grid alignment to stay centered in tunnel\n // When moving horizontally, align vertically to grid\n if (\n direction === DIRECTIONS.LEFT ||\n direction === DIRECTIONS.RIGHT\n ) {\n const centerY = this.y + TILE_SIZE / 2;\n const gridY = Math.floor(centerY / TILE_SIZE);\n const targetY = gridY * TILE_SIZE;\n // Gradually snap to grid\n const diff = targetY - this.y;\n if (Math.abs(diff) > 0) {\n this.y +=\n Math.sign(diff) * Math.min(Math.abs(diff), this.speed);\n }\n }\n\n // When moving vertically, align horizontally to grid\n if (direction === DIRECTIONS.UP || direction === DIRECTIONS.DOWN) {\n const centerX = this.x + TILE_SIZE / 2;\n const gridX = Math.floor(centerX / TILE_SIZE);\n const targetX = gridX * TILE_SIZE;\n // Gradually snap to grid\n const diff = targetX - this.x;\n if (Math.abs(diff) > 0) {\n this.x +=\n Math.sign(diff) * Math.min(Math.abs(diff), this.speed);\n }\n }\n }\n }\n\n /**\n * Check if player can move to position\n */\n canMoveToPosition(x, y, grid) {\n const corners = [\n { x: x, y: y },\n { x: x + TILE_SIZE - 1, y: y },\n { x: x, y: y + TILE_SIZE - 1 },\n { x: x + TILE_SIZE - 1, y: y + TILE_SIZE - 1 },\n ];\n\n // Player can move through empty tiles or dirt (will dig)\n return corners.every((corner) => {\n const { x: gx, y: gy } = grid.pixelToGrid(corner.x, corner.y);\n // Cannot move into top row (sky boundary)\n if (gy === 0) return false;\n // Cannot move through rocks\n return !grid.isRock(gx, gy);\n });\n }\n\n /**\n * Check if player is in contact with dirt (digging state)\n */\n checkDiggingState(grid) {\n // Check tiles around the player based on movement direction\n const centerX = this.x + TILE_SIZE / 2;\n const centerY = this.y + TILE_SIZE / 2;\n const { x: gridX, y: gridY } = grid.pixelToGrid(centerX, centerY);\n\n // Check adjacent tiles in the direction of movement\n let isInContactWithDirt = false;\n\n switch (this.direction) {\n case DIRECTIONS.LEFT:\n isInContactWithDirt = grid.isDirt(gridX - 1, gridY);\n break;\n case DIRECTIONS.RIGHT:\n isInContactWithDirt = grid.isDirt(gridX + 1, gridY);\n break;\n case DIRECTIONS.UP:\n isInContactWithDirt = grid.isDirt(gridX, gridY - 1);\n break;\n case DIRECTIONS.DOWN:\n isInContactWithDirt = grid.isDirt(gridX, gridY + 1);\n break;\n }\n\n this.isDigging = isInContactWithDirt;\n }\n\n /**\n * Dig dirt at current position\n */\n dig(grid) {\n // Use player's center point to determine which tile to dig\n // This ensures only one tile is dug at a time\n const centerX = this.x + TILE_SIZE / 2;\n const centerY = this.y + TILE_SIZE / 2;\n const { x, y } = grid.pixelToGrid(centerX, centerY);\n\n // Dig the tile the player's center is in\n grid.dig(x, y);\n }\n\n /**\n * Update sprite orientation based on direction change\n */\n updateSpriteOrientation(newDirection) {\n if (\n newDirection === DIRECTIONS.LEFT ||\n newDirection === DIRECTIONS.RIGHT\n ) {\n // Horizontal movement\n if (newDirection === DIRECTIONS.RIGHT) {\n this.spriteFlipH = true;\n this.spriteFlipV = false;\n } else {\n this.spriteFlipH = false;\n this.spriteFlipV = false;\n }\n } else if (newDirection === DIRECTIONS.DOWN) {\n // Moving DOWN\n if (this.previousDirection === DIRECTIONS.RIGHT) {\n // From RIGHT: flip horizontally\n this.spriteFlipH = true;\n this.spriteFlipV = false;\n } else if (this.previousDirection === DIRECTIONS.UP) {\n // From UP: flip vertically from current orientation\n // Keep H as it was, invert V\n this.spriteFlipV = !this.spriteFlipV;\n } else if (this.previousDirection === DIRECTIONS.LEFT) {\n // From LEFT: no transformation needed\n this.spriteFlipH = false;\n this.spriteFlipV = false;\n }\n } else if (newDirection === DIRECTIONS.UP) {\n // Moving UP\n if (this.previousDirection === DIRECTIONS.LEFT) {\n // From LEFT: flip both horizontally and vertically\n this.spriteFlipH = true;\n this.spriteFlipV = true;\n } else if (this.previousDirection === DIRECTIONS.RIGHT) {\n // From RIGHT: flip vertically only\n this.spriteFlipH = false;\n this.spriteFlipV = true;\n } else if (this.previousDirection === DIRECTIONS.DOWN) {\n // From DOWN: flip vertically from current orientation\n // Keep H as it was, invert V\n this.spriteFlipV = !this.spriteFlipV;\n }\n }\n }\n\n /**\n * Start pumping attack\n */\n startPump() {\n this.isShooting = true;\n this.isMoving = false; // Can't move while pumping\n }\n\n /**\n * Extend pump line while Space is held\n */\n extendPump(deltaTime) {\n // Stop extending if we've hit an enemy (pumpTarget is set)\n if (this.pumpTarget) {\n this.isShooting = false;\n this.isPumping = true;\n // If the target was destroyed (popped), immediately clear the pump\n if (this.pumpTarget.isDestroyed) {\n this.pumpLength = 0;\n this.stopPump();\n }\n return;\n }\n\n if (this.pumpLength < this.pumpMaxLength) {\n // Check if extending would go into dirt\n const nextLength = Math.min(\n this.pumpLength + this.pumpExtendSpeed,\n this.pumpMaxLength\n );\n\n if (this.canPumpExtendTo(nextLength)) {\n this.pumpLength = nextLength;\n // Reset full length timer while still extending\n this.fullLengthTimer = 0;\n } else {\n // Hit dirt - treat as max length reached\n this.fullLengthTimer += deltaTime;\n if (this.fullLengthTimer >= this.fullLengthDelay) {\n this.shouldAutoRetract = true;\n }\n }\n } else {\n // At max length without hitting enemy - wait for delay then auto-retract\n this.fullLengthTimer += deltaTime;\n if (this.fullLengthTimer >= this.fullLengthDelay) {\n this.shouldAutoRetract = true;\n }\n }\n }\n\n /**\n * Check if pump can extend to a given length (not blocked by dirt or rocks)\n */\n canPumpExtendTo(length) {\n const centerX = this.x + TILE_SIZE / 2;\n const centerY = this.y + TILE_SIZE / 2;\n\n let endX = centerX;\n let endY = centerY;\n\n switch (this.direction) {\n case DIRECTIONS.UP:\n endY = centerY - length;\n break;\n case DIRECTIONS.DOWN:\n endY = centerY + length;\n break;\n case DIRECTIONS.LEFT:\n endX = centerX - length;\n break;\n case DIRECTIONS.RIGHT:\n endX = centerX + length;\n break;\n }\n\n // Check if end point is in dirt or rock\n const { x: gx, y: gy } = this.grid.pixelToGrid(endX, endY);\n return !this.grid.isDirt(gx, gy) && !this.grid.isRock(gx, gy);\n }\n\n /**\n * Retract pump line when Space is released\n */\n retractPump() {\n // If the target was destroyed (popped), immediately clear the pump\n if (this.pumpTarget && this.pumpTarget.isDestroyed) {\n this.pumpLength = 0;\n this.stopPump();\n return;\n }\n\n this.pumpLength = Math.max(0, this.pumpLength - this.pumpRetractSpeed);\n if (this.pumpLength === 0) {\n this.stopPump();\n }\n }\n\n /**\n * Stop pumping\n */\n stopPump() {\n this.isShooting = false;\n this.isPumping = false;\n this.pumpTarget = null;\n this.pumpLength = 0;\n this.shouldAutoRetract = false;\n this.pumpUsed = true; // Require Space re-press for next pump\n this.fullLengthTimer = 0;\n }\n\n /**\n * Get the end point of the pump line\n */\n getPumpEndPoint() {\n const centerX = this.x + TILE_SIZE / 2;\n const centerY = this.y + TILE_SIZE / 2;\n\n switch (this.direction) {\n case DIRECTIONS.UP:\n return { x: centerX, y: centerY - this.pumpLength };\n case DIRECTIONS.DOWN:\n return { x: centerX, y: centerY + this.pumpLength };\n case DIRECTIONS.LEFT:\n return { x: centerX - this.pumpLength, y: centerY };\n case DIRECTIONS.RIGHT:\n return { x: centerX + this.pumpLength, y: centerY };\n default:\n return { x: centerX, y: centerY };\n }\n }\n\n /**\n * Get grid position\n */\n getGridPosition() {\n return this.grid.pixelToGrid(this.x, this.y);\n }\n\n /**\n * Get center position\n */\n getCenter() {\n return {\n x: this.x + TILE_SIZE / 2,\n y: this.y + TILE_SIZE / 2,\n };\n }\n\n /**\n * Reset all timers - called when game starts or respawns\n */\n resetTimers() {\n this.pumpCooldownTimer = 0;\n this.invincibilityTimer = 0;\n this.animationTimer = 0;\n this.fullLengthTimer = 0;\n this.pumpLength = 0;\n this.isShooting = false;\n this.isPumping = false;\n this.pumpTarget = null;\n this.shouldAutoRetract = false;\n this.pumpUsed = false;\n // Reset smooshed state\n this.isSmooshed = false;\n this.attachedToRock = null;\n this.smooshedDelayTimer = 0;\n }\n}\n","import {\n CANVAS_WIDTH,\n CANVAS_HEIGHT,\n GAME_STATES,\n COLORS,\n DEATH,\n TILE_SIZE,\n ENEMY_TYPES,\n DIRECTIONS,\n} from './utils/constants.js';\nimport { Renderer } from './Renderer.js';\nimport { Grid } from './utils/Grid.js';\nimport { InputManager } from './systems/InputManager.js';\nimport { CollisionSystem } from './systems/CollisionSystem.js';\nimport { LevelManager } from './systems/LevelManager.js';\nimport { ScoreManager } from './systems/ScoreManager.js';\nimport { Player } from './entities/Player.js';\n\nexport class Game {\n constructor(config = {}) {\n this.config = {\n container: config.container || document.body,\n width: config.width || CANVAS_WIDTH,\n height: config.height || CANVAS_HEIGHT,\n scale: config.scale || 1,\n debug: config.debug || false,\n onGameOver: config.onGameOver || (() => {}),\n onLevelComplete: config.onLevelComplete || (() => {}),\n onScoreChange: config.onScoreChange || (() => {}),\n };\n\n this.state = GAME_STATES.MENU;\n this.lastTime = 0;\n this.animationFrameId = null;\n\n // Initialize systems\n this.renderer = new Renderer(this.config);\n this.grid = new Grid();\n this.inputManager = new InputManager();\n this.collisionSystem = new CollisionSystem(this.grid);\n this.levelManager = new LevelManager(this.grid);\n this.scoreManager = new ScoreManager();\n\n // Game entities\n this.player = null;\n this.enemies = [];\n this.rocks = [];\n this.bonusItems = [];\n\n // Rock respawn timer\n this.rockRespawnTimer = 0;\n this.ROCK_RESPAWN_DELAY = 5000; // 5 seconds before spawning a new rock\n this.needsRockRespawn = false;\n\n // Bonus spawn tracking\n this.bonusSpawnCount = 0; // Sequential counter for prize order\n\n // Floating score display\n this.floatingScores = [];\n\n // Track dirt count for dig scoring\n this.lastDirtCount = 0;\n\n // Bind methods\n this.gameLoop = this.gameLoop.bind(this);\n }\n\n /**\n * Initialize the game\n */\n init() {\n // Attach canvas to container\n this.renderer.attachTo(this.config.container);\n\n // Set up input listeners\n this.inputManager.init();\n\n // Show menu\n this.showMenu();\n }\n\n /**\n * Show the main menu\n */\n showMenu() {\n this.state = GAME_STATES.MENU;\n this.renderer.clear();\n this.renderer.drawMenu();\n\n // Wait for space key (only set up listener once)\n if (!this.menuListenerAdded) {\n this.menuListenerAdded = true;\n const startGame = (e) => {\n if (e.code === 'Space' && this.state === GAME_STATES.MENU) {\n document.removeEventListener('keydown', startGame);\n this.menuListenerAdded = false;\n this.startGame();\n }\n };\n document.addEventListener('keydown', startGame);\n }\n }\n\n /**\n * Start a new game\n */\n startGame() {\n // Stop any existing game loop\n if (this.animationFrameId) {\n cancelAnimationFrame(this.animationFrameId);\n this.animationFrameId = null;\n }\n\n this.scoreManager.reset();\n this.lastTime = 0; // Reset time tracking\n\n // Start the intro animation instead of jumping straight to playing\n this.startIntro();\n\n this.gameLoop(0);\n }\n\n /**\n * Start the intro animation sequence\n */\n startIntro() {\n this.state = GAME_STATES.INTRO;\n\n // Generate the actual level first (creates tunnels, but we'll add player's tunnel too)\n this.levelManager.generateLevel(1);\n\n // Create enemies (spawned in tunnels, but frozen during intro)\n this.enemies = this.config.debug\n ? []\n : this.levelManager.spawnEnemies(1);\n\n // Create rocks AFTER enemies\n this.levelManager.placeRocksAfterEnemies(1, this.enemies);\n this.rocks = this.levelManager.getRocks();\n\n // Clear bonus items\n this.bonusItems = [];\n\n // Reset dropped rocks counter and bonus spawn count\n this.droppedRocksCount = 0;\n this.bonusSpawnCount = 0;\n\n // Reset rock respawn timer\n this.rockRespawnTimer = 0;\n this.needsRockRespawn = false;\n\n // Calculate target positions for player\n this.introCenterX = Math.floor(this.grid.width / 2); // Center column\n this.introCenterY = Math.floor(this.grid.height / 2); // Center row\n this.introTargetX = this.introCenterX * TILE_SIZE; // Pixel position for center\n this.introTargetY = this.introCenterY * TILE_SIZE;\n\n // Create player at the right edge of row 1 (in the sky)\n this.player = new Player(this.grid);\n this.player.x = this.grid.width * TILE_SIZE; // Start off-screen right\n this.player.y = TILE_SIZE; // Row 1 (second row, in the sky)\n this.player.direction = DIRECTIONS.LEFT;\n this.player.spriteFlipH = false;\n this.player.isMoving = true;\n\n // Intro animation state\n this.introPhase = 'walk_left'; // 'walk_left', 'dig_down', 'dig_tunnel', 'ready', 'done'\n this.introTimer = 0;\n this.introReadyDelay = 1000; // 1 second delay after reaching position\n }\n\n /**\n * Reset all game timers - called when game starts\n */\n resetAllTimers() {\n // Reset enemy timers\n this.enemies.forEach((enemy) => {\n enemy.resetTimers();\n });\n\n // Reset player timers\n if (this.player) {\n this.player.resetTimers();\n }\n }\n\n /**\n * Start a new level\n */\n startLevel(levelNumber) {\n // Generate level (creates tunnels)\n this.levelManager.generateLevel(levelNumber);\n\n // Create player\n this.player = new Player(this.grid);\n\n // Create enemies (must spawn in tunnels)\n this.enemies = this.levelManager.spawnEnemies(levelNumber);\n\n // Create rocks AFTER enemies (so rocks avoid enemy positions)\n this.levelManager.placeRocksAfterEnemies(levelNumber, this.enemies);\n this.rocks = this.levelManager.getRocks();\n\n // Clear bonus items\n this.bonusItems = [];\n\n // Reset dropped rocks counter (but NOT bonusSpawnCount - that persists across levels)\n this.droppedRocksCount = 0;\n\n // Reset rock respawn timer\n this.rockRespawnTimer = 0;\n this.needsRockRespawn = false;\n\n this.floatingScores = [];\n }\n\n /**\n * Main game loop\n */\n gameLoop(currentTime) {\n // Calculate delta time (cap at 100ms to prevent huge jumps on first frame or tab switch)\n const rawDelta = currentTime - this.lastTime;\n const deltaTime = Math.min(rawDelta, 100);\n this.lastTime = currentTime;\n\n // Update and render based on game state\n switch (this.state) {\n case GAME_STATES.INTRO:\n this.updateIntro(deltaTime);\n this.renderIntro();\n break;\n case GAME_STATES.PLAYING:\n this.update(deltaTime);\n this.render();\n break;\n case GAME_STATES.DYING:\n this.updateDeath(deltaTime);\n this.render(); // Render death animation\n break;\n case GAME_STATES.RESPAWNING:\n this.updateRespawn(deltaTime);\n this.renderRespawning();\n break;\n case GAME_STATES.PAUSED:\n this.renderPaused();\n break;\n case GAME_STATES.LEVEL_COMPLETE:\n this.renderLevelComplete();\n break;\n case GAME_STATES.GAME_OVER:\n this.renderGameOver();\n break;\n }\n\n // Continue loop\n this.animationFrameId = requestAnimationFrame(this.gameLoop);\n }\n\n /**\n * Update intro animation\n */\n updateIntro(deltaTime) {\n const speed = this.config.debug ? 3 : 1.6; // Player movement speed during intro\n\n if (this.introPhase !== 'ready') {\n // Update player animation\n this.player.animationTimer += deltaTime;\n if (this.player.animationTimer > 100) {\n this.player.animationFrame =\n (this.player.animationFrame + 1) % 2;\n this.player.animationTimer = 0;\n }\n }\n\n switch (this.introPhase) {\n case 'walk_left':\n // Walk left across the sky to center column\n this.player.x -= speed;\n this.player.isMoving = true;\n this.player.direction = DIRECTIONS.LEFT;\n this.player.spriteFlipH = false;\n\n if (this.player.x <= this.introTargetX) {\n this.player.x = this.introTargetX;\n this.introPhase = 'dig_down';\n this.player.direction = DIRECTIONS.DOWN;\n this.player.spriteFlipH = false;\n this.player.spriteFlipV = false;\n }\n break;\n\n case 'dig_down': {\n // Dig straight down to center row\n this.player.y += speed;\n this.player.isMoving = true;\n this.player.isDigging = true;\n this.player.direction = DIRECTIONS.DOWN;\n\n // Dig the tile we're moving through\n const currentTile = this.grid.pixelToGrid(\n this.player.x + TILE_SIZE / 2,\n this.player.y + TILE_SIZE / 2\n );\n this.grid.dig(currentTile.x, currentTile.y);\n\n if (this.player.y >= this.introTargetY) {\n this.player.y = this.introTargetY;\n // Stop in center and go directly to ready phase\n this.introPhase = 'ready';\n this.introTimer = 0;\n this.player.isMoving = false;\n this.player.isDigging = false;\n this.player.direction = DIRECTIONS.RIGHT;\n this.player.spriteFlipH = true;\n }\n break;\n }\n\n case 'ready':\n this.player.animationFrame = 0;\n this.introTimer += deltaTime;\n if (this.introTimer >= this.introReadyDelay) {\n this.introPhase = 'done';\n this.finishIntro();\n }\n break;\n }\n }\n\n /**\n * Finish intro and start actual gameplay\n */\n finishIntro() {\n // Level is already set up from startIntro - just reset timers and start\n\n // Reset player timers\n this.player.resetTimers();\n\n // Reset all timers (unfreezes enemies)\n this.resetAllTimers();\n\n // Start playing\n this.state = GAME_STATES.PLAYING;\n }\n\n /**\n * Render intro animation\n */\n renderIntro() {\n this.renderer.clear();\n\n // Draw grid (shows the tunnel being dug)\n this.renderer.drawGrid(this.grid, this.levelManager.currentLevel);\n\n // Draw rocks (already placed)\n this.rocks.forEach((rock) => {\n this.renderer.drawRock(rock);\n });\n\n // Draw player\n if (this.player) {\n this.renderer.drawPlayer(this.player);\n }\n\n // Draw enemies (frozen but visible)\n this.enemies.forEach((enemy) => {\n this.renderer.drawEnemy(enemy);\n });\n\n // Draw UI\n this.renderer.drawUI(this.scoreManager, this.levelManager);\n\n // Always show \"Player 1 Ready\" during intro\n this.renderer.renderRespawning();\n }\n\n /**\n * Update game state\n */\n update(deltaTime) {\n if (this.inputManager.isKeyPressed('Escape')) {\n this.pause();\n return;\n }\n\n // Update player and track digging for scoring\n if (this.player) {\n const dirtBefore = this.countDirtTiles();\n this.player.update(deltaTime, this.inputManager, this.grid);\n const dirtAfter = this.countDirtTiles();\n\n // Award points for each tile dug\n const tilesDigged = dirtBefore - dirtAfter;\n for (let i = 0; i < tilesDigged; i++) {\n this.scoreManager.addDigScore();\n }\n if (tilesDigged > 0) {\n this.config.onScoreChange(this.scoreManager.score);\n }\n }\n\n // Mark last enemy\n if (this.enemies.length === 1) {\n this.enemies[0].isLastEnemy = true;\n }\n\n // Update enemies\n this.enemies.forEach((enemy) => {\n enemy.update(deltaTime, this.player, this.grid);\n });\n\n // Update rocks (pass player so rocks can check for trigger)\n this.rocks.forEach((rock) => {\n rock.update(deltaTime, this.grid, this.player);\n });\n\n // Update rock respawn timer if no rocks remain\n if (this.needsRockRespawn && this.rocks.length === 0) {\n this.rockRespawnTimer += deltaTime;\n if (this.rockRespawnTimer >= this.ROCK_RESPAWN_DELAY) {\n // Try to spawn a new rock in dirt\n const newRock = this.levelManager.spawnSingleRock();\n if (newRock) {\n // Start spawn animation for fade-in/flash effect\n newRock.startSpawnAnimation();\n // Add rock to Game's rocks array\n this.rocks.push(newRock);\n this.needsRockRespawn = false;\n this.rockRespawnTimer = 0;\n } else {\n // No valid dirt position found, stop trying\n this.needsRockRespawn = false;\n }\n }\n }\n\n // Update bonus items\n this.bonusItems.forEach((item) => {\n item.update(deltaTime);\n });\n\n // Update floating scores\n this.updateFloatingScores(deltaTime);\n\n // Check collisions\n this.checkCollisions();\n\n // Check level complete\n if (!this.config.debug && this.enemies.length === 0) {\n this.levelComplete();\n }\n }\n\n /**\n * Check all collisions\n */\n checkCollisions() {\n // Pump-enemy collisions (check if pump line hits any enemy)\n if (this.player.pumpLength > 0) {\n this.checkPumpCollisions();\n }\n\n // Fygar fire-player collisions\n this.checkFireCollisions();\n\n // Player-enemy collisions\n this.enemies.forEach((enemy) => {\n if (\n enemy.deflateTimer === 0 &&\n this.collisionSystem.checkPlayerEnemyCollision(\n this.player,\n enemy\n )\n ) {\n // Player hit by enemy\n this.playerHit('enemy');\n }\n });\n\n // Rock-entity collisions\n this.rocks.forEach((rock) => {\n if (rock.isFalling) {\n // Check rock-enemy\n this.enemies.forEach((enemy) => {\n // Skip already smooshed enemies\n if (enemy.isSmooshed) return;\n\n if (\n this.collisionSystem.checkRockEntityCollision(\n rock,\n enemy\n )\n ) {\n rock.markEnemyCrushed(); // Mark rock as having crushed an enemy\n // Track this kill for multi-kill scoring (scored when rock finishes)\n rock.incrementKillCount(enemy.x, enemy.y);\n this.droppedRocksCount++;\n this.checkBonusSpawn();\n\n // Smoosh the enemy and attach to rock\n enemy.smoosh();\n enemy.attachedToRock = rock;\n // Immediately sync position so enemy doesn't lag behind\n enemy.x = rock.x;\n enemy.y = rock.y;\n }\n });\n\n // Check rock-player (skip if already smooshed)\n if (\n !this.player.isSmooshed &&\n this.collisionSystem.checkRockEntityCollision(\n rock,\n this.player\n )\n ) {\n // Smoosh player and attach to rock\n this.player.smoosh(rock);\n // Immediately sync position so player doesn't lag behind\n this.player.x = rock.x;\n this.player.y = rock.y;\n this.state = GAME_STATES.DYING;\n this.deathStartTime = Date.now();\n }\n }\n\n // Update smooshed enemies attached to this rock - they fall with it\n this.enemies.forEach((enemy) => {\n if (enemy.isSmooshed && enemy.attachedToRock === rock) {\n enemy.x = rock.x;\n enemy.y = rock.y;\n }\n });\n\n // Update smooshed player attached to this rock - falls with it\n if (this.player.isSmooshed && this.player.attachedToRock === rock) {\n this.player.x = rock.x;\n this.player.y = rock.y;\n }\n });\n\n // Remove destroyed/crumbled rocks and score multi-kills\n const rocksBeforeFilter = this.rocks.length;\n this.rocks = this.rocks.filter((rock) => {\n if (rock.isDestroyed) {\n // Score rock kills based on total enemies killed by this rock\n if (rock.enemiesKilled > 0) {\n const points = this.scoreManager.addRockKill(\n rock.enemiesKilled\n );\n this.config.onScoreChange(this.scoreManager.score);\n // Show floating score at rock's final position (where it crumbled)\n this.spawnFloatingScore(points, rock.x, rock.y);\n }\n return false;\n }\n return true;\n });\n\n // Check if all rocks are gone and start respawn timer\n if (this.rocks.length === 0 && rocksBeforeFilter > 0) {\n this.needsRockRespawn = true;\n this.rockRespawnTimer = 0;\n }\n\n // Remove escaped enemies\n this.enemies = this.enemies.filter((enemy) => !enemy.hasEscaped);\n\n // Remove destroyed enemies (popped from inflation or smooshed by rock)\n this.enemies = this.enemies.filter((enemy) => {\n if (enemy.isDestroyed) {\n // Only award points for pumped enemies (not smooshed - those are scored when rock finishes)\n if (!enemy.isSmooshed) {\n // Determine if this was a horizontal kill (pump direction from player)\n const isHorizontalKill =\n this.player.direction === DIRECTIONS.LEFT ||\n this.player.direction === DIRECTIONS.RIGHT;\n const points = this.scoreManager.addEnemyKill(\n enemy.type,\n enemy.y,\n isHorizontalKill\n );\n this.config.onScoreChange(this.scoreManager.score);\n // Show floating score at enemy position\n this.spawnFloatingScore(points, enemy.x, enemy.y);\n }\n return false;\n }\n return true;\n });\n\n // Player-bonus item collisions\n this.bonusItems = this.bonusItems.filter((item) => {\n if (\n this.collisionSystem.checkPlayerBonusCollision(\n this.player,\n item\n )\n ) {\n const points = this.scoreManager.addBonusItem(item.bonusIndex);\n this.config.onScoreChange(this.scoreManager.score);\n // Show floating score at item position\n this.spawnFloatingScore(points, item.x, item.y);\n return false;\n }\n return true;\n });\n }\n\n /**\n * Check if pump line hits any enemies (only the closest one)\n */\n checkPumpCollisions() {\n // If already inflating an enemy, only continue inflating that one\n if (this.player.pumpTarget && !this.player.pumpTarget.isDestroyed) {\n this.player.pumpTarget.startInflation();\n return;\n }\n\n const playerCenter = this.player.getCenter();\n const pumpEnd = this.player.getPumpEndPoint();\n const pumpLength = this.player.pumpLength;\n\n // Find the closest enemy that the pump line intersects\n let closestEnemy = null;\n let closestDistance = Infinity;\n\n this.enemies.forEach((enemy) => {\n if (enemy.isDestroyed) return;\n\n // Check if pump line intersects with enemy\n const enemyCenter = enemy.getCenter();\n\n // Calculate distance from enemy center to pump line\n const distToLine = this.pointToLineDistance(\n enemyCenter.x,\n enemyCenter.y,\n playerCenter.x,\n playerCenter.y,\n pumpEnd.x,\n pumpEnd.y\n );\n\n // Distance from player to enemy\n const distToPlayer = Math.sqrt(\n Math.pow(enemyCenter.x - playerCenter.x, 2) +\n Math.pow(enemyCenter.y - playerCenter.y, 2)\n );\n\n // Hit if close to line and within pump range\n const hitRadius = TILE_SIZE * 0.6; // Slightly forgiving hit box\n if (\n distToLine < hitRadius &&\n distToPlayer <= pumpLength + TILE_SIZE / 2\n ) {\n // Check enemy is in front of player (in pump direction)\n if (\n this.isInPumpDirection(playerCenter, pumpEnd, enemyCenter)\n ) {\n // Track the closest enemy\n if (distToPlayer < closestDistance) {\n closestDistance = distToPlayer;\n closestEnemy = enemy;\n }\n }\n }\n });\n\n // Only inflate the closest enemy (and lock onto them)\n if (closestEnemy) {\n closestEnemy.startInflation();\n this.player.pumpTarget = closestEnemy;\n }\n }\n\n /**\n * Calculate perpendicular distance from point to line segment\n */\n pointToLineDistance(px, py, x1, y1, x2, y2) {\n const dx = x2 - x1;\n const dy = y2 - y1;\n const lengthSq = dx * dx + dy * dy;\n\n if (lengthSq === 0) {\n // Line is a point\n return Math.sqrt((px - x1) ** 2 + (py - y1) ** 2);\n }\n\n // Project point onto line, clamped to segment\n let t = ((px - x1) * dx + (py - y1) * dy) / lengthSq;\n t = Math.max(0, Math.min(1, t));\n\n const nearestX = x1 + t * dx;\n const nearestY = y1 + t * dy;\n\n return Math.sqrt((px - nearestX) ** 2 + (py - nearestY) ** 2);\n }\n\n /**\n * Check if enemy is in the direction the pump is pointing\n */\n isInPumpDirection(playerCenter, pumpEnd, enemyCenter) {\n const pumpDx = pumpEnd.x - playerCenter.x;\n const pumpDy = pumpEnd.y - playerCenter.y;\n const enemyDx = enemyCenter.x - playerCenter.x;\n const enemyDy = enemyCenter.y - playerCenter.y;\n\n // Dot product should be positive if enemy is in pump direction\n return pumpDx * enemyDx + pumpDy * enemyDy > 0;\n }\n\n /**\n * Check if Fygar fire hits the player\n */\n checkFireCollisions() {\n this.enemies.forEach((enemy) => {\n // Only Fygars can breathe fire\n if (enemy.type === ENEMY_TYPES.POOKA || !enemy.isFireActive()) {\n return;\n }\n\n const fireHitbox = enemy.getFireHitbox();\n if (!fireHitbox) return;\n\n // Check AABB collision between fire and player\n const playerHit = this.collisionSystem.checkAABB(\n fireHitbox.x,\n fireHitbox.y,\n fireHitbox.width,\n fireHitbox.height,\n this.player.x,\n this.player.y,\n TILE_SIZE,\n TILE_SIZE\n );\n\n if (playerHit) {\n this.playerHit('enemy');\n }\n });\n }\n\n /**\n * Check if bonus item should spawn\n */\n checkBonusSpawn() {\n // Spawn bonus after 2 rocks dropped\n if (this.droppedRocksCount === 2 && this.bonusItems.length === 0) {\n const bonusItem = this.levelManager.spawnBonusItem(\n this.bonusSpawnCount\n );\n if (bonusItem) {\n this.bonusItems.push(bonusItem);\n this.bonusSpawnCount++; // Increment for next spawn to get next prize in sequence\n }\n }\n }\n\n /**\n * Spawn a floating score display at the given position\n */\n spawnFloatingScore(points, x, y) {\n this.floatingScores.push({\n points,\n x,\n y,\n timer: 0,\n duration: 1000, // 1 second display\n });\n }\n\n /**\n * Update floating scores\n */\n updateFloatingScores(deltaTime) {\n this.floatingScores = this.floatingScores.filter((score) => {\n score.timer += deltaTime;\n return score.timer < score.duration;\n });\n }\n\n /**\n * Count dirt tiles in the grid\n */\n countDirtTiles() {\n return this.grid.countDirt();\n }\n\n /**\n * Handle player getting hit\n */\n playerHit(deathType = 'enemy') {\n if (this.player.isInvincible) return; // Skip if invincible\n\n // Start death animation\n this.player.startDeath(deathType);\n this.state = GAME_STATES.DYING;\n this.deathStartTime = Date.now();\n }\n\n /**\n * Update death animation state\n */\n updateDeath(deltaTime) {\n this.player.update(deltaTime, null, this.grid);\n\n // If player is smooshed, continue updating rocks so the attached rock can finish falling\n if (this.player.isSmooshed) {\n this.rocks.forEach((rock) => {\n rock.update(deltaTime, this.grid, null); // Pass null for player to skip trigger checks\n });\n\n // Sync player position with attached rock\n if (this.player.attachedToRock) {\n this.player.x = this.player.attachedToRock.x;\n this.player.y = this.player.attachedToRock.y;\n }\n }\n\n if (this.player.deathTimer >= DEATH.ANIMATION_DURATION) {\n // Death animation complete\n this.scoreManager.loseLife();\n\n if (this.scoreManager.lives <= 0) {\n this.gameOver();\n } else {\n this.startRespawn();\n }\n }\n }\n\n /**\n * Start respawn sequence\n */\n startRespawn() {\n this.state = GAME_STATES.RESPAWNING;\n this.respawnStartTime = Date.now();\n\n // Reset player position\n this.player = new Player(this.grid);\n this.player.isInvincible = true;\n\n // Reset enemy positions to their spawn tunnels\n this.enemies.forEach((enemy, index) => {\n const spawnPos = this.levelManager.getEnemySpawnPosition(index);\n enemy.x = spawnPos.x;\n enemy.y = spawnPos.y;\n\n // Reset all timers and state (includes Fygar fire state)\n enemy.resetTimers();\n\n // Reset additional state not covered by resetTimers\n enemy.inTunnel = true;\n enemy.state = 'roaming';\n enemy.isLastEnemy = false;\n enemy.isEscaping = false;\n enemy.hasEscaped = false;\n });\n\n // Reset rock states (cancel any pending falls triggered by player)\n this.rocks.forEach((rock) => {\n rock.reset();\n });\n }\n\n /**\n * Update respawn state\n */\n updateRespawn(deltaTime) {\n const elapsed = Date.now() - this.respawnStartTime;\n\n if (elapsed >= DEATH.RESPAWN_DELAY) {\n this.state = GAME_STATES.PLAYING;\n // Reset all timers when gameplay resumes\n this.resetAllTimers();\n }\n }\n\n /**\n * Level complete\n */\n levelComplete() {\n this.state = GAME_STATES.LEVEL_COMPLETE;\n const nextLevel = this.levelManager.currentLevel + 1;\n this.config.onLevelComplete(nextLevel - 1);\n\n // Wait 2 seconds then start next level\n setTimeout(() => {\n this.startLevel(nextLevel);\n this.state = GAME_STATES.PLAYING;\n }, 2000);\n }\n\n /**\n * Game over\n */\n gameOver() {\n this.state = GAME_STATES.GAME_OVER;\n this.config.onGameOver(this.scoreManager.score);\n if (this.scoreManager.score > this.scoreManager.highScore) {\n this.scoreManager.highScore = this.scoreManager.score;\n this.scoreManager.saveHighScore();\n }\n }\n\n /**\n * Pause the game\n */\n pause() {\n this.state = GAME_STATES.PAUSED;\n }\n\n /**\n * Resume the game\n */\n resume() {\n this.state = GAME_STATES.PLAYING;\n }\n\n /**\n * Render the game\n */\n render() {\n this.renderer.clear();\n\n // Draw grid (dirt/tunnels)\n this.renderer.drawGrid(this.grid, this.levelManager.currentLevel);\n\n // Draw rocks\n this.rocks.forEach((rock) => {\n this.renderer.drawRock(rock);\n });\n\n // Draw bonus items\n this.bonusItems.forEach((item) => {\n this.renderer.drawBonusItem(item, this.levelManager.currentLevel);\n });\n\n // Draw player\n if (this.player) {\n this.renderer.drawPlayer(this.player);\n }\n\n // Draw enemies\n this.enemies.forEach((enemy) => {\n this.renderer.drawEnemy(enemy);\n });\n\n // Draw floating scores\n this.renderer.drawFloatingScores(this.floatingScores);\n\n // Draw UI\n this.renderer.drawUI(this.scoreManager, this.levelManager);\n\n // Debug mode\n if (this.config.debug) {\n this.renderer.drawDebugInfo(this.player, this.enemies);\n }\n }\n\n /**\n * Render paused state\n */\n renderPaused() {\n if (this.inputManager.isKeyPressed('Escape')) {\n this.resume();\n return;\n }\n\n this.render();\n this.renderer.drawText('PAUSED', CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2, {\n size: 16,\n color: COLORS.TEXT_WHITE,\n align: 'center',\n });\n }\n\n /**\n * Render respawning state\n */\n renderRespawning() {\n this.render();\n this.renderer.renderRespawning();\n }\n\n /**\n * Render level complete\n */\n renderLevelComplete() {\n this.render();\n this.renderer.drawText(\n `LEVEL ${this.levelManager.currentLevel} `,\n CANVAS_WIDTH / 2,\n CANVAS_HEIGHT / 2 - 10,\n {\n size: 12,\n color: COLORS.TEXT_WHITE,\n align: 'center',\n }\n );\n this.renderer.drawText(\n 'COMPLETE',\n CANVAS_WIDTH / 2,\n CANVAS_HEIGHT / 2 + 10,\n {\n size: 12,\n color: COLORS.TEXT_WHITE,\n align: 'center',\n }\n );\n }\n\n /**\n * Render game over\n */\n renderGameOver() {\n this.renderer.forceClear();\n this.renderer.drawText(\n 'GAME OVER',\n CANVAS_WIDTH / 2,\n CANVAS_HEIGHT / 2 - 40,\n {\n size: 12,\n color: COLORS.TEXT_RED,\n align: 'center',\n }\n );\n this.renderer.drawText('SCORE', CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2, {\n size: 8,\n color: COLORS.TEXT_WHITE,\n align: 'center',\n });\n this.renderer.drawText(\n `${this.scoreManager.score}`,\n CANVAS_WIDTH / 2,\n CANVAS_HEIGHT / 2 + 20,\n {\n size: 10,\n color: COLORS.TEXT_RED,\n align: 'center',\n }\n );\n this.renderer.drawText(\n 'PRESS SPACE',\n CANVAS_WIDTH / 2,\n CANVAS_HEIGHT / 2 + 50,\n {\n size: 6,\n color: COLORS.TEXT_WHITE,\n align: 'center',\n }\n );\n this.renderer.drawText(\n 'TO RESTART',\n CANVAS_WIDTH / 2,\n CANVAS_HEIGHT / 2 + 65,\n {\n size: 6,\n color: COLORS.TEXT_WHITE,\n align: 'center',\n }\n );\n\n // Check if space is pressed to restart (only set up listener once)\n if (!this.restartListenerAdded) {\n this.restartListenerAdded = true;\n const restart = (e) => {\n if (\n e.code === 'Space' &&\n this.state === GAME_STATES.GAME_OVER\n ) {\n document.removeEventListener('keydown', restart);\n this.restartListenerAdded = false;\n this.startGame();\n }\n };\n document.addEventListener('keydown', restart);\n }\n }\n\n /**\n * Start the game\n */\n start() {\n this.init();\n }\n\n /**\n * Stop the game\n */\n stop() {\n if (this.animationFrameId) {\n cancelAnimationFrame(this.animationFrameId);\n this.animationFrameId = null;\n }\n this.inputManager.destroy();\n }\n}\n","import './styles.css';\nimport { Game } from './Game.js';\n\n// Export the Game class as the default export for npm package\nexport default Game;\n\n// Also export as named export\nexport { Game };\n\n// Auto-initialize if running in development mode (with #game element)\nif (typeof window !== 'undefined') {\n const gameContainer = document.getElementById('game');\n\n if (gameContainer) {\n // Development mode - auto-start the game\n const game = new Game({\n container: gameContainer,\n scale: window.devicePixelRatio,\n debug: false,\n onGameOver: (score) => {\n // console.log('Game Over! Final Score:', score);\n },\n onLevelComplete: (level) => {\n // console.log('Level', level, 'Complete!');\n },\n onScoreChange: (score) => {\n // Could update external UI here\n },\n });\n\n game.start();\n\n // Make game accessible globally for debugging\n window.digdugGame = game;\n }\n}\n"],"names":["HI_SCORE_KEY","GAME_STATES","TILE_TYPES","DIRECTIONS","PLAYER","ENEMY_TYPES","ENEMY","SCORES","COLORS","ROCK","LEVEL","DEATH","loadImage","url","resolve","reject","img","err","DIRT_COLORS","rgbStringToHSL","rgbStr","r","g","b","rN","gN","bN","max","min","h","s","l","d","getDirtGradient","level","finalIndex","DIRT_TOP","DIRT_MID","DIRT_LOW","DIRT_LOWEST","Renderer","config","spriteFiles","loadedCount","totalSprites","filename","spriteName","container","width","grid","currentLevel","currentGridState","ratio","dirtGradient","lower","upper","i","range","mixPercent","ctx","y","x","tile","px","py","depthRatio","shadowColor","lightColor","STEP","SPEC_SIZE","dy","dx","seedX","seedY","player","spriteAction","frameNumber","orientation","sprite","centerX","centerY","flipH","flipV","_a","spriteKey","progress","TILE","halfTile","endPoint","nozzleSprite","lineSprite","diffX","diffY","segments","enemy","state","inflateProgress","stage","spriteWidth","spriteHeight","direction","flashRate","offsetX","facingLeft","tileCount","fireX","fireY","rock","baseAlpha","flashFrequency","flashPhase","flashIntensity","alpha","item","floorIndex","localIndex","floatingScores","score","spriteInfo","drawX","drawY","scoreManager","levelManager","text","options","size","color","align","enemies","Grid","type","count","gx","gy","entitySize","newX","newY","corner","startX","endX","minX","maxX","startY","endY","minY","maxY","rocks","InputManager","e","code","pressed","CollisionSystem","x1","y1","w1","h1","x2","y2","w2","h2","entity","bonus","target","entity1","entity2","gx1","gy1","gx2","gy2","sx","sy","e2","Enemy","speed","deltaTime","currentlyInTunnel","currentlyInDirt","adjacentTunnel","atEscapeRow","blockGhostForEscape","distanceToPlayer","ESCAPE_CHASE_DISTANCE","CHASE_DISTANCE","ROAM_DISTANCE","ghostSpeed","targetX","targetY","distance","normalizedDx","normalizedDy","moveX","moveY","newDirection","canMoveX","canMoveY","perpDirs","dir","newPos","allDirs","clockwise","validDirections","preferredDirection","isCurrentHorizontal","isPreferredHorizontal","isPerpendicular","tileCenterX","tileCenterY","alignmentError","tolerance","currentIsValid","oppositeDirection","newDir","otherDir","viableDirections","directionToPrevTile","nonBacktrackDirections","directionsToConsider","primaryDirection","secondaryDirection","nonReverseDirections","fromGx","fromGy","toGx","toGy","adjacents","adj","pixelDistX","pixelDistY","currentGx","currentGy","currentDir","depth","nextGx","nextGy","oppositeDir","exits","forwardDirections","nonReverseViable","finalDirections","nonReverseValid","priorityOrder","checkX","checkY","diff","snapAmount","Pooka","Fygar","fygarGridX","playerGridX","gridY","fireWidth","Rock","hasDirtBelow","hasRockBelow","isBelow","playerTop","rockBottom","verticalOverlap","horizontalOverlap","playerLeft","playerRight","rockLeft","rockRight","horizontalClear","verticalClear","newGridY","hitBottom","enemyX","enemyY","LevelManager","levelNumber","extraRocks","targetRocks","minRocks","gridW","gridH","midX","noGoMin","noGoMax","enemyGridPos","candidates","padX","padY","j","isValidSpot","candidate","minPathDist","minEnemyDist","minRockDist","passes","pass","numEnemies","fygarRatio","numFygars","numPookas","pos","playerCX","playerCY","STRICT_PLAYER_DIST_SQ","RELAXED_PLAYER_DIST_SQ","getTunnelBounds","horizontal","length","actualLen","checkOverlap","rectA","rectB","spacing","attemptPlacement","spacingBuffer","minPlayerDistSq","pdx","pdy","newRect","valid","existing","enemyIndex","tunnel","offsetY","gridX","bonusIndex","validPositions","tooCloseToRock","rockGridX","rockGridY","ScoreManager","points","pixelY","dirtStartRow","quarterSize","relativeRow","enemyType","isHorizontalKill","depthQuarter","enemyCount","index","safeIndex","saved","Player","deathType","inputManager","canPump","spacePressed","inputDirection","newCenterX","newCenterY","newTile","isInContactWithDirt","nextLength","Game","startGame","currentTime","rawDelta","currentTile","dirtBefore","dirtAfter","tilesDigged","newRock","rocksBeforeFilter","playerCenter","pumpEnd","pumpLength","closestEnemy","closestDistance","enemyCenter","distToLine","distToPlayer","hitRadius","lengthSq","t","nearestX","nearestY","pumpDx","pumpDy","enemyDx","enemyDy","fireHitbox","bonusItem","spawnPos","nextLevel","restart","gameContainer","game"],"mappings":"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"}
|