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 = `