diff --git a/src/samples/slay-the-spire-like/system/combat/triggers.ts b/src/samples/slay-the-spire-like/system/combat/triggers.ts index 683c4a0..d9242f6 100644 --- a/src/samples/slay-the-spire-like/system/combat/triggers.ts +++ b/src/samples/slay-the-spire-like/system/combat/triggers.ts @@ -1,346 +1,29 @@ -import { cardDesertData } from "../data"; -import { createStatusCard } from "../deck/factory"; -import type { BuffTable, CombatEffectEntry, CombatState } from "./types"; -import { applyDamage, removeBuff } from "./effects"; +import {createMiddlewareChain} from "../utils/middleware"; +import {CombatGameContext} from "./types"; -export type TriggerContext = { - state: CombatState; - rng: { nextInt: (n: number) => number }; -}; +export type Triggers = { + onTurnStart: { entityKey: "player" | string, }, + onTurnEnd: { entityKey: "player" | string, }, + onShuffle: { entityKey: "player" | string, }, + onCardPlayed: { cardId: string, }, + onCardDiscarded: { cardId: string, }, + onCardDrawn: { cardId: string, }, + onEffectApplied: { effectId: string, entityKey: "player" | string, stacks: 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; - onCardDrawn?: (ctx: TriggerContext, cardId: string, stacks: number) => void; -}; - -export type TriggerEvent = - | "onTurnStart" - | "onTurnEnd" - | "onAttacked" - | "onDamage" - | "modifyOutgoingDamage" - | "modifyIncomingDamage" - | "onShuffle" - | "onCardPlayed" - | "onCardDiscarded" - | "onCardDrawn"; - -export type CombatTriggerRegistry = Record; - -export function createCombatTriggerRegistry(): CombatTriggerRegistry { +export function createTriggers(){ 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; - } - } - } - }, - }, - 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 = 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); + onTurnStart: createTrigger("onTurnStart"), + onTurnEnd: createTrigger("onTurnEnd"), + onShuffle: createTrigger("onShuffle"), + onCardPlayed: createTrigger("onCardPlayed"), + onCardDiscarded: createTrigger("onCardDiscarded"), + onCardDrawn: createTrigger("onCardDrawn"), + onEffectApplied: createTrigger("onEffectApplied"), } } -export function dispatchTrigger( - ctx: TriggerContext, - event: TriggerEvent, - 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); - } - } -} - -export function dispatchCardDrawnTrigger( - ctx: TriggerContext, - cardId: string, - registry: CombatTriggerRegistry, -): void { - const buffs = ctx.state.player.buffs; - if (!buffs) return; - - for (const [buffId, stacks] of Object.entries(buffs)) { - const behavior = registry[buffId]; - if (!behavior?.onCardDrawn) continue; - behavior.onCardDrawn(ctx, cardId, stacks); - } -} +export function createTrigger(event: TKey) { + type Ctx = Triggers[TKey] & { event: TKey, game: CombatGameContext }; + return createMiddlewareChain(); +} \ No newline at end of file diff --git a/src/samples/slay-the-spire-like/system/combat/types.ts b/src/samples/slay-the-spire-like/system/combat/types.ts index d1fc064..f2cd0dd 100644 --- a/src/samples/slay-the-spire-like/system/combat/types.ts +++ b/src/samples/slay-the-spire-like/system/combat/types.ts @@ -1,71 +1,30 @@ -import type { PlayerDeck, GameCard } from "../deck/types"; -import type { PlayerState } from "../progress/types"; +import type { PlayerDeck } from "../deck/types"; +import {EnemyData, IntentData} from "@/samples/slay-the-spire-like/system/type"; -export type BuffTable = Record; +export type EffectTable = Record; -export type EffectTiming = "instant" | "temporary" | "lingering" | "permanent" | "posture" | "card" | "cardDraw" | "cardHand" | "item" | "itemUntilPlayed"; - -export type EffectData = { - readonly id: string; - readonly timing: EffectTiming; -}; - -export type EffectTarget = "self" | "target" | "all" | "random" | "player" | "team"; - -export type EnemyIntentData = { - readonly enemy: string; - readonly intentId: string; - readonly initialIntent: boolean; - readonly nextIntents: readonly string[]; - readonly brokenIntent: readonly string[]; - readonly initBuffs: readonly [EffectData, number]; - readonly effects: readonly ["self" | "player" | "team", EffectData, number]; -}; - -export type EncounterData = { - readonly type: "minion" | "elite" | "event" | "shop" | "camp" | "curio"; - readonly name: string; - readonly description: string; - readonly enemies: readonly [string, number, number]; - readonly dialogue: string; -}; - -export type ItemBuff = { - effectId: string; - stacks: number; - timing: EffectTiming; - sourceItemId: string; - targetItemId: string; -}; - -export type EnemyState = { - id: string; - templateId: string; +export type CombatEntity = { + effects: EffectTable; hp: number; maxHp: number; - buffs: BuffTable; - currentIntentId: string; - intentData: Record; isAlive: boolean; - hadDefendBroken: boolean; }; -export type PlayerCombatState = { - hp: number; - maxHp: number; +export type PlayerEntity = CombatEntity & { energy: number; maxEnergy: number; - buffs: BuffTable; deck: PlayerDeck; - damageTakenThisTurn: number; - damagedThisTurn: boolean; - cardsDiscardedThisTurn: number; - itemBuffs: ItemBuff[]; - fatigueAddedCount: number; + itemEffects: Record; +} + +export type EnemyEntity = CombatEntity & { + id: string; + enemy: EnemyData; + intents: Record; + currentIntentId: string; }; export type CombatPhase = "playerTurn" | "enemyTurn" | "combatEnd"; - export type CombatResult = "victory" | "defeat"; export type LootEntry = { @@ -75,23 +34,14 @@ export type LootEntry = { }; export type CombatState = { - enemies: Record; - enemyOrder: string[]; - player: PlayerCombatState; + enemies: EnemyEntity[]; + player: PlayerEntity; + phase: CombatPhase; turnNumber: number; result: CombatResult | null; + loot: LootEntry[]; - enemyTemplateData: Record; -}; - -export type CombatEffectEntry = [EffectTarget, EffectData, number]; - -export type CombatEntity = { - buffs: BuffTable; - hp: number; - maxHp: number; - isAlive: boolean; }; export type CombatGameContext = import("@/core/game").IGameContext; diff --git a/src/samples/slay-the-spire-like/system/type.ts b/src/samples/slay-the-spire-like/system/types.ts similarity index 100% rename from src/samples/slay-the-spire-like/system/type.ts rename to src/samples/slay-the-spire-like/system/types.ts diff --git a/src/samples/slay-the-spire-like/system/utils/middleware.ts b/src/samples/slay-the-spire-like/system/utils/middleware.ts new file mode 100644 index 0000000..5b415ec --- /dev/null +++ b/src/samples/slay-the-spire-like/system/utils/middleware.ts @@ -0,0 +1,34 @@ +type Middleware = ( + context: TContext, + next: () => Promise +) => Promise; + +export type MiddlewareChain = { + use: (middleware: Middleware) => void; + execute: (context: TContext) => Promise; +}; + +export function createMiddlewareChain( + fallback?: (context: TContext) => Promise +): MiddlewareChain { + const middlewares: Middleware[] = []; + + return { + use(middleware: Middleware) { + middlewares.push(middleware); + }, + async execute(context: TContext) { + let index = 0; + + async function dispatch(ctx: TContext): Promise { + if (index >= middlewares.length) { + return fallback ? fallback(ctx) : ctx as unknown as TReturn; + } + const current = middlewares[index++]; + return current(ctx, () => dispatch(ctx)); + } + + return dispatch(context); + }, + }; +}