import {CombatGameContext} 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"; import {getAdjacentItems} from "@/samples/slay-the-spire-like/system/grid-inventory"; import {GameItemMeta} from "@/samples/slay-the-spire-like/system/progress"; type TriggerTypes = { onCombatStart: {}, onTurnStart: { entityKey: "player" | string, }, onTurnEnd: { entityKey: "player" | string, }, onShuffle: {}, onCardPlayed: { cardId: string, targetId?: string, sourceEntityKey?: "player" | string }, onCardDiscarded: { cardId: string, sourceEntityKey?: "player" | string }, onCardDrawn: { cardId: string, sourceEntityKey?: "player" | string }, onDraw: {count: number}, onEffectApplied: { effect: EffectData, entityKey: "player" | string, stacks: number, cardId?: string, sourceEntityKey?: "player" | string }, onHpChange: { entityKey: "player" | string, amount: number}, onDamage: { entityKey: "player" | string, amount: number, prevented?: number, sourceEntityKey?: "player" | string}, onEnemyIntent: { enemyId: string, sourceEntityKey?: "player" | string }, onIntentUpdate: { enemyId: string }, } function createTriggers(){ const triggers = { onCombatStart: createTrigger("onCombatStart"), onTurnStart: createTrigger("onTurnStart", async ctx => { await ctx.game.produceAsync(draft => { const entity = getCombatEntity(draft, ctx.entityKey); if(entity) onEntityEffectUpkeep(entity); if(entity === draft.player) onPlayerItemEffectUpkeep(draft.player); }) }), onTurnEnd: createTrigger("onTurnEnd", async ctx => { if(ctx.entityKey !== "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, draft.inventory); 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.sourceEntityKey ?? "player"; for(const [trigger, target, effect, stacks] of card.cardData.effects){ if(trigger !== 'onPlay') continue; for(const entity of getEffectTargets(target, ctx.game, ctx.targetId)) await triggers.onEffectApplied.execute(ctx.game,{effect, entityKey: entity.id, stacks, cardId: ctx.cardId, sourceEntityKey: source}); } }), 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.sourceEntityKey ?? "player"; for(const [trigger, target, effect, stacks] of card.cardData.effects){ if(trigger !== 'onDiscard') continue; for(const entity of getEffectTargets(target, ctx.game)) await triggers.onEffectApplied.execute(ctx.game,{effect, entityKey: entity.id, stacks, cardId: ctx.cardId, sourceEntityKey: 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.sourceEntityKey ?? "player"; for(const [trigger, target, effect, stacks] of card.cardData.effects){ if(trigger !== 'onDraw') continue; for(const entity of getEffectTargets(target, ctx.game)) await triggers.onEffectApplied.execute(ctx.game,{effect, entityKey: entity.id, stacks, cardId: ctx.cardId, sourceEntityKey: 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 = getAdjacentItems(ctx.game.value.inventory, card.itemId); for(const itemId of nearby.keys()){ await ctx.game.produceAsync(draft => { addItemEffect(draft.player, itemId, ctx.effect, ctx.stacks); }); } } return; } await ctx.game.produceAsync(draft => { const entity = ctx.entityKey === "player" ? draft.player : draft.enemies.find(e => e.id === ctx.entityKey); if(entity) addEntityEffect(entity, ctx.effect, ctx.stacks); }) }), onHpChange: createTrigger("onHpChange", async ctx => { await ctx.game.produceAsync(draft => { const entity = ctx.entityKey === "player" ? draft.player : draft.enemies.find(e => e.id === ctx.entityKey); 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.entityKey === "player" ? ctx.game.value.player : ctx.game.value.enemies.find(e => e.id === ctx.entityKey); 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,{entityKey: ctx.entityKey, 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.sourceEntityKey ?? enemy.id; for(const [target, effect, stacks] of intent.effects){ for(const entity of getEffectTargets(target, ctx.game)) await triggers.onEffectApplied.execute(ctx.game, { effect, entityKey: entity.id, stacks, sourceEntityKey: 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) => void){ const triggers = createTriggers(); build(triggers); return async function(game: CombatGameContext){ await triggers.onCombatStart.execute(game,{}); try { while (true) { await triggers.onTurnStart.execute(game, {entityKey: "player"}); while (true) { const action = await promptMainAction(game); if (action.action === "end-turn") break; if (action.action === "play") { await triggers.onCardPlayed.execute(game, action); } } await triggers.onTurnEnd.execute(game, {entityKey: "player"}); for (const enemy of getAliveEnemies(game.value)) { await triggers.onTurnStart.execute(game, {entityKey: 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, {entityKey: 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; }, } }