2026-07-03 01:08:48 +00:00

406 lines
12 KiB
JavaScript

// === 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();