Browse Source

add acts, refactor mappings to be more together, cleaning up

main
Stephanie Gredell 4 months ago
parent
commit
224b3f8053
  1. 3
      index.html
  2. 59
      src/data/cards.js
  3. 57
      src/data/enemies.js
  4. 18
      src/data/maps.js
  5. 6
      src/data/relics.js
  6. 2
      src/engine/battle.js
  7. 73
      src/main.js
  8. 182
      src/ui/render.js
  9. 95
      style.css

3
index.html

@ -11,7 +11,7 @@
<link rel="stylesheet" href="./style.css"> <link rel="stylesheet" href="./style.css">
<!-- Preload critical map background --> <!-- Preload critical map background -->
<link rel="preload" as="image" href="assets/card-art/terrace.png"> <link rel="preload" as="image" href="assets/backgrounds/terrace.png">
<!-- Prefetch map node icons --> <!-- Prefetch map node icons -->
<link rel="prefetch" as="image" href="assets/card-art/staff.png"> <link rel="prefetch" as="image" href="assets/card-art/staff.png">
@ -25,7 +25,6 @@
<!-- Prefetch enemy avatars for map tooltips --> <!-- Prefetch enemy avatars for map tooltips -->
<link rel="prefetch" as="image" href="assets/avatars/13.png"> <link rel="prefetch" as="image" href="assets/avatars/13.png">
<link rel="prefetch" as="image" href="assets/avatars/2.png"> <link rel="prefetch" as="image" href="assets/avatars/2.png">
<link rel="prefetch" as="image" href="assets/avatars/merge_conflict_enemy.png">
<link rel="prefetch" as="image" href="assets/avatars/11.png"> <link rel="prefetch" as="image" href="assets/avatars/11.png">
<link rel="prefetch" as="image" href="assets/avatars/elite_refactor.png"> <link rel="prefetch" as="image" href="assets/avatars/elite_refactor.png">
<link rel="prefetch" as="image" href="assets/avatars/boss_birthday_bug.png"> <link rel="prefetch" as="image" href="assets/avatars/boss_birthday_bug.png">

59
src/data/cards.js

@ -3,35 +3,42 @@
export const CARDS = { export const CARDS = {
strike: { strike: {
id: "strike", name: "Strike", cost: 1, type: "attack", text: "Deal 6.", target: "enemy", 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)), effect: (ctx) => ctx.deal(ctx.enemy, ctx.scalarFromWeak(6)),
upgrades: "strike+" upgrades: "strike+"
}, },
"strike+": { "strike+": {
id: "strike+", name: "Strike+", cost: 1, type: "attack", text: "Deal 9.", target: "enemy", 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)) effect: (ctx) => ctx.deal(ctx.enemy, ctx.scalarFromWeak(9))
}, },
defend: { defend: {
id: "defend", name: "Defend", cost: 1, type: "skill", text: "Gain 5 Block.", target: "self", id: "defend", name: "Defend", cost: 1, type: "skill", text: "Gain 5 Block.", target: "self",
art: "Monk_3.png",
effect: (ctx) => ctx.player.block += 5, effect: (ctx) => ctx.player.block += 5,
upgrades: "defend+" upgrades: "defend+"
}, },
"defend+": { "defend+": {
id: "defend+", name: "Defend+", cost: 1, type: "skill", text: "Gain 8 Block.", target: "self", id: "defend+", name: "Defend+", cost: 1, type: "skill", text: "Gain 8 Block.", target: "self",
art: "Monk_4.png",
effect: (ctx) => ctx.player.block += 8 effect: (ctx) => ctx.player.block += 8
}, },
coffee_rush: { coffee_rush: {
id: "coffee_rush", name: "Terminal Coffee Rush", cost: 0, type: "skill", text: "+2 Energy (this turn).", 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, effect: (ctx) => ctx.player.energy += 2,
upgrades: "coffee_rush+" upgrades: "coffee_rush+"
}, },
"coffee_rush+": { "coffee_rush+": {
id: "coffee_rush+", name: "Terminal Coffee Rush+", cost: 0, type: "skill", text: "+3 Energy (this turn).", 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 effect: (ctx) => ctx.player.energy += 3
}, },
macro: { macro: {
id: "macro", name: "Macrobation", cost: 1, type: "skill", text: "Replay last card for free (once/fight).", id: "macro", name: "Macrobation", cost: 1, type: "skill", text: "Replay last card for free (once/fight).",
art: "Monk_7.png",
oncePerFight: true, oncePerFight: true,
effect: (ctx) => { effect: (ctx) => {
if (ctx.lastCard && ctx.lastCard !== "macro") { if (ctx.lastCard && ctx.lastCard !== "macro") {
@ -51,26 +58,31 @@ export const CARDS = {
segfault: { segfault: {
id: "segfault", name: "Segfault", cost: 2, type: "attack", text: "Deal 7. Draw 1.", 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); } effect: (ctx) => { ctx.deal(ctx.enemy, ctx.scalarFromWeak(7)); ctx.draw(1); }
}, },
skill_issue: { skill_issue: {
id: "skill_issue", name: "Skill Issue", cost: 1, type: "skill", text: "Gain 6 Block. If enemy intends attack → apply Weak(1).", 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); } effect: (ctx) => { ctx.player.block += 6; if (ctx.intentIsAttack()) ctx.applyWeak(ctx.enemy, 1); }
}, },
"404": { "404": {
id: "404", name: "404", cost: 1, type: "skill", text: "Apply Weak(2).", id: "404", name: "404", cost: 1, type: "skill", text: "Apply Weak(2).",
art: "Monk_10.png",
effect: (ctx) => ctx.applyWeak(ctx.enemy, 2) effect: (ctx) => ctx.applyWeak(ctx.enemy, 2)
}, },
dark_mode: { dark_mode: {
id: "dark_mode", name: "Dark Mode", cost: 2, type: "attack", text: "Deal 20. End your turn.", 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() } effect: (ctx) => { ctx.deal(ctx.enemy, ctx.scalarFromWeak(20)); ctx.forceEndTurn() }
}, },
object_object: { object_object: {
id: "object_object", name: "[object Object]", cost: 1, type: "skill", text: "Exhaust 1 card, draw 2.", id: "object_object", name: "[object Object]", cost: 1, type: "skill", text: "Exhaust 1 card, draw 2.",
art: "Monk_17.png",
effect: (ctx) => { effect: (ctx) => {
ctx.promptExhaust(1); ctx.promptExhaust(1);
ctx.draw(2); ctx.draw(2);
@ -79,27 +91,32 @@ export const CARDS = {
just_one_game: { just_one_game: {
id: "just_one_game", name: "Just One Game", cost: 1, type: "power", text: "Skip this turn. Next turn +2 Energy.", 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; } effect: (ctx) => { ctx.flags.skipThisTurn = true; ctx.flags.nextTurnEnergyBonus = (ctx.flags.nextTurnEnergyBonus || 0) + 2; }
}, },
colon_q: { colon_q: {
id: "colon_q", name: "Colon Q", cost: 2, type: "attack", text: "Deal 1 per card in discard.", 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))) effect: (ctx) => ctx.deal(ctx.enemy, ctx.scalarFromWeak(Math.max(0, ctx.discard.length)))
}, },
vibe_code: { vibe_code: {
id: "vibe_code", name: "Vibe Code", cost: 1, type: "skill", text: "Next card costs 0.", 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 effect: (ctx) => ctx.flags.nextCardFree = true
}, },
raw_dog: { raw_dog: {
id: "raw_dog", name: "Raw Dog", cost: 0, type: "skill", text: "Draw 2. Exhaust.", id: "raw_dog", name: "Raw Dog", cost: 0, type: "skill", text: "Draw 2. Exhaust.",
art: "Monk_21.png",
exhaust: true, exhaust: true,
effect: (ctx) => ctx.draw(2) effect: (ctx) => ctx.draw(2)
}, },
task_failed_successfully: { 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.", 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) => { effect: (ctx) => {
let dmg = ctx.scalarFromWeak(8); let dmg = ctx.scalarFromWeak(8);
if (ctx.enemy.block === 0) dmg += ctx.scalarFromWeak(4); if (ctx.enemy.block === 0) dmg += ctx.scalarFromWeak(4);
@ -109,6 +126,7 @@ export const CARDS = {
recursion: { recursion: {
id: "recursion", name: "Recursion", cost: 2, type: "attack", text: "Deal 5. If this kills enemy, play again.", id: "recursion", name: "Recursion", cost: 2, type: "attack", text: "Deal 5. If this kills enemy, play again.",
art: "Monk_13.png",
effect: (ctx) => { effect: (ctx) => {
const prevHp = ctx.enemy.hp; const prevHp = ctx.enemy.hp;
ctx.deal(ctx.enemy, ctx.scalarFromWeak(5)); ctx.deal(ctx.enemy, ctx.scalarFromWeak(5));
@ -123,11 +141,13 @@ export const CARDS = {
git_commit: { git_commit: {
id: "git_commit", name: "Git Commit", cost: 1, type: "skill", text: "Gain 4 Block. Draw 1.", 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); } effect: (ctx) => { ctx.player.block += 4; ctx.draw(1); }
}, },
memory_leak: { memory_leak: {
id: "memory_leak", name: "Memory Leak", cost: 0, type: "skill", text: "Gain 1 Energy. Next turn -1 Energy.", id: "memory_leak", name: "Memory Leak", cost: 0, type: "skill", text: "Gain 1 Energy. Next turn -1 Energy.",
art: "Monk_23.png",
effect: (ctx) => { effect: (ctx) => {
ctx.player.energy += 1; ctx.player.energy += 1;
ctx.flags.nextTurnEnergyPenalty = (ctx.flags.nextTurnEnergyPenalty || 0) + 1; ctx.flags.nextTurnEnergyPenalty = (ctx.flags.nextTurnEnergyPenalty || 0) + 1;
@ -136,6 +156,7 @@ export const CARDS = {
code_review: { 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.", 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) => { effect: (ctx) => {
ctx.draw(1); ctx.draw(1);
@ -145,11 +166,13 @@ export const CARDS = {
pair_programming: { pair_programming: {
id: "pair_programming", name: "Pair Programming", cost: 2, type: "skill", text: "Double next card's effect.", 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 effect: (ctx) => ctx.flags.doubleNextCard = true
}, },
hotfix: { hotfix: {
id: "hotfix", name: "Hotfix", cost: 2, type: "attack", text: "Deal 10. Can only be played if HP < 50%.", id: "hotfix", name: "Hotfix", cost: 2, type: "attack", text: "Deal 10. Can only be played if HP < 50%.",
art: "Monk_15.png",
effect: (ctx) => { effect: (ctx) => {
if (ctx.player.hp <= ctx.player.maxHp * 0.5) { if (ctx.player.hp <= ctx.player.maxHp * 0.5) {
ctx.deal(ctx.enemy, ctx.scalarFromWeak(10)); ctx.deal(ctx.enemy, ctx.scalarFromWeak(10));
@ -161,6 +184,7 @@ export const CARDS = {
ligma: { ligma: {
id: "ligma", name: "Ligma", cost: 0, type: "skill", text: "Unalive yourself with -69 hit points. Courtesy of Defysall.", id: "ligma", name: "Ligma", cost: 0, type: "skill", text: "Unalive yourself with -69 hit points. Courtesy of Defysall.",
art: "Monk_26.png",
effect: (ctx) => { effect: (ctx) => {
ctx.player.hp = Math.max(0, ctx.player.hp - 69); ctx.player.hp = Math.max(0, ctx.player.hp - 69);
ctx.draw(1); ctx.draw(1);
@ -170,6 +194,7 @@ export const CARDS = {
merge_conflict: { merge_conflict: {
id: "merge_conflict", name: "Merge Conflict", cost: 2, type: "attack", text: "Deal 6 damage twice.", id: "merge_conflict", name: "Merge Conflict", cost: 2, type: "attack", text: "Deal 6 damage twice.",
art: "Monk_14.png",
effect: (ctx) => { effect: (ctx) => {
ctx.deal(ctx.enemy, ctx.scalarFromWeak(6)); ctx.deal(ctx.enemy, ctx.scalarFromWeak(6));
ctx.deal(ctx.enemy, ctx.scalarFromWeak(6)); ctx.deal(ctx.enemy, ctx.scalarFromWeak(6));
@ -178,6 +203,7 @@ export const CARDS = {
virgin: { virgin: {
id: "virgin", name: "Virgin", cost: 1, type: "skill", text: "If enemy intends to attack, gain 8 Block.", id: "virgin", name: "Virgin", cost: 1, type: "skill", text: "If enemy intends to attack, gain 8 Block.",
art: "Monk_27.png",
effect: (ctx) => { effect: (ctx) => {
if (ctx.intentIsAttack()) { if (ctx.intentIsAttack()) {
ctx.player.block += 8; ctx.player.block += 8;
@ -188,6 +214,7 @@ export const CARDS = {
production_deploy: { production_deploy: {
id: "production_deploy", name: "Production Deploy", cost: 2, type: "attack", text: "Deal 25. Lose 5 HP.", id: "production_deploy", name: "Production Deploy", cost: 2, type: "attack", text: "Deal 25. Lose 5 HP.",
art: "Monk_16.png",
effect: (ctx) => { effect: (ctx) => {
ctx.deal(ctx.enemy, ctx.scalarFromWeak(25)); ctx.deal(ctx.enemy, ctx.scalarFromWeak(25));
ctx.player.hp = Math.max(1, ctx.player.hp - 5); ctx.player.hp = Math.max(1, ctx.player.hp - 5);
@ -198,6 +225,7 @@ export const CARDS = {
sugar_crash: { sugar_crash: {
id: "sugar_crash", name: "Sugar Crash", cost: 1, type: "curse", text: "Unplayable. -1 Energy when drawn.", id: "sugar_crash", name: "Sugar Crash", cost: 1, type: "curse", text: "Unplayable. -1 Energy when drawn.",
art: "Monk_28.png",
effect: (ctx) => { effect: (ctx) => {
ctx.player.energy = Math.max(0, ctx.player.energy - 1); ctx.player.energy = Math.max(0, ctx.player.energy - 1);
ctx.log("The sugar crash hits hard, draining your energy!"); ctx.log("The sugar crash hits hard, draining your energy!");
@ -206,11 +234,13 @@ export const CARDS = {
stack_overflow: { stack_overflow: {
id: "stack_overflow", name: "Stack Overflow", cost: 1, type: "attack", text: "Deal damage equal to cards in hand.", 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)) effect: (ctx) => ctx.deal(ctx.enemy, ctx.scalarFromWeak(ctx.player.hand.length))
}, },
ctrl_z: { ctrl_z: {
id: "ctrl_z", name: "Ctrl+Z", cost: 1, type: "skill", text: "Return a random card from discard to hand.", 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) => { effect: (ctx) => {
if (ctx.player.discard.length > 0) { if (ctx.player.discard.length > 0) {
const randomId = ctx.player.discard[Math.floor(Math.random() * ctx.player.discard.length)]; const randomId = ctx.player.discard[Math.floor(Math.random() * ctx.player.discard.length)];
@ -227,6 +257,7 @@ export const CARDS = {
rubber_duck: { rubber_duck: {
id: "rubber_duck", name: "Rubber Duck Debug", cost: 0, type: "skill", text: "Draw 1. Reveal enemy intent.", id: "rubber_duck", name: "Rubber Duck Debug", cost: 0, type: "skill", text: "Draw 1. Reveal enemy intent.",
art: "Monk_31.png",
effect: (ctx) => { effect: (ctx) => {
ctx.draw(1); ctx.draw(1);
const intent = ctx.enemy.intent; const intent = ctx.enemy.intent;
@ -236,6 +267,7 @@ export const CARDS = {
infinite_loop: { infinite_loop: {
id: "infinite_loop", name: "Infinite Loop", cost: 2, type: "skill", text: "Play the same card twice this turn. Exhaust.", 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, exhaust: true,
effect: (ctx) => { effect: (ctx) => {
if (ctx.lastCard && ctx.lastCard !== "infinite_loop") { if (ctx.lastCard && ctx.lastCard !== "infinite_loop") {
@ -253,6 +285,7 @@ export const CARDS = {
npm_audit: { npm_audit: {
id: "npm_audit", name: "npm audit", cost: 1, type: "skill", text: "Gain 3 Block per curse in deck.", id: "npm_audit", name: "npm audit", cost: 1, type: "skill", text: "Gain 3 Block per curse in deck.",
art: "Monk_33.png",
effect: (ctx) => { effect: (ctx) => {
const curseCount = ctx.countCardType("curse"); const curseCount = ctx.countCardType("curse");
const blockGain = curseCount * 3; const blockGain = curseCount * 3;
@ -263,6 +296,7 @@ export const CARDS = {
git_push_force: { 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.", 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) => { effect: (ctx) => {
ctx.deal(ctx.enemy, ctx.scalarFromWeak(15)); ctx.deal(ctx.enemy, ctx.scalarFromWeak(15));
if (ctx.player.hand.length > 1) { // Don't remove this card itself 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 = [ export const STARTER_DECK = [
"strike", "strike", "defend", "defend", "strike", "strike", "defend", "defend",
"segfault", "coffee_rush", "skill_issue", "git_commit", "segfault", "coffee_rush", "skill_issue", "git_commit",
"ligma", "raw_dog" "stack_trace", "raw_dog"
]; ];
export const CARD_POOL = [ export const CARD_POOL = [
@ -292,5 +347,5 @@ export const CARD_POOL = [
"raw_dog", "task_failed_successfully", "recursion", "git_commit", "memory_leak", "raw_dog", "task_failed_successfully", "recursion", "git_commit", "memory_leak",
"code_review", "pair_programming", "hotfix", "ligma", "merge_conflict", "code_review", "pair_programming", "hotfix", "ligma", "merge_conflict",
"virgin", "production_deploy", "stack_overflow", "ctrl_z", "rubber_duck", "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"
]; ];

57
src/data/enemies.js

@ -16,8 +16,8 @@ export const ENEMIES = {
id: "codegirl", name: "Codegirl", maxHp: 50, id: "codegirl", name: "Codegirl", maxHp: 50,
avatar: "assets/avatars/codegirl.png", // Warrior with conflicted expression avatar: "assets/avatars/codegirl.png", // Warrior with conflicted expression
background: "assets/backgrounds/terrace.png", // Repeat background background: "assets/backgrounds/terrace.png", // Repeat background
ai: (turn) => turn <= 4 ? { type: "attack", value: 8 } : { type: "debuff", value: 1 }, ai: (turn) => turn % 4 === 0 ? { type: "heal", value: 8 } : { type: "attack", value: 8 },
onDebuff: (ctx) => { onHeal: (ctx) => {
ctx.enemy.hp = Math.min(ctx.enemy.maxHp, ctx.enemy.hp + 8); ctx.enemy.hp = Math.min(ctx.enemy.maxHp, ctx.enemy.hp + 8);
ctx.log("Codegirl resolves the merge conflict and heals 8 HP!"); ctx.log("Codegirl resolves the merge conflict and heals 8 HP!");
} }
@ -31,13 +31,13 @@ export const ENEMIES = {
}, },
defyusall: { defyusall: {
id: "defyusall", name: "Defyusall", maxHp: 65, 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", background: "assets/backgrounds/castle.png",
ai: (turn) => turn % 3 === 0 ? { type: "block", value: 8 } : { type: "attack", value: 10 }, ai: (turn) => turn % 3 === 0 ? { type: "block", value: 8 } : { type: "attack", value: 10 },
}, },
lithium: { lithium: {
id: "lithium", name: "Lithium", maxHp: 55, 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", background: "assets/backgrounds/dead forest.png",
ai: (turn) => (turn % 2 === 0) ? { type: "debuff", value: 1 } : { type: "attack", value: 12 }, ai: (turn) => (turn % 2 === 0) ? { type: "debuff", value: 1 } : { type: "attack", value: 12 },
onDebuff: (ctx) => ctx.applyWeak(ctx.player, 1) onDebuff: (ctx) => ctx.applyWeak(ctx.player, 1)
@ -54,5 +54,54 @@ export const ENEMIES = {
return { type: "attack", value: 22 }; // Burst 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!"); } 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!"); }
} }
}; };

18
src/data/maps.js

@ -18,6 +18,24 @@ export const MAPS = {
{ id: "n13", kind: "rest", next: ["n14"], x: 500, y: 125 }, { 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: "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 },
]
} }
}; };

6
src/data/relics.js

@ -2,26 +2,31 @@ export const RELICS = {
mech_kb: { mech_kb: {
id: "mech_kb", name: "Kinesis", id: "mech_kb", name: "Kinesis",
text: "+1 card draw each turn.", text: "+1 card draw each turn.",
art: "Monk_29.png",
hooks: { onTurnStart: (ctx) => ctx.draw(1) } hooks: { onTurnStart: (ctx) => ctx.draw(1) }
}, },
standing_desk: { standing_desk: {
id: "standing_desk", name: "Vim Motions", id: "standing_desk", name: "Vim Motions",
text: "+10 Max HP.", text: "+10 Max HP.",
art: "Monk_30.png",
hooks: { onRunStart: (ctx) => { ctx.player.maxHp += 10; ctx.player.hp += 10; } } hooks: { onRunStart: (ctx) => { ctx.player.maxHp += 10; ctx.player.hp += 10; } }
}, },
prime_hat: { prime_hat: {
id: "prime_hat", name: "VS Code", id: "prime_hat", name: "VS Code",
text: "-10% damage taken.", text: "-10% damage taken.",
art: "Monk_31.png",
hooks: { onDamageTaken: (ctx, dmg) => Math.ceil(dmg * 0.9) } hooks: { onDamageTaken: (ctx, dmg) => Math.ceil(dmg * 0.9) }
}, },
coffee_thermos: { coffee_thermos: {
id: "coffee_thermos", name: "Terminal Coffee Thermos", id: "coffee_thermos", name: "Terminal Coffee Thermos",
text: "Start each fight with Coffee Rush.", 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!") } } hooks: { onBattleStart: (ctx) => { ctx.player.energy += 2; ctx.log("Your coffee thermos provides an energizing boost!") } }
}, },
cpp_compiler: { cpp_compiler: {
id: "cpp_compiler", name: "Haskell", id: "cpp_compiler", name: "Haskell",
text: "First attack each turn deals double.", text: "First attack each turn deals double.",
art: "Monk_33.png",
state: { used: false }, state: { used: false },
hooks: { hooks: {
onTurnStart: (ctx, st) => st.used = false, onTurnStart: (ctx, st) => st.used = false,
@ -31,6 +36,7 @@ export const RELICS = {
chat_mod_sword: { chat_mod_sword: {
id: "chat_mod_sword", name: "Worst Streamer Award", id: "chat_mod_sword", name: "Worst Streamer Award",
text: "Start fights with 1 Weak on all enemies.", text: "Start fights with 1 Weak on all enemies.",
art: "Monk_34.png",
hooks: { hooks: {
onBattleStart: (ctx) => { onBattleStart: (ctx) => {
ctx.applyWeak(ctx.enemy, 1); ctx.applyWeak(ctx.enemy, 1);

2
src/engine/battle.js

@ -111,6 +111,8 @@ export function enemyTurn(ctx) {
} else if (e.intent.type === "debuff") { } else if (e.intent.type === "debuff") {
ENEMIES[e.id].onDebuff?.(ctx, e.intent.value); ENEMIES[e.id].onDebuff?.(ctx, e.intent.value);
ctx.log(`${e.name} casts a debuffing spell.`); ctx.log(`${e.name} casts a debuffing spell.`);
} else if (e.intent.type === "heal") {
ENEMIES[e.id].onHeal?.(ctx, e.intent.value);
} }

73
src/main.js

@ -11,6 +11,7 @@ const app = document.getElementById("app");
const root = { const root = {
app, logs: [], map: MAPS.act1, nodeId: "n1", app, logs: [], map: MAPS.act1, nodeId: "n1",
currentAct: "act1",
player: makePlayer(), player: makePlayer(),
relicStates: [], relicStates: [],
completedNodes: [], completedNodes: [],
@ -23,7 +24,7 @@ const root = {
end() { endTurn(this); }, end() { endTurn(this); },
go(nextId) { async go(nextId) {
this.nodeId = nextId; // Always set nodeId (needed for battle logic) this.nodeId = nextId; // Always set nodeId (needed for battle logic)
const node = this.map.nodes.find(n => n.id === nextId); const node = this.map.nodes.find(n => n.id === nextId);
if (!node) return; if (!node) return;
@ -32,12 +33,12 @@ const root = {
this._battleInProgress = true; this._battleInProgress = true;
createBattle(this, node.enemy); createBattle(this, node.enemy);
renderBattle(this); await renderBattle(this);
} else { } else {
this.save(); this.save();
if (node.kind === "rest") { if (node.kind === "rest") {
renderRest(this); await renderRest(this);
} else if (node.kind === "shop") { } else if (node.kind === "shop") {
renderShop(this); renderShop(this);
} else if (node.kind === "event") { } else if (node.kind === "event") {
@ -48,7 +49,7 @@ const root = {
} }
}, },
afterNode() { async afterNode() {
if (this.nodeId && !this.completedNodes.includes(this.nodeId)) { if (this.nodeId && !this.completedNodes.includes(this.nodeId)) {
this.completedNodes.push(this.nodeId); this.completedNodes.push(this.nodeId);
} }
@ -58,11 +59,11 @@ const root = {
if (node.kind === "battle" || node.kind === "elite") { if (node.kind === "battle" || node.kind === "elite") {
const choices = pickCards(3); const choices = pickCards(3);
this._pendingChoices = choices; this._pendingChoices = choices;
renderReward(this, choices); await renderReward(this, choices);
return; return;
} }
if (node.kind === "boss") { if (node.kind === "boss") {
renderWin(this); return; await renderWin(this); return;
} }
renderMap(this); renderMap(this);
@ -84,7 +85,7 @@ const root = {
renderMap(this); renderMap(this);
}, },
onWin() { async onWin() {
this.log("Enemy defeated! 🎉"); this.log("Enemy defeated! 🎉");
const goldReward = Math.floor(Math.random() * 20) + 15; // 15-35 gold 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); 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]) {
// 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.save(); // Save progress before clearing on victory
this.clearSave(); // Clear save on victory this.clearSave(); // Clear save on victory
renderWin(this); await renderWin(this);
}
} }
else { else {
@ -106,17 +121,19 @@ const root = {
this.afterNode(); this.afterNode();
} }
}, },
onLose() { async onLose() {
this._battleInProgress = false; this._battleInProgress = false;
this.clearSave(); // Clear save on defeat this.clearSave(); // Clear save on defeat
renderLose(this); await renderLose(this);
}, },
reset() { reset() {
this.logs = []; this.logs = [];
this.player = makePlayer(); this.player = makePlayer();
initDeck(this.player); initDeck(this.player);
this.currentAct = "act1";
this.map = MAPS.act1;
this.nodeId = "n1"; this.nodeId = "n1";
this.completedNodes = []; this.completedNodes = [];
@ -135,6 +152,7 @@ const root = {
const saveData = { const saveData = {
player: this.player, player: this.player,
nodeId: this.nodeId, nodeId: this.nodeId,
currentAct: this.currentAct,
relicStates: this.relicStates, relicStates: this.relicStates,
completedNodes: this.completedNodes, completedNodes: this.completedNodes,
logs: this.logs.slice(-50), // Keep last 50 logs logs: this.logs.slice(-50), // Keep last 50 logs
@ -154,6 +172,8 @@ const root = {
const data = JSON.parse(saveData); const data = JSON.parse(saveData);
this.player = data.player; this.player = data.player;
this.nodeId = data.nodeId; this.nodeId = data.nodeId;
this.currentAct = data.currentAct || "act1";
this.map = MAPS[this.currentAct];
this.relicStates = data.relicStates || []; this.relicStates = data.relicStates || [];
this.completedNodes = data.completedNodes || []; this.completedNodes = data.completedNodes || [];
this.logs = data.logs || []; 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 urlParams = new URLSearchParams(window.location.search);
const screenParam = urlParams.get('screen'); const screenParam = urlParams.get('screen');
const dev = urlParams.get('dev'); const dev = urlParams.get('dev');
@ -238,17 +258,10 @@ function initializeGame() {
const now = new Date(); const now = new Date();
const birthday = new Date('2025-09-09T00:00:00'); 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') { if (now < birthday && dev !== 'true') {
console.log('Showing countdown!');
showCountdown(birthday); showCountdown(birthday);
return; return;
} else {
console.log('Showing game - either past birthday or dev mode');
} }
if (screenParam) { if (screenParam) {
@ -257,20 +270,20 @@ function initializeGame() {
switch (screenParam.toLowerCase()) { switch (screenParam.toLowerCase()) {
case 'victory': case 'victory':
case 'win': case 'win':
renderWin(root); await renderWin(root);
return; return;
case 'defeat': case 'defeat':
case 'lose': case 'lose':
renderLose(root); await renderLose(root);
return; return;
case 'map': case 'map':
renderMap(root); await renderMap(root);
return; return;
case 'shop': case 'shop':
renderShop(root); renderShop(root);
return; return;
case 'rest': case 'rest':
renderRest(root); await renderRest(root);
return; return;
case 'event': case 'event':
renderEvent(root); renderEvent(root);
@ -279,7 +292,7 @@ function initializeGame() {
root.go('n2'); // Battle node root.go('n2'); // Battle node
return; return;
case 'upgrade': case 'upgrade':
renderRest(root); await renderRest(root);
setTimeout(() => { setTimeout(() => {
const upgradeBtn = root.app.querySelector("[data-act='upgrade']"); const upgradeBtn = root.app.querySelector("[data-act='upgrade']");
if (upgradeBtn) upgradeBtn.click(); if (upgradeBtn) upgradeBtn.click();
@ -311,6 +324,19 @@ function setupMockData() {
attachRelics(root, ['coffee_thermos', 'cpp_compiler']); 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') {
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.completedNodes = ['n1', 'n2', 'n4'];
root.nodeId = 'n7'; root.nodeId = 'n7';
root.logs = [ root.logs = [
@ -318,6 +344,7 @@ function setupMockData() {
'Mock data initialized', 'Mock data initialized',
'Ready for screen testing!' 'Ready for screen testing!'
]; ];
}
} }
function showCountdown(birthday) { function showCountdown(birthday) {

182
src/ui/render.js

@ -47,6 +47,8 @@ export async function renderBattle(root) {
const { ENEMIES } = await import("../data/enemies.js"); 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 enemyData = ENEMIES[e.id];
const backgroundImage = enemyData?.background || null; const backgroundImage = enemyData?.background || null;
@ -54,7 +56,8 @@ export async function renderBattle(root) {
const intentInfo = { const intentInfo = {
attack: { emoji: '', text: `Will attack for ${e.intent.value} damage`, color: 'danger' }, attack: { emoji: '', text: `Will attack for ${e.intent.value} damage`, color: 'danger' },
block: { emoji: '', text: `Will gain ${e.intent.value} block`, color: 'info' }, 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' }; }[e.intent.type] || { emoji: '', text: 'Unknown intent', color: 'neutral' };
app.innerHTML = ` app.innerHTML = `
@ -198,7 +201,7 @@ export async function renderBattle(root) {
</div> </div>
<div class="card-artwork"> <div class="card-artwork">
<div class="card-art-icon">${getCardArt(card.id)}</div> <div class="card-art-icon">${getCardArt(card.id, CARDS)}</div>
<div class="card-type-badge ${cardType}">${card.type}</div> <div class="card-type-badge ${cardType}">${card.type}</div>
</div> </div>
@ -317,6 +320,7 @@ export async function renderBattle(root) {
export async function renderMap(root) { export async function renderMap(root) {
const { CARDS } = await import("../data/cards.js"); const { CARDS } = await import("../data/cards.js");
const { ENEMIES } = await import("../data/enemies.js"); const { ENEMIES } = await import("../data/enemies.js");
const { RELICS } = await import("../data/relics.js");
const m = root.map; const m = root.map;
const currentId = root.nodeId; const currentId = root.nodeId;
@ -439,8 +443,8 @@ export async function renderMap(root) {
<img src="assets/card-art/runestone.png" alt="Relics" class="status-icon-img"> <img src="assets/card-art/runestone.png" alt="Relics" class="status-icon-img">
<div class="relics-inline"> <div class="relics-inline">
${root.relicStates.map(r => ` ${root.relicStates.map(r => `
<div class="relic-inline" title="${getRelicText(r.id)}"> <div class="relic-inline" title="${getRelicText(r.id, RELICS)}">
${getRelicEmoji(r.id)} ${getRelicArt(r.id, RELICS)}
</div> </div>
`).join('')} `).join('')}
</div> </div>
@ -481,6 +485,20 @@ May this birthday bring joy in each moment you’ve got. </em></p>
</div> </div>
</div> </div>
</div> </div>
<div class = "map-act-container">
<div class="act-progress-indicator">
<div class="act-progress-bar">
<div class="act-step ${root.currentAct === 'act1' ? 'current' : 'completed'}">
<div class="act-number">Act I</div>
<div class="act-name">Junior Dev</div>
</div>
<div class="act-connector ${root.currentAct === 'act2' ? 'active' : ''}"></div>
<div class="act-step ${root.currentAct === 'act2' ? 'current' : root.currentAct === 'act1' ? 'locked' : 'completed'}">
<div class="act-number">Act II</div>
<div class="act-name">Corporate Ladder</div>
</div>
</div>
</div>
<div class="spire-map"> <div class="spire-map">
@ -561,6 +579,7 @@ May this birthday bring joy in each moment you’ve got. </em></p>
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="deck-stack-container"> <div class="deck-stack-container">
<div class="deck-stack-header"> <div class="deck-stack-header">
@ -585,7 +604,7 @@ May this birthday bring joy in each moment you’ve got. </em></p>
<div class="card-title">${card.name}</div> <div class="card-title">${card.name}</div>
<div class="card-cost-orb">${card.cost}</div> <div class="card-cost-orb">${card.cost}</div>
</div> </div>
<div class="card-art">${getCardArt(cardId)}</div> <div class="card-art">${getCardArt(cardId, CARDS)}</div>
<div class="card-description-box"> <div class="card-description-box">
<div class="card-text">${card.text}</div> <div class="card-text">${card.text}</div>
</div> </div>
@ -662,7 +681,8 @@ May this birthday bring joy in each moment you’ve got. </em></p>
}); });
} }
export function renderReward(root, choices) { export async function renderReward(root, choices) {
const { CARDS } = await import("../data/cards.js");
root.app.innerHTML = ` root.app.innerHTML = `
<div class="reward-screen"> <div class="reward-screen">
<h1>Choose a Card</h1> <h1>Choose a Card</h1>
@ -680,7 +700,7 @@ export function renderReward(root, choices) {
</div> </div>
<div class="card-artwork"> <div class="card-artwork">
<div class="card-art-icon">${getCardArt(c.id)}</div> <div class="card-art-icon">${getCardArt(c.id, CARDS)}</div>
<div class="card-type-badge ${cardType}">${c.type}</div> <div class="card-type-badge ${cardType}">${c.type}</div>
</div> </div>
@ -709,7 +729,8 @@ export function renderReward(root, choices) {
root.app.querySelector("[data-skip]").addEventListener("click", () => root.skipReward()); 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 = ` root.app.innerHTML = `
<div class="rest-screen"> <div class="rest-screen">
<div class="rest-header"> <div class="rest-header">
@ -804,7 +825,7 @@ export function renderUpgrade(root) {
</div> </div>
<div class="card-artwork"> <div class="card-artwork">
<div class="card-art-icon">${getCardArt(card.id)}</div> <div class="card-art-icon">${getCardArt(card.id, CARDS)}</div>
<div class="card-type-badge ${card.type}">${card.type}</div> <div class="card-type-badge ${card.type}">${card.type}</div>
</div> </div>
@ -826,7 +847,7 @@ export function renderUpgrade(root) {
</div> </div>
<div class="card-artwork"> <div class="card-artwork">
<div class="card-art-icon">${getCardArt(upgradedCard.id)}</div> <div class="card-art-icon">${getCardArt(upgradedCard.id, CARDS)}</div>
<div class="card-type-badge ${upgradedCard.type}">${upgradedCard.type}</div> <div class="card-type-badge ${upgradedCard.type}">${upgradedCard.type}</div>
</div> </div>
@ -913,7 +934,7 @@ export function renderShop(root) {
</div> </div>
<div class="card-artwork"> <div class="card-artwork">
<div class="card-art-icon">${getCardArt(card.id)}</div> <div class="card-art-icon">${getCardArt(card.id, CARDS)}</div>
<div class="card-type-badge ${cardType}">${card.type}</div> <div class="card-type-badge ${cardType}">${card.type}</div>
</div> </div>
@ -943,7 +964,7 @@ export function renderShop(root) {
<div class="shop-relics"> <div class="shop-relics">
<div class="shop-relic-container"> <div class="shop-relic-container">
<div class="shop-relic ${(root.player.gold || 100) >= 100 ? 'affordable' : 'unaffordable'}" data-buy-relic> <div class="shop-relic ${(root.player.gold || 100) >= 100 ? 'affordable' : 'unaffordable'}" data-buy-relic>
<div class="relic-icon">${getRelicEmoji(shopRelic.id)}</div> <div class="relic-icon">${getRelicArt(shopRelic.id, RELICS)}</div>
<div class="relic-info"> <div class="relic-info">
<h3>${shopRelic.name}</h3> <h3>${shopRelic.name}</h3>
<p>${shopRelic.text}</p> <p>${shopRelic.text}</p>
@ -952,7 +973,6 @@ export function renderShop(root) {
<img src="assets/card-art/bag_of_gold.png" alt="Gold" class="price-icon"> <img src="assets/card-art/bag_of_gold.png" alt="Gold" class="price-icon">
<span>100</span> <span>100</span>
</div> </div>
${(root.player.gold || 100) < 100 ? `<div class="relic-disabled-overlay"><span>Need 100 gold</span></div>` : ''}
</div> </div>
</div> </div>
</div> </div>
@ -1105,122 +1125,36 @@ function shuffle(array) {
return array; return array;
} }
function getRelicEmoji(relicId) { function getRelicArt(relicId, RELICS = null) {
const relicArt = { if (RELICS && RELICS[relicId]?.art) {
mech_kb: '<img src="assets/skill-art/Monk_29.png" alt="Kinesis" class="relic-skill-art">', const imagePath = RELICS[relicId].art;
standing_desk: '<img src="assets/skill-art/Monk_30.png" alt="Motions" class="relic-skill-art">', return `<img src="assets/skill-art/${imagePath}" alt="${relicId}" class="relic-skill-art">`;
prime_hat: '<img src="assets/skill-art/Monk_31.png" alt="VS Code" class="relic-skill-art">', }
coffee_thermos: '<img src="assets/skill-art/Monk_32.png" alt="Coffee Thermos" class="relic-skill-art">', return '💎';
cpp_compiler: '<img src="assets/skill-art/Monk_33.png" alt="CPP Compiler" class="relic-skill-art">',
chat_mod_sword: '<img src="assets/skill-art/Monk_34.png" alt="Chat Mod Sword" class="relic-skill-art">'
};
return relicArt[relicId] || '💎';
} }
function getRelicName(relicId) { function getRelicName(relicId, RELICS = null) {
const names = { return RELICS?.[relicId]?.name || relicId;
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 getRelicText(relicId) { function getRelicText(relicId, RELICS = null) {
const texts = { return RELICS?.[relicId]?.text || 'Unknown relic';
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) { function getCardArt(cardId, CARDS = null) {
if (CARDS && CARDS[cardId]?.art) {
const artMappings = { const imagePath = CARDS[cardId].art;
return `<img src="assets/skill-art/${imagePath}" alt="${cardId}" class="card-art-image">`;
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 `<img src="assets/skill-art/${imagePath}" alt="${cardId}" class="card-art-image" onerror="this.style.display='none'; this.nextElementSibling.style.display='inline';">
<span class="card-art-fallback" style="display: none;">${getCardArtFallback(cardId)}</span>`;
} }
return getCardArtFallback(cardId); // Fallback for cases where CARDS is not passed (shouldn't happen in normal operation)
} return `<span>🃏</span>`;
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] || '🃏';
} }
function getEnemyArt(enemyId, ENEMIES = null) { function getEnemyArt(enemyId, ENEMIES = null) {
const enemyData = ENEMIES?.[enemyId]; const enemyData = ENEMIES?.[enemyId];
const avatarPath = enemyData?.avatar || `assets/avatars/${enemyId}.png`; const avatarPath = enemyData?.avatar || `assets/avatars/${enemyId}.png`;
return `<img src="${avatarPath}" alt="${enemyId}" class="enemy-avatar-img" onerror="this.style.display='none'; this.nextElementSibling.style.display='inline';"> return `<img src="${avatarPath}" alt="${enemyId}" class="enemy-avatar-img">`;
<span class="enemy-fallback-emoji" style="display: none;">${getEnemyFallbackEmoji(enemyId)}</span>`;
}
function getEnemyFallbackEmoji(enemyId) {
const arts = {
old_man_judo: '👹',
beastco: '🌀',
codegirl: '⚔',
defyusall: '🚫',
lithium: '⚡',
nightshadedude: '😈',
teej: '🎂👾'
};
return arts[enemyId] || '👾';
} }
function getEnemyType(enemyId) { function getEnemyType(enemyId) {
@ -1285,7 +1219,7 @@ export function renderRelicSelection(root) {
return ` return `
<div class="relic-option" data-relic="${relicId}"> <div class="relic-option" data-relic="${relicId}">
<div class="relic-portrait"> <div class="relic-portrait">
<div class="relic-icon">${getRelicEmoji(relicId)}</div> <div class="relic-icon">${getRelicArt(relicId, RELICS)}</div>
</div> </div>
<div class="relic-info"> <div class="relic-info">
<div class="relic-name">${relic.name}</div> <div class="relic-name">${relic.name}</div>
@ -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 = { const finalStats = {
totalTurns: root.turnCount || 0, totalTurns: root.turnCount || 0,
cardsPlayed: root.cardsPlayedCount || 0, cardsPlayed: root.cardsPlayedCount || 0,
@ -1560,9 +1495,9 @@ export function renderWin(root) {
<div class="relics-showcase"> <div class="relics-showcase">
${root.relicStates.length > 0 ? ${root.relicStates.length > 0 ?
root.relicStates.map(r => ` root.relicStates.map(r => `
<div class="relic-showcase-item" title="${getRelicText(r.id)}"> <div class="relic-showcase-item" title="${getRelicText(r.id, RELICS)}">
<div class="relic-showcase-icon">${getRelicEmoji(r.id)}</div> <div class="relic-showcase-icon">${getRelicArt(r.id, RELICS)}</div>
<div class="relic-showcase-name">${getRelicName(r.id)}</div> <div class="relic-showcase-name">${getRelicName(r.id, RELICS)}</div>
</div> </div>
`).join('') : `).join('') :
'<div class="no-relics">No relics collected this run</div>' '<div class="no-relics">No relics collected this run</div>'
@ -1590,7 +1525,8 @@ 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) { export async function renderLose(root) {
const { RELICS } = await import("../data/relics.js");
const finalStats = { const finalStats = {
totalTurns: root.turnCount || 0, totalTurns: root.turnCount || 0,
cardsPlayed: root.cardsPlayedCount || 0, cardsPlayed: root.cardsPlayedCount || 0,
@ -1659,7 +1595,7 @@ Better luck on the next run!</p>
<div class="relics-showcase"> <div class="relics-showcase">
${root.relicStates.map(relic => ` ${root.relicStates.map(relic => `
<div class="relic-showcase-item"> <div class="relic-showcase-item">
<div class="relic-showcase-icon">${getRelicEmoji(relic.id)}</div> <div class="relic-showcase-icon">${getRelicArt(relic.id, RELICS)}</div>
<div class="relic-showcase-name">${relic.id.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}</div> <div class="relic-showcase-name">${relic.id.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}</div>
</div> </div>
`).join('')} `).join('')}

95
style.css

@ -1241,6 +1241,7 @@ h3 {
.intent-danger { border-color: #dc3545; background: rgba(220, 53, 69, 0.1); } .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-info { border-color: #17a2b8; background: rgba(23, 162, 184, 0.1); }
.intent-warning { border-color: #ffc107; } .intent-warning { border-color: #ffc107; }
.intent-success { border-color: #28a745; background: rgba(40, 167, 69, 0.1); }
.intent-icon { .intent-icon {
font-size: 24px; font-size: 24px;
@ -2177,7 +2178,6 @@ h3 {
.map-screen { .map-screen {
width: 100%; width: 100%;
margin: 0; margin: 0;
padding: 20px;
min-height: 100vh; min-height: 100vh;
background: url('assets/backgrounds/terrace.png'); background: url('assets/backgrounds/terrace.png');
background-size: cover; background-size: cover;
@ -2195,7 +2195,6 @@ h3 {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 20px;
} }
.map-header-section h1 { .map-header-section h1 {
@ -2270,7 +2269,7 @@ h3 {
.player-status { .player-status {
width: 1450px; width: 1450px;
margin: 0 auto 20px auto; margin: 0 auto 10px auto;
display: flex; display: flex;
gap: 30px; gap: 30px;
align-items: center; align-items: center;
@ -2279,7 +2278,7 @@ h3 {
linear-gradient(135deg, #2a2a3a 0%, #1a1a2a 100%); linear-gradient(135deg, #2a2a3a 0%, #1a1a2a 100%);
border: 2px solid #3a3a4a; border: 2px solid #3a3a4a;
border-radius: 6px; border-radius: 6px;
padding: 20px; padding: 10px 20px;
box-shadow: inset 0 1px 0 rgba(255,255,255,0.1); box-shadow: inset 0 1px 0 rgba(255,255,255,0.1);
} }
@ -2854,6 +2853,94 @@ h3 {
gap: 20px; 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 { .spire-map {
position: relative; position: relative;
width: 930px; width: 930px;

Loading…
Cancel
Save