import { effectDesertData, cardDesertData } from "../data"; import { createStatusCard } from "../deck/factory"; import type { PlayerDeck, GameCard } from "../deck/types"; import type { BuffTable, CombatEffectEntry, CombatState, EffectData, EffectTarget, EffectTiming, EnemyState, ItemBuff, PlayerCombatState, } from "./types"; import { drawCardsToHand, addFatigueCards, discardCard, exhaustCard, getEffectData, discardHand, } from "./state"; export type DamageResult = { damageDealt: number; blockedByDefend: number; targetDied: boolean; }; export function applyDamage( state: CombatState, targetKey: "player" | string, amount: number, sourceKey?: "player" | string, ): DamageResult { if (amount <= 0) return { damageDealt: 0, blockedByDefend: 0, targetDied: false }; let actualDamage = amount; let blockedByDefend = 0; if (targetKey === "player") { const defendStacks = state.player.buffs["defend"] ?? 0; if (defendStacks > 0) { blockedByDefend = Math.min(defendStacks, actualDamage); actualDamage -= blockedByDefend; state.player.buffs["defend"] = defendStacks - blockedByDefend; if (state.player.buffs["defend"] === 0) { delete state.player.buffs["defend"]; } } const damageReduce = state.player.buffs["damageReduce"] ?? 0; if (damageReduce > 0 && actualDamage > 0) { actualDamage = Math.max(0, actualDamage - damageReduce); } if (actualDamage > 0) { state.player.hp = Math.max(0, state.player.hp - actualDamage); state.player.damageTakenThisTurn += actualDamage; state.player.damagedThisTurn = true; } if (blockedByDefend > 0 && defendStacks - blockedByDefend <= 0) { for (const enemyId of state.enemyOrder) { const enemy = state.enemies[enemyId]; if (enemy.isAlive && enemy.buffs["defend"] !== undefined) { // Not relevant for player, skip } } } return { damageDealt: actualDamage, blockedByDefend, targetDied: state.player.hp <= 0, }; } const enemy = state.enemies[targetKey]; if (!enemy || !enemy.isAlive) { return { damageDealt: 0, blockedByDefend: 0, targetDied: false }; } const defendStacks = enemy.buffs["defend"] ?? 0; if (defendStacks > 0) { blockedByDefend = Math.min(defendStacks, actualDamage); actualDamage -= blockedByDefend; enemy.buffs["defend"] = defendStacks - blockedByDefend; if (enemy.buffs["defend"] === 0) { delete enemy.buffs["defend"]; } if (defendStacks > 0 && defendStacks - blockedByDefend <= 0) { enemy.hadDefendBroken = true; } } if (actualDamage > 0) { enemy.hp = Math.max(0, enemy.hp - actualDamage); if (enemy.hp <= 0) { enemy.isAlive = false; } } return { damageDealt: actualDamage, blockedByDefend, targetDied: !enemy.isAlive, }; } export function applyDefend( targetBuffs: BuffTable, amount: number, ): void { if (amount <= 0) return; targetBuffs["defend"] = (targetBuffs["defend"] ?? 0) + amount; } export function applyBuff( buffs: BuffTable, effectId: string, timing: EffectTiming, stacks: number, ): void { if (stacks <= 0) return; buffs[effectId] = (buffs[effectId] ?? 0) + stacks; } export function removeBuff(buffs: BuffTable, effectId: string, stacks?: number): number { const current = buffs[effectId] ?? 0; if (stacks === undefined || stacks >= current) { delete buffs[effectId]; return current; } buffs[effectId] = current - stacks; return stacks; } export function updateBuffs(buffs: BuffTable): void { const toDelete: string[] = []; const toDecrement: string[] = []; for (const [effectId] of Object.entries(buffs)) { const effectData = getEffectData(effectId); if (!effectData) continue; switch (effectData.timing) { case "temporary": toDelete.push(effectId); break; case "lingering": toDecrement.push(effectId); break; } } for (const id of toDelete) { delete buffs[id]; } for (const id of toDecrement) { buffs[id] = (buffs[id] ?? 0) - 1; if (buffs[id] <= 0) { delete buffs[id]; } } } export type ResolveEffectContext = { state: CombatState; rng: { nextInt: (n: number) => number }; }; export function resolveEffect( ctx: ResolveEffectContext, target: EffectTarget, effect: EffectData, stacks: number, sourceKey?: "player" | string, sourceCardId?: string, ): void { const { state } = ctx; const timing = effect.timing; switch (timing) { case "instant": resolveInstantEffect(ctx, target, effect, stacks, sourceKey, sourceCardId); break; case "posture": applyBuffToTarget(state, target, effect.id, stacks, sourceKey); break; case "temporary": case "lingering": case "permanent": applyBuffToTarget(state, target, effect.id, stacks, sourceKey); break; case "card": addStatusCardToDiscard(state, effect.id, stacks); break; case "cardDraw": addStatusCardToDrawPile(state, effect.id, stacks); break; case "cardHand": addStatusCardToHand(state, effect.id, stacks); break; case "item": case "itemUntilPlayed": applyItemBuff(state, effect.id, timing, stacks, sourceCardId); break; } } function applyBuffToTarget( state: CombatState, target: EffectTarget, effectId: string, stacks: number, sourceKey?: "player" | string, ): void { if (target === "self") { if (sourceKey === "player") { applyBuff(state.player.buffs, effectId, getEffectData(effectId)?.timing ?? "permanent", stacks); } else if (sourceKey && state.enemies[sourceKey]) { applyBuff(state.enemies[sourceKey].buffs, effectId, getEffectData(effectId)?.timing ?? "permanent", stacks); } } else if (target === "player" || target === "team") { applyBuff(state.player.buffs, effectId, getEffectData(effectId)?.timing ?? "permanent", stacks); } else if (target === "target" || target === "all" || target === "random") { // For attack/defend effects, these are handled by resolveInstantEffect } } function resolveInstantEffect( ctx: ResolveEffectContext, target: EffectTarget, effect: EffectData, stacks: number, sourceKey?: "player" | string, sourceCardId?: string, ): void { const { state, rng } = ctx; switch (effect.id) { case "attack": { const damageAmount = stacks; if (target === "all") { for (const enemyId of state.enemyOrder) { const enemy = state.enemies[enemyId]; if (enemy.isAlive) { applyDamage(state, enemyId, damageAmount, sourceKey); } } } else if (target === "random") { const aliveEnemies = state.enemyOrder.filter(id => state.enemies[id].isAlive); if (aliveEnemies.length > 0) { const targetId = aliveEnemies[rng.nextInt(aliveEnemies.length)]; applyDamage(state, targetId, damageAmount, sourceKey); } } else if (target === "target") { if (sourceKey && sourceKey !== "player" && state.enemies[sourceKey]?.isAlive) { applyDamage(state, "player", damageAmount, sourceKey); } } break; } case "defend": { if (target === "self" && sourceKey === "player") { applyDefend(state.player.buffs, stacks); } else if (target === "self" && sourceKey && state.enemies[sourceKey]) { applyDefend(state.enemies[sourceKey].buffs, stacks); } break; } case "draw": { drawCardsToHand(state.player.deck, stacks); break; } case "gainEnergy": { state.player.energy += stacks; break; } case "removeWound": { removeWoundCards(state.player.deck, stacks); break; } case "tailSting": { applyDamage(state, "player", stacks, sourceKey); break; } case "rollDamage": { const rollStacks = sourceKey && sourceKey !== "player" ? state.enemies[sourceKey]?.buffs["roll"] ?? 0 : 0; if (rollStacks >= 10) { const damageFromRoll = Math.floor(rollStacks / 10) * 10; applyDamage(state, "player", Math.floor(damageFromRoll / 10), sourceKey); removeBuff(state.enemies[sourceKey!].buffs, "roll", damageFromRoll); } break; } case "crossbow": { break; } case "discard": { break; } case "summonMummy": case "summonSandwormLarva": case "reviveMummy": { break; } case "drawChoice": case "transformRandom": { break; } default: break; } } function addStatusCardToDiscard(state: CombatState, effectId: string, count: number): void { const cardDef = cardDesertData.find(c => c.id === effectId); if (!cardDef) return; for (let i = 0; i < count; i++) { const cardId = `status-${effectId}-${Date.now()}-${i}`; const card = createStatusCard(cardId, cardDef.name, cardDef.desc); state.player.deck.cards[card.id] = card; state.player.deck.discardPile.push(card.id); } } function addStatusCardToDrawPile(state: CombatState, effectId: string, count: number): void { const cardDef = cardDesertData.find(c => c.id === effectId); if (!cardDef) return; for (let i = 0; i < count; i++) { const cardId = `status-${effectId}-${Date.now()}-${i}`; const card = createStatusCard(cardId, cardDef.name, cardDef.desc); state.player.deck.cards[card.id] = card; state.player.deck.drawPile.push(card.id); } } function addStatusCardToHand(state: CombatState, effectId: string, count: number): void { const cardDef = cardDesertData.find(c => c.id === effectId); if (!cardDef) return; for (let i = 0; i < count; i++) { const cardId = `status-${effectId}-${Date.now()}-${i}`; const card = createStatusCard(cardId, cardDef.name, cardDef.desc); state.player.deck.cards[card.id] = card; state.player.deck.hand.push(card.id); } } function applyItemBuff( state: CombatState, effectId: string, timing: EffectTiming, stacks: number, sourceCardId?: string, ): void { if (!sourceCardId) return; const card = state.player.deck.cards[sourceCardId]; if (!card || !card.sourceItemId) return; const itemBuff: ItemBuff = { effectId, stacks, timing, sourceItemId: card.sourceItemId, targetItemId: card.sourceItemId, }; state.player.itemBuffs.push(itemBuff); } function removeWoundCards(deck: PlayerDeck, count: number): void { let removed = 0; for (let i = deck.drawPile.length - 1; i >= 0 && removed < count; i--) { const card = deck.cards[deck.drawPile[i]]; if (card && card.itemData === null && card.displayName === "伤口") { delete deck.cards[deck.drawPile[i]]; deck.drawPile.splice(i, 1); removed++; } } for (let i = deck.discardPile.length - 1; i >= 0 && removed < count; i--) { const card = deck.cards[deck.discardPile[i]]; if (card && card.itemData === null && card.displayName === "伤口") { delete deck.cards[deck.discardPile[i]]; deck.discardPile.splice(i, 1); removed++; } } } export function resolveCardEffects( ctx: ResolveEffectContext, cardId: string, targetEnemyId?: string, ): void { const { state } = ctx; const card = state.player.deck.cards[cardId]; if (!card || !card.itemData) return; const sourceKey: "player" | string = "player"; const effects = card.itemData.onPlay as unknown as CombatEffectEntry[]; for (const entry of effects) { const [target, effect, stacks] = entry; if (target === "target") { if (targetEnemyId && state.enemies[targetEnemyId]?.isAlive) { if (effect.id === "attack") { const actualDamage = getModifiedAttackDamage(state, cardId, stacks); applyDamage(state, targetEnemyId, actualDamage, "player"); continue; } } } resolveEffect(ctx, target, effect, stacks, sourceKey, cardId); } } export function getModifiedAttackDamage( state: CombatState, cardId: string, baseDamage: number, ): number { let damage = baseDamage; const attackBuff = state.player.itemBuffs .filter((b) => b.effectId === "attackBuff" || b.effectId === "attackBuffUntilPlay") .filter((b) => { const card = state.player.deck.cards[cardId]; return card && card.sourceItemId === b.targetItemId; }) .reduce((sum, b) => sum + b.stacks, 0); damage += attackBuff; return Math.max(0, damage); } export function getModifiedDefendAmount( state: CombatState, cardId: string, baseDefend: number, ): number { let defend = baseDefend; const defendBuff = state.player.itemBuffs .filter((b) => b.effectId === "defendBuff" || b.effectId === "defendBuffUntilPlay") .filter((b) => { const card = state.player.deck.cards[cardId]; return card && card.sourceItemId === b.targetItemId; }) .reduce((sum, b) => sum + b.stacks, 0); defend += defendBuff; return Math.max(0, defend); } export function canPlayCard( state: CombatState, cardId: string, ): { canPlay: boolean; reason?: string } { const card = state.player.deck.cards[cardId]; if (!card) return { canPlay: false, reason: "卡牌不存在" }; if (!card.itemData) return { canPlay: false, reason: "状态牌不可打出" }; const handIdx = state.player.deck.hand.indexOf(cardId); if (handIdx < 0) return { canPlay: false, reason: "卡牌不在手牌中" }; if (card.itemData.costType === "energy") { if (state.player.energy < card.itemData.costCount) { return { canPlay: false, reason: "能量不足" }; } } return { canPlay: true }; } export function playCard( ctx: ResolveEffectContext, cardId: string, targetEnemyId?: string, ): { success: boolean; reason?: string } { const { state } = ctx; const check = canPlayCard(state, cardId); if (!check.canPlay) return { success: false, reason: check.reason }; const card = state.player.deck.cards[cardId]; if (!card || !card.itemData) return { success: false, reason: "卡牌无效" }; if (card.itemData.costType === "energy") { state.player.energy -= card.itemData.costCount; } resolveCardEffects(ctx, cardId, targetEnemyId); if (card.itemData.costType === "uses") { exhaustCard(state.player.deck, cardId); } else { discardCard(state.player.deck, cardId); } expireItemBuffsOnCardPlayed(state, cardId); return { success: true }; } function expireItemBuffsOnCardPlayed(state: CombatState, cardId: string): void { const card = state.player.deck.cards[cardId]; if (!card || !card.sourceItemId) return; state.player.itemBuffs = state.player.itemBuffs.filter((buff) => { if (buff.timing === "itemUntilPlayed" && buff.sourceItemId === card.sourceItemId) { return false; } return true; }); } export function areAllEnemiesDead(state: CombatState): boolean { return state.enemyOrder.every(id => !state.enemies[id].isAlive); } export function isPlayerDead(state: CombatState): boolean { return state.player.hp <= 0; }