Browse Source

Card updates

main
Stephanie Gredell 4 months ago
parent
commit
94676fae77
  1. 85
      src/data/cards.js
  2. 28
      src/engine/battle.js
  3. 23
      src/main.js
  4. 6
      style.css

85
src/data/cards.js

@ -187,7 +187,7 @@ export const CARDS = {
}, },
production_deploy: { 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) => { 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);
@ -203,13 +203,87 @@ export const CARDS = {
ctx.log("The sugar crash hits hard, draining your energy!"); 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 = [ export const STARTER_DECK = [
"segfault", "raw_dog", "coffee_rush", "strike", "strike", "defend", "defend",
"skill_issue", "vibe_code", "404", "segfault", "coffee_rush", "skill_issue", "git_commit",
"git_commit", "ligma", "task_failed_successfully", "virgin" "ligma", "raw_dog"
]; ];
export const CARD_POOL = [ export const CARD_POOL = [
@ -217,5 +291,6 @@ export const CARD_POOL = [
"dark_mode", "object_object", "just_one_game", "colon_q", "vibe_code", "dark_mode", "object_object", "just_one_game", "colon_q", "vibe_code",
"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" "virgin", "production_deploy", "stack_overflow", "ctrl_z", "rubber_duck",
"infinite_loop", "npm_audit", "git_push_force"
]; ];

28
src/engine/battle.js

@ -1,6 +1,7 @@
import { ENEMIES } from "../data/enemies.js"; import { ENEMIES } from "../data/enemies.js";
import { RELICS } from "../data/relics.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) { export function createBattle(ctx, enemyId) {
const enemyData = ENEMIES[enemyId]; const enemyData = ENEMIES[enemyId];
@ -174,6 +175,31 @@ export function makeBattleContext(root) {
showDamageNumber: root.showDamageNumber, showDamageNumber: root.showDamageNumber,
lastCard: null, lastCard: null,
flags: {}, 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!`);
}
},
}; };
} }

23
src/main.js

@ -131,10 +131,6 @@ const root = {
}, },
save() { save() {
if (this._battleInProgress) {
return;
}
try { try {
const saveData = { const saveData = {
player: this.player, player: this.player,
@ -142,6 +138,7 @@ const root = {
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
battleInProgress: this._battleInProgress || false,
timestamp: Date.now() timestamp: Date.now()
}; };
localStorage.setItem('birthday-spire-save', JSON.stringify(saveData)); localStorage.setItem('birthday-spire-save', JSON.stringify(saveData));
@ -160,6 +157,7 @@ const root = {
this.relicStates = data.relicStates || []; this.relicStates = data.relicStates || [];
this.completedNodes = data.completedNodes || []; this.completedNodes = data.completedNodes || [];
this.logs = data.logs || []; this.logs = data.logs || [];
this._battleInProgress = data.battleInProgress || false;
this.restoreCardEffects(); this.restoreCardEffects();
@ -382,12 +380,21 @@ function showCountdown(birthday) {
} }
function loadNormalGame() { function loadNormalGame() {
// Clear old saves to prevent card ID conflicts after refactoring
root.clearOldSaves();
const hasLoadedData = root.load(); const hasLoadedData = root.load();
if (hasLoadedData) { 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 { } else {
root.reset(); root.reset();
} }

6
style.css

@ -14,7 +14,7 @@
body { body {
margin: 0; margin: 0;
font-family: "JetBrains Mono", ui-monospace, Menlo, Consolas; font-family: "JetBrains Mono", ui-monospace, Menlo, Consolas;
background: linear-gradient(180deg, var(--bg), var(--bg2)); background: #000;
color: var(--text) color: var(--text)
} }
@ -3364,8 +3364,8 @@ h3 {
.deck-stack-card .card-count-badge { .deck-stack-card .card-count-badge {
position: absolute; position: absolute;
top: -3px; top: 3px;
right: -3px; right: 5%;
background: #dc3545; background: #dc3545;
color: white; color: white;
border-radius: 50%; border-radius: 50%;

Loading…
Cancel
Save