diff --git a/src/data/cards.js b/src/data/cards.js index 74ee1a7..a725776 100644 --- a/src/data/cards.js +++ b/src/data/cards.js @@ -41,9 +41,9 @@ export const CARDS = { card.cost = 0; card.effect(ctx); card.cost = savedCost; - ctx.log(`Replayed ${card.name} for free!`); + ctx.log(`Macro replays ${card.name} at no cost!`); } else { - ctx.log("No valid card to replay."); + ctx.log("Macro fizzles - no valid card to replay."); } } } @@ -113,7 +113,7 @@ export const CARDS = { const prevHp = ctx.enemy.hp; ctx.deal(ctx.enemy, ctx.scalarFromWeak(5)); if (prevHp > 0 && ctx.enemy.hp <= 0) { - ctx.log("Recursion triggered!"); + ctx.log("Recursion activates and strikes again!"); ctx.enemy.hp = 1; ctx.deal(ctx.enemy, ctx.scalarFromWeak(5)); @@ -139,7 +139,7 @@ export const CARDS = { effect: (ctx) => { ctx.draw(1); - ctx.log("Reviewed code, drew 1 card."); + ctx.log("Code review reveals useful insights. You draw a card."); } }, @@ -154,7 +154,7 @@ export const CARDS = { if (ctx.player.hp <= ctx.player.maxHp * 0.5) { ctx.deal(ctx.enemy, ctx.scalarFromWeak(10)); } else { - ctx.log("Can only hotfix in emergencies!"); + ctx.log("Hotfix can only be deployed when HP is below 50%!"); } } }, @@ -180,7 +180,7 @@ export const CARDS = { effect: (ctx) => { if (ctx.intentIsAttack()) { ctx.player.block += 8; - ctx.log("Tests passed! Gained Block."); + ctx.log("Unit tests pass! You feel more confident and gain block."); } } }, @@ -190,7 +190,7 @@ export const CARDS = { effect: (ctx) => { ctx.deal(ctx.enemy, ctx.scalarFromWeak(25)); ctx.player.hp = Math.max(1, ctx.player.hp - 5); - ctx.log("Deployed to prod... at what cost?"); + ctx.log("Production deployment succeeds, but at what cost to your health?"); } }, @@ -199,7 +199,7 @@ export const CARDS = { id: "sugar_crash", name: "Sugar Crash", cost: 1, type: "curse", text: "Unplayable. -1 Energy when drawn.", effect: (ctx) => { ctx.player.energy = Math.max(0, ctx.player.energy - 1); - ctx.log("Sugar crash! -1 Energy"); + ctx.log("The sugar crash hits hard, draining your energy!"); } }, }; diff --git a/src/data/enemies.js b/src/data/enemies.js index bbcf7c1..7f4a741 100644 --- a/src/data/enemies.js +++ b/src/data/enemies.js @@ -24,7 +24,7 @@ export const ENEMIES = { id: "infinite_loop", name: "Beastco", maxHp: 35, avatar: "assets/avatars/2.png", // Dizzy/confused character background: "assets/backgrounds/throne room.png", - ai: (turn) => ({ type: "attack", value: 4 }), // Always attacks for small damage + ai: (turn) => ({ type: "attack", value: 4 }), }, merge_conflict_enemy: { id: "merge_conflict_enemy", name: "Codegirl", maxHp: 50, @@ -34,7 +34,7 @@ export const ENEMIES = { onDebuff: (ctx) => { ctx.enemy.hp = Math.min(ctx.enemy.maxHp, ctx.enemy.hp + 8); - ctx.log("Merge Conflict splits! Enemy heals 8!"); + ctx.log("Codegirl resolves the merge conflict and heals 8 HP!"); } }, bug_404: { @@ -68,6 +68,6 @@ export const ENEMIES = { if (cyc === 3) return { type: "block", value: 0 }; // Crash β†’ heal return { type: "attack", value: 22 }; // Burst }, - onBlock: (ctx) => { ctx.enemy.hp = Math.min(ctx.enemy.maxHp, ctx.enemy.hp + 8); ctx.log("Bug crashes, heals 8!"); } + onBlock: (ctx) => { ctx.enemy.hp = Math.min(ctx.enemy.maxHp, ctx.enemy.hp + 8); ctx.log("Teej crashes and reboots, healing 8 HP!"); } } }; diff --git a/src/data/relics.js b/src/data/relics.js index 1a49411..d407944 100644 --- a/src/data/relics.js +++ b/src/data/relics.js @@ -17,7 +17,7 @@ export const RELICS = { coffee_thermos: { id: "coffee_thermos", name: "Terminal Coffee Thermos", text: "Start each fight with Coffee Rush.", - hooks: { onBattleStart: (ctx) => { ctx.player.energy += 2; ctx.log("Thermos: +2 energy") } } + hooks: { onBattleStart: (ctx) => { ctx.player.energy += 2; ctx.log("Your coffee thermos provides an energizing boost!") } } }, cpp_compiler: { id: "cpp_compiler", name: "Haskell", @@ -34,7 +34,7 @@ export const RELICS = { hooks: { onBattleStart: (ctx) => { ctx.applyWeak(ctx.enemy, 1); - ctx.log("Worst Streamer Award: Enemy starts Weak!"); + ctx.log("Your Worst Streamer Award intimidates the enemy, making them weak!"); } } }, diff --git a/src/engine/battle.js b/src/engine/battle.js index c1fea92..da98c6a 100644 --- a/src/engine/battle.js +++ b/src/engine/battle.js @@ -13,8 +13,8 @@ export function createBattle(ctx, enemyId) { const relicCtx = { ...ctx, draw: (n) => draw(ctx.player, n), - applyWeak: (who, amt) => { who.weak = (who.weak || 0) + amt; ctx.log(`Weak +${amt}`) }, - applyVulnerable: (who, amt) => { who.vuln = (who.vuln || 0) + amt; ctx.log(`Vulnerable +${amt}`) } + applyWeak: (who, amt) => { who.weak = (who.weak || 0) + amt; ctx.log(`${who === ctx.player ? 'You are' : ctx.enemy.name + ' is'} weakened for ${amt} turn${amt > 1 ? 's' : ''}.`) }, + applyVulnerable: (who, amt) => { who.vuln = (who.vuln || 0) + amt; ctx.log(`${who === ctx.player ? 'You are' : ctx.enemy.name + ' is'} vulnerable for ${amt} turn${amt > 1 ? 's' : ''}.`) } }; for (const r of ctx.relicStates) r.hooks?.onBattleStart?.(relicCtx, r.state); @@ -30,7 +30,7 @@ export function startPlayerTurn(ctx) { const relicCtx = { ...ctx, draw: (n) => draw(ctx.player, n) }; for (const r of ctx.relicStates) r.hooks?.onTurnStart?.(relicCtx, r.state); - ctx.log(`Your turn. Energy ${ctx.player.energy}.`); + ctx.log(`Your turn begins. You have ${ctx.player.energy} energy to spend.`); ctx.render(); } @@ -44,11 +44,11 @@ export function playCard(ctx, handIndex) { if (ctx.flags.nextCardFree) { actualCost = 0; ctx.flags.nextCardFree = false; - ctx.log("Infinite Vim: Card costs 0!"); + ctx.log("Infinite Vim makes your next card free!"); } - if (ctx.player.energy < actualCost) { ctx.log("Not enough energy."); return; } - if (card.oncePerFight && card._used) { ctx.log("That macro already ran."); return; } + if (ctx.player.energy < actualCost) { ctx.log(`You need ${actualCost} energy but only have ${ctx.player.energy}.`); return; } + if (card.oncePerFight && card._used) { ctx.log(`${card.name} can only be used once per fight.`); return; } ctx.player.energy -= actualCost; ctx.lastCard = card.id; @@ -79,7 +79,7 @@ export function playCard(ctx, handIndex) { if (!card.exhaust) { ctx.player.discard.push(used.id); } else { - ctx.log(`${used.name} exhausted.`); + ctx.log(`${used.name} is exhausted and removed from the fight.`); } } @@ -102,10 +102,10 @@ export function enemyTurn(ctx) { } else if (e.intent.type === "block") { ENEMIES[e.id].onBlock?.(ctx, e.intent.value); e.block += e.intent.value; - ctx.log(`${e.name} gains ${e.intent.value} Block.`); + ctx.log(`${e.name} defends and gains ${e.intent.value} block.`); } else if (e.intent.type === "debuff") { ENEMIES[e.id].onDebuff?.(ctx, e.intent.value); - ctx.log(`${e.name} applies a debuff.`); + ctx.log(`${e.name} casts a debuffing spell.`); } @@ -123,7 +123,7 @@ export function enemyTurn(ctx) { function applyDamage(ctx, target, raw, label) { if (target === ctx.enemy && ctx.enemy.id === "bug_404" && ctx.enemy.turn % 3 === 0) { - ctx.log("404 Bug dodges the attack!"); + ctx.log(`${ctx.enemy.name} phases out and dodges your attack completely!`); return; } @@ -136,7 +136,14 @@ function applyDamage(ctx, target, raw, label) { const hpLoss = Math.max(0, dmg - blocked); target.block -= blocked; target.hp = clamp(target.hp - hpLoss, 0, target.maxHp); - ctx.log(`${label}: ${dmg} β†’ Block ${blocked} β†’ HP -${hpLoss}`); + const isPlayer = target === ctx.player; + if (blocked > 0 && hpLoss > 0) { + ctx.log(`${label} for ${dmg} damage. ${blocked} blocked, ${hpLoss} damage taken.`); + } else if (blocked > 0) { + ctx.log(`${label} for ${dmg} damage, but it's completely blocked!`); + } else { + ctx.log(`${label} for ${dmg} damage!`); + } if (hpLoss > 0 && ctx.showDamageNumber) { @@ -154,16 +161,16 @@ export function makeBattleContext(root) { log: (m) => root.log(m), render: () => root.render(), intentIsAttack: () => root.enemy.intent.type === "attack", - deal: (target, amount) => applyDamage(root, target, amount, "You hit"), - applyWeak: (who, amt) => { who.weak = (who.weak || 0) + amt; root.log(`Weak +${amt}`) }, - applyVulnerable: (who, amt) => { who.vuln = (who.vuln || 0) + amt; root.log(`Vulnerable +${amt}`) }, + deal: (target, amount) => applyDamage(root, target, amount, target === root.enemy ? "You attack" : `${root.enemy.name} hits you`), + applyWeak: (who, amt) => { who.weak = (who.weak || 0) + amt; root.log(`${who === root.player ? 'You are' : root.enemy.name + ' is'} weakened for ${amt} turn${amt > 1 ? 's' : ''}.`) }, + applyVulnerable: (who, amt) => { who.vuln = (who.vuln || 0) + amt; root.log(`${who === root.player ? 'You are' : root.enemy.name + ' is'} vulnerable for ${amt} turn${amt > 1 ? 's' : ''}.`) }, forceEndTurn: () => endTurn(root), promptExhaust: async (count) => { // MVP: exhaust first N non-basics while (count-- > 0 && root.player.hand.length > 0) { const idx = root.player.hand.findIndex(c => !["strike", "defend"].includes(c.id)); const drop = idx >= 0 ? idx : 0; const [ex] = root.player.hand.splice(drop, 1); - root.log(`Exhausted ${ex.name}`); + root.log(`${ex.name} is exhausted and removed from the fight.`); } }, scalarFromWeak: (base) => (root.player.weak > 0 ? Math.floor(base * 0.75) : base), diff --git a/src/ui/render.js b/src/ui/render.js index fc042d9..02a7236 100644 --- a/src/ui/render.js +++ b/src/ui/render.js @@ -31,8 +31,8 @@ export function showDamageNumber(damage, target, isPlayer = false) { } 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"); @@ -40,13 +40,13 @@ export async function renderBattle(root) { const backgroundImage = enemyData?.background || null; - const intentInfo = { + 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' } }[e.intent.type] || { emoji: '', text: 'Unknown intent', color: 'neutral' }; - app.innerHTML = ` + app.innerHTML = `
@@ -172,12 +172,12 @@ 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 ` + ${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 `
@@ -198,8 +198,8 @@ export async function renderBattle(root) { ${!canPlay ? `
Need ${card.cost} energy
` : ''}
`; - }).join('') - } + }).join('') + }
@@ -211,63 +211,78 @@ export async function renderBattle(root) {
+
+ +
+
+ Combat Log +
+
+ ${root.logs.slice(-20).map(log => `
${log}
`).join('')} +
`; - app.querySelectorAll("[data-play]").forEach(btn => { - btn.addEventListener("click", () => { - const index = parseInt(btn.dataset.play, 10); - const card = p.hand[index]; - if (p.energy >= card.cost) { - root.play(index); - } + app.querySelectorAll("[data-play]").forEach(btn => { + btn.addEventListener("click", () => { + const index = parseInt(btn.dataset.play, 10); + const card = p.hand[index]; + if (p.energy >= card.cost) { + root.play(index); + } + }); }); - }); - - 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); - } - }); - } + try { + root.end(); + } catch (error) { + console.error("Error ending turn:", error); + } + }); + } + + window.onkeydown = (e) => { + if (e.key.toLowerCase() === "e") { - 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 card = p.hand[n - 1]; + if (p.energy >= card.cost) { + root.play(n - 1); + } + } + }; - 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 card = p.hand[n - 1]; - if (p.energy >= card.cost) { - root.play(n - 1); - } + // Auto-scroll fight log to bottom + const logContent = document.getElementById('fight-log-content'); + if (logContent) { + logContent.scrollTop = logContent.scrollHeight; } - }; } export async function renderMap(root) { - const { CARDS } = await import("../data/cards.js"); - const { ENEMIES } = await import("../data/enemies.js"); - const m = root.map; - const currentId = root.nodeId; + const { CARDS } = await import("../data/cards.js"); + const { ENEMIES } = await import("../data/enemies.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 = { + const getNodeEmoji = (kind) => { + const emojis = { start: 'Start', battle: 'Battle', elite: 'Battle', @@ -275,49 +290,49 @@ export async function renderMap(root) { rest: 'Rest', shop: 'Shop', event: 'Event' + }; + return emojis[kind] || '❓'; }; - return emojis[kind] || '❓'; - }; - const getNodeDescription = (node) => { + const getNodeDescription = (node) => { switch (node.kind) { - case 'start': + case 'start': return 'Starting Point\nBegin your journey up ThePrimeagen Spire'; - case 'battle': - const enemy = ENEMIES[node.enemy]; + case 'battle': + const enemy = ENEMIES[node.enemy]; return `Battle\nFight: ${enemy?.name || 'Unknown Enemy'}\nHP: ${enemy?.maxHp || '?'}`; - case 'elite': - const elite = ENEMIES[node.enemy]; + 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]; + 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': + case 'rest': return 'Rest Site\nHeal up to 30% max HP\nor upgrade a card'; - case 'shop': + case 'shop': return 'Shop\nSpend your hard-earned gold'; - case 'event': + case 'event': return 'Random Event\nBirthday-themed encounter\nUnknown outcome\nPotential rewards or challenges'; - default: + default: return 'Unknown\nMysterious node'; - } - }; - - const getNodeTooltipData = (node) => { - const description = getNodeDescription(node); - let avatarPath = null; - - if (['battle', 'elite', 'boss'].includes(node.kind) && node.enemy) { - const enemy = ENEMIES[node.enemy]; - if (enemy?.avatar) { - avatarPath = enemy.avatar; - } - } + } + }; - return { description, avatarPath }; - }; + const getNodeTooltipData = (node) => { + const description = getNodeDescription(node); + let avatarPath = null; + + if (['battle', 'elite', 'boss'].includes(node.kind) && node.enemy) { + const enemy = ENEMIES[node.enemy]; + if (enemy?.avatar) { + avatarPath = enemy.avatar; + } + } + + return { description, avatarPath }; + }; - root.app.innerHTML = ` + root.app.innerHTML = `
@@ -531,17 +546,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; - }, {}) + 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'; - - return ` + const card = CARDS[cardId]; + if (!card) return ''; + + const cardType = card.type === 'attack' ? 'attack' : card.type === 'skill' ? 'skill' : 'power'; + + return `
@@ -556,29 +571,29 @@ May this birthday bring joy in each moment you’ve got.

`; - }).join('')} + }).join('')}
-
+
`; - 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; - - let tooltipHTML = ''; - if (avatarPath) { - tooltipHTML = ` + 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 = `
Enemy Avatar

${content}
`; - } else { - tooltipHTML = content; - } - - 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'; + } else { + tooltipHTML = content; + } + + 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 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'; - }; + 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 function renderReward(root, choices) { - root.app.innerHTML = ` + root.app.innerHTML = `

Choose a Card

@@ -663,17 +678,17 @@ export function renderReward(root, choices) {
`; - 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 function renderRest(root) { - root.app.innerHTML = ` + root.app.innerHTML = `

Rest and Recover

@@ -703,35 +718,35 @@ export 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 })) + 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; - } + .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

@@ -740,14 +755,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 } - return ` + return `
@@ -803,7 +818,7 @@ export function renderUpgrade(root) {
`; - }).join("")} + }).join("")}
@@ -812,23 +827,23 @@ 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.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 => { @@ -841,9 +856,9 @@ export function renderShop(root) { 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 = ` + const shopRelic = availableRelics.length > 0 ? RELICS[availableRelics[0]] : null; + + root.app.innerHTML = `

Merchant's Shop

@@ -932,56 +947,56 @@ 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"; - } else { - root.log("Not enough gold!"); - } - }); - }); + 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"; + } 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.`); + 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 }) => { + 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"; - } else { - root.log("Not enough gold!"); - } - }); - } + }); + root.app.querySelector("[data-buy-relic]").disabled = true; + root.app.querySelector("[data-buy-relic]").textContent = "SOLD"; + } 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 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 getRelicEmoji(relicId) { @@ -997,124 +1012,124 @@ function getRelicEmoji(relicId) { } function getRelicName(relicId) { - const names = { + const names = { mech_kb: 'Kinesis', standing_desk: 'Motions', prime_hat: 'VS Code', - coffee_thermos: 'Coffee Thermos', - cpp_compiler: 'C++ Compiler', - chat_mod_sword: 'Chat Mod Sword' - }; - return names[relicId] || relicId; + coffee_thermos: 'Coffee Thermos', + cpp_compiler: 'C++ Compiler', + chat_mod_sword: 'Chat Mod Sword' + }; + return names[relicId] || relicId; } function getRelicText(relicId) { - const texts = { - mech_kb: '+1 card draw each turn.', - standing_desk: '+10 Max HP.', - prime_hat: '-10% damage taken.', - coffee_thermos: 'Start each fight with Coffee Rush.', - cpp_compiler: 'First attack each turn deals double.', - chat_mod_sword: 'Start fights with 1 Weak on all enemies.' - }; - return texts[relicId] || 'Unknown relic'; + const texts = { + mech_kb: '+1 card draw each turn.', + standing_desk: '+10 Max HP.', + prime_hat: '-10% damage taken.', + coffee_thermos: 'Start each fight with Coffee Rush.', + cpp_compiler: 'First attack each turn deals double.', + chat_mod_sword: 'Start fights with 1 Weak on all enemies.' + }; + return texts[relicId] || 'Unknown relic'; } function getCardArt(cardId) { - const artMappings = { - - strike: 'Monk_1.png', - 'strike+': 'Monk_2.png', - - defend: 'Monk_3.png', - 'defend+': 'Monk_4.png', - - coffee_rush: 'Monk_5.png', // Energy boost - 'coffee_rush+': 'Monk_6.png', // Upgraded energy - macro: 'Monk_7.png', // Replay magic - refactor: 'Monk_8.png', // Refactoring tool - type_safety: 'Monk_9.png', // Protection - chat_ban: 'Monk_10.png', // Ban/restriction - - segfault: 'Monk_11.png', // Powerful attack - null_pointer: 'Monk_12.png', // Precise strike - recursion: 'Monk_13.png', // Repetition - merge_conflict: 'Monk_14.png', // Dual attack - hotfix: 'Monk_15.png', // Emergency fix - production_deploy: 'Monk_16.png', // High risk/reward - - gc: 'Monk_17.png', // Cleanup - async_await: 'Monk_18.png', // Time manipulation - stack_overflow: 'Monk_19.png', // Knowledge overflow - infinite_vim: 'Monk_20.png', // Infinite power - debug_print: 'Monk_21.png', // Information - git_commit: 'Monk_22.png', // Recording - memory_leak: 'Monk_23.png', // Draining effect - code_review: 'Monk_24.png', // Investigation - pair_programming: 'Monk_25.png', // Cooperation - rubber_duck: 'Monk_26.png', // Helpful companion - unit_test: 'Monk_27.png', // Testing/verification - - sugar_crash: 'Monk_28.png' // Negative effect - }; + const artMappings = { - const imagePath = artMappings[cardId]; - if (imagePath) { - return `${cardId} + strike: 'Monk_1.png', + 'strike+': 'Monk_2.png', + + defend: 'Monk_3.png', + 'defend+': 'Monk_4.png', + + coffee_rush: 'Monk_5.png', // Energy boost + 'coffee_rush+': 'Monk_6.png', // Upgraded energy + macro: 'Monk_7.png', // Replay magic + refactor: 'Monk_8.png', // Refactoring tool + type_safety: 'Monk_9.png', // Protection + chat_ban: 'Monk_10.png', // Ban/restriction + + segfault: 'Monk_11.png', // Powerful attack + null_pointer: 'Monk_12.png', // Precise strike + recursion: 'Monk_13.png', // Repetition + merge_conflict: 'Monk_14.png', // Dual attack + hotfix: 'Monk_15.png', // Emergency fix + production_deploy: 'Monk_16.png', // High risk/reward + + gc: 'Monk_17.png', // Cleanup + async_await: 'Monk_18.png', // Time manipulation + stack_overflow: 'Monk_19.png', // Knowledge overflow + infinite_vim: 'Monk_20.png', // Infinite power + debug_print: 'Monk_21.png', // Information + git_commit: 'Monk_22.png', // Recording + memory_leak: 'Monk_23.png', // Draining effect + code_review: 'Monk_24.png', // Investigation + pair_programming: 'Monk_25.png', // Cooperation + rubber_duck: 'Monk_26.png', // Helpful companion + unit_test: 'Monk_27.png', // Testing/verification + + sugar_crash: 'Monk_28.png' // Negative effect + }; + + const imagePath = artMappings[cardId]; + if (imagePath) { + return `${cardId} `; - } - - return getCardArtFallback(cardId); + } + + return getCardArtFallback(cardId); } function getCardArtFallback(cardId) { - const fallbacks = { - strike: 'πŸ‘Š', defend: 'πŸ›‘οΈ', coffee_rush: 'β˜•', macro: 'πŸ”„', - refactor: '⚑', type_safety: 'πŸ”’', chat_ban: '🚫', segfault: 'πŸ’₯', - gc: 'πŸ—‘οΈ', async_await: '⏳', stack_overflow: 'πŸ“š', infinite_vim: '♾️', - debug_print: 'πŸ›', null_pointer: '❌', recursion: 'πŸ”', git_commit: 'πŸ“', - memory_leak: 'πŸ•³οΈ', code_review: 'πŸ‘€', pair_programming: 'πŸ‘₯', hotfix: '🚨', - rubber_duck: 'πŸ¦†', merge_conflict: 'βš”οΈ', unit_test: 'βœ…', production_deploy: 'πŸš€', - sugar_crash: '🍰' - }; - return fallbacks[cardId] || 'πŸƒ'; + const fallbacks = { + strike: 'πŸ‘Š', defend: 'πŸ›‘οΈ', coffee_rush: 'β˜•', macro: 'πŸ”„', + refactor: '⚑', type_safety: 'πŸ”’', chat_ban: '🚫', segfault: 'πŸ’₯', + gc: 'πŸ—‘οΈ', async_await: '⏳', stack_overflow: 'πŸ“š', infinite_vim: '♾️', + debug_print: 'πŸ›', null_pointer: '❌', recursion: 'πŸ”', git_commit: 'πŸ“', + memory_leak: 'πŸ•³οΈ', code_review: 'πŸ‘€', pair_programming: 'πŸ‘₯', hotfix: '🚨', + rubber_duck: 'πŸ¦†', merge_conflict: 'βš”οΈ', unit_test: 'βœ…', production_deploy: 'πŸš€', + sugar_crash: '🍰' + }; + return fallbacks[cardId] || 'πŸƒ'; } function getEnemyArt(enemyId, ENEMIES = null) { const enemyData = ENEMIES?.[enemyId]; const avatarPath = enemyData?.avatar || `assets/avatars/${enemyId}.png`; - return `${enemyId} + return `${enemyId} `; } function getEnemyFallbackEmoji(enemyId) { - const arts = { - chat_gremlin: 'πŸ‘Ή', - type_checker: 'πŸ€–', - js_blob: '🟒', - infinite_loop: 'πŸŒ€', - merge_conflict_enemy: 'βš”οΈ', - bug_404: '❌', - elite_ts_demon: '😈', - elite_refactor: 'πŸ‰', - boss_birthday_bug: 'πŸŽ‚πŸ‘Ύ' - }; - return arts[enemyId] || 'πŸ‘Ύ'; + const arts = { + chat_gremlin: 'πŸ‘Ή', + type_checker: 'πŸ€–', + js_blob: '🟒', + infinite_loop: 'πŸŒ€', + merge_conflict_enemy: 'βš”οΈ', + bug_404: '❌', + elite_ts_demon: '😈', + elite_refactor: 'πŸ‰', + boss_birthday_bug: 'πŸŽ‚πŸ‘Ύ' + }; + return arts[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 - - root.app.innerHTML = ` + 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.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 = [ - { + const events = [ + { title: "Birthday Cake", - text: "You find a delicious birthday cake! But it looks suspicious...", + text: "You find a delicious birthday cake! But it looks suspicious...", artwork: "assets/card-art/bread.png", - choices: [ - { + 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)", + 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)", + 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"); - } - } - ] - }, - { + 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?", + 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)", + 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)", + 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)", + 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"); - } - } - ] - }, - { + 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!'", + text: "Colorful balloons float by. One has a note attached: 'Pop me for a surprise!'", artwork: "assets/card-art/feather.png", - choices: [ - { + 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)", + 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)", + 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"); - } + 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)]; - - root.app.innerHTML = ` + const event = events[Math.floor(Math.random() * events.length)]; + + root.app.innerHTML = `

${event.title}

@@ -1363,13 +1378,13 @@ 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 function renderWin(root) { @@ -1383,7 +1398,7 @@ export function renderWin(root) { relicsCollected: root.relicStates.length }; - root.app.innerHTML = ` + root.app.innerHTML = `
@@ -1468,7 +1483,7 @@ export function renderWin(root) {
`; - root.app.querySelector("[data-replay]").addEventListener("click", () => root.reset()); + root.app.querySelector("[data-replay]").addEventListener("click", () => root.reset()); } export function renderLose(root) { @@ -1483,7 +1498,7 @@ export function renderLose(root) { nodeId: root.nodeId || 'unknown' }; - root.app.innerHTML = ` + root.app.innerHTML = `

You Failed!

diff --git a/style.css b/style.css index cc105aa..ff236ea 100644 --- a/style.css +++ b/style.css @@ -197,6 +197,81 @@ h3 { grid-template-rows: auto 1fr; } +.fight-log-panel { + position: fixed; + bottom: 20px; + left: 20px; + width: 500px; + height: 300px; + background: linear-gradient(135deg, rgba(42, 42, 58, 0.4) 0%, rgba(26, 26, 42, 0.4) 100%); + border: 2px solid #3a3a4a; + border-radius: 8px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); + z-index: 1000; + display: flex; + flex-direction: column; +} + +.fight-log-header { + background: linear-gradient(135deg, #3a3a4a 0%, #2a2a3a 100%); + padding: 8px 12px; + border-bottom: 1px solid #4a4a5a; + border-radius: 6px 6px 0 0; +} + +.fight-log-title { + color: #e0e0e0; + font-size: 14px; + font-weight: 600; + font-family: 'JetBrains Mono', monospace; +} + +.fight-log-content { + flex: 1; + padding: 8px; + overflow-y: auto; + overflow-x: hidden; + scrollbar-width: thin; + scrollbar-color: #4a4a5a #2a2a3a; +} + +.fight-log-content::-webkit-scrollbar { + width: 6px; +} + +.fight-log-content::-webkit-scrollbar-track { + background: #2a2a3a; + border-radius: 3px; +} + +.fight-log-content::-webkit-scrollbar-thumb { + background: #4a4a5a; + border-radius: 3px; +} + +.fight-log-content::-webkit-scrollbar-thumb:hover { + background: #5a5a6a; +} + +.log-entry { + color: #c0c0c0; + font-size: 12px; + font-family: 'JetBrains Mono', monospace; + line-height: 1.4; + margin-bottom: 4px; + padding: 2px 4px; + border-radius: 3px; + word-wrap: break-word; +} + +.log-entry:last-child { + margin-bottom: 0; +} + +.log-entry:hover { + background: rgba(255, 255, 255, 0.05); +} + .top-hud { position: relative;