diff --git a/src/data/cards.js b/src/data/cards.js index 7d9bedc..c926713 100644 --- a/src/data/cards.js +++ b/src/data/cards.js @@ -331,7 +331,7 @@ export const CARDS = { ctx.draw(1); ctx.log(`Clean code heals the soul! Heal ${healAmount} HP and draw 1.`); } - }, + } }; diff --git a/src/engine/battle.js b/src/engine/battle.js index a4c4ae1..e969437 100644 --- a/src/engine/battle.js +++ b/src/engine/battle.js @@ -74,8 +74,14 @@ export function playCard(ctx, handIndex) { return; } - card.effect(ctx); - card._used = true; + try { + card.effect(ctx); + card._used = true; + } catch (error) { + console.error('Card effect error:', error, 'Card:', card); + ctx.log(`Error playing ${card.name || 'Unknown card'}: ${error.message}`); + return; + } if (card.type !== "power") { @@ -105,14 +111,29 @@ export function enemyTurn(ctx) { if (e.weak > 0) dmg = Math.floor(dmg * 0.75); applyDamage(ctx, ctx.player, dmg, `${e.name} attacks`); } else if (e.intent.type === "block") { - ENEMIES[e.id].onBlock?.(ctx, e.intent.value); - e.block += e.intent.value; - ctx.log(`${e.name} defends and gains ${e.intent.value} block.`); + try { + ENEMIES[e.id].onBlock?.(ctx, e.intent.value); + e.block += e.intent.value; + ctx.log(`${e.name} defends and gains ${e.intent.value} block.`); + } catch (error) { + console.error('Enemy block effect error:', error, 'Enemy:', e.id); + ctx.log(`${e.name} tries to defend but fumbles!`); + } } else if (e.intent.type === "debuff") { - ENEMIES[e.id].onDebuff?.(ctx, e.intent.value); - ctx.log(`${e.name} casts a debuffing spell.`); + try { + ENEMIES[e.id].onDebuff?.(ctx, e.intent.value); + ctx.log(`${e.name} casts a debuffing spell.`); + } catch (error) { + console.error('Enemy debuff effect error:', error, 'Enemy:', e.id); + ctx.log(`${e.name} tries to cast a spell but it fizzles!`); + } } else if (e.intent.type === "heal") { - ENEMIES[e.id].onHeal?.(ctx, e.intent.value); + try { + ENEMIES[e.id].onHeal?.(ctx, e.intent.value); + } catch (error) { + console.error('Enemy heal effect error:', error, 'Enemy:', e.id); + ctx.log(`${e.name} tries to heal but something goes wrong!`); + } } @@ -123,7 +144,16 @@ export function enemyTurn(ctx) { if (ctx.player.hp <= 0) { ctx.onLose(); return; } e.turn++; - e.intent = ENEMIES[e.id].ai(e.turn); + try { + e.intent = ENEMIES[e.id].ai(e.turn); + if (!e.intent || !e.intent.type) { + throw new Error('Invalid enemy intent returned'); + } + } catch (error) { + console.error('Enemy AI error:', error, 'Enemy:', e.id); + ctx.log(`Enemy AI malfunction! ${e.name} does nothing this turn.`); + e.intent = { type: "block", value: 0 }; // Safe fallback + } startPlayerTurn(ctx); } diff --git a/src/main.js b/src/main.js index a94b652..b5d4bd0 100644 --- a/src/main.js +++ b/src/main.js @@ -18,7 +18,7 @@ const root = { enemy: null, log(m) { this.logs.push(m); this.logs = this.logs.slice(-200); }, - render() { renderBattle(this); }, + async render() { await renderBattle(this); }, play(i) { playCard(this, i); }, showDamageNumber: showDamageNumber, end() { endTurn(this); }, @@ -44,7 +44,7 @@ const root = { } else if (node.kind === "event") { renderEvent(this); } else if (node.kind === "start") { - renderMap(this); + await renderMap(this); } } }, @@ -66,10 +66,10 @@ const root = { await renderWin(this); return; } - renderMap(this); + await renderMap(this); }, - takeReward(idx) { + async takeReward(idx) { const card = this._pendingChoices[idx]; if (card) { this.player.deck.push(card.id); @@ -77,12 +77,12 @@ const root = { } this._pendingChoices = null; this.save(); - renderMap(this); + await renderMap(this); }, - skipReward() { + async skipReward() { this._pendingChoices = null; this.save(); - renderMap(this); + await renderMap(this); }, async onWin() { @@ -107,7 +107,7 @@ const root = { this.completedNodes = []; this.log(`🎉 Act ${this.currentAct === "act2" ? "II" : "I"} Complete! Advancing to the next challenge...`); this.save(); - renderMap(this); + await renderMap(this); } else { // Final victory this.save(); // Save progress before clearing on victory @@ -140,11 +140,11 @@ const root = { renderRelicSelection(this); }, - selectStartingRelic(relicId) { + async selectStartingRelic(relicId) { attachRelics(this, [relicId]); this.log(`Selected starting relic: ${relicId}`); this.save(); - renderMap(this); + await renderMap(this); }, save() { @@ -170,16 +170,57 @@ const root = { const saveData = localStorage.getItem('birthday-spire-save'); if (saveData) { const data = JSON.parse(saveData); - this.player = data.player; - this.nodeId = data.nodeId; - this.currentAct = data.currentAct || "act1"; + + // Validate essential save data + if (!data || typeof data !== 'object') { + throw new Error('Invalid save data format'); + } + + if (!data.player || typeof data.player !== 'object') { + throw new Error('Invalid player data'); + } + + if (!data.nodeId || typeof data.nodeId !== 'string') { + throw new Error('Invalid node ID'); + } + + // Validate current act and ensure map exists + const actId = data.currentAct || "act1"; + if (!MAPS[actId]) { + console.warn(`Invalid act ${actId}, falling back to act1`); + this.currentAct = "act1"; + } else { + this.currentAct = actId; + } + this.map = MAPS[this.currentAct]; - this.relicStates = data.relicStates || []; - this.completedNodes = data.completedNodes || []; - this.logs = data.logs || []; - this._battleInProgress = data.battleInProgress || false; - + // Validate that the nodeId exists in the current map + const nodeExists = this.map.nodes.some(n => n.id === data.nodeId); + if (!nodeExists) { + console.warn(`Node ${data.nodeId} not found in ${this.currentAct}, starting from beginning`); + this.nodeId = this.map.nodes.find(n => n.kind === "start").id; + } else { + this.nodeId = data.nodeId; + } + + // Validate player data has required fields + if (typeof data.player.hp !== 'number' || data.player.hp < 0) { + throw new Error('Invalid player HP'); + } + if (typeof data.player.maxHp !== 'number' || data.player.maxHp <= 0) { + throw new Error('Invalid player max HP'); + } + if (!Array.isArray(data.player.deck)) { + throw new Error('Invalid player deck'); + } + + this.player = data.player; + this.relicStates = Array.isArray(data.relicStates) ? data.relicStates : []; + this.completedNodes = Array.isArray(data.completedNodes) ? data.completedNodes : []; + this.logs = Array.isArray(data.logs) ? data.logs : []; + this._battleInProgress = Boolean(data.battleInProgress); + this.restoreCardEffects(); this.log('Game loaded from save.'); @@ -187,6 +228,8 @@ const root = { } } catch (e) { console.warn('Failed to load game:', e); + console.warn('Clearing corrupted save data'); + this.clearSave(); } return false; }, @@ -227,8 +270,8 @@ function pickCards(n) { function shuffle(a) { for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1));[a[i], a[j]] = [a[j], a[i]] } return a; } const _createBattle = root.go.bind(root); -root.go = function(nextId) { - _createBattle(nextId); +root.go = async function(nextId) { + await _createBattle(nextId); const node = this.map.nodes.find(n => n.id === this.nodeId); if (node && (node.kind === "battle" || node.kind === "elite" || node.kind === "boss")) { const ctx = makeBattleContext(this); @@ -304,11 +347,11 @@ async function initializeGame() { return; default: console.warn(`Unknown screen: ${screenParam}. Loading normal game.`); - loadNormalGame(); + await loadNormalGame(); return; } } else { - loadNormalGame(); + await loadNormalGame(); } } @@ -406,21 +449,21 @@ function showCountdown(birthday) { }, 1000); } -function loadNormalGame() { +async function loadNormalGame() { const hasLoadedData = root.load(); if (hasLoadedData) { // If we were in a battle, resume it if (root._battleInProgress) { const node = root.map.nodes.find(n => n.id === root.nodeId); if (node && (node.kind === "battle" || node.kind === "elite" || node.kind === "boss")) { - root.go(root.nodeId); + await root.go(root.nodeId); } else { // Battle state inconsistent, go to map root._battleInProgress = false; - renderMap(root); + await renderMap(root); } } else { - renderMap(root); + await renderMap(root); } } else { root.reset();