boardgame-core/src/samples/slay-the-spire-like/system/combat/triggers.ts

183 lines
8.5 KiB
TypeScript
Raw Normal View History

2026-04-17 08:33:02 +08:00
import {CombatGameContext} from "./types";
2026-04-17 09:27:20 +08:00
import {
2026-04-17 11:06:09 +08:00
addEntityEffect,
addItemEffect,
getAliveEnemies, onEntityPostureDamage,
2026-04-17 09:27:20 +08:00
onEntityEffectUpkeep,
onPlayerItemEffectUpkeep, onItemDiscard, onItemPlay
2026-04-17 09:27:20 +08:00
} from "@/samples/slay-the-spire-like/system/combat/effects";
import {promptMainAction} from "@/samples/slay-the-spire-like/system/combat/prompts";
2026-04-17 11:06:09 +08:00
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";
2026-04-17 09:27:20 +08:00
type TriggerTypes = {
onCombatStart: {},
2026-04-17 08:33:02 +08:00
onTurnStart: { entityKey: "player" | string, },
onTurnEnd: { entityKey: "player" | string, },
2026-04-17 11:06:09 +08:00
onShuffle: {},
2026-04-17 10:00:14 +08:00
onCardPlayed: { cardId: string, targetId?: string },
2026-04-17 08:33:02 +08:00
onCardDiscarded: { cardId: string, },
onCardDrawn: { cardId: string, },
2026-04-17 11:06:09 +08:00
onDraw: {count: number},
onEffectApplied: { effect: EffectData, entityKey: "player" | string, stacks: number, cardId?: string },
onHpChange: { entityKey: "player" | string, amount: number},
onDamage: { entityKey: "player" | string, amount: number, dealt?: number, prevented?: number},
}
2026-04-17 09:27:20 +08:00
function createTriggers(){
2026-04-17 11:06:09 +08:00
const triggers = {
2026-04-17 09:27:20 +08:00
onCombatStart: createTrigger("onCombatStart"),
2026-04-17 11:06:09 +08:00
onTurnStart: createTrigger("onTurnStart", async ctx => {
await ctx.game.produceAsync(draft => {
const entity = ctx.entityKey === "player" ? draft.player : draft.enemies.find(e => e.id === 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;
moveToRegion(cards[ctx.cardId], regions.hand, regions.discardPile);
onItemPlay(draft.player, cards[ctx.cardId].itemId);
2026-04-17 11:06:09 +08:00
});
}),
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);
2026-04-17 11:06:09 +08:00
});
}),
onCardDrawn: createTrigger("onCardDrawn", async ctx => {
await ctx.game.produceAsync(draft => {
const {cards, regions} = draft.player.deck;
moveToRegion(cards[ctx.cardId], regions.drawPile, regions.hand);
});
}),
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<GameItemMeta>(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 = ctx.dealt = Math.min(Math.max(0,entity.hp), ctx.amount);
ctx.prevented = ctx.amount - ctx.dealt;
await ctx.game.produceAsync(draft => {
onEntityPostureDamage(entity, dealt);
});
await triggers.onHpChange.execute(ctx.game,{entityKey: ctx.entityKey, amount: -ctx.amount});
2026-04-17 11:06:09 +08:00
}),
}
2026-04-17 11:06:09 +08:00
return triggers;
}
2026-04-17 09:27:20 +08:00
export type Triggers = ReturnType<typeof createTriggers>
export function createStartWith(build: (triggers: Triggers) => void){
const triggers = createTriggers();
build(triggers);
return async function(game: CombatGameContext){
await triggers.onCombatStart.execute(game,{});
2026-04-17 11:06:09 +08:00
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") {
// TODO: energy/use consumption handling
await triggers.onCardPlayed.execute(game, action);
}
2026-04-17 10:00:14 +08:00
}
2026-04-17 11:06:09 +08:00
await triggers.onTurnEnd.execute(game, {entityKey: "player"});
for (const enemy of getAliveEnemies(game.value)) {
await triggers.onTurnStart.execute(game, {entityKey: enemy.id});
}
// TODO execute enemy intent, then update with new one here
for (const enemy of getAliveEnemies(game.value)) {
await triggers.onTurnEnd.execute(game, {entityKey: enemy.id});
2026-04-17 09:27:20 +08:00
}
}
2026-04-17 11:06:09 +08:00
}catch(e){
if(e === game.value) return game.value.result;
throw e;
2026-04-17 09:27:20 +08:00
}
}
}
2026-04-17 11:06:09 +08:00
type TriggerContext<TKey extends keyof TriggerTypes> = TriggerTypes[TKey] & { event: TKey, game: CombatGameContext };
function createTrigger<TKey extends keyof TriggerTypes>(event: TKey, fallback?: (ctx: TriggerContext<TKey>) => Promise<void>) {
const {use, execute} = createMiddlewareChain<TriggerContext<TKey>,void>(fallback);
2026-04-17 09:27:20 +08:00
return {
use,
2026-04-17 11:06:09 +08:00
execute: async (game: CombatGameContext, ctx: TriggerTypes[TKey]) => {
const param = {...ctx, game, event};
await execute(param);
return param;
},
2026-04-17 09:27:20 +08:00
}
2026-04-17 08:33:02 +08:00
}