diff --git a/assets/sounds/played-card.mp3 b/assets/sounds/played-card.mp3 new file mode 100644 index 0000000..4564ffb Binary files /dev/null and b/assets/sounds/played-card.mp3 differ diff --git a/assets/sounds/would-you-like-to-play.mp3 b/assets/sounds/would-you-like-to-play.mp3 new file mode 100644 index 0000000..a9b5c6d Binary files /dev/null and b/assets/sounds/would-you-like-to-play.mp3 differ diff --git a/src/data/cards.js b/src/data/cards.js index c926713..5465f27 100644 --- a/src/data/cards.js +++ b/src/data/cards.js @@ -1,5 +1,3 @@ - - export const CARDS = { strike: { id: "strike", name: "Strike", cost: 1, type: "attack", text: "Deal 6.", target: "enemy", @@ -158,7 +156,6 @@ export const CARDS = { id: "code_review", name: "Code Review", cost: 1, type: "skill", text: "Look at top 3 cards. Put 1 in hand, rest on bottom of deck.", art: "Monk_24.png", effect: (ctx) => { - ctx.draw(1); ctx.log("Code review reveals useful insights. You draw a card."); } @@ -338,7 +335,7 @@ export const CARDS = { export const STARTER_DECK = [ "strike", "strike", "defend", "defend", "segfault", "coffee_rush", "skill_issue", "git_commit", - "stack_trace", "raw_dog" + "stack_trace", "stack_trace" ]; export const CARD_POOL = [ diff --git a/src/data/enemies.js b/src/data/enemies.js index 66745ad..a7e11f0 100644 --- a/src/data/enemies.js +++ b/src/data/enemies.js @@ -22,8 +22,15 @@ export const ENEMIES = { ctx.log("Codegirl resolves the merge conflict and heals 8 HP!"); } }, + lowkeyabu: { + id: "lowkeyabu", name: "LowKeyAbu", maxHp: 85, + avatar: "assets/avatars/7.png", // Powerful demon/witch + background: "assets/backgrounds/castle.png", // Repeat background + ai: (turn) => turn % 3 === 1 ? { type: "debuff", value: 1 } : { type: "attack", value: 10 }, + onDebuff: (ctx) => ctx.applyVulnerable(ctx.player, 1) + }, nightshadedude: { - id: "nightshadedude", name: "Nightshadedude", maxHp: 85, + id: "nightshadedude", name: "Nightshadedude", maxHp: 120, avatar: "assets/avatars/11.png", // Powerful demon/witch background: "assets/backgrounds/dead forest.png", // Repeat background ai: (turn) => turn % 3 === 1 ? { type: "debuff", value: 1 } : { type: "attack", value: 14 }, @@ -57,28 +64,28 @@ export const ENEMIES = { }, // ACT 2 ENEMIES - Harder versions - senior_dev: { - id: "senior_dev", name: "Senior Dev", maxHp: 65, + teej: { + id: "teej", name: "Teej", maxHp: 65, avatar: "assets/avatars/elite_ts_demon.png", background: "assets/backgrounds/castle.png", ai: (turn) => turn % 3 === 0 ? { type: "debuff", value: 2 } : { type: "attack", value: turn % 2 === 0 ? 12 : 14 }, onDebuff: (ctx) => ctx.applyWeak(ctx.player, 2) }, - tech_lead: { - id: "tech_lead", name: "Tech Lead", maxHp: 80, + begin: { + id: "begin", name: "Begin", maxHp: 80, avatar: "assets/avatars/infinite_loop.png", background: "assets/backgrounds/dead forest.png", ai: (turn) => (turn % 2 === 0) ? { type: "attack", value: 16 } : { type: "block", value: 12 } }, - code_reviewer: { - id: "code_reviewer", name: "Code Reviewer", maxHp: 70, + adam: { + id: "adam", name: "Adam", maxHp: 70, avatar: "assets/avatars/chat_gremlin.png", background: "assets/backgrounds/terrace.png", ai: (turn) => turn % 4 === 0 ? { type: "debuff", value: 1 } : { type: "attack", value: 13 }, - onDebuff: (ctx) => { ctx.applyVulnerable(ctx.player, 1); ctx.log("Code Reviewer finds bugs in your logic!"); } + onDebuff: (ctx) => { ctx.applyVulnerable(ctx.player, 1); ctx.log("Adam finds bugs in your logic!"); } }, - scrum_master: { - id: "scrum_master", name: "Scrum Master", maxHp: 90, + david: { + id: "david", name: "David", maxHp: 90, avatar: "assets/avatars/js_blob.png", background: "assets/backgrounds/castle.png", ai: (turn) => { @@ -87,10 +94,10 @@ export const ENEMIES = { if (cyc === 1) return { type: "attack", value: 11 }; return { type: "debuff", value: 1 }; }, - onDebuff: (ctx) => { ctx.flags.nextTurnEnergyPenalty = (ctx.flags.nextTurnEnergyPenalty || 0) + 1; ctx.log("Scrum Master schedules another meeting! Lose 1 energy next turn."); } + onDebuff: (ctx) => { ctx.flags.nextTurnEnergyPenalty = (ctx.flags.nextTurnEnergyPenalty || 0) + 1; ctx.log("David schedules another meeting! Lose 1 energy next turn."); } }, - architect: { - id: "architect", name: "The Architect", maxHp: 150, + dax: { + id: "dax", name: "Dax", maxHp: 150, avatar: "assets/avatars/bug_404.png", background: "assets/backgrounds/throne room.png", ai: (turn) => { @@ -101,7 +108,23 @@ export const ENEMIES = { if (cyc === 4) return { type: "attack", value: 30 }; return { type: "attack", value: 20 }; }, - onDebuff: (ctx) => { ctx.applyWeak(ctx.player, 2); ctx.applyVulnerable(ctx.player, 1); ctx.log("The Architect redesigns your entire approach!"); }, - onBlock: (ctx) => { ctx.enemy.hp = Math.min(ctx.enemy.maxHp, ctx.enemy.hp + 12); ctx.log("The Architect refactors and optimizes, healing 12 HP!"); } + onDebuff: (ctx) => { ctx.applyWeak(ctx.player, 2); ctx.applyVulnerable(ctx.player, 1); ctx.log("Dax redesigns your entire approach!"); }, + onBlock: (ctx) => { ctx.enemy.hp = Math.min(ctx.enemy.maxHp, ctx.enemy.hp + 12); ctx.log("Dax refactors and optimizes, healing 12 HP!"); } + }, + taylor: { + id: "taylor", name: "Taylor Otwell", maxHp: 150, + avatar: "assets/avatars/bug_404.png", + background: "assets/backgrounds/throne room.png", + ai: (turn) => { + const cyc = turn % 5; + if (cyc === 1) return { type: "debuff", value: 2 }; + if (cyc === 2) return { type: "attack", value: 25 }; + if (cyc === 3) return { type: "block", value: 15 }; + if (cyc === 4) return { type: "attack", value: 30 }; + return { type: "attack", value: 20 }; + }, + onDebuff: (ctx) => { ctx.applyWeak(ctx.player, 2); ctx.applyVulnerable(ctx.player, 1); ctx.log("Taylor redesigns your entire approach!"); }, + onBlock: (ctx) => { ctx.enemy.hp = Math.min(ctx.enemy.maxHp, ctx.enemy.hp + 12); ctx.log("Taylor refactors and optimizes, healing 12 HP!"); } } + }; diff --git a/src/data/maps.js b/src/data/maps.js index 26c5f07..9b772c4 100644 --- a/src/data/maps.js +++ b/src/data/maps.js @@ -14,27 +14,27 @@ export const MAPS = { { id: "n9", kind: "rest", next: ["n11"], x: 350, y: 330 }, { id: "n10", kind: "shop", next: ["n11"], x: 650, y: 330 }, { id: "n11", kind: "battle", enemy: "lithium", next: ["n12"], x: 500, y: 280 }, - { id: "n12", kind: "elite", enemy: "nightshadedude", next: ["n13"], x: 500, y: 205 }, + { id: "n12", kind: "elite", enemy: "lowkeyabu", next: ["n13"], x: 500, y: 205 }, { id: "n13", kind: "rest", next: ["n14"], x: 500, y: 125 }, - { id: "n14", kind: "boss", enemy: "teej", next: [], x: 500, y: 40 }, + { id: "n14", kind: "boss", enemy: "nightshadedude", next: [], x: 500, y: 40 }, ] }, act2: { id: "act2", name: "Birthday Spire — Act II: The Corporate Ladder", nodes: [ { id: "a2n1", kind: "start", next: ["a2n2", "a2n3"], x: 500, y: 760 }, - { id: "a2n2", kind: "battle", enemy: "senior_dev", next: ["a2n4", "a2n5"], x: 400, y: 680 }, - { id: "a2n3", kind: "event", next: ["a2n5"], x: 600, y: 680 }, - { id: "a2n4", kind: "shop", next: ["a2n6"], x: 300, y: 600 }, - { id: "a2n5", kind: "battle", enemy: "tech_lead", next: ["a2n6", "a2n7"], x: 500, y: 600 }, - { id: "a2n6", kind: "battle", enemy: "code_reviewer", next: ["a2n8"], x: 400, y: 520 }, - { id: "a2n7", kind: "rest", next: ["a2n8"], x: 600, y: 520 }, - { id: "a2n8", kind: "battle", enemy: "scrum_master", next: ["a2n9", "a2n10"], x: 500, y: 440 }, - { id: "a2n9", kind: "event", next: ["a2n11"], x: 350, y: 360 }, - { id: "a2n10", kind: "shop", next: ["a2n11"], x: 650, y: 360 }, - { id: "a2n11", kind: "elite", enemy: "senior_dev", next: ["a2n12"], x: 500, y: 280 }, - { id: "a2n12", kind: "rest", next: ["a2n13"], x: 500, y: 200 }, - { id: "a2n13", kind: "boss", enemy: "architect", next: [], x: 500, y: 120 }, + { id: "a2n2", kind: "battle", enemy: "teej", next: ["a2n5"], x: 300, y: 695 }, + { id: "a2n3", kind: "event", next: ["a2n5"], x: 650, y: 695 }, + { id: "a2n5", kind: "battle", enemy: "begin", next: ["a2n4", "a2n7"], x: 500, y: 600 }, + { id: "a2n4", kind: "shop", next: ["a2n6"], x: 350, y: 525 }, + { id: "a2n7", kind: "rest", next: ["a2n6"], x: 650, y: 525 }, + { id: "a2n6", kind: "battle", enemy: "adam", next: ["a2n8"], x: 500, y: 460 }, + { id: "a2n8", kind: "battle", enemy: "david", next: ["a2n9", "a2n10"], x: 500, y: 360 }, + { id: "a2n9", kind: "rest", next: ["a2n11"], x: 350, y: 320 }, + { id: "a2n10", kind: "shop", next: ["a2n11"], x: 650, y: 320 }, + { id: "a2n11", kind: "elite", enemy: "dax", next: ["a2n12"], x: 500, y: 250 }, + { id: "a2n12", kind: "rest", next: ["a2n13"], x: 500, y: 140 }, + { id: "a2n13", kind: "boss", enemy: "taylor", next: [], x: 500, y: 40 }, ] } }; diff --git a/src/main.js b/src/main.js index b5d4bd0..3020457 100644 --- a/src/main.js +++ b/src/main.js @@ -28,7 +28,7 @@ const root = { this.nodeId = nextId; // Always set nodeId (needed for battle logic) const node = this.map.nodes.find(n => n.id === nextId); if (!node) return; - + if (node.kind === "battle" || node.kind === "elite" || node.kind === "boss") { this._battleInProgress = true; @@ -53,7 +53,7 @@ const root = { if (this.nodeId && !this.completedNodes.includes(this.nodeId)) { this.completedNodes.push(this.nodeId); } - + const node = this.map.nodes.find(n => n.id === this.nodeId); if (node.kind === "battle" || node.kind === "elite") { @@ -79,10 +79,10 @@ const root = { this.save(); await renderMap(this); }, - async skipReward() { - this._pendingChoices = null; + async skipReward() { + this._pendingChoices = null; this.save(); - await renderMap(this); + await renderMap(this); }, async onWin() { @@ -91,12 +91,12 @@ const root = { const goldReward = Math.floor(Math.random() * 20) + 15; // 15-35 gold this.player.gold = (this.player.gold || 0) + goldReward; this.log(`+${goldReward} gold`); - + this._battleInProgress = false; - + const node = this.map.nodes.find(n => n.id === this.nodeId); - if (node.kind === "boss") { + if (node.kind === "boss") { // Check if there's a next act const nextAct = this.currentAct === "act1" ? "act2" : null; if (nextAct && MAPS[nextAct]) { @@ -112,20 +112,19 @@ const root = { // Final victory this.save(); // Save progress before clearing on victory this.clearSave(); // Clear save on victory - await renderWin(this); + await renderWin(this); } } else { - this.save(); this.afterNode(); } }, - async onLose() { + async onLose() { this._battleInProgress = false; this.clearSave(); // Clear save on defeat - await renderLose(this); + await renderLose(this); }, reset() { @@ -142,7 +141,6 @@ const root = { async selectStartingRelic(relicId) { attachRelics(this, [relicId]); - this.log(`Selected starting relic: ${relicId}`); this.save(); await renderMap(this); }, @@ -170,20 +168,20 @@ const root = { const saveData = localStorage.getItem('birthday-spire-save'); if (saveData) { const data = JSON.parse(saveData); - + // 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]) { @@ -192,9 +190,9 @@ const root = { } else { this.currentAct = actId; } - + this.map = MAPS[this.currentAct]; - + // Validate that the nodeId exists in the current map const nodeExists = this.map.nodes.some(n => n.id === data.nodeId); if (!nodeExists) { @@ -203,7 +201,7 @@ const root = { } 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'); @@ -214,15 +212,15 @@ const root = { 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.'); return true; } @@ -255,7 +253,7 @@ const root = { clearSave() { localStorage.removeItem('birthday-spire-save'); }, - + // Clear any old saves with outdated card IDs clearOldSaves() { localStorage.removeItem('birthday-spire-save'); @@ -295,21 +293,21 @@ async function initializeGame() { const urlParams = new URLSearchParams(window.location.search); const screenParam = urlParams.get('screen'); const dev = urlParams.get('dev'); - + // Check if it's ThePrimeagen's birthday yet (September 9, 2025) // Skip countdown if ?dev=true is in URL const now = new Date(); const birthday = new Date('2025-09-09T00:00:00'); - - + + if (now < birthday && dev !== 'true') { showCountdown(birthday); return; } - + if (screenParam) { setupMockData(); - + switch (screenParam.toLowerCase()) { case 'victory': case 'win': @@ -364,9 +362,9 @@ function setupMockData() { root.player.hand = ['strike', 'coffee_rush', 'raw_dog']; root.player.draw = ['defend', 'segfault']; root.player.discard = ['virgin']; - + attachRelics(root, ['coffee_thermos', 'cpp_compiler']); - + // Test Act 2 if ?act2=true is in URL const urlParams = new URLSearchParams(window.location.search); if (urlParams.get('act2') === 'true') { @@ -424,24 +422,24 @@ function showCountdown(birthday) { `; - + // Start the countdown timer const timer = setInterval(() => { const now = new Date(); const timeLeft = birthday - now; - + if (timeLeft <= 0) { clearInterval(timer); // Birthday reached! Reload to show the game window.location.reload(); return; } - + const days = Math.floor(timeLeft / (1000 * 60 * 60 * 24)); const hours = Math.floor((timeLeft % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); const minutes = Math.floor((timeLeft % (1000 * 60 * 60)) / (1000 * 60)); const seconds = Math.floor((timeLeft % (1000 * 60)) / 1000); - + document.getElementById('days').textContent = days.toString().padStart(2, '0'); document.getElementById('hours').textContent = hours.toString().padStart(2, '0'); document.getElementById('minutes').textContent = minutes.toString().padStart(2, '0'); diff --git a/src/ui/render.js b/src/ui/render.js index 175f339..ef77813 100644 --- a/src/ui/render.js +++ b/src/ui/render.js @@ -1,66 +1,66 @@ // Simple audio utility function playSound(soundFile) { - try { - const audio = new Audio(`assets/sounds/${soundFile}`); - audio.volume = 0.3; - audio.play().catch(e => { }); // Silently fail if no audio - } catch (e) { - // Silently fail if audio not available - } + try { + const audio = new Audio(`assets/sounds/${soundFile}`); + audio.volume = 0.3; + audio.play().catch(e => { console.log(e) }); // Silently fail if no audio + } catch (e) { + // Silently fail if audio not available + } } export function showDamageNumber(damage, target, isPlayer = false) { - const targetElement = isPlayer ? - document.querySelector('.player-battle-zone') : - document.querySelector('.enemy-battle-zone'); + const targetElement = isPlayer ? + document.querySelector('.player-battle-zone') : + document.querySelector('.enemy-battle-zone'); - if (!targetElement) return; + if (!targetElement) return; - const damageNumber = document.createElement('div'); - damageNumber.className = 'damage-number'; - damageNumber.textContent = damage; + const damageNumber = document.createElement('div'); + damageNumber.className = 'damage-number'; + damageNumber.textContent = damage; - const rect = targetElement.getBoundingClientRect(); - damageNumber.style.left = `${rect.left + rect.width / 2}px`; - damageNumber.style.top = `${rect.top + rect.height / 2}px`; + const rect = targetElement.getBoundingClientRect(); + damageNumber.style.left = `${rect.left + rect.width / 2}px`; + damageNumber.style.top = `${rect.top + rect.height / 2}px`; - document.body.appendChild(damageNumber); + document.body.appendChild(damageNumber); - requestAnimationFrame(() => { - damageNumber.classList.add('damage-number-animate'); - }); + requestAnimationFrame(() => { + damageNumber.classList.add('damage-number-animate'); + }); - setTimeout(() => { - if (damageNumber.parentNode) { - damageNumber.parentNode.removeChild(damageNumber); - } - }, 1000); + setTimeout(() => { + if (damageNumber.parentNode) { + damageNumber.parentNode.removeChild(damageNumber); + } + }, 1000); } export async function renderBattle(root) { - const app = root.app; - const p = root.player, e = root.enemy; + const app = root.app; + const p = root.player, e = root.enemy; - const { ENEMIES } = await import("../data/enemies.js"); - const { CARDS } = await import("../data/cards.js"); - const { RELICS } = await import("../data/relics.js"); - const enemyData = ENEMIES[e.id]; - const backgroundImage = enemyData?.background || null; + const { ENEMIES } = await import("../data/enemies.js"); + const { CARDS } = await import("../data/cards.js"); + const { RELICS } = await import("../data/relics.js"); + const enemyData = ENEMIES[e.id]; + const backgroundImage = enemyData?.background || null; - const intentInfo = { - attack: { emoji: '', text: `Will attack for ${e.intent.value} damage`, color: 'danger' }, - block: { emoji: '', text: `Will gain ${e.intent.value} block`, color: 'info' }, - debuff: { emoji: '', text: 'Will apply a debuff', color: 'warning' }, - heal: { emoji: '', text: `Will heal for ${e.intent.value} HP`, color: 'success' } - }[e.intent.type] || { emoji: '', text: 'Unknown intent', color: 'neutral' }; + const intentInfo = { + attack: { emoji: '', text: `Will attack for ${e.intent.value} damage`, color: 'danger' }, + block: { emoji: '', text: `Will gain ${e.intent.value} block`, color: 'info' }, + debuff: { emoji: '', text: 'Will apply a debuff', color: 'warning' }, + heal: { emoji: '', text: `Will heal for ${e.intent.value} HP`, color: 'success' } + }[e.intent.type] || { emoji: '', text: 'Unknown intent', color: 'neutral' }; - app.innerHTML = ` + app.innerHTML = `
@@ -168,8 +168,8 @@ export async function renderBattle(root) {
${Array.from({ length: p.maxEnergy }, (_, i) => - `
` - ).join('')} + `
` + ).join('')}
@@ -187,11 +187,11 @@ export async function renderBattle(root) {
${p.hand.length === 0 ? - '
🎴 No cards in hand - End turn to draw new cards
' : - p.hand.map((card, i) => { - const canPlay = p.energy >= card.cost; - const cardType = card.type === 'attack' ? 'attack' : card.type === 'skill' ? 'skill' : 'power'; - return ` + '
🎴 No cards in hand - End turn to draw new cards
' : + p.hand.map((card, i) => { + const canPlay = p.energy >= card.cost; + const cardType = card.type === 'attack' ? 'attack' : card.type === 'skill' ? 'skill' : 'power'; + return `
@@ -212,8 +212,8 @@ export async function renderBattle(root) { ${!canPlay ? `
Need ${card.cost} energy
` : ''}
`; - }).join('') - } + }).join('') + }
@@ -238,147 +238,148 @@ export async function renderBattle(root) {
`; - app.querySelectorAll("[data-play]").forEach(btn => { - btn.addEventListener("mouseenter", () => { - if (btn.classList.contains('playable')) { - playSound('swipe.mp3'); - root.selectedCardIndex = null; - updateCardSelection(root); - } - }); + app.querySelectorAll("[data-play]").forEach(btn => { + btn.addEventListener("mouseenter", () => { + if (btn.classList.contains('playable')) { + playSound('swipe.mp3'); + root.selectedCardIndex = null; + updateCardSelection(root); + } + }); - btn.addEventListener("click", () => { - const index = parseInt(btn.dataset.play, 10); - const card = p.hand[index]; - if (p.energy >= card.cost) { - root.play(index); - // Clear selection when card is played via mouse - root.selectedCardIndex = null; - updateCardSelection(root); - } + btn.addEventListener("click", () => { + const index = parseInt(btn.dataset.play, 10); + const card = p.hand[index]; + if (p.energy >= card.cost) { + playSound('played-card.mp3') + root.play(index); + // Clear selection when card is played via mouse + root.selectedCardIndex = null; + updateCardSelection(root); + } + }); }); - }); - const endTurnBtn = app.querySelector("[data-action='end']"); - if (endTurnBtn) { + const endTurnBtn = app.querySelector("[data-action='end']"); + if (endTurnBtn) { - endTurnBtn.addEventListener("click", () => { + endTurnBtn.addEventListener("click", () => { - try { - root.end(); - } catch (error) { - console.error("Error ending turn:", error); - } - }); - } - - // Initialize card selection state if not exists - if (!root.selectedCardIndex) { - root.selectedCardIndex = null; - } - - window.onkeydown = (e) => { - if (e.key.toLowerCase() === "e") { - try { - root.end(); - } catch (error) { - console.error("Error ending turn via keyboard:", error); - } + try { + root.end(); + } catch (error) { + console.error("Error ending turn:", error); + } + }); } - const n = parseInt(e.key, 10); - if (n >= 1 && n <= p.hand.length) { - const cardIndex = n - 1; - const card = p.hand[cardIndex]; - - if (root.selectedCardIndex === cardIndex) { - // Second press of same key - play the card - if (p.energy >= card.cost) { - root.play(cardIndex); - root.selectedCardIndex = null; // Clear selection - updateCardSelection(root); - } - } else { - // First press or different key - select the card - root.selectedCardIndex = cardIndex; - updateCardSelection(root); - playSound('swipe.mp3'); // Play swipe sound on keyboard selection - } + // Initialize card selection state if not exists + if (!root.selectedCardIndex) { + root.selectedCardIndex = null; } - }; - // Auto-scroll fight log to bottom - const logContent = document.getElementById('fight-log-content'); - if (logContent) { - logContent.scrollTop = logContent.scrollHeight; - } + window.onkeydown = (e) => { + if (e.key.toLowerCase() === "e") { + try { + root.end(); + } catch (error) { + console.error("Error ending turn via keyboard:", error); + } + } + + const n = parseInt(e.key, 10); + if (n >= 1 && n <= p.hand.length) { + const cardIndex = n - 1; + const card = p.hand[cardIndex]; + + if (root.selectedCardIndex === cardIndex) { + // Second press of same key - play the card + if (p.energy >= card.cost) { + root.play(cardIndex); + root.selectedCardIndex = null; // Clear selection + updateCardSelection(root); + } + } else { + // First press or different key - select the card + root.selectedCardIndex = cardIndex; + updateCardSelection(root); + playSound('swipe.mp3'); // Play swipe sound on keyboard selection + } + } + }; + + // Auto-scroll fight log to bottom + const logContent = document.getElementById('fight-log-content'); + if (logContent) { + logContent.scrollTop = logContent.scrollHeight; + } - // Apply initial card selection visual state - updateCardSelection(root); + // Apply initial card selection visual state + updateCardSelection(root); } export async function renderMap(root) { - const { CARDS } = await import("../data/cards.js"); - const { ENEMIES } = await import("../data/enemies.js"); - const { RELICS } = await import("../data/relics.js"); - const m = root.map; - const currentId = root.nodeId; - - const currentNode = m.nodes.find(n => n.id === currentId); - const nextIds = currentNode ? currentNode.next : []; - - const getNodeEmoji = (kind) => { - const emojis = { - start: 'Start', - battle: 'Battle', - elite: 'Battle', - boss: 'Boss', - rest: 'Rest', - shop: 'Shop', - event: 'Event' + const { CARDS } = await import("../data/cards.js"); + const { ENEMIES } = await import("../data/enemies.js"); + const { RELICS } = await import("../data/relics.js"); + const m = root.map; + const currentId = root.nodeId; + + const currentNode = m.nodes.find(n => n.id === currentId); + const nextIds = currentNode ? currentNode.next : []; + + const getNodeEmoji = (kind) => { + const emojis = { + start: 'Start', + battle: 'Battle', + elite: 'Battle', + boss: 'Boss', + rest: 'Rest', + shop: 'Shop', + event: 'Event' + }; + return emojis[kind] || '❓'; }; - return emojis[kind] || '❓'; - }; - - const getNodeDescription = (node) => { - switch (node.kind) { - case 'start': - return 'Starting Point\nBegin your journey up ThePrimeagen Spire'; - case 'battle': - const enemy = ENEMIES[node.enemy]; - return `Battle\nFight: ${enemy?.name || 'Unknown Enemy'}\nHP: ${enemy?.maxHp || '?'}`; - case 'elite': - const elite = ENEMIES[node.enemy]; - return `Elite Battle\nFight: ${elite?.name || 'Unknown Elite'}\nHP: ${elite?.maxHp || '?'}\nTough enemy with better rewards`; - case 'boss': - const boss = ENEMIES[node.enemy]; - return `Boss Battle\nFight: ${boss?.name || 'Unknown Boss'}\nHP: ${boss?.maxHp || '?'}\nFinal challenge of the act`; - case 'rest': - return 'Rest Site\nHeal up to 30% max HP\nor upgrade a card'; - case 'shop': - return 'Shop\nSpend your hard-earned gold'; - case 'event': - return 'Random Event\nBirthday-themed encounter\nUnknown outcome\nPotential rewards or challenges'; - default: - return 'Unknown\nMysterious node'; - } - }; - const getNodeTooltipData = (node) => { - const description = getNodeDescription(node); - let avatarPath = null; + const getNodeDescription = (node) => { + switch (node.kind) { + case 'start': + return 'Starting Point\nBegin your journey up ThePrimeagen Spire'; + case 'battle': + const enemy = ENEMIES[node.enemy]; + return `Battle\nFight: ${enemy?.name || 'Unknown Enemy'}\nHP: ${enemy?.maxHp || '?'}`; + case 'elite': + const elite = ENEMIES[node.enemy]; + return `Elite Battle\nFight: ${elite?.name || 'Unknown Elite'}\nHP: ${elite?.maxHp || '?'}\nTough enemy with better rewards`; + case 'boss': + const boss = ENEMIES[node.enemy]; + return `Boss Battle\nFight: ${boss?.name || 'Unknown Boss'}\nHP: ${boss?.maxHp || '?'}\nFinal challenge of the act`; + case 'rest': + return 'Rest Site\nHeal up to 30% max HP\nor upgrade a card'; + case 'shop': + return 'Shop\nSpend your hard-earned gold'; + case 'event': + return 'Random Event\nBirthday-themed encounter\nUnknown outcome\nPotential rewards or challenges'; + default: + return 'Unknown\nMysterious node'; + } + }; - if (['battle', 'elite', 'boss'].includes(node.kind) && node.enemy) { - const enemy = ENEMIES[node.enemy]; - if (enemy?.avatar) { - avatarPath = enemy.avatar; - } - } + const getNodeTooltipData = (node) => { + const description = getNodeDescription(node); + let avatarPath = null; - return { description, avatarPath }; - }; + if (['battle', 'elite', 'boss'].includes(node.kind) && node.enemy) { + const enemy = ENEMIES[node.enemy]; + if (enemy?.avatar) { + avatarPath = enemy.avatar; + } + } - root.app.innerHTML = ` + return { description, avatarPath }; + }; + + root.app.innerHTML = `
@@ -587,17 +588,17 @@ May this birthday bring joy in each moment you’ve got.

${Object.entries( - root.player.deck.reduce((acc, cardId) => { - acc[cardId] = (acc[cardId] || 0) + 1; - return acc; - }, {}) - ).map(([cardId, count], index) => { - const card = CARDS[cardId]; - if (!card) return ''; + root.player.deck.reduce((acc, cardId) => { + acc[cardId] = (acc[cardId] || 0) + 1; + return acc; + }, {}) + ).map(([cardId, count], index) => { + const card = CARDS[cardId]; + if (!card) return ''; - const cardType = card.type === 'attack' ? 'attack' : card.type === 'skill' ? 'skill' : 'power'; + const cardType = card.type === 'attack' ? 'attack' : card.type === 'skill' ? 'skill' : 'power'; - return ` + return `
@@ -612,7 +613,7 @@ May this birthday bring joy in each moment you’ve got.

`; - }).join('')} + }).join('')}
@@ -621,20 +622,20 @@ May this birthday bring joy in each moment you’ve got.

`; - root.app.querySelectorAll("[data-node]").forEach(el => { - if (!el.dataset.node) return; - el.addEventListener("click", () => root.go(el.dataset.node)); - }); + root.app.querySelectorAll("[data-node]").forEach(el => { + if (!el.dataset.node) return; + el.addEventListener("click", () => root.go(el.dataset.node)); + }); - window.showTooltip = function (event) { - const tooltip = document.getElementById('custom-tooltip'); - const node = event.target.closest('.spire-node'); - const content = node.dataset.tooltip; - const avatarPath = node.dataset.avatar; + window.showTooltip = function(event) { + const tooltip = document.getElementById('custom-tooltip'); + const node = event.target.closest('.spire-node'); + const content = node.dataset.tooltip; + const avatarPath = node.dataset.avatar; - let tooltipHTML = ''; - if (avatarPath) { - tooltipHTML = ` + let tooltipHTML = ''; + if (avatarPath) { + tooltipHTML = `
Enemy Avatar

${content}
`; - } else { - tooltipHTML = content; - } + } else { + tooltipHTML = content; + } - tooltip.innerHTML = tooltipHTML; - tooltip.style.display = 'block'; + tooltip.innerHTML = tooltipHTML; + tooltip.style.display = 'block'; - const rect = node.getBoundingClientRect(); - tooltip.style.left = (rect.right + 15) + 'px'; - tooltip.style.top = (rect.top + rect.height / 2 - tooltip.offsetHeight / 2) + 'px'; + const rect = node.getBoundingClientRect(); + tooltip.style.left = (rect.right + 15) + 'px'; + tooltip.style.top = (rect.top + rect.height / 2 - tooltip.offsetHeight / 2) + 'px'; - const tooltipRect = tooltip.getBoundingClientRect(); - if (tooltipRect.right > window.innerWidth) { - tooltip.style.left = (rect.left - tooltip.offsetWidth - 15) + 'px'; - } - if (tooltipRect.top < 0) { - tooltip.style.top = '10px'; - } - if (tooltipRect.bottom > window.innerHeight) { - tooltip.style.top = (window.innerHeight - tooltip.offsetHeight - 10) + 'px'; - } - }; + const tooltipRect = tooltip.getBoundingClientRect(); + if (tooltipRect.right > window.innerWidth) { + tooltip.style.left = (rect.left - tooltip.offsetWidth - 15) + 'px'; + } + if (tooltipRect.top < 0) { + tooltip.style.top = '10px'; + } + if (tooltipRect.bottom > window.innerHeight) { + tooltip.style.top = (window.innerHeight - tooltip.offsetHeight - 10) + 'px'; + } + }; - window.hideTooltip = function () { - const tooltip = document.getElementById('custom-tooltip'); - tooltip.style.display = 'none'; - }; + window.hideTooltip = function() { + const tooltip = document.getElementById('custom-tooltip'); + tooltip.style.display = 'none'; + }; - const resetBtn = root.app.querySelector("[data-reset]"); - resetBtn.addEventListener("click", () => { - root.clearSave(); - root.reset(); - }); + const resetBtn = root.app.querySelector("[data-reset]"); + resetBtn.addEventListener("click", () => { + root.clearSave(); + root.reset(); + }); } export async function renderReward(root, choices) { - const { CARDS } = await import("../data/cards.js"); - root.app.innerHTML = ` + const { CARDS } = await import("../data/cards.js"); + root.app.innerHTML = `

Choose a Card

${choices.map((c, idx) => { - const cardType = c.type === 'attack' ? 'attack' : c.type === 'skill' ? 'skill' : 'power'; - return ` + const cardType = c.type === 'attack' ? 'attack' : c.type === 'skill' ? 'skill' : 'power'; + return `
@@ -713,25 +714,25 @@ export async function renderReward(root, choices) {
`; - }).join("")} + }).join("")}
`; - root.app.querySelectorAll("[data-pick]").forEach(btn => { - btn.addEventListener("click", () => { - const idx = parseInt(btn.dataset.pick, 10); - root.takeReward(idx); + root.app.querySelectorAll("[data-pick]").forEach(btn => { + btn.addEventListener("click", () => { + const idx = parseInt(btn.dataset.pick, 10); + root.takeReward(idx); + }); }); - }); - root.app.querySelector("[data-skip]").addEventListener("click", () => root.skipReward()); + root.app.querySelector("[data-skip]").addEventListener("click", () => root.skipReward()); } export async function renderRest(root) { - const { CARDS } = await import("../data/cards.js"); - root.app.innerHTML = ` + const { CARDS } = await import("../data/cards.js"); + root.app.innerHTML = `

Rest and Recover

@@ -761,35 +762,35 @@ export async function renderRest(root) {
`; - root.app.querySelector("[data-act='heal']").addEventListener("click", () => { - const heal = Math.floor(root.player.maxHp * 0.2); - root.player.hp = Math.min(root.player.maxHp, root.player.hp + heal); - root.log(`Rested: +${heal} HP`); - root.afterNode(); - }); - root.app.querySelector("[data-act='upgrade']").addEventListener("click", () => { - renderUpgrade(root); - }); + root.app.querySelector("[data-act='heal']").addEventListener("click", () => { + const heal = Math.floor(root.player.maxHp * 0.2); + root.player.hp = Math.min(root.player.maxHp, root.player.hp + heal); + root.log(`Rested: +${heal} HP`); + root.afterNode(); + }); + root.app.querySelector("[data-act='upgrade']").addEventListener("click", () => { + renderUpgrade(root); + }); } export function renderUpgrade(root) { - import("../data/cards.js").then(({ CARDS }) => { - const upgradableCards = root.player.deck - .map((cardId, index) => ({ cardId, index })) - .filter(({ cardId }) => { - const card = CARDS[cardId]; - - return card?.upgrades && !cardId.endsWith('+'); - }) - .slice(0, 3); // Show max 3 options - - if (upgradableCards.length === 0) { - root.log("No cards can be upgraded."); - root.afterNode(); - return; - } + import("../data/cards.js").then(({ CARDS }) => { + const upgradableCards = root.player.deck + .map((cardId, index) => ({ cardId, index })) + .filter(({ cardId }) => { + const card = CARDS[cardId]; + + return card?.upgrades && !cardId.endsWith('+'); + }) + .slice(0, 3); // Show max 3 options + + if (upgradableCards.length === 0) { + root.log("No cards can be upgraded."); + root.afterNode(); + return; + } - root.app.innerHTML = ` + root.app.innerHTML = `

⬆️ Upgrade a Card

@@ -798,14 +799,14 @@ export function renderUpgrade(root) {
${upgradableCards.map(({ cardId, index }) => { - const card = CARDS[cardId]; - const upgradedCard = CARDS[card.upgrades]; + const card = CARDS[cardId]; + const upgradedCard = CARDS[card.upgrades]; - if (!upgradedCard) { - return ''; // Skip if no upgrade found - } + if (!upgradedCard) { + return ''; // Skip if no upgrade found + } - return ` + return `
@@ -861,7 +862,7 @@ export function renderUpgrade(root) {
`; - }).join("")} + }).join("")}
@@ -870,38 +871,38 @@ export function renderUpgrade(root) {
`; - root.app.querySelectorAll("[data-upgrade]").forEach(btn => { - btn.addEventListener("click", () => { - const deckIndex = parseInt(btn.dataset.upgrade, 10); - const oldCardId = root.player.deck[deckIndex]; - const newCardId = CARDS[oldCardId].upgrades; - root.player.deck[deckIndex] = newCardId; - root.log(`Upgraded ${CARDS[oldCardId].name} → ${CARDS[newCardId].name}`); - root.afterNode(); - }); + root.app.querySelectorAll("[data-upgrade]").forEach(btn => { + btn.addEventListener("click", () => { + const deckIndex = parseInt(btn.dataset.upgrade, 10); + const oldCardId = root.player.deck[deckIndex]; + const newCardId = CARDS[oldCardId].upgrades; + root.player.deck[deckIndex] = newCardId; + root.log(`Upgraded ${CARDS[oldCardId].name} → ${CARDS[newCardId].name}`); + root.afterNode(); + }); + }); + root.app.querySelector("[data-skip]").addEventListener("click", () => root.afterNode()); }); - root.app.querySelector("[data-skip]").addEventListener("click", () => root.afterNode()); - }); } export function renderShop(root) { - import("../data/cards.js").then(({ CARDS, CARD_POOL }) => { - import("../data/relics.js").then(({ RELICS, START_RELIC_CHOICES }) => { + import("../data/cards.js").then(({ CARDS, CARD_POOL }) => { + import("../data/relics.js").then(({ RELICS, START_RELIC_CHOICES }) => { - const availableCards = CARD_POOL.filter(cardId => { + const availableCards = CARD_POOL.filter(cardId => { - const ownedCount = root.player.deck.filter(deckCardId => deckCardId === cardId).length; + const ownedCount = root.player.deck.filter(deckCardId => deckCardId === cardId).length; - return ownedCount < 3; - }); + return ownedCount < 3; + }); - const cardsToShow = availableCards.length >= 3 ? availableCards : CARD_POOL; - const shopCards = shuffle(cardsToShow.slice()).slice(0, 3).map(id => CARDS[id]); - const ownedRelicIds = root.relicStates.map(r => r.id); - const availableRelics = START_RELIC_CHOICES.filter(id => !ownedRelicIds.includes(id)); - const shopRelic = availableRelics.length > 0 ? RELICS[availableRelics[0]] : null; + const cardsToShow = availableCards.length >= 3 ? availableCards : CARD_POOL; + const shopCards = shuffle(cardsToShow.slice()).slice(0, 3).map(id => CARDS[id]); + const ownedRelicIds = root.relicStates.map(r => r.id); + const availableRelics = START_RELIC_CHOICES.filter(id => !ownedRelicIds.includes(id)); + const shopRelic = availableRelics.length > 0 ? RELICS[availableRelics[0]] : null; - root.app.innerHTML = ` + root.app.innerHTML = `

Merchant's Shop

@@ -920,10 +921,10 @@ export function renderShop(root) {
${shopCards.map((card, idx) => { - const cardType = card.type === 'attack' ? 'attack' : card.type === 'skill' ? 'skill' : 'power'; - const canAfford = (root.player.gold || 100) >= 50; - const ownedCount = root.player.deck.filter(deckCardId => deckCardId === card.id).length; - return ` + const cardType = card.type === 'attack' ? 'attack' : card.type === 'skill' ? 'skill' : 'power'; + const canAfford = (root.player.gold || 100) >= 50; + const ownedCount = root.player.deck.filter(deckCardId => deckCardId === card.id).length; + return `
@@ -951,7 +952,7 @@ export function renderShop(root) {
`; - }).join("")} + }).join("")}
@@ -989,185 +990,185 @@ export function renderShop(root) {
`; - if (!root.player.gold) root.player.gold = 100; - - root.app.querySelectorAll("[data-buy-card]").forEach(btn => { - btn.addEventListener("click", () => { - const idx = parseInt(btn.dataset.buyCard, 10); - const card = shopCards[idx]; - if (root.player.gold >= 50) { - root.player.gold -= 50; - root.player.deck.push(card.id); - root.log(`Bought ${card.name} for 50 gold.`); - btn.disabled = true; - btn.textContent = "SOLD"; - - // Update gold display - const goldDisplay = root.app.querySelector('.gold-amount'); - if (goldDisplay) { - goldDisplay.textContent = root.player.gold; - } - - // Update affordability of remaining items - updateShopAffordability(root); - } else { - root.log("Not enough gold!"); - } - }); - }); - - - if (shopRelic) { - root.app.querySelector("[data-buy-relic]").addEventListener("click", () => { - if (root.player.gold >= 100) { - root.player.gold -= 100; - root.log(`Bought ${shopRelic.name} for 100 gold.`); - - import("../engine/battle.js").then(({ attachRelics }) => { - - const currentRelicIds = root.relicStates.map(r => r.id); - const newRelicIds = [...currentRelicIds, shopRelic.id]; - attachRelics(root, newRelicIds); + if (!root.player.gold) root.player.gold = 100; + + root.app.querySelectorAll("[data-buy-card]").forEach(btn => { + btn.addEventListener("click", () => { + const idx = parseInt(btn.dataset.buyCard, 10); + const card = shopCards[idx]; + if (root.player.gold >= 50) { + root.player.gold -= 50; + root.player.deck.push(card.id); + root.log(`Bought ${card.name} for 50 gold.`); + btn.disabled = true; + btn.textContent = "SOLD"; + + // Update gold display + const goldDisplay = root.app.querySelector('.gold-amount'); + if (goldDisplay) { + goldDisplay.textContent = root.player.gold; + } + + // Update affordability of remaining items + updateShopAffordability(root); + } else { + root.log("Not enough gold!"); + } + }); }); - root.app.querySelector("[data-buy-relic]").disabled = true; - root.app.querySelector("[data-buy-relic]").textContent = "SOLD"; - // Update gold display - const goldDisplay = root.app.querySelector('.gold-amount'); - if (goldDisplay) { - goldDisplay.textContent = root.player.gold; + + if (shopRelic) { + root.app.querySelector("[data-buy-relic]").addEventListener("click", () => { + if (root.player.gold >= 100) { + root.player.gold -= 100; + root.log(`Bought ${shopRelic.name} for 100 gold.`); + + import("../engine/battle.js").then(({ attachRelics }) => { + + const currentRelicIds = root.relicStates.map(r => r.id); + const newRelicIds = [...currentRelicIds, shopRelic.id]; + attachRelics(root, newRelicIds); + }); + root.app.querySelector("[data-buy-relic]").disabled = true; + root.app.querySelector("[data-buy-relic]").textContent = "SOLD"; + + // Update gold display + const goldDisplay = root.app.querySelector('.gold-amount'); + if (goldDisplay) { + goldDisplay.textContent = root.player.gold; + } + + // Update affordability of remaining items + updateShopAffordability(root); + } else { + root.log("Not enough gold!"); + } + }); } - // Update affordability of remaining items - updateShopAffordability(root); - } else { - root.log("Not enough gold!"); - } + root.app.querySelector("[data-leave]").addEventListener("click", () => root.afterNode()); }); - } - - root.app.querySelector("[data-leave]").addEventListener("click", () => root.afterNode()); }); - }); } function updateCardSelection(root) { - // Remove selection from all cards - root.app.querySelectorAll('.battle-card').forEach(card => { - card.classList.remove('card-selected'); - }); - - // Add selection to currently selected card - if (root.selectedCardIndex !== null) { - const selectedCard = root.app.querySelector(`[data-play="${root.selectedCardIndex}"]`); - if (selectedCard) { - selectedCard.classList.add('card-selected'); + // Remove selection from all cards + root.app.querySelectorAll('.battle-card').forEach(card => { + card.classList.remove('card-selected'); + }); + + // Add selection to currently selected card + if (root.selectedCardIndex !== null) { + const selectedCard = root.app.querySelector(`[data-play="${root.selectedCardIndex}"]`); + if (selectedCard) { + selectedCard.classList.add('card-selected'); + } } - } } function updateShopAffordability(root) { - // Update card affordability - root.app.querySelectorAll("[data-buy-card]").forEach(btn => { - if (!btn.disabled) { - const cardContainer = btn.closest('.shop-card-container'); - const overlay = cardContainer.querySelector('.card-disabled-overlay'); - - if (root.player.gold < 50) { - btn.classList.remove('playable'); - btn.classList.add('unplayable'); - if (!overlay) { - const newOverlay = document.createElement('div'); - newOverlay.className = 'card-disabled-overlay'; - newOverlay.innerHTML = 'Need 50 gold'; - cardContainer.appendChild(newOverlay); + // Update card affordability + root.app.querySelectorAll("[data-buy-card]").forEach(btn => { + if (!btn.disabled) { + const cardContainer = btn.closest('.shop-card-container'); + const overlay = cardContainer.querySelector('.card-disabled-overlay'); + + if (root.player.gold < 50) { + btn.classList.remove('playable'); + btn.classList.add('unplayable'); + if (!overlay) { + const newOverlay = document.createElement('div'); + newOverlay.className = 'card-disabled-overlay'; + newOverlay.innerHTML = 'Need 50 gold'; + cardContainer.appendChild(newOverlay); + } + } else { + btn.classList.remove('unplayable'); + btn.classList.add('playable'); + if (overlay) { + overlay.remove(); + } + } } - } else { - btn.classList.remove('unplayable'); - btn.classList.add('playable'); - if (overlay) { - overlay.remove(); + }); + + // Update relic affordability + const relicBtn = root.app.querySelector("[data-buy-relic]"); + if (relicBtn && !relicBtn.disabled) { + const relicContainer = relicBtn.closest('.shop-relic-container'); + const overlay = relicContainer.querySelector('.relic-disabled-overlay'); + + if (root.player.gold < 100) { + relicBtn.classList.remove('affordable'); + relicBtn.classList.add('unaffordable'); + if (!overlay) { + const newOverlay = document.createElement('div'); + newOverlay.className = 'relic-disabled-overlay'; + newOverlay.innerHTML = 'Need 100 gold'; + relicContainer.appendChild(newOverlay); + } + } else { + relicBtn.classList.remove('unaffordable'); + relicBtn.classList.add('affordable'); + if (overlay) { + overlay.remove(); + } } - } } - }); - - // Update relic affordability - const relicBtn = root.app.querySelector("[data-buy-relic]"); - if (relicBtn && !relicBtn.disabled) { - const relicContainer = relicBtn.closest('.shop-relic-container'); - const overlay = relicContainer.querySelector('.relic-disabled-overlay'); - - if (root.player.gold < 100) { - relicBtn.classList.remove('affordable'); - relicBtn.classList.add('unaffordable'); - if (!overlay) { - const newOverlay = document.createElement('div'); - newOverlay.className = 'relic-disabled-overlay'; - newOverlay.innerHTML = 'Need 100 gold'; - relicContainer.appendChild(newOverlay); - } - } else { - relicBtn.classList.remove('unaffordable'); - relicBtn.classList.add('affordable'); - if (overlay) { - overlay.remove(); - } - } - } } function shuffle(array) { - for (let i = array.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [array[i], array[j]] = [array[j], array[i]]; - } - return array; + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + return array; } function getRelicArt(relicId, RELICS = null) { - if (RELICS && RELICS[relicId]?.art) { - const imagePath = RELICS[relicId].art; - return `${relicId}`; - } - return '💎'; + if (RELICS && RELICS[relicId]?.art) { + const imagePath = RELICS[relicId].art; + return `${relicId}`; + } + return '💎'; } function getRelicName(relicId, RELICS = null) { - return RELICS?.[relicId]?.name || relicId; + return RELICS?.[relicId]?.name || relicId; } function getRelicText(relicId, RELICS = null) { - return RELICS?.[relicId]?.text || 'Unknown relic'; + return RELICS?.[relicId]?.text || 'Unknown relic'; } function getCardArt(cardId, CARDS = null) { - if (CARDS && CARDS[cardId]?.art) { - const imagePath = CARDS[cardId].art; - return `${cardId}`; - } - - // Fallback for cases where CARDS is not passed (shouldn't happen in normal operation) - return `🃏`; + if (CARDS && CARDS[cardId]?.art) { + const imagePath = CARDS[cardId].art; + return `${cardId}`; + } + + // Fallback for cases where CARDS is not passed (shouldn't happen in normal operation) + return `🃏`; } function getEnemyArt(enemyId, ENEMIES = null) { - const enemyData = ENEMIES?.[enemyId]; - const avatarPath = enemyData?.avatar || `assets/avatars/${enemyId}.png`; - return `${enemyId}`; + const enemyData = ENEMIES?.[enemyId]; + const avatarPath = enemyData?.avatar || `assets/avatars/${enemyId}.png`; + return `${enemyId}`; } function getEnemyType(enemyId) { - if (enemyId.includes('boss_')) return 'BOSS'; - if (enemyId.includes('elite_')) return 'ELITE'; - return 'ENEMY'; + if (enemyId.includes('boss_')) return 'BOSS'; + if (enemyId.includes('elite_')) return 'ELITE'; + return 'ENEMY'; } export function renderRelicSelection(root) { - import("../data/relics.js").then(({ RELICS, START_RELIC_CHOICES }) => { - const relicChoices = START_RELIC_CHOICES.slice(0, 3); // Show first 3 relics + import("../data/relics.js").then(({ RELICS, START_RELIC_CHOICES }) => { + const relicChoices = START_RELIC_CHOICES.slice(0, 3); // Show first 3 relics - root.app.innerHTML = ` + root.app.innerHTML = `
`; - root.app.querySelectorAll("[data-relic]").forEach(btn => { - btn.addEventListener("click", () => { - const relicId = btn.dataset.relic; - root.selectStartingRelic(relicId); - }); + root.app.querySelectorAll("[data-relic]").forEach(btn => { + btn.addEventListener("click", () => { + const relicId = btn.dataset.relic; + root.selectStartingRelic(relicId); + }); + }); }); - }); } export function renderEvent(root) { - const events = [ - { - title: "Birthday Cake", - text: "You find a delicious birthday cake! But it looks suspicious...", - artwork: "assets/card-art/bread.png", - choices: [ - { - text: "Eat the whole cake (+15 HP, gain Sugar Crash curse)", - icon: "assets/card-art/apple.png", - risk: "high", - effect: () => { - root.player.hp = Math.min(root.player.maxHp, root.player.hp + 15); - root.player.deck.push("sugar_crash"); - root.log("Ate cake: +15 HP, added Sugar Crash curse"); - } - }, - { - text: "Take a small bite (+8 HP)", - icon: "assets/card-art/heart.png", - risk: "low", - effect: () => { - root.player.hp = Math.min(root.player.maxHp, root.player.hp + 8); - root.log("Small bite: +8 HP"); - } - }, - { - text: "Leave it alone (gain 25 gold)", - icon: "assets/card-art/bag_of_gold.png", - risk: "none", - effect: () => { - root.player.gold += 25; - root.log("Resisted temptation: +25 gold"); - } - } - ] - }, - { - title: "Birthday Present", - text: "A mysterious gift box sits before you. What could be inside?", - artwork: "assets/card-art/chest_closed.png", - choices: [ - { - text: "Open it eagerly (Random card or lose 10 HP)", - icon: "assets/card-art/key.png", - risk: "high", - effect: () => { - if (Math.random() < 0.7) { - import("../data/cards.js").then(({ CARDS, CARD_POOL }) => { - const randomCard = CARD_POOL[Math.floor(Math.random() * CARD_POOL.length)]; - root.player.deck.push(randomCard); - root.log(`Found ${CARDS[randomCard].name}!`); - }); - } else { - root.player.hp = Math.max(1, root.player.hp - 10); - root.log("It was a trap! -10 HP"); - } - } - }, - { - text: "Open it carefully (+5 Max HP)", - icon: "assets/card-art/potion_heal.png", - risk: "low", - effect: () => { - root.player.maxHp += 5; - root.player.hp += 5; - root.log("Careful approach: +5 Max HP"); - } - }, - { - text: "Don't touch it (gain 30 gold)", - icon: "assets/card-art/bag_of_gold.png", - risk: "none", - effect: () => { - root.player.gold += 30; - root.log("Played it safe: +30 gold"); - } - } - ] - }, - { - title: "Birthday Balloons", - text: "Colorful balloons float by. One has a note attached: 'Pop me for a surprise!'", - artwork: "assets/card-art/feather.png", - choices: [ + const events = [ { - text: "Pop the balloon (Remove a random basic card from deck)", - icon: "assets/card-art/scroll.png", - risk: "medium", - effect: () => { - const basicCards = root.player.deck.filter(id => id === "strike" || id === "defend"); - if (basicCards.length > 0) { - const toRemove = basicCards[0]; - const index = root.player.deck.indexOf(toRemove); - root.player.deck.splice(index, 1); - root.log(`Removed ${toRemove} from deck`); - } else { - root.log("No basic cards to remove"); - } - } + title: "Birthday Cake", + text: "You find a delicious birthday cake! But it looks suspicious...", + artwork: "assets/card-art/bread.png", + choices: [ + { + text: "Eat the whole cake (+15 HP, gain Sugar Crash curse)", + icon: "assets/card-art/apple.png", + risk: "high", + effect: () => { + root.player.hp = Math.min(root.player.maxHp, root.player.hp + 15); + root.player.deck.push("sugar_crash"); + root.log("Ate cake: +15 HP, added Sugar Crash curse"); + } + }, + { + text: "Take a small bite (+8 HP)", + icon: "assets/card-art/heart.png", + risk: "low", + effect: () => { + root.player.hp = Math.min(root.player.maxHp, root.player.hp + 8); + root.log("Small bite: +8 HP"); + } + }, + { + text: "Leave it alone (gain 25 gold)", + icon: "assets/card-art/bag_of_gold.png", + risk: "none", + effect: () => { + root.player.gold += 25; + root.log("Resisted temptation: +25 gold"); + } + } + ] }, { - text: "Collect the balloons (+1 Energy next 3 fights)", - icon: "assets/card-art/magic_sphere.png", - risk: "low", - effect: () => { - root.flags.bonusEnergyFights = 3; - root.log("Collected balloons: +1 Energy next 3 fights"); - } + title: "Birthday Present", + text: "A mysterious gift box sits before you. What could be inside?", + artwork: "assets/card-art/chest_closed.png", + choices: [ + { + text: "Open it eagerly (Random card or lose 10 HP)", + icon: "assets/card-art/key.png", + risk: "high", + effect: () => { + if (Math.random() < 0.7) { + import("../data/cards.js").then(({ CARDS, CARD_POOL }) => { + const randomCard = CARD_POOL[Math.floor(Math.random() * CARD_POOL.length)]; + root.player.deck.push(randomCard); + root.log(`Found ${CARDS[randomCard].name}!`); + }); + } else { + root.player.hp = Math.max(1, root.player.hp - 10); + root.log("It was a trap! -10 HP"); + } + } + }, + { + text: "Open it carefully (+5 Max HP)", + icon: "assets/card-art/potion_heal.png", + risk: "low", + effect: () => { + root.player.maxHp += 5; + root.player.hp += 5; + root.log("Careful approach: +5 Max HP"); + } + }, + { + text: "Don't touch it (gain 30 gold)", + icon: "assets/card-art/bag_of_gold.png", + risk: "none", + effect: () => { + root.player.gold += 30; + root.log("Played it safe: +30 gold"); + } + } + ] }, { - text: "Ignore them (heal 12 HP)", - icon: "assets/card-art/heart.png", - risk: "none", - effect: () => { - root.player.hp = Math.min(root.player.maxHp, root.player.hp + 12); - root.log("Focused on rest: +12 HP"); - } + title: "Birthday Balloons", + text: "Colorful balloons float by. One has a note attached: 'Pop me for a surprise!'", + artwork: "assets/card-art/feather.png", + choices: [ + { + text: "Pop the balloon (Remove a random basic card from deck)", + icon: "assets/card-art/scroll.png", + risk: "medium", + effect: () => { + const basicCards = root.player.deck.filter(id => id === "strike" || id === "defend"); + if (basicCards.length > 0) { + const toRemove = basicCards[0]; + const index = root.player.deck.indexOf(toRemove); + root.player.deck.splice(index, 1); + root.log(`Removed ${toRemove} from deck`); + } else { + root.log("No basic cards to remove"); + } + } + }, + { + text: "Collect the balloons (+1 Energy next 3 fights)", + icon: "assets/card-art/magic_sphere.png", + risk: "low", + effect: () => { + root.flags.bonusEnergyFights = 3; + root.log("Collected balloons: +1 Energy next 3 fights"); + } + }, + { + text: "Ignore them (heal 12 HP)", + icon: "assets/card-art/heart.png", + risk: "none", + effect: () => { + root.player.hp = Math.min(root.player.maxHp, root.player.hp + 12); + root.log("Focused on rest: +12 HP"); + } + } + ] } - ] - } - ]; + ]; - const event = events[Math.floor(Math.random() * events.length)]; + const event = events[Math.floor(Math.random() * events.length)]; - root.app.innerHTML = ` + root.app.innerHTML = `

${event.title}

@@ -1416,28 +1417,28 @@ export function renderEvent(root) {
`; - root.app.querySelectorAll("[data-choice]").forEach(btn => { - btn.addEventListener("click", () => { - const idx = parseInt(btn.dataset.choice, 10); - event.choices[idx].effect(); - root.afterNode(); + root.app.querySelectorAll("[data-choice]").forEach(btn => { + btn.addEventListener("click", () => { + const idx = parseInt(btn.dataset.choice, 10); + event.choices[idx].effect(); + root.afterNode(); + }); }); - }); } export async function renderWin(root) { - const { RELICS } = await import("../data/relics.js"); - const finalStats = { - totalTurns: root.turnCount || 0, - cardsPlayed: root.cardsPlayedCount || 0, - finalHP: root.player.hp, - maxHP: root.player.maxHp, - finalGold: root.player.gold || 0, - deckSize: root.player.deck.length, - relicsCollected: root.relicStates.length - }; - - root.app.innerHTML = ` + const { RELICS } = await import("../data/relics.js"); + const finalStats = { + totalTurns: root.turnCount || 0, + cardsPlayed: root.cardsPlayedCount || 0, + finalHP: root.player.hp, + maxHP: root.player.maxHp, + finalGold: root.player.gold || 0, + deckSize: root.player.deck.length, + relicsCollected: root.relicStates.length + }; + + root.app.innerHTML = `
@@ -1494,14 +1495,14 @@ export async function renderWin(root) {

Relics Mastered

${root.relicStates.length > 0 ? - root.relicStates.map(r => ` + root.relicStates.map(r => `
${getRelicArt(r.id, RELICS)}
${getRelicName(r.id, RELICS)}
`).join('') : - '
No relics collected this run
' - } + '
No relics collected this run
' + }
@@ -1522,23 +1523,23 @@ export async function renderWin(root) {
`; - root.app.querySelector("[data-replay]").addEventListener("click", () => root.reset()); + root.app.querySelector("[data-replay]").addEventListener("click", () => root.reset()); } export async function renderLose(root) { - const { RELICS } = await import("../data/relics.js"); - const finalStats = { - totalTurns: root.turnCount || 0, - cardsPlayed: root.cardsPlayedCount || 0, - finalHP: 0, // Player is defeated - maxHP: root.player.maxHp, - finalGold: root.player.gold || 0, - deckSize: root.player.deck.length, - relicsCollected: root.relicStates.length, - nodeId: root.nodeId || 'unknown' - }; - - root.app.innerHTML = ` + const { RELICS } = await import("../data/relics.js"); + const finalStats = { + totalTurns: root.turnCount || 0, + cardsPlayed: root.cardsPlayedCount || 0, + finalHP: 0, // Player is defeated + maxHP: root.player.maxHp, + finalGold: root.player.gold || 0, + deckSize: root.player.deck.length, + relicsCollected: root.relicStates.length, + nodeId: root.nodeId || 'unknown' + }; + + root.app.innerHTML = `

You Failed!

@@ -1634,12 +1635,12 @@ Better luck on the next run!

`; - root.app.querySelector("[data-replay]").addEventListener("click", () => { - root.reset(); - }); + root.app.querySelector("[data-replay]").addEventListener("click", () => { + root.reset(); + }); - root.app.querySelector("[data-menu]").addEventListener("click", () => { - root.reset(); - }); + root.app.querySelector("[data-menu]").addEventListener("click", () => { + root.reset(); + }); }