I can't believe I made this either...
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

295 lines
11 KiB

import { ENEMIES } from "../data/enemies.js";
import { RELICS } from "../data/relics.js";
import { CARDS } from "../data/cards.js";
import { draw, endTurnDiscard, clamp, cloneCard, shuffle, peekTopCards, putCardOnBottomOfDeck, addCardToHand } from "./core.js";
export function createBattle(ctx, enemyId) {
const enemyData = ENEMIES[enemyId];
const enemy = { id: enemyId, name: enemyData.name, maxHp: enemyData.maxHp, hp: enemyData.maxHp, block: 0, weak: 0, vuln: 0, turn: 1, intent: enemyData.ai(1) };
ctx.enemy = enemy;
ctx.flags = {};
ctx.lastCard = null;
// Initialize draw pile from current deck for the battle
ctx.player.draw = shuffle(ctx.player.deck.slice());
ctx.player.discard = [];
ctx.player.hand = [];
const relicCtx = {
...ctx,
draw: (n) => draw(ctx.player, n, ctx),
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(`${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);
startPlayerTurn(ctx);
}
export function startPlayerTurn(ctx) {
if (ctx.flags.skipThisTurn) { ctx.flags.skipThisTurn = false; enemyTurn(ctx); return; }
ctx.player.energy = ctx.player.maxEnergy + (ctx.flags.nextTurnEnergyBonus || 0) - (ctx.flags.nextTurnEnergyPenalty || 0);
ctx.flags.nextTurnEnergyBonus = 0;
ctx.flags.nextTurnEnergyPenalty = 0;
draw(ctx.player, 5 - ctx.player.hand.length, ctx);
// Clear card selection when new turn starts
ctx.selectedCardIndex = null;
const relicCtx = { ...ctx, draw: (n) => draw(ctx.player, n, ctx) };
for (const r of ctx.relicStates) r.hooks?.onTurnStart?.(relicCtx, r.state);
ctx.log(`Your turn begins. You have ${ctx.player.energy} energy to spend.`);
ctx.render();
}
export function playCard(ctx, handIndex) {
const card = ctx.player.hand[handIndex];
if (!card) return;
let actualCost = card.cost;
if (ctx.flags.nextCardFree) {
actualCost = 0;
ctx.flags.nextCardFree = false;
ctx.log("Infinite Vim makes your next card free!");
}
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(`${card.name} can only be used once per fight.`); return; }
// Check card-specific play conditions
if (card.id === "hotfix" && ctx.player.hp > ctx.player.maxHp * 0.5) {
ctx.log("Hotfix can only be deployed when HP is below 50%!");
return;
}
// Prevent playing curse cards
if (card.type === "curse") {
ctx.log(`${card.name} cannot be played!`);
return;
}
ctx.player.energy -= actualCost;
ctx.lastCard = card.id;
// Remove the played card from hand BEFORE running effect to prevent index shifting issues
let usedCard = null;
if (card.type !== "power") {
const [removed] = ctx.player.hand.splice(handIndex, 1);
usedCard = removed;
}
const prevDeal = ctx.deal;
ctx.deal = (target, amount) => {
let amt = amount;
// Handle doubleNextCard flag
if (ctx.flags.doubleNextCard) {
amt *= 2;
ctx.flags.doubleNextCard = false;
ctx.log("Pair Programming doubles the damage!");
}
for (const r of ctx.relicStates) {
if (r.hooks?.onPlayerAttack) amt = r.hooks.onPlayerAttack(ctx, r.state, amt);
}
prevDeal(target, amt);
};
if (typeof card.effect !== 'function') {
console.error('Card effect is not a function:', card);
ctx.log(`Error: ${card.name || 'Unknown card'} has no effect function`);
return;
}
try {
card.effect(ctx);
card._used = true;
} catch (error) {
console.error('Card effect error:', error, 'Card:', card);
ctx.log(`Error playing ${card.name || 'Unknown card'}: ${error.message}`);
return;
}
// Handle card disposal after effect (if it was removed from hand)
if (usedCard) {
if (!card.exhaust) {
ctx.player.discard.push(usedCard.id);
} else {
ctx.log(`${usedCard.name} is exhausted and removed from the fight.`);
}
}
if (ctx.enemy.hp <= 0) { ctx.enemy.hp = 0; ctx.onWin(); return; }
if (ctx.player.hp <= 0) { ctx.onLose(); return; }
// Don't render if Code Review modal is active
if (ctx.root._codeReviewCards) {
return;
}
ctx.render();
}
export function endTurn(ctx) {
endTurnDiscard(ctx.player);
enemyTurn(ctx);
}
export function enemyTurn(ctx) {
const e = ctx.enemy;
if (e.intent.type === "attack") {
let dmg = e.intent.value;
if (e.weak > 0) dmg = Math.floor(dmg * 0.75);
applyDamage(ctx, ctx.player, dmg, `${e.name} attacks`);
} else if (e.intent.type === "block") {
try {
ENEMIES[e.id].onBlock?.(ctx, e.intent.value);
e.block += e.intent.value;
ctx.log(`${e.name} defends and gains ${e.intent.value} block.`);
} catch (error) {
console.error('Enemy block effect error:', error, 'Enemy:', e.id);
ctx.log(`${e.name} tries to defend but fumbles!`);
}
} else if (e.intent.type === "debuff") {
try {
ENEMIES[e.id].onDebuff?.(ctx, e.intent.value);
ctx.log(`${e.name} casts a debuffing spell.`);
} catch (error) {
console.error('Enemy debuff effect error:', error, 'Enemy:', e.id);
ctx.log(`${e.name} tries to cast a spell but it fizzles!`);
}
} else if (e.intent.type === "heal") {
try {
ENEMIES[e.id].onHeal?.(ctx, e.intent.value);
} catch (error) {
console.error('Enemy heal effect error:', error, 'Enemy:', e.id);
ctx.log(`${e.name} tries to heal but something goes wrong!`);
}
}
if (e.weak > 0) e.weak--;
if (ctx.player.weak > 0) ctx.player.weak--;
if (ctx.player.hp <= 0) { ctx.onLose(); return; }
if (ctx.enemy.hp <= 0) { ctx.enemy.hp = 0; ctx.onWin(); return; }
e.turn++;
try {
e.intent = ENEMIES[e.id].ai(e.turn);
if (!e.intent || !e.intent.type) {
throw new Error('Invalid enemy intent returned');
}
} catch (error) {
console.error('Enemy AI error:', error, 'Enemy:', e.id);
ctx.log(`Enemy AI malfunction! ${e.name} does nothing this turn.`);
e.intent = { type: "block", value: 0 }; // Safe fallback
}
startPlayerTurn(ctx);
}
function applyDamage(ctx, target, raw, label) {
let dmg = raw;
for (const r of ctx.relicStates) {
if (r.hooks?.onDamageTaken && target === ctx.player) dmg = r.hooks.onDamageTaken(ctx, dmg);
}
const blocked = Math.min(dmg, target.block);
const hpLoss = Math.max(0, dmg - blocked);
target.block -= blocked;
target.hp = clamp(target.hp - hpLoss, 0, target.maxHp);
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) {
const isPlayer = target === ctx.player;
ctx.showDamageNumber(hpLoss, target, isPlayer);
}
}
export function makeBattleContext(root) {
return {
player: root.player,
enemy: root.enemy,
discard: root.player.discard,
relicStates: root.relicStates || [],
draw: (n) => draw(root.player, n, root),
log: (m) => root.log(m),
render: () => root.render(),
onWin: () => root.onWin(),
onLose: () => root.onLose(),
intentIsAttack: () => root.enemy.intent.type === "attack",
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(`${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(`${who === root.player ? 'You are' : root.enemy.name + ' is'} vulnerable for ${amt} turn${amt > 1 ? 's' : ''}.`) },
forceEndTurn: () => endTurn(root),
promptExhaust: async (count) => { // MVP: exhaust first N non-basics
while (count-- > 0 && root.player.hand.length > 0) {
const idx = root.player.hand.findIndex(c => !["strike", "defend"].includes(c.id));
const drop = idx >= 0 ? idx : 0;
const [ex] = root.player.hand.splice(drop, 1);
root.log(`${ex.name} is exhausted and removed from the fight.`);
}
},
scalarFromWeak: (base) => (root.player.weak > 0 ? Math.floor(base * 0.75) : base),
showDamageNumber: root.showDamageNumber,
lastCard: null,
flags: {},
root: root, // Provide access to root for complex card effects
// 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;
},
// Code Review card mechanics
peekTop: (n) => peekTopCards(root.player, n),
putOnBottom: (cardId) => putCardOnBottomOfDeck(root.player, cardId),
addToHand: (card) => addCardToHand(root.player, card),
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') {
const battleCtx = makeBattleContext(root);
card.effect(battleCtx);
root.log(`${card.name} is replayed!`);
}
},
};
}
export function attachRelics(root, relicIds) {
root.relicStates = relicIds.map(id => ({ id, hooks: RELICS[id].hooks || {}, state: structuredClone(RELICS[id].state || {}) }));
const relicCtx = {
...root,
draw: (n) => draw(root.player, n),
applyWeak: (who, amt) => { who.weak = (who.weak || 0) + amt; root.log(`Weak +${amt}`) },
applyVulnerable: (who, amt) => { who.vuln = (who.vuln || 0) + amt; root.log(`Vulnerable +${amt}`) }
};
for (const r of root.relicStates) r.hooks?.onRunStart?.(relicCtx, r.state);
}