From 94676fae77a8024df3ad89cc692e01d36b1b97cb Mon Sep 17 00:00:00 2001 From: Stephanie Gredell Date: Mon, 1 Sep 2025 20:58:12 -0700 Subject: [PATCH] Card updates --- src/data/cards.js | 85 +++++++++++++++++++++++++++++++++++++++++--- src/engine/battle.js | 28 ++++++++++++++- src/main.js | 23 +++++++----- style.css | 6 ++-- 4 files changed, 125 insertions(+), 17 deletions(-) diff --git a/src/data/cards.js b/src/data/cards.js index 7988b0e..876f5cb 100644 --- a/src/data/cards.js +++ b/src/data/cards.js @@ -187,7 +187,7 @@ export const CARDS = { }, production_deploy: { - id: "production_deploy", name: "Production Deploy", cost: 3, type: "attack", text: "Deal 25. Lose 5 HP.", + id: "production_deploy", name: "Production Deploy", cost: 2, type: "attack", text: "Deal 25. Lose 5 HP.", effect: (ctx) => { ctx.deal(ctx.enemy, ctx.scalarFromWeak(25)); ctx.player.hp = Math.max(1, ctx.player.hp - 5); @@ -203,13 +203,87 @@ export const CARDS = { ctx.log("The sugar crash hits hard, draining your energy!"); } }, + + stack_overflow: { + id: "stack_overflow", name: "Stack Overflow", cost: 1, type: "attack", text: "Deal damage equal to cards in hand.", + 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.", + effect: (ctx) => { + if (ctx.player.discard.length > 0) { + const randomId = ctx.player.discard[Math.floor(Math.random() * ctx.player.discard.length)]; + if (ctx.moveFromDiscardToHand(randomId)) { + ctx.log(`Ctrl+Z brings back ${CARDS[randomId].name}!`); + } else { + ctx.log("Ctrl+Z failed to undo anything."); + } + } else { + ctx.log("Nothing to undo!"); + } + } + }, + + rubber_duck: { + id: "rubber_duck", name: "Rubber Duck Debug", cost: 0, type: "skill", text: "Draw 1. Reveal enemy intent.", + effect: (ctx) => { + ctx.draw(1); + const intent = ctx.enemy.intent; + ctx.log(`Rubber duck reveals: Enemy will ${intent.type} for ${intent.value || 'unknown'} next turn.`); + } + }, + + infinite_loop: { + id: "infinite_loop", name: "Infinite Loop", cost: 2, type: "skill", text: "Play the same card twice this turn. Exhaust.", + exhaust: true, + effect: (ctx) => { + if (ctx.lastCard && ctx.lastCard !== "infinite_loop") { + const card = ctx.player.hand.find(c => c.id === ctx.lastCard); + if (card) { + ctx.replayCard(card); + } else { + ctx.log("Infinite loop has nothing to repeat!"); + } + } else { + ctx.log("Infinite loop needs a previous card to repeat!"); + } + } + }, + + npm_audit: { + id: "npm_audit", name: "npm audit", cost: 1, type: "skill", text: "Gain 3 Block per curse in deck.", + effect: (ctx) => { + const curseCount = ctx.countCardType("curse"); + const blockGain = curseCount * 3; + ctx.player.block += blockGain; + ctx.log(`npm audit found ${curseCount} vulnerabilities. Gain ${blockGain} Block.`); + } + }, + + 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.", + effect: (ctx) => { + ctx.deal(ctx.enemy, ctx.scalarFromWeak(15)); + if (ctx.player.hand.length > 1) { // Don't remove this card itself + const otherCards = ctx.player.hand.filter(c => c.id !== "git_push_force"); + if (otherCards.length > 0) { + const randomCard = otherCards[Math.floor(Math.random() * otherCards.length)]; + const handIdx = ctx.player.hand.findIndex(c => c === randomCard); + const [card] = ctx.player.hand.splice(handIdx, 1); + ctx.player.draw.push(card.id); + ctx.log(`${card.name} was force-pushed back to your deck!`); + } + } + } + }, }; export const STARTER_DECK = [ - "segfault", "raw_dog", "coffee_rush", - "skill_issue", "vibe_code", "404", - "git_commit", "ligma", "task_failed_successfully", "virgin" + "strike", "strike", "defend", "defend", + "segfault", "coffee_rush", "skill_issue", "git_commit", + "ligma", "raw_dog" ]; export const CARD_POOL = [ @@ -217,5 +291,6 @@ export const CARD_POOL = [ "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" + "virgin", "production_deploy", "stack_overflow", "ctrl_z", "rubber_duck", + "infinite_loop", "npm_audit", "git_push_force" ]; diff --git a/src/engine/battle.js b/src/engine/battle.js index dd3c47f..dc7c34f 100644 --- a/src/engine/battle.js +++ b/src/engine/battle.js @@ -1,6 +1,7 @@ import { ENEMIES } from "../data/enemies.js"; import { RELICS } from "../data/relics.js"; -import { draw, endTurnDiscard, clamp } from "./core.js"; +import { CARDS } from "../data/cards.js"; +import { draw, endTurnDiscard, clamp, cloneCard } from "./core.js"; export function createBattle(ctx, enemyId) { const enemyData = ENEMIES[enemyId]; @@ -174,6 +175,31 @@ export function makeBattleContext(root) { showDamageNumber: root.showDamageNumber, lastCard: null, flags: {}, + // New mechanics for advanced cards + moveFromDiscardToHand: (cardId) => { + const idx = root.player.discard.findIndex(id => id === cardId); + if (idx >= 0) { + const [id] = root.player.discard.splice(idx, 1); + const originalCard = CARDS[id]; + if (originalCard) { + const clonedCard = cloneCard(originalCard); + root.player.hand.push(clonedCard); + return true; + } + } + return false; + }, + countCardType: (type) => { + const allCards = [...root.player.deck, ...root.player.hand.map(c => c.id), ...root.player.draw, ...root.player.discard]; + return allCards.filter(id => CARDS[id]?.type === type).length; + }, + replayCard: (card) => { + // Temporarily replay a card without removing it from hand + if (typeof card.effect === 'function') { + card.effect(root); + root.log(`${card.name} is replayed!`); + } + }, }; } diff --git a/src/main.js b/src/main.js index b2f2141..dc2db9f 100644 --- a/src/main.js +++ b/src/main.js @@ -131,10 +131,6 @@ const root = { }, save() { - if (this._battleInProgress) { - return; - } - try { const saveData = { player: this.player, @@ -142,6 +138,7 @@ const root = { relicStates: this.relicStates, completedNodes: this.completedNodes, logs: this.logs.slice(-50), // Keep last 50 logs + battleInProgress: this._battleInProgress || false, timestamp: Date.now() }; localStorage.setItem('birthday-spire-save', JSON.stringify(saveData)); @@ -160,6 +157,7 @@ const root = { this.relicStates = data.relicStates || []; this.completedNodes = data.completedNodes || []; this.logs = data.logs || []; + this._battleInProgress = data.battleInProgress || false; this.restoreCardEffects(); @@ -382,12 +380,21 @@ function showCountdown(birthday) { } function loadNormalGame() { - // Clear old saves to prevent card ID conflicts after refactoring - root.clearOldSaves(); - const hasLoadedData = root.load(); if (hasLoadedData) { - renderMap(root); + // If we were in a battle, resume it + if (root._battleInProgress) { + const node = root.map.nodes.find(n => n.id === root.nodeId); + if (node && (node.kind === "battle" || node.kind === "elite" || node.kind === "boss")) { + root.go(root.nodeId); + } else { + // Battle state inconsistent, go to map + root._battleInProgress = false; + renderMap(root); + } + } else { + renderMap(root); + } } else { root.reset(); } diff --git a/style.css b/style.css index 68a34fd..c3adb9a 100644 --- a/style.css +++ b/style.css @@ -14,7 +14,7 @@ body { margin: 0; font-family: "JetBrains Mono", ui-monospace, Menlo, Consolas; - background: linear-gradient(180deg, var(--bg), var(--bg2)); + background: #000; color: var(--text) } @@ -3364,8 +3364,8 @@ h3 { .deck-stack-card .card-count-badge { position: absolute; - top: -3px; - right: -3px; + top: 3px; + right: 5%; background: #dc3545; color: white; border-radius: 50%;