Stephanie Gredell 4 months ago
parent
commit
b09509bd81
  1. 16
      src/data/cards.js
  2. 6
      src/data/enemies.js
  3. 4
      src/data/relics.js
  4. 37
      src/engine/battle.js
  5. 961
      src/ui/render.js
  6. 75
      style.css

16
src/data/cards.js

@ -41,9 +41,9 @@ export const CARDS = {
card.cost = 0; card.cost = 0;
card.effect(ctx); card.effect(ctx);
card.cost = savedCost; card.cost = savedCost;
ctx.log(`Replayed ${card.name} for free!`); ctx.log(`Macro replays ${card.name} at no cost!`);
} else { } else {
ctx.log("No valid card to replay."); ctx.log("Macro fizzles - no valid card to replay.");
} }
} }
} }
@ -113,7 +113,7 @@ export const CARDS = {
const prevHp = ctx.enemy.hp; const prevHp = ctx.enemy.hp;
ctx.deal(ctx.enemy, ctx.scalarFromWeak(5)); ctx.deal(ctx.enemy, ctx.scalarFromWeak(5));
if (prevHp > 0 && ctx.enemy.hp <= 0) { if (prevHp > 0 && ctx.enemy.hp <= 0) {
ctx.log("Recursion triggered!"); ctx.log("Recursion activates and strikes again!");
ctx.enemy.hp = 1; ctx.enemy.hp = 1;
ctx.deal(ctx.enemy, ctx.scalarFromWeak(5)); ctx.deal(ctx.enemy, ctx.scalarFromWeak(5));
@ -139,7 +139,7 @@ export const CARDS = {
effect: (ctx) => { effect: (ctx) => {
ctx.draw(1); ctx.draw(1);
ctx.log("Reviewed code, drew 1 card."); ctx.log("Code review reveals useful insights. You draw a card.");
} }
}, },
@ -154,7 +154,7 @@ export const CARDS = {
if (ctx.player.hp <= ctx.player.maxHp * 0.5) { if (ctx.player.hp <= ctx.player.maxHp * 0.5) {
ctx.deal(ctx.enemy, ctx.scalarFromWeak(10)); ctx.deal(ctx.enemy, ctx.scalarFromWeak(10));
} else { } else {
ctx.log("Can only hotfix in emergencies!"); ctx.log("Hotfix can only be deployed when HP is below 50%!");
} }
} }
}, },
@ -180,7 +180,7 @@ export const CARDS = {
effect: (ctx) => { effect: (ctx) => {
if (ctx.intentIsAttack()) { if (ctx.intentIsAttack()) {
ctx.player.block += 8; ctx.player.block += 8;
ctx.log("Tests passed! Gained Block."); ctx.log("Unit tests pass! You feel more confident and gain block.");
} }
} }
}, },
@ -190,7 +190,7 @@ export const CARDS = {
effect: (ctx) => { 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);
ctx.log("Deployed to prod... at what cost?"); ctx.log("Production deployment succeeds, but at what cost to your health?");
} }
}, },
@ -199,7 +199,7 @@ export const CARDS = {
id: "sugar_crash", name: "Sugar Crash", cost: 1, type: "curse", text: "Unplayable. -1 Energy when drawn.", id: "sugar_crash", name: "Sugar Crash", cost: 1, type: "curse", text: "Unplayable. -1 Energy when drawn.",
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("Sugar crash! -1 Energy"); ctx.log("The sugar crash hits hard, draining your energy!");
} }
}, },
}; };

6
src/data/enemies.js

@ -24,7 +24,7 @@ export const ENEMIES = {
id: "infinite_loop", name: "Beastco", maxHp: 35, id: "infinite_loop", name: "Beastco", maxHp: 35,
avatar: "assets/avatars/2.png", // Dizzy/confused character avatar: "assets/avatars/2.png", // Dizzy/confused character
background: "assets/backgrounds/throne room.png", background: "assets/backgrounds/throne room.png",
ai: (turn) => ({ type: "attack", value: 4 }), // Always attacks for small damage ai: (turn) => ({ type: "attack", value: 4 }),
}, },
merge_conflict_enemy: { merge_conflict_enemy: {
id: "merge_conflict_enemy", name: "Codegirl", maxHp: 50, id: "merge_conflict_enemy", name: "Codegirl", maxHp: 50,
@ -34,7 +34,7 @@ export const ENEMIES = {
onDebuff: (ctx) => { onDebuff: (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("Merge Conflict splits! Enemy heals 8!"); ctx.log("Codegirl resolves the merge conflict and heals 8 HP!");
} }
}, },
bug_404: { bug_404: {
@ -68,6 +68,6 @@ export const ENEMIES = {
if (cyc === 3) return { type: "block", value: 0 }; // Crash → heal if (cyc === 3) return { type: "block", value: 0 }; // Crash → heal
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("Bug crashes, heals 8!"); } onBlock: (ctx) => { ctx.enemy.hp = Math.min(ctx.enemy.maxHp, ctx.enemy.hp + 8); ctx.log("Teej crashes and reboots, healing 8 HP!"); }
} }
}; };

4
src/data/relics.js

@ -17,7 +17,7 @@ export const RELICS = {
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.",
hooks: { onBattleStart: (ctx) => { ctx.player.energy += 2; ctx.log("Thermos: +2 energy") } } hooks: { onBattleStart: (ctx) => { ctx.player.energy += 2; ctx.log("Your coffee thermos provides an energizing boost!") } }
}, },
cpp_compiler: { cpp_compiler: {
id: "cpp_compiler", name: "Haskell", id: "cpp_compiler", name: "Haskell",
@ -34,7 +34,7 @@ export const RELICS = {
hooks: { hooks: {
onBattleStart: (ctx) => { onBattleStart: (ctx) => {
ctx.applyWeak(ctx.enemy, 1); ctx.applyWeak(ctx.enemy, 1);
ctx.log("Worst Streamer Award: Enemy starts Weak!"); ctx.log("Your Worst Streamer Award intimidates the enemy, making them weak!");
} }
} }
}, },

37
src/engine/battle.js

@ -13,8 +13,8 @@ export function createBattle(ctx, enemyId) {
const relicCtx = { const relicCtx = {
...ctx, ...ctx,
draw: (n) => draw(ctx.player, n), draw: (n) => draw(ctx.player, n),
applyWeak: (who, amt) => { who.weak = (who.weak || 0) + amt; ctx.log(`Weak +${amt}`) }, applyWeak: (who, amt) => { who.weak = (who.weak || 0) + amt; ctx.log(`${who === ctx.player ? 'You are' : ctx.enemy.name + ' is'} weakened for ${amt} turn${amt > 1 ? 's' : ''}.`) },
applyVulnerable: (who, amt) => { who.vuln = (who.vuln || 0) + amt; ctx.log(`Vulnerable +${amt}`) } applyVulnerable: (who, amt) => { who.vuln = (who.vuln || 0) + amt; ctx.log(`${who === ctx.player ? 'You are' : ctx.enemy.name + ' is'} vulnerable for ${amt} turn${amt > 1 ? 's' : ''}.`) }
}; };
for (const r of ctx.relicStates) r.hooks?.onBattleStart?.(relicCtx, r.state); for (const r of ctx.relicStates) r.hooks?.onBattleStart?.(relicCtx, r.state);
@ -30,7 +30,7 @@ export function startPlayerTurn(ctx) {
const relicCtx = { ...ctx, draw: (n) => draw(ctx.player, n) }; const relicCtx = { ...ctx, draw: (n) => draw(ctx.player, n) };
for (const r of ctx.relicStates) r.hooks?.onTurnStart?.(relicCtx, r.state); for (const r of ctx.relicStates) r.hooks?.onTurnStart?.(relicCtx, r.state);
ctx.log(`Your turn. Energy ${ctx.player.energy}.`); ctx.log(`Your turn begins. You have ${ctx.player.energy} energy to spend.`);
ctx.render(); ctx.render();
} }
@ -44,11 +44,11 @@ export function playCard(ctx, handIndex) {
if (ctx.flags.nextCardFree) { if (ctx.flags.nextCardFree) {
actualCost = 0; actualCost = 0;
ctx.flags.nextCardFree = false; ctx.flags.nextCardFree = false;
ctx.log("Infinite Vim: Card costs 0!"); ctx.log("Infinite Vim makes your next card free!");
} }
if (ctx.player.energy < actualCost) { ctx.log("Not enough energy."); return; } if (ctx.player.energy < actualCost) { ctx.log(`You need ${actualCost} energy but only have ${ctx.player.energy}.`); return; }
if (card.oncePerFight && card._used) { ctx.log("That macro already ran."); return; } if (card.oncePerFight && card._used) { ctx.log(`${card.name} can only be used once per fight.`); return; }
ctx.player.energy -= actualCost; ctx.player.energy -= actualCost;
ctx.lastCard = card.id; ctx.lastCard = card.id;
@ -79,7 +79,7 @@ export function playCard(ctx, handIndex) {
if (!card.exhaust) { if (!card.exhaust) {
ctx.player.discard.push(used.id); ctx.player.discard.push(used.id);
} else { } else {
ctx.log(`${used.name} exhausted.`); ctx.log(`${used.name} is exhausted and removed from the fight.`);
} }
} }
@ -102,10 +102,10 @@ export function enemyTurn(ctx) {
} else if (e.intent.type === "block") { } else if (e.intent.type === "block") {
ENEMIES[e.id].onBlock?.(ctx, e.intent.value); ENEMIES[e.id].onBlock?.(ctx, e.intent.value);
e.block += e.intent.value; e.block += e.intent.value;
ctx.log(`${e.name} gains ${e.intent.value} Block.`); ctx.log(`${e.name} defends and gains ${e.intent.value} block.`);
} else if (e.intent.type === "debuff") { } 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} applies a debuff.`); ctx.log(`${e.name} casts a debuffing spell.`);
} }
@ -123,7 +123,7 @@ export function enemyTurn(ctx) {
function applyDamage(ctx, target, raw, label) { function applyDamage(ctx, target, raw, label) {
if (target === ctx.enemy && ctx.enemy.id === "bug_404" && ctx.enemy.turn % 3 === 0) { if (target === ctx.enemy && ctx.enemy.id === "bug_404" && ctx.enemy.turn % 3 === 0) {
ctx.log("404 Bug dodges the attack!"); ctx.log(`${ctx.enemy.name} phases out and dodges your attack completely!`);
return; return;
} }
@ -136,7 +136,14 @@ function applyDamage(ctx, target, raw, label) {
const hpLoss = Math.max(0, dmg - blocked); const hpLoss = Math.max(0, dmg - blocked);
target.block -= blocked; target.block -= blocked;
target.hp = clamp(target.hp - hpLoss, 0, target.maxHp); target.hp = clamp(target.hp - hpLoss, 0, target.maxHp);
ctx.log(`${label}: ${dmg} → Block ${blocked} → HP -${hpLoss}`); const isPlayer = target === ctx.player;
if (blocked > 0 && hpLoss > 0) {
ctx.log(`${label} for ${dmg} damage. ${blocked} blocked, ${hpLoss} damage taken.`);
} else if (blocked > 0) {
ctx.log(`${label} for ${dmg} damage, but it's completely blocked!`);
} else {
ctx.log(`${label} for ${dmg} damage!`);
}
if (hpLoss > 0 && ctx.showDamageNumber) { if (hpLoss > 0 && ctx.showDamageNumber) {
@ -154,16 +161,16 @@ export function makeBattleContext(root) {
log: (m) => root.log(m), log: (m) => root.log(m),
render: () => root.render(), render: () => root.render(),
intentIsAttack: () => root.enemy.intent.type === "attack", intentIsAttack: () => root.enemy.intent.type === "attack",
deal: (target, amount) => applyDamage(root, target, amount, "You hit"), deal: (target, amount) => applyDamage(root, target, amount, target === root.enemy ? "You attack" : `${root.enemy.name} hits you`),
applyWeak: (who, amt) => { who.weak = (who.weak || 0) + amt; root.log(`Weak +${amt}`) }, applyWeak: (who, amt) => { who.weak = (who.weak || 0) + amt; root.log(`${who === root.player ? 'You are' : root.enemy.name + ' is'} weakened for ${amt} turn${amt > 1 ? 's' : ''}.`) },
applyVulnerable: (who, amt) => { who.vuln = (who.vuln || 0) + amt; root.log(`Vulnerable +${amt}`) }, applyVulnerable: (who, amt) => { who.vuln = (who.vuln || 0) + amt; root.log(`${who === root.player ? 'You are' : root.enemy.name + ' is'} vulnerable for ${amt} turn${amt > 1 ? 's' : ''}.`) },
forceEndTurn: () => endTurn(root), forceEndTurn: () => endTurn(root),
promptExhaust: async (count) => { // MVP: exhaust first N non-basics promptExhaust: async (count) => { // MVP: exhaust first N non-basics
while (count-- > 0 && root.player.hand.length > 0) { while (count-- > 0 && root.player.hand.length > 0) {
const idx = root.player.hand.findIndex(c => !["strike", "defend"].includes(c.id)); const idx = root.player.hand.findIndex(c => !["strike", "defend"].includes(c.id));
const drop = idx >= 0 ? idx : 0; const drop = idx >= 0 ? idx : 0;
const [ex] = root.player.hand.splice(drop, 1); const [ex] = root.player.hand.splice(drop, 1);
root.log(`Exhausted ${ex.name}`); root.log(`${ex.name} is exhausted and removed from the fight.`);
} }
}, },
scalarFromWeak: (base) => (root.player.weak > 0 ? Math.floor(base * 0.75) : base), scalarFromWeak: (base) => (root.player.weak > 0 ? Math.floor(base * 0.75) : base),

961
src/ui/render.js

File diff suppressed because it is too large Load Diff

75
style.css

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

Loading…
Cancel
Save