import type { EffectDesert } from "../data/effectDesert.csv"; import { statusCardDesertData } from "../data"; import { createStatusCard } from "../deck/factory"; import type { BuffTable, CombatEffectEntry, CombatState } from "./types"; import { applyDamage, removeBuff } from "./effects"; export type TriggerContext = { state: CombatState; rng: { nextInt: (n: number) => number }; }; export type BuffTriggerBehavior = { onTurnStart?: (ctx: TriggerContext, entityKey: "player" | string, stacks: number) => void; onTurnEnd?: (ctx: TriggerContext, entityKey: "player" | string, stacks: number) => void; onAttacked?: (ctx: TriggerContext, attackerKey: "player" | string, defenderKey: "player" | string, damage: number, stacks: number) => number; onDamage?: (ctx: TriggerContext, targetKey: "player" | string, damage: number, stacks: number) => void; modifyOutgoingDamage?: (ctx: TriggerContext, sourceKey: "player" | string, damage: number, stacks: number) => number; modifyIncomingDamage?: (ctx: TriggerContext, targetKey: "player" | string, damage: number, stacks: number) => number; onShuffle?: (ctx: TriggerContext, stacks: number) => void; onCardPlayed?: (ctx: TriggerContext, cardId: string, stacks: number) => void; onCardDiscarded?: (ctx: TriggerContext, cardId: string, stacks: number) => void; }; export type CombatTriggerRegistry = Record; export function createCombatTriggerRegistry(): CombatTriggerRegistry { return { spike: { onAttacked(ctx, attackerKey, _defenderKey, damage, stacks) { const { state } = ctx; applyDamage(state, attackerKey, stacks, _defenderKey); return damage; }, }, aim: { modifyOutgoingDamage(_ctx, _sourceKey, damage, stacks) { if (stacks > 0) return damage * 2; return damage; }, onDamage(ctx, targetKey, damage, stacks) { const { state } = ctx; const entity = targetKey === "player" ? null : state.enemies[targetKey]; if (entity) { const loss = Math.min(stacks, damage); removeBuff(entity.buffs, "aim", loss); } }, }, charge: { modifyOutgoingDamage(_ctx, _sourceKey, damage, stacks) { if (stacks > 0) { return damage * 2; } return damage; }, modifyIncomingDamage(_ctx, _targetKey, damage, stacks) { if (stacks > 0) { return damage * 2; } return damage; }, onDamage(ctx, targetKey, damage, stacks) { const { state } = ctx; const entity = targetKey === "player" ? { buffs: state.player.buffs } as { buffs: BuffTable } : state.enemies[targetKey]; if (entity) { const loss = Math.min(stacks, damage); removeBuff(entity.buffs, "charge", loss); } }, }, roll: { modifyOutgoingDamage(ctx, sourceKey, damage, stacks) { if (stacks >= 10) { const { state } = ctx; const entity = sourceKey === "player" ? { buffs: state.player.buffs } as { buffs: BuffTable } : state.enemies[sourceKey]; if (entity) { const spendable = Math.floor(stacks / 10) * 10; const bonusDamage = Math.floor(spendable / 10); removeBuff(entity.buffs, "roll", spendable); return damage + bonusDamage; } } return damage; }, }, tailSting: { onTurnEnd(ctx, entityKey, stacks) { const { state } = ctx; if (entityKey !== "player" && state.enemies[entityKey]?.isAlive) { applyDamage(state, "player", stacks, entityKey); } }, }, energyDrain: { onDamage(ctx, targetKey, _damage, _stacks) { const { state } = ctx; if (targetKey === "player" && state.player.damagedThisTurn === false) { // This is the first damage; mark it. // actual energy drain happens in onTurnStart check } }, onTurnStart(ctx, entityKey, _stacks) { // energyDrain: first damage each turn loses 1 energy // We just mark that the enemy has this; actual drain is in onDamage }, }, molt: { onDamage(ctx, targetKey, _damage, _stacks) { const { state } = ctx; if (targetKey !== "player") { const enemy = state.enemies[targetKey]; if (enemy && enemy.isAlive) { const moltStacks = enemy.buffs["molt"] ?? 0; if (moltStacks >= enemy.maxHp) { enemy.isAlive = false; state.result = "fled"; } } } }, }, storm: { onAttacked(ctx, attackerKey, defenderKey, damage, stacks) { const { state } = ctx; if (defenderKey !== "player" && state.enemies[defenderKey]?.isAlive) { addStatusCardToHand(state, "static", 1); } return damage; }, }, vultureEye: { onDamage(ctx, targetKey, damage, stacks) { const { state } = ctx; if (targetKey === "player" && damage > 0) { const vultureEnemies = state.enemyOrder.filter( id => state.enemies[id].isAlive && state.enemies[id].buffs["vultureEye"] && state.enemies[id].templateId === "秃鹫" ); if (vultureEnemies.length > 0) { for (const vultureId of vultureEnemies) { const vulture = state.enemies[vultureId]; const intent = vulture.intentData["attack"]; if (intent) { const effects = intent.effects as unknown as CombatEffectEntry[]; for (const entry of effects) { if (entry[0] === "player" && entry[1].id === "attack") { applyDamage(state, "player", entry[2], vultureId); } } } } } } }, }, venom: { onCardDiscarded(ctx, cardId, stacks) { const { state } = ctx; state.player.cardsDiscardedThisTurn++; const venomCards = state.player.deck.hand.filter(id => { const card = state.player.deck.cards[id]; return card && card.itemData === null && card.displayName === "蛇毒"; }); if (state.player.cardsDiscardedThisTurn > 1 && venomCards.length > 0) { applyDamage(state, "player", 6, undefined); } }, }, static: { modifyIncomingDamage(_ctx, targetKey, damage, stacks) { if (targetKey === "player") { return damage + stacks; } return damage; }, }, discard: { onShuffle(ctx, stacks) { // Bandit: shuffle discards random item cards // Simplified: mark the effect for the procedure to handle }, }, curse: { onDamage(ctx, targetKey, damage, stacks) { // Curse: when attacked, item attack -1 until card from that item is discarded // This is handled via itemBuffs in effects }, }, }; } function addStatusCardToHand(state: CombatState, effectId: string, count: number): void { const cardDef = statusCardDesertData.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); } } export type TriggerEvent = | "onTurnStart" | "onTurnEnd" | "onAttacked" | "onDamage" | "modifyOutgoingDamage" | "modifyIncomingDamage" | "onShuffle" | "onCardPlayed" | "onCardDiscarded"; export function dispatchTrigger( ctx: TriggerContext, event: "onTurnStart" | "onTurnEnd", entityKey: "player" | string, registry: CombatTriggerRegistry, ): void { const buffs = entityKey === "player" ? ctx.state.player.buffs : ctx.state.enemies[entityKey]?.buffs; if (!buffs) return; for (const [buffId, stacks] of Object.entries(buffs)) { const behavior = registry[buffId]; if (!behavior) continue; const handler = behavior[event]; if (handler) { handler(ctx, entityKey, stacks); } } } export function dispatchAttackedTrigger( ctx: TriggerContext, attackerKey: "player" | string, defenderKey: "player" | string, damage: number, registry: CombatTriggerRegistry, ): number { const buffs = defenderKey === "player" ? ctx.state.player.buffs : ctx.state.enemies[defenderKey]?.buffs; if (!buffs) return damage; let modifiedDamage = damage; for (const [buffId, stacks] of Object.entries(buffs)) { const behavior = registry[buffId]; if (!behavior?.onAttacked) continue; modifiedDamage = behavior.onAttacked(ctx, attackerKey, defenderKey, modifiedDamage, stacks); } return modifiedDamage; } export function dispatchDamageTrigger( ctx: TriggerContext, targetKey: "player" | string, damage: number, registry: CombatTriggerRegistry, ): void { const buffs = targetKey === "player" ? ctx.state.player.buffs : ctx.state.enemies[targetKey]?.buffs; if (!buffs) return; for (const [buffId, stacks] of Object.entries(buffs)) { const behavior = registry[buffId]; if (!behavior?.onDamage) continue; behavior.onDamage(ctx, targetKey, damage, stacks); } } export function dispatchOutgoingDamageTrigger( ctx: TriggerContext, sourceKey: "player" | string, damage: number, registry: CombatTriggerRegistry, ): number { const buffs = sourceKey === "player" ? ctx.state.player.buffs : ctx.state.enemies[sourceKey]?.buffs; if (!buffs) return damage; let modifiedDamage = damage; for (const [buffId, stacks] of Object.entries(buffs)) { const behavior = registry[buffId]; if (!behavior?.modifyOutgoingDamage) continue; modifiedDamage = behavior.modifyOutgoingDamage(ctx, sourceKey, modifiedDamage, stacks); } return modifiedDamage; } export function dispatchIncomingDamageTrigger( ctx: TriggerContext, targetKey: "player" | string, damage: number, registry: CombatTriggerRegistry, ): number { const buffs = targetKey === "player" ? ctx.state.player.buffs : ctx.state.enemies[targetKey]?.buffs; if (!buffs) return damage; let modifiedDamage = damage; for (const [buffId, stacks] of Object.entries(buffs)) { const behavior = registry[buffId]; if (!behavior?.modifyIncomingDamage) continue; modifiedDamage = behavior.modifyIncomingDamage(ctx, targetKey, modifiedDamage, stacks); } return modifiedDamage; } export function dispatchShuffleTrigger( ctx: TriggerContext, registry: CombatTriggerRegistry, ): void { for (const enemyId of ctx.state.enemyOrder) { const enemy = ctx.state.enemies[enemyId]; if (!enemy.isAlive) continue; for (const [buffId, stacks] of Object.entries(enemy.buffs)) { const behavior = registry[buffId]; if (!behavior?.onShuffle) continue; behavior.onShuffle(ctx, stacks); } } }