diff --git a/index.html b/index.html index f37bb5b..4e083d7 100644 --- a/index.html +++ b/index.html @@ -11,7 +11,7 @@ - + @@ -25,7 +25,6 @@ - diff --git a/src/data/cards.js b/src/data/cards.js index 876f5cb..7d9bedc 100644 --- a/src/data/cards.js +++ b/src/data/cards.js @@ -3,35 +3,42 @@ export const CARDS = { strike: { id: "strike", name: "Strike", cost: 1, type: "attack", text: "Deal 6.", target: "enemy", + art: "Monk_1.png", effect: (ctx) => ctx.deal(ctx.enemy, ctx.scalarFromWeak(6)), upgrades: "strike+" }, "strike+": { id: "strike+", name: "Strike+", cost: 1, type: "attack", text: "Deal 9.", target: "enemy", + art: "Monk_2.png", effect: (ctx) => ctx.deal(ctx.enemy, ctx.scalarFromWeak(9)) }, defend: { id: "defend", name: "Defend", cost: 1, type: "skill", text: "Gain 5 Block.", target: "self", + art: "Monk_3.png", effect: (ctx) => ctx.player.block += 5, upgrades: "defend+" }, "defend+": { id: "defend+", name: "Defend+", cost: 1, type: "skill", text: "Gain 8 Block.", target: "self", + art: "Monk_4.png", effect: (ctx) => ctx.player.block += 8 }, coffee_rush: { id: "coffee_rush", name: "Terminal Coffee Rush", cost: 0, type: "skill", text: "+2 Energy (this turn).", + art: "Monk_5.png", effect: (ctx) => ctx.player.energy += 2, upgrades: "coffee_rush+" }, "coffee_rush+": { id: "coffee_rush+", name: "Terminal Coffee Rush+", cost: 0, type: "skill", text: "+3 Energy (this turn).", + art: "Monk_6.png", effect: (ctx) => ctx.player.energy += 3 }, macro: { id: "macro", name: "Macrobation", cost: 1, type: "skill", text: "Replay last card for free (once/fight).", + art: "Monk_7.png", oncePerFight: true, effect: (ctx) => { if (ctx.lastCard && ctx.lastCard !== "macro") { @@ -51,26 +58,31 @@ export const CARDS = { segfault: { id: "segfault", name: "Segfault", cost: 2, type: "attack", text: "Deal 7. Draw 1.", + art: "Monk_8.png", effect: (ctx) => { ctx.deal(ctx.enemy, ctx.scalarFromWeak(7)); ctx.draw(1); } }, skill_issue: { id: "skill_issue", name: "Skill Issue", cost: 1, type: "skill", text: "Gain 6 Block. If enemy intends attack β†’ apply Weak(1).", + art: "Monk_9.png", effect: (ctx) => { ctx.player.block += 6; if (ctx.intentIsAttack()) ctx.applyWeak(ctx.enemy, 1); } }, "404": { id: "404", name: "404", cost: 1, type: "skill", text: "Apply Weak(2).", + art: "Monk_10.png", effect: (ctx) => ctx.applyWeak(ctx.enemy, 2) }, dark_mode: { id: "dark_mode", name: "Dark Mode", cost: 2, type: "attack", text: "Deal 20. End your turn.", + art: "Monk_11.png", effect: (ctx) => { ctx.deal(ctx.enemy, ctx.scalarFromWeak(20)); ctx.forceEndTurn() } }, object_object: { id: "object_object", name: "[object Object]", cost: 1, type: "skill", text: "Exhaust 1 card, draw 2.", + art: "Monk_17.png", effect: (ctx) => { ctx.promptExhaust(1); ctx.draw(2); @@ -79,27 +91,32 @@ export const CARDS = { just_one_game: { id: "just_one_game", name: "Just One Game", cost: 1, type: "power", text: "Skip this turn. Next turn +2 Energy.", + art: "Monk_18.png", effect: (ctx) => { ctx.flags.skipThisTurn = true; ctx.flags.nextTurnEnergyBonus = (ctx.flags.nextTurnEnergyBonus || 0) + 2; } }, colon_q: { id: "colon_q", name: "Colon Q", cost: 2, type: "attack", text: "Deal 1 per card in discard.", + art: "Monk_19.png", effect: (ctx) => ctx.deal(ctx.enemy, ctx.scalarFromWeak(Math.max(0, ctx.discard.length))) }, vibe_code: { id: "vibe_code", name: "Vibe Code", cost: 1, type: "skill", text: "Next card costs 0.", + art: "Monk_20.png", effect: (ctx) => ctx.flags.nextCardFree = true }, raw_dog: { id: "raw_dog", name: "Raw Dog", cost: 0, type: "skill", text: "Draw 2. Exhaust.", + art: "Monk_21.png", exhaust: true, effect: (ctx) => ctx.draw(2) }, task_failed_successfully: { id: "task_failed_successfully", name: "Task failed successfully", cost: 2, type: "attack", text: "Deal 8. If enemy has no Block, deal 4 more.", + art: "Monk_12.png", effect: (ctx) => { let dmg = ctx.scalarFromWeak(8); if (ctx.enemy.block === 0) dmg += ctx.scalarFromWeak(4); @@ -109,6 +126,7 @@ export const CARDS = { recursion: { id: "recursion", name: "Recursion", cost: 2, type: "attack", text: "Deal 5. If this kills enemy, play again.", + art: "Monk_13.png", effect: (ctx) => { const prevHp = ctx.enemy.hp; ctx.deal(ctx.enemy, ctx.scalarFromWeak(5)); @@ -123,11 +141,13 @@ export const CARDS = { git_commit: { id: "git_commit", name: "Git Commit", cost: 1, type: "skill", text: "Gain 4 Block. Draw 1.", + art: "Monk_22.png", effect: (ctx) => { ctx.player.block += 4; ctx.draw(1); } }, memory_leak: { id: "memory_leak", name: "Memory Leak", cost: 0, type: "skill", text: "Gain 1 Energy. Next turn -1 Energy.", + art: "Monk_23.png", effect: (ctx) => { ctx.player.energy += 1; ctx.flags.nextTurnEnergyPenalty = (ctx.flags.nextTurnEnergyPenalty || 0) + 1; @@ -136,6 +156,7 @@ export const CARDS = { code_review: { 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); @@ -145,11 +166,13 @@ export const CARDS = { pair_programming: { id: "pair_programming", name: "Pair Programming", cost: 2, type: "skill", text: "Double next card's effect.", + art: "Monk_25.png", effect: (ctx) => ctx.flags.doubleNextCard = true }, hotfix: { id: "hotfix", name: "Hotfix", cost: 2, type: "attack", text: "Deal 10. Can only be played if HP < 50%.", + art: "Monk_15.png", effect: (ctx) => { if (ctx.player.hp <= ctx.player.maxHp * 0.5) { ctx.deal(ctx.enemy, ctx.scalarFromWeak(10)); @@ -161,6 +184,7 @@ export const CARDS = { ligma: { id: "ligma", name: "Ligma", cost: 0, type: "skill", text: "Unalive yourself with -69 hit points. Courtesy of Defysall.", + art: "Monk_26.png", effect: (ctx) => { ctx.player.hp = Math.max(0, ctx.player.hp - 69); ctx.draw(1); @@ -170,6 +194,7 @@ export const CARDS = { merge_conflict: { id: "merge_conflict", name: "Merge Conflict", cost: 2, type: "attack", text: "Deal 6 damage twice.", + art: "Monk_14.png", effect: (ctx) => { ctx.deal(ctx.enemy, ctx.scalarFromWeak(6)); ctx.deal(ctx.enemy, ctx.scalarFromWeak(6)); @@ -178,6 +203,7 @@ export const CARDS = { virgin: { id: "virgin", name: "Virgin", cost: 1, type: "skill", text: "If enemy intends to attack, gain 8 Block.", + art: "Monk_27.png", effect: (ctx) => { if (ctx.intentIsAttack()) { ctx.player.block += 8; @@ -188,6 +214,7 @@ export const CARDS = { production_deploy: { id: "production_deploy", name: "Production Deploy", cost: 2, type: "attack", text: "Deal 25. Lose 5 HP.", + art: "Monk_16.png", effect: (ctx) => { ctx.deal(ctx.enemy, ctx.scalarFromWeak(25)); ctx.player.hp = Math.max(1, ctx.player.hp - 5); @@ -198,6 +225,7 @@ export const CARDS = { sugar_crash: { id: "sugar_crash", name: "Sugar Crash", cost: 1, type: "curse", text: "Unplayable. -1 Energy when drawn.", + art: "Monk_28.png", effect: (ctx) => { ctx.player.energy = Math.max(0, ctx.player.energy - 1); ctx.log("The sugar crash hits hard, draining your energy!"); @@ -206,11 +234,13 @@ export const CARDS = { stack_overflow: { id: "stack_overflow", name: "Stack Overflow", cost: 1, type: "attack", text: "Deal damage equal to cards in hand.", + art: "Monk_29.png", effect: (ctx) => ctx.deal(ctx.enemy, ctx.scalarFromWeak(ctx.player.hand.length)) }, ctrl_z: { id: "ctrl_z", name: "Ctrl+Z", cost: 1, type: "skill", text: "Return a random card from discard to hand.", + art: "Monk_30.png", effect: (ctx) => { if (ctx.player.discard.length > 0) { const randomId = ctx.player.discard[Math.floor(Math.random() * ctx.player.discard.length)]; @@ -227,6 +257,7 @@ export const CARDS = { rubber_duck: { id: "rubber_duck", name: "Rubber Duck Debug", cost: 0, type: "skill", text: "Draw 1. Reveal enemy intent.", + art: "Monk_31.png", effect: (ctx) => { ctx.draw(1); const intent = ctx.enemy.intent; @@ -236,6 +267,7 @@ export const CARDS = { infinite_loop: { id: "infinite_loop", name: "Infinite Loop", cost: 2, type: "skill", text: "Play the same card twice this turn. Exhaust.", + art: "Monk_32.png", exhaust: true, effect: (ctx) => { if (ctx.lastCard && ctx.lastCard !== "infinite_loop") { @@ -253,6 +285,7 @@ export const CARDS = { npm_audit: { id: "npm_audit", name: "npm audit", cost: 1, type: "skill", text: "Gain 3 Block per curse in deck.", + art: "Monk_33.png", effect: (ctx) => { const curseCount = ctx.countCardType("curse"); const blockGain = curseCount * 3; @@ -263,6 +296,7 @@ export const CARDS = { git_push_force: { id: "git_push_force", name: "git push --force", cost: 0, type: "attack", text: "Deal 15. Put random card from hand on top of draw pile.", + art: "Monk_34.png", effect: (ctx) => { ctx.deal(ctx.enemy, ctx.scalarFromWeak(15)); if (ctx.player.hand.length > 1) { // Don't remove this card itself @@ -277,13 +311,34 @@ export const CARDS = { } } }, + + stack_trace: { + id: "stack_trace", name: "Stack Trace", cost: 1, type: "skill", text: "Heal 5 HP.", + art: "Monk_35.png", + effect: (ctx) => { + const healAmount = 5; + ctx.player.hp = Math.min(ctx.player.maxHp, ctx.player.hp + healAmount); + ctx.log(`Stack trace reveals the bug! Heal ${healAmount} HP.`); + } + }, + + refactor: { + id: "refactor", name: "Refactor", cost: 2, type: "skill", text: "Heal 8 HP. Draw 1 card.", + art: "Monk_36.png", + effect: (ctx) => { + const healAmount = 8; + ctx.player.hp = Math.min(ctx.player.maxHp, ctx.player.hp + healAmount); + ctx.draw(1); + ctx.log(`Clean code heals the soul! Heal ${healAmount} HP and draw 1.`); + } + }, }; export const STARTER_DECK = [ "strike", "strike", "defend", "defend", "segfault", "coffee_rush", "skill_issue", "git_commit", - "ligma", "raw_dog" + "stack_trace", "raw_dog" ]; export const CARD_POOL = [ @@ -292,5 +347,5 @@ export const CARD_POOL = [ "raw_dog", "task_failed_successfully", "recursion", "git_commit", "memory_leak", "code_review", "pair_programming", "hotfix", "ligma", "merge_conflict", "virgin", "production_deploy", "stack_overflow", "ctrl_z", "rubber_duck", - "infinite_loop", "npm_audit", "git_push_force" + "infinite_loop", "npm_audit", "git_push_force", "stack_trace", "refactor" ]; diff --git a/src/data/enemies.js b/src/data/enemies.js index 6baffb1..66745ad 100644 --- a/src/data/enemies.js +++ b/src/data/enemies.js @@ -16,8 +16,8 @@ export const ENEMIES = { id: "codegirl", name: "Codegirl", maxHp: 50, avatar: "assets/avatars/codegirl.png", // Warrior with conflicted expression background: "assets/backgrounds/terrace.png", // Repeat background - ai: (turn) => turn <= 4 ? { type: "attack", value: 8 } : { type: "debuff", value: 1 }, - onDebuff: (ctx) => { + ai: (turn) => turn % 4 === 0 ? { type: "heal", value: 8 } : { type: "attack", value: 8 }, + onHeal: (ctx) => { ctx.enemy.hp = Math.min(ctx.enemy.maxHp, ctx.enemy.hp + 8); ctx.log("Codegirl resolves the merge conflict and heals 8 HP!"); } @@ -31,13 +31,13 @@ export const ENEMIES = { }, defyusall: { id: "defyusall", name: "Defyusall", maxHp: 65, - avatar: "assets/avatars/bug_404.png", // Elusive character + avatar: "assets/avatars/15.png", // Elusive character background: "assets/backgrounds/castle.png", ai: (turn) => turn % 3 === 0 ? { type: "block", value: 8 } : { type: "attack", value: 10 }, }, lithium: { id: "lithium", name: "Lithium", maxHp: 55, - avatar: "assets/avatars/type_checker.png", // Scholar/mage with glasses + avatar: "assets/avatars/19.png", // Scholar/mage with glasses background: "assets/backgrounds/dead forest.png", ai: (turn) => (turn % 2 === 0) ? { type: "debuff", value: 1 } : { type: "attack", value: 12 }, onDebuff: (ctx) => ctx.applyWeak(ctx.player, 1) @@ -54,5 +54,54 @@ export const ENEMIES = { return { type: "attack", value: 22 }; // Burst }, onBlock: (ctx) => { ctx.enemy.hp = Math.min(ctx.enemy.maxHp, ctx.enemy.hp + 8); ctx.log("Teej crashes and reboots, healing 8 HP!"); } + }, + + // ACT 2 ENEMIES - Harder versions + senior_dev: { + id: "senior_dev", name: "Senior Dev", 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, + 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, + 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!"); } + }, + scrum_master: { + id: "scrum_master", name: "Scrum Master", maxHp: 90, + avatar: "assets/avatars/js_blob.png", + background: "assets/backgrounds/castle.png", + ai: (turn) => { + const cyc = turn % 3; + if (cyc === 0) return { type: "attack", value: 11 }; + 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."); } + }, + architect: { + id: "architect", name: "The Architect", 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("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!"); } } }; diff --git a/src/data/maps.js b/src/data/maps.js index d125856..26c5f07 100644 --- a/src/data/maps.js +++ b/src/data/maps.js @@ -18,6 +18,24 @@ export const MAPS = { { id: "n13", kind: "rest", next: ["n14"], x: 500, y: 125 }, { id: "n14", kind: "boss", enemy: "teej", 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 }, + ] } }; diff --git a/src/data/relics.js b/src/data/relics.js index d407944..97201ed 100644 --- a/src/data/relics.js +++ b/src/data/relics.js @@ -2,26 +2,31 @@ export const RELICS = { mech_kb: { id: "mech_kb", name: "Kinesis", text: "+1 card draw each turn.", + art: "Monk_29.png", hooks: { onTurnStart: (ctx) => ctx.draw(1) } }, standing_desk: { id: "standing_desk", name: "Vim Motions", text: "+10 Max HP.", + art: "Monk_30.png", hooks: { onRunStart: (ctx) => { ctx.player.maxHp += 10; ctx.player.hp += 10; } } }, prime_hat: { id: "prime_hat", name: "VS Code", text: "-10% damage taken.", + art: "Monk_31.png", hooks: { onDamageTaken: (ctx, dmg) => Math.ceil(dmg * 0.9) } }, coffee_thermos: { id: "coffee_thermos", name: "Terminal Coffee Thermos", text: "Start each fight with Coffee Rush.", + art: "Monk_32.png", hooks: { onBattleStart: (ctx) => { ctx.player.energy += 2; ctx.log("Your coffee thermos provides an energizing boost!") } } }, cpp_compiler: { id: "cpp_compiler", name: "Haskell", text: "First attack each turn deals double.", + art: "Monk_33.png", state: { used: false }, hooks: { onTurnStart: (ctx, st) => st.used = false, @@ -31,6 +36,7 @@ export const RELICS = { chat_mod_sword: { id: "chat_mod_sword", name: "Worst Streamer Award", text: "Start fights with 1 Weak on all enemies.", + art: "Monk_34.png", hooks: { onBattleStart: (ctx) => { ctx.applyWeak(ctx.enemy, 1); diff --git a/src/engine/battle.js b/src/engine/battle.js index dc7c34f..a4c4ae1 100644 --- a/src/engine/battle.js +++ b/src/engine/battle.js @@ -111,6 +111,8 @@ export function enemyTurn(ctx) { } else if (e.intent.type === "debuff") { ENEMIES[e.id].onDebuff?.(ctx, e.intent.value); ctx.log(`${e.name} casts a debuffing spell.`); + } else if (e.intent.type === "heal") { + ENEMIES[e.id].onHeal?.(ctx, e.intent.value); } diff --git a/src/main.js b/src/main.js index dc2db9f..a94b652 100644 --- a/src/main.js +++ b/src/main.js @@ -11,6 +11,7 @@ const app = document.getElementById("app"); const root = { app, logs: [], map: MAPS.act1, nodeId: "n1", + currentAct: "act1", player: makePlayer(), relicStates: [], completedNodes: [], @@ -23,7 +24,7 @@ const root = { end() { endTurn(this); }, - go(nextId) { + async go(nextId) { this.nodeId = nextId; // Always set nodeId (needed for battle logic) const node = this.map.nodes.find(n => n.id === nextId); if (!node) return; @@ -32,12 +33,12 @@ const root = { this._battleInProgress = true; createBattle(this, node.enemy); - renderBattle(this); + await renderBattle(this); } else { this.save(); if (node.kind === "rest") { - renderRest(this); + await renderRest(this); } else if (node.kind === "shop") { renderShop(this); } else if (node.kind === "event") { @@ -48,7 +49,7 @@ const root = { } }, - afterNode() { + async afterNode() { if (this.nodeId && !this.completedNodes.includes(this.nodeId)) { this.completedNodes.push(this.nodeId); } @@ -58,11 +59,11 @@ const root = { if (node.kind === "battle" || node.kind === "elite") { const choices = pickCards(3); this._pendingChoices = choices; - renderReward(this, choices); + await renderReward(this, choices); return; } if (node.kind === "boss") { - renderWin(this); return; + await renderWin(this); return; } renderMap(this); @@ -84,7 +85,7 @@ const root = { renderMap(this); }, - onWin() { + async onWin() { this.log("Enemy defeated! πŸŽ‰"); const goldReward = Math.floor(Math.random() * 20) + 15; // 15-35 gold @@ -96,9 +97,23 @@ const root = { const node = this.map.nodes.find(n => n.id === this.nodeId); if (node.kind === "boss") { - this.save(); // Save progress before clearing on victory - this.clearSave(); // Clear save on victory - renderWin(this); + // Check if there's a next act + const nextAct = this.currentAct === "act1" ? "act2" : null; + if (nextAct && MAPS[nextAct]) { + // Advance to next act + this.currentAct = nextAct; + this.map = MAPS[nextAct]; + this.nodeId = this.map.nodes.find(n => n.kind === "start").id; + this.completedNodes = []; + this.log(`πŸŽ‰ Act ${this.currentAct === "act2" ? "II" : "I"} Complete! Advancing to the next challenge...`); + this.save(); + renderMap(this); + } else { + // Final victory + this.save(); // Save progress before clearing on victory + this.clearSave(); // Clear save on victory + await renderWin(this); + } } else { @@ -106,17 +121,19 @@ const root = { this.afterNode(); } }, - onLose() { + async onLose() { this._battleInProgress = false; this.clearSave(); // Clear save on defeat - renderLose(this); + await renderLose(this); }, reset() { this.logs = []; this.player = makePlayer(); initDeck(this.player); + this.currentAct = "act1"; + this.map = MAPS.act1; this.nodeId = "n1"; this.completedNodes = []; @@ -135,6 +152,7 @@ const root = { const saveData = { player: this.player, nodeId: this.nodeId, + currentAct: this.currentAct, relicStates: this.relicStates, completedNodes: this.completedNodes, logs: this.logs.slice(-50), // Keep last 50 logs @@ -154,6 +172,8 @@ const root = { const data = JSON.parse(saveData); this.player = data.player; this.nodeId = data.nodeId; + this.currentAct = data.currentAct || "act1"; + this.map = MAPS[this.currentAct]; this.relicStates = data.relicStates || []; this.completedNodes = data.completedNodes || []; this.logs = data.logs || []; @@ -228,7 +248,7 @@ root.go = function(nextId) { }; -function initializeGame() { +async function initializeGame() { const urlParams = new URLSearchParams(window.location.search); const screenParam = urlParams.get('screen'); const dev = urlParams.get('dev'); @@ -238,17 +258,10 @@ function initializeGame() { const now = new Date(); const birthday = new Date('2025-09-09T00:00:00'); - console.log('Current date:', now); - console.log('Birthday date:', birthday); - console.log('Before birthday?', now < birthday); - console.log('Dev mode?', dev); if (now < birthday && dev !== 'true') { - console.log('Showing countdown!'); showCountdown(birthday); return; - } else { - console.log('Showing game - either past birthday or dev mode'); } if (screenParam) { @@ -257,20 +270,20 @@ function initializeGame() { switch (screenParam.toLowerCase()) { case 'victory': case 'win': - renderWin(root); + await renderWin(root); return; case 'defeat': case 'lose': - renderLose(root); + await renderLose(root); return; case 'map': - renderMap(root); + await renderMap(root); return; case 'shop': renderShop(root); return; case 'rest': - renderRest(root); + await renderRest(root); return; case 'event': renderEvent(root); @@ -279,7 +292,7 @@ function initializeGame() { root.go('n2'); // Battle node return; case 'upgrade': - renderRest(root); + await renderRest(root); setTimeout(() => { const upgradeBtn = root.app.querySelector("[data-act='upgrade']"); if (upgradeBtn) upgradeBtn.click(); @@ -311,13 +324,27 @@ function setupMockData() { attachRelics(root, ['coffee_thermos', 'cpp_compiler']); - root.completedNodes = ['n1', 'n2', 'n4']; - root.nodeId = 'n7'; - root.logs = [ - 'Game loaded for testing', - 'Mock data initialized', - 'Ready for screen testing!' - ]; + // Test Act 2 if ?act2=true is in URL + const urlParams = new URLSearchParams(window.location.search); + if (urlParams.get('act2') === 'true') { + root.currentAct = 'act2'; + root.map = MAPS.act2; + root.completedNodes = ['a2n1', 'a2n2']; + root.nodeId = 'a2n5'; + root.logs = [ + 'Game loaded for testing', + 'Mock data initialized', + 'Testing Act 2: The Corporate Ladder!' + ]; + } else { + root.completedNodes = ['n1', 'n2', 'n4']; + root.nodeId = 'n7'; + root.logs = [ + 'Game loaded for testing', + 'Mock data initialized', + 'Ready for screen testing!' + ]; + } } function showCountdown(birthday) { diff --git a/src/ui/render.js b/src/ui/render.js index d94cf2f..175f339 100644 --- a/src/ui/render.js +++ b/src/ui/render.js @@ -47,6 +47,8 @@ export async function renderBattle(root) { 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; @@ -54,7 +56,8 @@ export async function renderBattle(root) { 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' } + 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 = ` @@ -198,7 +201,7 @@ export async function renderBattle(root) {
-
${getCardArt(card.id)}
+
${getCardArt(card.id, CARDS)}
${card.type}
@@ -317,6 +320,7 @@ export async function renderBattle(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; @@ -439,8 +443,8 @@ export async function renderMap(root) { Relics
${root.relicStates.map(r => ` -
- ${getRelicEmoji(r.id)} +
+ ${getRelicArt(r.id, RELICS)}
`).join('')}
@@ -481,6 +485,20 @@ May this birthday bring joy in each moment you’ve got.

+
+
+
+
+
Act I
+
Junior Dev
+
+
+
+
Act II
+
Corporate Ladder
+
+
+
@@ -496,37 +514,37 @@ May this birthday bring joy in each moment you’ve got.

${(() => { - // Use positions directly from the map data - const getNodePos = (nodeId) => { - const node = m.nodes.find(n => n.id === nodeId); - return node ? { x: node.x, y: node.y } : null; - }; - - return m.nodes.map(node => { - if (!node.next || node.next.length === 0) return ''; + // Use positions directly from the map data + const getNodePos = (nodeId) => { + const node = m.nodes.find(n => n.id === nodeId); + return node ? { x: node.x, y: node.y } : null; + }; + + return m.nodes.map(node => { + if (!node.next || node.next.length === 0) return ''; - return node.next.map(nextId => { - const fromPos = { x: node.x, y: node.y }; - const toPos = getNodePos(nextId); - if (!fromPos || !toPos) return ''; + return node.next.map(nextId => { + const fromPos = { x: node.x, y: node.y }; + const toPos = getNodePos(nextId); + if (!fromPos || !toPos) return ''; - const isActivePath = (node.id === currentId && nextIds.includes(nextId)) || - (parseInt(nextId.replace('n', '')) <= parseInt(currentId.replace('n', ''))); + const isActivePath = (node.id === currentId && nextIds.includes(nextId)) || + (parseInt(nextId.replace('n', '')) <= parseInt(currentId.replace('n', ''))); - return ``; - }).join(''); - }).join(''); - })()} + }).join(''); + }).join(''); + })()}
${(() => { - // Use positions directly from map data + // Use positions directly from map data return m.nodes.map(n => { const isNext = nextIds.includes(n.id); @@ -561,6 +579,7 @@ May this birthday bring joy in each moment you’ve got.

+
@@ -585,7 +604,7 @@ May this birthday bring joy in each moment you’ve got.

${card.name}
${card.cost}
-
${getCardArt(cardId)}
+
${getCardArt(cardId, CARDS)}
${card.text}
@@ -662,7 +681,8 @@ May this birthday bring joy in each moment you’ve got.

}); } -export function renderReward(root, choices) { +export async function renderReward(root, choices) { + const { CARDS } = await import("../data/cards.js"); root.app.innerHTML = `

Choose a Card

@@ -680,7 +700,7 @@ export function renderReward(root, choices) {
-
${getCardArt(c.id)}
+
${getCardArt(c.id, CARDS)}
${c.type}
@@ -709,7 +729,8 @@ export function renderReward(root, choices) { root.app.querySelector("[data-skip]").addEventListener("click", () => root.skipReward()); } -export function renderRest(root) { +export async function renderRest(root) { + const { CARDS } = await import("../data/cards.js"); root.app.innerHTML = `
@@ -804,7 +825,7 @@ export function renderUpgrade(root) {
-
${getCardArt(card.id)}
+
${getCardArt(card.id, CARDS)}
${card.type}
@@ -826,7 +847,7 @@ export function renderUpgrade(root) {
-
${getCardArt(upgradedCard.id)}
+
${getCardArt(upgradedCard.id, CARDS)}
${upgradedCard.type}
@@ -913,7 +934,7 @@ export function renderShop(root) {
-
${getCardArt(card.id)}
+
${getCardArt(card.id, CARDS)}
${card.type}
@@ -943,7 +964,7 @@ export function renderShop(root) {
-
${getRelicEmoji(shopRelic.id)}
+
${getRelicArt(shopRelic.id, RELICS)}

${shopRelic.name}

${shopRelic.text}

@@ -952,7 +973,6 @@ export function renderShop(root) { Gold 100
- ${(root.player.gold || 100) < 100 ? `
Need 100 gold
` : ''}
@@ -1105,122 +1125,36 @@ function shuffle(array) { return array; } -function getRelicEmoji(relicId) { - const relicArt = { - mech_kb: 'Kinesis', - standing_desk: 'Motions', - prime_hat: 'VS Code', - coffee_thermos: 'Coffee Thermos', - cpp_compiler: 'CPP Compiler', - chat_mod_sword: 'Chat Mod Sword' - }; - return relicArt[relicId] || 'πŸ’Ž'; +function getRelicArt(relicId, RELICS = null) { + if (RELICS && RELICS[relicId]?.art) { + const imagePath = RELICS[relicId].art; + return `${relicId}`; + } + return 'πŸ’Ž'; } -function getRelicName(relicId) { - 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; +function getRelicName(relicId, RELICS = null) { + return RELICS?.[relicId]?.name || 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'; +function getRelicText(relicId, RELICS = null) { + return RELICS?.[relicId]?.text || '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 - segfault: 'Monk_8.png', // Refactoring tool - skill_issue: 'Monk_9.png', // Protection - "404": 'Monk_10.png', // Ban/restriction - - dark_mode: 'Monk_11.png', // Powerful attack - task_failed_successfully: '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 - - object_object: 'Monk_17.png', // Cleanup - just_one_game: 'Monk_18.png', // Time manipulation - colon_q: 'Monk_19.png', // Knowledge overflow - vibe_code: 'Monk_20.png', // Infinite power - raw_dog: '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 - ligma: 'Monk_26.png', // Helpful companion - virgin: 'Monk_27.png', // Testing/verification - - sugar_crash: 'Monk_28.png' // Negative effect - }; - - const imagePath = artMappings[cardId]; - if (imagePath) { - return `${cardId} - `; +function getCardArt(cardId, CARDS = null) { + if (CARDS && CARDS[cardId]?.art) { + const imagePath = CARDS[cardId].art; + return `${cardId}`; } - - return getCardArtFallback(cardId); -} - -function getCardArtFallback(cardId) { - const fallbacks = { - strike: 'πŸ‘Š', defend: 'πŸ›‘οΈ', coffee_rush: 'β˜•', macro: 'πŸ”„', - segfault: '⚑', skill_issue: 'πŸ”’', "404": '🚫', dark_mode: 'πŸ’₯', - object_object: 'πŸ—‘οΈ', just_one_game: '⏳', colon_q: 'πŸ“š', vibe_code: '♾️', - raw_dog: 'πŸ›', task_failed_successfully: '❌', recursion: 'πŸ”', git_commit: 'πŸ“', - memory_leak: 'πŸ•³οΈ', code_review: 'πŸ‘€', pair_programming: 'πŸ‘₯', hotfix: '🚨', - ligma: 'πŸ¦†', merge_conflict: 'βš”οΈ', virgin: 'βœ…', production_deploy: 'πŸš€', - sugar_crash: '🍰' - }; - return fallbacks[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} - `; -} - -function getEnemyFallbackEmoji(enemyId) { - const arts = { - old_man_judo: 'πŸ‘Ή', - beastco: 'πŸŒ€', - codegirl: 'βš”οΈ', - defyusall: '🚫', - lithium: '⚑', - nightshadedude: '😈', - teej: 'πŸŽ‚πŸ‘Ύ' - }; - return arts[enemyId] || 'πŸ‘Ύ'; + return `${enemyId}`; } function getEnemyType(enemyId) { @@ -1285,7 +1219,7 @@ export function renderRelicSelection(root) { return `
-
${getRelicEmoji(relicId)}
+
${getRelicArt(relicId, RELICS)}
${relic.name}
@@ -1491,7 +1425,8 @@ export function renderEvent(root) { }); } -export function renderWin(root) { +export async function renderWin(root) { + const { RELICS } = await import("../data/relics.js"); const finalStats = { totalTurns: root.turnCount || 0, cardsPlayed: root.cardsPlayedCount || 0, @@ -1560,9 +1495,9 @@ export function renderWin(root) {
${root.relicStates.length > 0 ? root.relicStates.map(r => ` -
-
${getRelicEmoji(r.id)}
-
${getRelicName(r.id)}
+
+
${getRelicArt(r.id, RELICS)}
+
${getRelicName(r.id, RELICS)}
`).join('') : '
No relics collected this run
' @@ -1590,7 +1525,8 @@ export function renderWin(root) { root.app.querySelector("[data-replay]").addEventListener("click", () => root.reset()); } -export function renderLose(root) { +export async function renderLose(root) { + const { RELICS } = await import("../data/relics.js"); const finalStats = { totalTurns: root.turnCount || 0, cardsPlayed: root.cardsPlayedCount || 0, @@ -1659,7 +1595,7 @@ Better luck on the next run!

${root.relicStates.map(relic => `
-
${getRelicEmoji(relic.id)}
+
${getRelicArt(relic.id, RELICS)}
${relic.id.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
`).join('')} diff --git a/style.css b/style.css index c3adb9a..df89bbd 100644 --- a/style.css +++ b/style.css @@ -1241,6 +1241,7 @@ h3 { .intent-danger { border-color: #dc3545; background: rgba(220, 53, 69, 0.1); } .intent-info { border-color: #17a2b8; background: rgba(23, 162, 184, 0.1); } .intent-warning { border-color: #ffc107; } +.intent-success { border-color: #28a745; background: rgba(40, 167, 69, 0.1); } .intent-icon { font-size: 24px; @@ -2177,7 +2178,6 @@ h3 { .map-screen { width: 100%; margin: 0; - padding: 20px; min-height: 100vh; background: url('assets/backgrounds/terrace.png'); background-size: cover; @@ -2195,7 +2195,6 @@ h3 { display: flex; justify-content: space-between; align-items: center; - margin-bottom: 20px; } .map-header-section h1 { @@ -2270,7 +2269,7 @@ h3 { .player-status { width: 1450px; - margin: 0 auto 20px auto; + margin: 0 auto 10px auto; display: flex; gap: 30px; align-items: center; @@ -2279,7 +2278,7 @@ h3 { linear-gradient(135deg, #2a2a3a 0%, #1a1a2a 100%); border: 2px solid #3a3a4a; border-radius: 6px; - padding: 20px; + padding: 10px 20px; box-shadow: inset 0 1px 0 rgba(255,255,255,0.1); } @@ -2854,6 +2853,94 @@ h3 { gap: 20px; } +/* Act Progress Indicator */ +.act-progress-indicator { + margin: 0 0 10px 0; + border-radius: 4px; + width: 930px; +} + +.act-progress-title { + font-size: 11px; + font-weight: 500; + color: rgba(212, 175, 55, 0.7); + margin-bottom: 6px; + text-align: center; + opacity: 0.8; +} + +.act-progress-bar { + display: flex; + align-items: center; + justify-content: center; + gap: 0; +} + +.act-step { + display: flex; + flex-direction: column; + align-items: center; + padding: 4px 12px; + border-radius: 3px; + transition: all 0.2s ease; +} + +.act-step.locked { + opacity: 0.3; + border: 1px solid rgba(212, 175, 55, 0.4); +} + +.act-step.current { + background: rgba(212, 175, 55, 0.1); + border: 1px solid rgba(212, 175, 55, 0.4); +} + +.act-step.completed { + background: rgba(76, 175, 80, 0.1); + border: 1px solid rgba(76, 175, 80, 0.4); +} + +.act-number { + font-size: 14px; + font-weight: 600; + font-family: "Kreon", serif; + margin-bottom: 2px; +} + +.act-step.current .act-number { + color: var(--accent); +} + +.act-step.completed .act-number { + color: #4CAF50; +} + +.act-step.locked .act-number { + color: #555; +} + +.act-name { + font-size: 12px; + font-weight: 400; + text-align: center; + line-height: 1.1; + opacity: 0.8; +} + +.act-connector { + width: 40px; + height: 2px; + background: rgba(68, 68, 68, 0.4); + margin: 0 6px; + border-radius: 1px; + transition: all 0.2s ease; +} + +.act-connector.active { + background: rgba(212, 175, 55, 0.6); + box-shadow: 0 0 4px rgba(212, 175, 55, 0.3); +} + .spire-map { position: relative; width: 930px;