import type { IGameContext } from "@/core/game"; import type { CombatState, CombatResult, CombatGameContext, CombatEffectEntry } from "./types"; import type { CombatTriggerRegistry, TriggerContext } from "./triggers"; import { createCombatTriggerRegistry, dispatchTrigger, dispatchShuffleTrigger, dispatchOutgoingDamageTrigger, dispatchIncomingDamageTrigger, dispatchDamageTrigger } from "./triggers"; import { prompts } from "./prompts"; import { drawCardsToHand, addFatigueCards, discardHand, discardCard, getEnemyCurrentIntent, advanceEnemyIntent, DEFAULT_MAX_ENERGY, FATIGUE_CARDS_PER_SHUFFLE, } from "./state"; import { applyDamage, applyDefend, updateBuffs, canPlayCard, playCard, areAllEnemiesDead, isPlayerDead, resolveCardEffects, removeBuff, } from "./effects"; export async function runCombat( game: CombatGameContext, ): Promise { const triggerRegistry = createCombatTriggerRegistry(); game.produce(state => { state.phase = "playerTurn"; state.player.energy = state.player.maxEnergy; state.player.damageTakenThisTurn = 0; state.player.damagedThisTurn = false; state.player.cardsDiscardedThisTurn = 0; }); while (true) { const currentState = game.value; if (currentState.result) { return currentState.result; } if (currentState.phase === "playerTurn") { await runPlayerTurn(game, triggerRegistry); } else if (currentState.phase === "enemyTurn") { await runEnemyTurn(game, triggerRegistry); } else { break; } if (isPlayerDead(game.value)) { game.produce(state => { state.result = "defeat"; state.phase = "combatEnd"; }); return "defeat"; } if (areAllEnemiesDead(game.value)) { game.produce(state => { state.result = "victory"; state.phase = "combatEnd"; state.loot = generateLoot(state); }); return "victory"; } if (game.value.result === "fled") { game.produce(state => { state.phase = "combatEnd"; }); return "fled"; } } return game.value.result ?? "defeat"; } async function runPlayerTurn( game: CombatGameContext, triggerRegistry: CombatTriggerRegistry, ): Promise { const triggerCtx = createTriggerContext(game); game.produce(state => { updateBuffs(state.player.buffs); state.player.damageTakenThisTurn = 0; state.player.damagedThisTurn = false; state.player.cardsDiscardedThisTurn = 0; if (state.player.buffs["energyNext"]) { state.player.energy += state.player.buffs["energyNext"]; } if (state.player.buffs["drawNext"]) { drawCardsToHand(state.player.deck, state.player.buffs["drawNext"]); } if (state.player.buffs["defendNext"]) { applyDefend(state.player.buffs, state.player.buffs["defendNext"]); } }); dispatchTrigger(triggerCtx, "onTurnStart", "player", triggerRegistry); while (game.value.phase === "playerTurn") { const action = await game.prompt<{ action: "play" | "end"; cardId?: string; targetId?: string }>( prompts.playCard, (cardId, targetId) => { const state = game.value; if (!cardId) throw "请选择卡牌"; const check = canPlayCard(state, cardId); if (!check.canPlay) throw check.reason ?? "无法打出"; const card = state.player.deck.cards[cardId]; if (card?.itemData?.targetType === "single") { const aliveEnemies = state.enemyOrder.filter(id => state.enemies[id].isAlive); if (!targetId && aliveEnemies.length > 0) { throw "请指定目标"; } if (targetId && !state.enemies[targetId]?.isAlive) { throw "目标无效"; } } return { action: "play" as const, cardId, targetId }; }, "player" ); if (action.action === "play" && action.cardId) { const ctx = createEffectContext(game); game.produce(state => { playCard({ state, rng: game._rng }, action.cardId!, action.targetId); }); if (areAllEnemiesDead(game.value)) { return; } if (isPlayerDead(game.value)) { return; } continue; } break; } const endAction = await game.prompt<{ action: "end" }>( prompts.endTurn, () => { return { action: "end" as const }; }, "player" ); dispatchTrigger(createTriggerContext(game), "onTurnEnd", "player", triggerRegistry); game.produce(state => { for (const cardId of [...state.player.deck.hand]) { state.player.cardsDiscardedThisTurn++; } discardHand(state.player.deck); }); game.produce(state => { if (state.player.deck.drawPile.length === 0) { reshuffleWithFatigue(state); dispatchShuffleTrigger(createTriggerContext(game), triggerRegistry); } drawCardsToHand(state.player.deck, 5); state.player.energy = state.player.maxEnergy; }); game.produce(state => { state.phase = "enemyTurn"; }); } async function runEnemyTurn( game: CombatGameContext, triggerRegistry: CombatTriggerRegistry, ): Promise { const state = game.value; game.produce(state => { for (const enemyId of state.enemyOrder) { const enemy = state.enemies[enemyId]; if (!enemy.isAlive) continue; updateBuffs(enemy.buffs); } }); const triggerCtx = createTriggerContext(game); for (const enemyId of game.value.enemyOrder) { const enemy = game.value.enemies[enemyId]; if (!enemy.isAlive) continue; dispatchTrigger(triggerCtx, "onTurnStart", enemyId, triggerRegistry); } game.produce(state => { for (const enemyId of state.enemyOrder) { const enemy = state.enemies[enemyId]; if (!enemy.isAlive) continue; const intent = getEnemyCurrentIntent(enemy); if (!intent) continue; const effects = intent.effects as unknown as CombatEffectEntry[]; for (const entry of effects) { const [target, effect, stacks] = entry; if (effect.id === "attack") { let damage = stacks; damage = dispatchOutgoingDamageTrigger(createTriggerContext(game), enemyId, damage, triggerRegistry); damage = dispatchIncomingDamageTrigger(createTriggerContext(game), "player", damage, triggerRegistry); const result = applyDamage(state, "player", damage, enemyId); if (result.damageDealt > 0) { dispatchDamageTrigger(createTriggerContext(game), "player", result.damageDealt, triggerRegistry); } } else if (effect.id === "defend") { if (target === "self") { applyDefend(enemy.buffs, stacks); } } else { resolveEnemyEffect(state, enemyId, target, effect, stacks); } } advanceEnemyIntent(enemy); } }); for (const enemyId of game.value.enemyOrder) { const enemy = game.value.enemies[enemyId]; if (!enemy.isAlive) continue; dispatchTrigger(createTriggerContext(game), "onTurnEnd", enemyId, triggerRegistry); } game.produce(state => { state.phase = "playerTurn"; state.turnNumber++; }); } function resolveEnemyEffect( state: CombatState, enemyId: string, target: string, effect: { id: string; timing: string }, stacks: number, ): void { switch (effect.id) { case "spike": case "venom": case "curse": case "aim": case "roll": case "vultureEye": case "tailSting": case "energyDrain": case "molt": case "storm": case "static": case "charge": case "discard": state.enemies[enemyId].buffs[effect.id] = (state.enemies[enemyId].buffs[effect.id] ?? 0) + stacks; break; case "summonMummy": case "summonSandwormLarva": case "reviveMummy": break; default: break; } } function reshuffleWithFatigue(state: CombatState): void { if (state.player.deck.discardPile.length === 0) return; state.player.deck.drawPile.push(...state.player.deck.discardPile); state.player.deck.discardPile = []; addFatigueCards(state.player.deck, FATIGUE_CARDS_PER_SHUFFLE, { value: state.fatigueAddedCount }); state.fatigueAddedCount += FATIGUE_CARDS_PER_SHUFFLE; for (let i = state.player.deck.drawPile.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [state.player.deck.drawPile[i], state.player.deck.drawPile[j]] = [state.player.deck.drawPile[j], state.player.deck.drawPile[i]]; } } function createTriggerContext(game: CombatGameContext): TriggerContext { return { state: game.value, rng: game._rng, }; } function createEffectContext(game: CombatGameContext) { return { state: game.value, rng: game._rng, }; } function generateLoot(state: CombatState): CombatState["loot"] { const loot: CombatState["loot"] = []; let totalGold = 0; for (const enemyId of state.enemyOrder) { const enemy = state.enemies[enemyId]; totalGold += Math.floor(enemy.maxHp * 0.5); } if (totalGold > 0) { loot.push({ type: "gold", amount: totalGold }); } return loot; }