import { CombatGameContext, IRunContext } from "./types"; import { addEntityEffect, addItemEffect, getAliveEnemies, onEntityPostureDamage, onEntityEffectUpkeep, onPlayerItemEffectUpkeep, onItemDiscard, onItemPlay, payCardCost, getCombatEntity, getEffectTargets, } from "@/samples/slay-the-spire-like/system/combat/effects"; import { promptMainAction } from "@/samples/slay-the-spire-like/system/combat/prompts"; import { moveToRegion, shuffle } from "@/core/region"; import { createMiddlewareChain } from "@/utils/middleware"; import { EffectData } from "@/samples/slay-the-spire-like/system/types"; type TriggerTypes = { onCombatStart: {}; onTurnStart: { entityId: "player" | string }; onTurnEnd: { entityId: "player" | string }; onShuffle: {}; onCardPlayed: { cardId: string; targetId?: string; sourceEntityId?: "player" | string; }; onCardDiscarded: { cardId: string; sourceEntityId?: "player" | string }; onCardDrawn: { cardId: string; sourceEntityId?: "player" | string }; onDraw: { count: number }; onEffectApplied: { effect: EffectData; entityId: "player" | string; stacks: number; cardId?: string; sourceEntityId?: "player" | string; targetId?: string; }; onHpChange: { entityId: "player" | string; amount: number }; onDamage: { entityId: "player" | string; amount: number; prevented?: number; sourceEntityId?: "player" | string; }; onEnemyIntent: { enemyId: string; sourceEntityId?: "player" | string }; onIntentUpdate: { enemyId: string }; }; export function createTriggers(run: IRunContext) { const triggers = { onCombatStart: createTrigger("onCombatStart", async (ctx) => { await triggers.onShuffle.execute(ctx.game, {}); await triggers.onDraw.execute(ctx.game, { count: 5 }); }), onTurnStart: createTrigger("onTurnStart", async (ctx) => { await ctx.game.produceAsync((draft) => { const entity = getCombatEntity(draft, ctx.entityId); if (entity) onEntityEffectUpkeep(entity); if (entity === draft.player) onPlayerItemEffectUpkeep(draft.player); }); }), onTurnEnd: createTrigger("onTurnEnd", async (ctx) => { if (ctx.entityId !== "player") return; const { regions } = ctx.game.value.player.deck; for (const cardId of Object.values(regions.hand.childIds)) { await triggers.onCardDiscarded.execute(ctx.game, { cardId }); } await ctx.game.produceAsync((draft) => { draft.player.energy = draft.player.maxEnergy; }); await triggers.onDraw.execute(ctx.game, { count: 5 }); }), onShuffle: createTrigger("onShuffle", async (ctx) => { await ctx.game.produceAsync((draft) => { const { cards, regions } = draft.player.deck; for (const cardId of Object.values(regions.discardPile.childIds)) moveToRegion(cards[cardId], regions.discardPile, regions.drawPile); shuffle(regions.drawPile, cards, ctx.game.rng); }); }), onCardPlayed: createTrigger("onCardPlayed", async (ctx) => { await ctx.game.produceAsync((draft) => { const { cards, regions } = draft.player.deck; const card = cards[ctx.cardId]; payCardCost( draft.player, card.cardData.costType, card.cardData.costCount, card.itemId, run, ); moveToRegion(card, regions.hand, regions.discardPile); onItemPlay(draft.player, card.itemId); }); const { cards, regions } = ctx.game.value.player.deck; const card = cards[ctx.cardId]; const source = ctx.sourceEntityId ?? "player"; for (const { trigger, target, effects } of card.cardData.effects) { if (trigger !== "onPlay") continue; for (const [effect, stacks] of effects) for (const entity of getEffectTargets( target, ctx.game, ctx.targetId, source, )) await triggers.onEffectApplied.execute(ctx.game, { effect, entityId: entity.id, stacks, cardId: ctx.cardId, sourceEntityId: source, targetId: ctx.targetId, }); } }), onCardDiscarded: createTrigger("onCardDiscarded", async (ctx) => { await ctx.game.produceAsync((draft) => { const { cards, regions } = draft.player.deck; moveToRegion(cards[ctx.cardId], regions.hand, regions.discardPile); onItemDiscard(draft.player, cards[ctx.cardId].itemId); }); const { cards, regions } = ctx.game.value.player.deck; const card = cards[ctx.cardId]; const source = ctx.sourceEntityId ?? "player"; for (const { trigger, target, effects } of card.cardData.effects) { if (trigger !== "onDiscard") continue; for (const [effect, stacks] of effects) for (const entity of getEffectTargets( target, ctx.game, undefined, source, )) await triggers.onEffectApplied.execute(ctx.game, { effect, entityId: entity.id, stacks, cardId: ctx.cardId, sourceEntityId: source, }); } }), onCardDrawn: createTrigger("onCardDrawn", async (ctx) => { await ctx.game.produceAsync((draft) => { const { cards, regions } = draft.player.deck; moveToRegion(cards[ctx.cardId], regions.drawPile, regions.hand); }); const { cards, regions } = ctx.game.value.player.deck; const card = cards[ctx.cardId]; const source = ctx.sourceEntityId ?? "player"; for (const { trigger, target, effects } of card.cardData.effects) { if (trigger !== "onDraw") continue; for (const [effect, stacks] of effects) for (const entity of getEffectTargets( target, ctx.game, undefined, source, )) await triggers.onEffectApplied.execute(ctx.game, { effect, entityId: entity.id, stacks, cardId: ctx.cardId, sourceEntityId: source, }); } }), onDraw: createTrigger("onDraw", async (ctx) => { let toDraw = ctx.count; while (toDraw > 0) { let inDraw = ctx.game.value.player.deck.regions.drawPile.childIds.length; if (inDraw <= 0) await triggers.onShuffle.execute(ctx.game, {}); inDraw = ctx.game.value.player.deck.regions.drawPile.childIds.length; if (inDraw <= 0) break; const children = ctx.game.value.player.deck.regions.drawPile.childIds; const cardId = children[children.length - 1]; await triggers.onCardDrawn.execute(ctx.game, { cardId }); toDraw--; } }), onEffectApplied: createTrigger("onEffectApplied", async (ctx) => { if (ctx.effect.lifecycle === "instant") return; if (ctx.effect.lifecycle.startsWith("item")) { if (ctx.cardId) { const card = ctx.game.value.player.deck.cards[ctx.cardId]; const nearby = run.getAdjacentItems(card.itemId); for (const itemId of nearby) { await ctx.game.produceAsync((draft) => { addItemEffect(draft.player, itemId, ctx.effect, ctx.stacks); }); } } return; } await ctx.game.produceAsync((draft) => { const entity = ctx.entityId === "player" ? draft.player : draft.enemies.find((e) => e.id === ctx.entityId); if (entity) addEntityEffect(entity, ctx.effect, ctx.stacks); }); }), onHpChange: createTrigger("onHpChange", async (ctx) => { await ctx.game.produceAsync((draft) => { const entity = ctx.entityId === "player" ? draft.player : draft.enemies.find((e) => e.id === ctx.entityId); if (!entity) return; entity.hp += ctx.amount; entity.isAlive = entity.hp > 0; draft.result = !draft.player.isAlive ? "defeat" : draft.enemies.every((e) => !e.isAlive) ? "victory" : null; }); if (ctx.game.value.result) throw ctx.game.value; }), onDamage: createTrigger("onDamage", async (ctx) => { const entity = ctx.entityId === "player" ? ctx.game.value.player : ctx.game.value.enemies.find((e) => e.id === ctx.entityId); if (!entity || !entity.isAlive) return; const dealt = Math.min( Math.max(0, entity.hp), ctx.amount - (ctx.prevented || 0), ); await ctx.game.produceAsync((draft) => { onEntityPostureDamage(entity, dealt); }); await triggers.onHpChange.execute(ctx.game, { entityId: ctx.entityId, amount: -dealt, }); }), onEnemyIntent: createTrigger("onEnemyIntent", async (ctx) => { const enemy = ctx.game.value.enemies.find((e) => e.id === ctx.enemyId); if (!enemy || !enemy.isAlive) return; const intent = enemy.currentIntent; if (!intent) return; const source = ctx.sourceEntityId ?? enemy.id; for (const [target, effect, stacks] of intent.effects) { for (const entity of getEffectTargets( target, ctx.game, undefined, source, )) await triggers.onEffectApplied.execute(ctx.game, { effect, entityId: entity.id, stacks, sourceEntityId: source, }); } }), onIntentUpdate: createTrigger("onIntentUpdate", async (ctx) => { await ctx.game.produceAsync((draft) => { const enemy = draft.enemies.find((e) => e.id === ctx.enemyId); if (!enemy) return; const intent = enemy.currentIntent; if (!intent) return; const nextIntents = intent.nextIntents; if (nextIntents.length > 0) { const nextIndex = ctx.game.rng.nextInt(nextIntents.length); enemy.currentIntent = nextIntents[nextIndex]; } }); }), }; return triggers; } export type Triggers = ReturnType; export function createStartWith( build: (triggers: Triggers, run: IRunContext) => void, run: IRunContext, ) { const triggers = createTriggers(run); build(triggers, run); return async function (game: CombatGameContext) { await triggers.onCombatStart.execute(game, {}); try { while (true) { await triggers.onTurnStart.execute(game, { entityId: "player" }); while (true) { const action = await promptMainAction(game, run); if (action.action === "end-turn") break; if (action.action === "play") { await triggers.onCardPlayed.execute(game, action); } } await triggers.onTurnEnd.execute(game, { entityId: "player" }); for (const enemy of getAliveEnemies(game.value)) { await triggers.onTurnStart.execute(game, { entityId: enemy.id }); } for (const enemy of getAliveEnemies(game.value)) { await triggers.onEnemyIntent.execute(game, { enemyId: enemy.id }); await triggers.onIntentUpdate.execute(game, { enemyId: enemy.id }); } for (const enemy of getAliveEnemies(game.value)) { await triggers.onTurnEnd.execute(game, { entityId: enemy.id }); } } } catch (e) { if (e === game.value) return game.value.result; throw e; } }; } type TriggerContext = TriggerTypes[TKey] & { event: TKey; game: CombatGameContext; }; function createTrigger( event: TKey, fallback?: (ctx: TriggerContext) => Promise, ) { const { use, execute } = createMiddlewareChain, void>( fallback, ); return { use, execute: async (game: CombatGameContext, ctx: TriggerTypes[TKey]) => { const param = { ...ctx, game, event }; await execute(param); return param; }, }; }