// === APP BOOTSTRAP === const canvas = document.getElementById('game'); const ctx = canvas.getContext('2d'); const debugCanvas = document.getElementById('debug-canvas'); const debugCtx = debugCanvas.getContext('2d'); const menuOverlay = document.getElementById('menu-overlay'); const debugOverlay = document.getElementById('debug-overlay'); // Expose canvas contexts and DOM elements to window for other modules window.ctx = ctx; window.debugCtx = debugCtx; window.menuOverlay = menuOverlay; // Debug mode const debugMode = new URLSearchParams(window.location.search).has('debug'); let debugVisible = false; // Screen state let currentScreen = SCREEN_TITLE; let transitionProgress = 1; // 1 = fully visible, 0 = fully faded let transitioning = false; let targetScreen = null; // Game loop timing let lastTime = 0; let animFrameId = null; // === SCREEN MANAGEMENT === function switchScreen(screen) { if (transitioning) return; transitioning = true; targetScreen = screen; transitionProgress = 1; } function updateTransition(dt) { if (!transitioning) return; transitionProgress -= dt / TRANSITION_DURATION; if (transitionProgress <= 0) { transitionProgress = 1; transitioning = false; currentScreen = targetScreen; targetScreen = null; onScreenChange(currentScreen); } } function onScreenChange(screen) { menuOverlay.innerHTML = ''; menuOverlay.classList.remove('active'); inputSystem.clearKeys(); switch (screen) { case SCREEN_TITLE: renderTitleScreen(menuOverlay); menuOverlay.classList.add('active'); break; case SCREEN_DUNGEON_SELECT: renderDungeonSelect(menuOverlay, saveData, onDungeonSelected); menuOverlay.classList.add('active'); break; case SCREEN_PLAYING: if (!gameState.currentDungeon) return; loadDungeonRoom(gameState.currentDungeon, gameState.currentRoomIdx); break; case SCREEN_DEAD: renderDeathScreen(menuOverlay, gameState); menuOverlay.classList.add('active'); break; case SCREEN_DUNGEON_COMPLETE: renderDungeonCompleteScreen(menuOverlay, gameState); menuOverlay.classList.add('active'); break; case SCREEN_GAME_COMPLETE: renderGameCompleteScreen(menuOverlay, gameState); menuOverlay.classList.add('active'); break; case SCREEN_INVENTORY: renderInventoryOverlay(menuOverlay, gameState); menuOverlay.classList.add('active'); break; } } function onDungeonSelected(dungeonIdx) { gameState.currentDungeon = dungeonIdx; gameState.currentRoomIdx = 0; gameState.dungeonRooms = generateDungeon(DUNGEONS[dungeonIdx]); gameState.player.hp = gameState.player.maxHp; gameState.player.invincibleTimer = 1000; switchScreen(SCREEN_PLAYING); } // === DEBUG === function toggleDebug() { debugVisible = !debugVisible; debugCanvas.classList.toggle('visible', debugVisible); debugOverlay.classList.toggle('visible', debugVisible); } function renderDebug() { if (!debugVisible) return; debugCtx.clearRect(0, 0, CANVAS_W, CANVAS_H); debugCtx.fillStyle = '#0F0'; debugCtx.font = '10px monospace'; let y = 10; const lines = []; lines.push(`Screen: ${currentScreen}`); lines.push(`Dungeon: ${gameState.currentDungeon !== null ? DUNGEONS[gameState.currentDungeon].name : 'none'}`); lines.push(`Room: ${gameState.currentRoomIdx}/${gameState.dungeonRooms ? gameState.dungeonRooms.length - 1 : 0}`); lines.push(`Player HP: ${gameState.player.hp}/${gameState.player.maxHp}`); lines.push(`Weapons: ${gameState.player.weapons.map(w => w.name).join(', ')}`); lines.push(`Active: ${gameState.player.activeWeapon ? gameState.player.activeWeapon.name : 'none'}`); lines.push(`Enemies: ${gameState.enemies.length}`); lines.push(`Particles: ${gameState.particles ? gameState.particles.length : 0}`); if (gameState.currentDungeon !== null && gameState.dungeonRooms && gameState.currentRoom < gameState.dungeonRooms.length) { const room = gameState.currentRoom; lines.push(`Room type: ${room.type}`); lines.push(`Room enemies: ${room.enemies.length}`); lines.push(`Exit open: ${gameState.exitOpen}`); } for (const line of lines) { debugCtx.fillText(line, 5, y); y += 12; } // Draw collision boxes on debug canvas if (currentScreen === SCREEN_PLAYING && gameState.layout) { debugCtx.strokeStyle = 'rgba(0,255,0,0.3)'; debugCtx.lineWidth = 0.5; for (let y2 = 0; y2 < ROWS; y2++) { for (let x = 0; x < COLS; x++) { if (gameState.layout[y2][x] === TILE_TYPES.FLOOR) { debugCtx.strokeRect(x * TILE, y2 * TILE, TILE, TILE); } } } } } // === CONSOLE API === window._genDungeon = function (id) { const dungeon = DUNGEONS[id]; if (!dungeon) { console.error(`Dungeon ${id} not found`); return null; } const rooms = generateDungeon(dungeon); console.log(`Generated ${rooms.length} rooms for "${dungeon.name}" (seed: ${dungeon.seed})`); console.table(rooms.map((r, i) => ({ idx: i, type: r.type, enemies: r.enemies.length, puzzles: r.puzzles ? r.puzzles.length : 0, items: r.chests ? r.chests.length : 0, hasBoss: r.boss !== undefined }))); return rooms; }; window.spawnEnemy = function (type) { if (currentScreen !== SCREEN_PLAYING) { console.error('Not in game'); return; } const def = ENEMY_TYPES.find(e => e.name === type || e.id === type); if (!def) { console.error(`Enemy type "${type}" not found`); return; } const enemy = createEnemyInstance(def, gameState.player.x + 2, gameState.player.y); gameState.enemies.push(enemy); console.log(`Spawned ${def.name} at (${enemy.x}, ${enemy.y})`); }; window.giveWeapon = function (name) { if (currentScreen !== SCREEN_PLAYING) { console.error('Not in game'); return; } const def = WEAPONS.find(w => w.name === name); if (!def) { console.error(`Weapon "${name}" not found`); return; } if (!gameState.player.weapons.find(w => w.name === def.name)) { gameState.player.weapons.push({ ...def }); console.log(`Added ${def.name} to inventory`); } else { console.log(`${def.name} already in inventory`); } }; window.teleportToRoom = function (idx) { if (currentScreen !== SCREEN_PLAYING || !gameState.dungeonRooms) { console.error('Not in game'); return; } if (idx < 0 || idx >= gameState.dungeonRooms.length) { console.error(`Room index ${idx} out of range [0, ${gameState.dungeonRooms.length - 1}]`); return; } gameState.currentRoomIdx = idx; loadDungeonRoom(gameState.currentDungeon, idx); console.log(`Teleported to room ${idx}`); }; window.dumpState = function () { console.log('=== Game State ==='); console.log('Screen:', currentScreen); console.log('Dungeon:', gameState.currentDungeon !== null ? DUNGEONS[gameState.currentDungeon].name : 'none'); console.log('Room:', gameState.currentRoomIdx); console.log('Player:', JSON.stringify(gameState.player)); console.log('Enemies:', gameState.enemies.length); console.log('Particles:', gameState.particles ? gameState.particles.length : 0); }; // Keyboard shortcut for debug document.addEventListener('keydown', (e) => { if (e.code === 'F1') { e.preventDefault(); toggleDebug(); } if (e.code === 'F2' && currentScreen === SCREEN_PLAYING) { e.preventDefault(); if (gameState.exitOpen || gameState.currentRoomIdx >= (gameState.dungeonRooms ? gameState.dungeonRooms.length - 1 : 0)) { if (gameState.currentRoomIdx >= (gameState.dungeonRooms ? gameState.dungeonRooms.length - 1 : 0)) { completeDungeon(); } } else { gameState.exitOpen = true; showMessage('Debug: Door opened'); tryExit(); } } if (e.code === 'F3' && currentScreen === SCREEN_PLAYING) { e.preventDefault(); gameState.player.invincible = true; console.log('God mode enabled'); } if (e.code === 'F4' && currentScreen === SCREEN_PLAYING) { e.preventDefault(); spawnEnemy('goblin'); } }); // === MAIN GAME LOOP === function gameLoop(timestamp) { const dt = Math.min(timestamp - lastTime, 50); // Cap delta to prevent spiral of death lastTime = timestamp; // Update transition updateTransition(dt); // Screen-specific updates if (currentScreen === SCREEN_PLAYING && !transitioning) { updateGame(dt); } // Update input inputSystem.update(dt); // Render render(); // Debug overlay renderDebug(); animFrameId = requestAnimationFrame(gameLoop); } function updateGame(dt) { // Update player timers if (gameState.player.moveTimer > 0) gameState.player.moveTimer -= dt; if (gameState.player.attackTimer > 0) gameState.player.attackTimer -= dt; if (gameState.player.attackAnim > 0) gameState.player.attackAnim -= dt; if (gameState.player.hitTimer > 0) gameState.player.hitTimer -= dt; if (gameState.player.invincibleTimer > 0) gameState.player.invincibleTimer -= dt; else if (!gameState.player.invincible) gameState.player.invincibleTimer = 0; if (gameState.messageTimer > 0) gameState.messageTimer -= dt; // Update particles if (gameState.particles) { for (let i = gameState.particles.length - 1; i >= 0; i--) { const p = gameState.particles[i]; p.life -= dt; if (p.life <= 0) { gameState.particles.splice(i, 1); continue; } p.x += p.vx * (dt / 1000); p.y += p.vy * (dt / 1000); p.vy += (p.gravity || 0) * (dt / 1000); p.alpha = (p.life / p.maxLife) * (p.maxAlpha || 1); } if (gameState.particles.length > MAX_PARTICLES) { gameState.particles.splice(0, gameState.particles.length - MAX_PARTICLES); } } // Handle movement if (gameState.player.moveTimer <= 0 && !gameState.player.invincibleTimer) { const move = inputSystem.getMove(); if (move) { tryMove(move.dx, move.dy); gameState.player.moveTimer = MOVE_COOLDOWN; } } // Handle actions if (inputSystem.getAction('attack') && gameState.player.attackTimer <= 0) { tryAttack(); } if (inputSystem.getAction('interact')) { tryInteract(); } if (inputSystem.getAction('menu') && currentScreen === SCREEN_PLAYING) { switchScreen(SCREEN_INVENTORY); } if (inputSystem.getAction('back') && currentScreen === SCREEN_INVENTORY) { switchScreen(SCREEN_PLAYING); } // Update enemies updateEnemies(dt); // Check death if (gameState.player.hp <= 0 && !gameState.player.invincible) { gameState.player.hp = 0; switchScreen(SCREEN_DEAD); } } // === RENDERING === function render() { ctx.clearRect(0, 0, CANVAS_W, CANVAS_H); // Draw black background ctx.fillStyle = '#000'; ctx.fillRect(0, 0, CANVAS_W, CANVAS_H); if (currentScreen === SCREEN_PLAYING) { renderGame(); } // Draw transition overlay if (transitioning || transitionProgress < 1) { ctx.fillStyle = `rgba(0,0,0,${1 - transitionProgress})`; ctx.fillRect(0, 0, CANVAS_W, CANVAS_H); } } function renderGame() { if (!gameState.layout) return; const theme = DUNGEONS[gameState.currentDungeon] ? DUNGEONS[gameState.currentDungeon].theme : 'stone'; const themeData = THEMES[theme] || THEMES.stone; renderFloor(themeData); renderTargets(); renderWalls(themeData); renderTorches(themeData); renderDoors(); renderSoftwalls(); renderLava(); renderWater(); renderTraps(); renderLevers(); renderSwitches(); renderRocks(); renderChests(); renderItems(); renderEnemies(); if (gameState.currentBoss) renderBoss(gameState.currentBoss); renderPlayer(); renderAttackAnim(); renderParticles(); // Hit flash overlay if (gameState.player.hitTimer > 0) { ctx.fillStyle = COLORS.hitFlash; ctx.globalAlpha = gameState.player.hitTimer / HIT_FLASH; ctx.fillRect(0, 0, CANVAS_W, CANVAS_H); ctx.globalAlpha = 1; } renderMessages(); renderHUD(); } // === INITIALIZATION === function init() { // Initialize subsystems initKeyboard(); initActions(); initGameState(); loadSaveData(); // Expose inputSystem and gameState to window for other modules window.inputSystem = inputSystem; window.gameState = gameState; // Start with title screen currentScreen = SCREEN_TITLE; menuOverlay.innerHTML = ''; renderTitleScreen(menuOverlay); menuOverlay.classList.add('active'); // Start game loop lastTime = performance.now(); requestAnimationFrame(gameLoop); console.log('Zelda Dungeon Quest initialized'); console.log('Debug mode:', debugMode ? 'ON (?debug=1)' : 'OFF'); console.log('Controls: F1=debug, F2=skip room, F3=god mode, F4=spawn enemy'); console.log('Console API: generateDungeon(id), spawnEnemy(type), giveWeapon(name), teleportToRoom(idx), dumpState()'); } // Boot! init();