From fa92b5d865a531f9374fa2798d825c94ba0f8ff9 Mon Sep 17 00:00:00 2001 From: hyper Date: Sat, 18 Apr 2026 15:08:50 +0800 Subject: [PATCH] refactor: reorganize trigger code --- .../data/desert/triggers/card-events.ts | 121 ++++ .../data/desert/triggers/damage.ts | 192 ++++++ .../data/desert/triggers/effect.ts | 559 +----------------- .../data/desert/triggers/index.ts | 7 +- .../data/desert/triggers/instant.ts | 158 +++++ .../data/desert/triggers/turn-start.ts | 88 +++ 6 files changed, 568 insertions(+), 557 deletions(-) create mode 100644 src/samples/slay-the-spire-like/data/desert/triggers/card-events.ts create mode 100644 src/samples/slay-the-spire-like/data/desert/triggers/damage.ts create mode 100644 src/samples/slay-the-spire-like/data/desert/triggers/instant.ts create mode 100644 src/samples/slay-the-spire-like/data/desert/triggers/turn-start.ts diff --git a/src/samples/slay-the-spire-like/data/desert/triggers/card-events.ts b/src/samples/slay-the-spire-like/data/desert/triggers/card-events.ts new file mode 100644 index 0000000..a6d0ec3 --- /dev/null +++ b/src/samples/slay-the-spire-like/data/desert/triggers/card-events.ts @@ -0,0 +1,121 @@ +import { Triggers } from "@/samples/slay-the-spire-like/system/combat/triggers"; +import { getCombatEntity } from "@/samples/slay-the-spire-like/system/combat/effects"; +import { getAdjacentItems } from "@/samples/slay-the-spire-like/system/grid-inventory"; +import { GameItemMeta } from "@/samples/slay-the-spire-like/system/progress"; +import { EffectData } from "@/samples/slay-the-spire-like/system/types"; +import getEffects from "../effect.csv"; + +export function addCardEventTriggers(triggers: Triggers) { + const effects = getEffects(); + + function findEffect(id: string): EffectData { + const found = effects.find(e => e.id === id); + if (found) return found; + return { id, name: id, description: "", lifecycle: "instant" } as EffectData; + } + + // storm: give static card to player when storm enemy attacks + triggers.onEnemyIntent.use(async (ctx, next) => { + await next(); + + const enemy = getCombatEntity(ctx.game.value, ctx.enemyId); + if (!enemy || !enemy.isAlive) return; + + const storm = enemy.effects.storm?.stacks ?? 0; + if (storm > 0) { + for (let i = 0; i < storm; i++) { + await triggers.onEffectApplied.execute(ctx.game, { + effect: findEffect("static"), + entityKey: "player", + stacks: 1, + sourceEntityKey: ctx.enemyId, + }); + } + } + }); + + // crossbow: replay other crossbows on same target + triggers.onEffectApplied.use(async (ctx, next) => { + await next(); + + if (ctx.effect.id !== "crossbow" || !ctx.cardId || !ctx.targetId) return; + + const { cards, regions } = ctx.game.value.player.deck; + const handIds = [...regions.hand.childIds]; + for (const id of handIds) { + const card = cards[id]; + if (card && card.itemId === "crossbow" && id !== ctx.cardId) { + await triggers.onCardPlayed.execute(ctx.game, { + cardId: id, + targetId: ctx.targetId, + sourceEntityKey: "player", + }); + } + } + }); + + // burnForEnergy: consume adjacent item, gain energy when its card is played + triggers.onCardPlayed.use(async (ctx, next) => { + await next(); + + const card = ctx.game.value.player.deck.cards[ctx.cardId]; + if (!card) return; + const playedItemId = card.itemId; + + const adjacent = getAdjacentItems(ctx.game.value.inventory, playedItemId); + for (const [adjItemId] of adjacent) { + const adjEffects = ctx.game.value.player.itemEffects[adjItemId]; + if (!adjEffects) continue; + const burn = adjEffects.burnForEnergy; + if (!burn || burn.stacks <= 0) continue; + + await ctx.game.produceAsync(draft => { + const item = draft.inventory.items.get(adjItemId); + if (item) { + draft.inventory.items.delete(adjItemId); + } + draft.player.energy += burn.stacks; + delete draft.player.itemEffects[adjItemId]; + }); + break; + } + }); + + // sandwormKing: heal 10 hp when player discards fatigue + triggers.onCardDiscarded.use(async (ctx, next) => { + await next(); + + const card = ctx.game.value.player.deck.cards[ctx.cardId]; + if (!card || card.cardData.id !== "fatigue") return; + + const sandwormKing = ctx.game.value.enemies.find( + e => e.enemy.id === "沙虫王" && e.isAlive + ); + if (!sandwormKing) return; + + await ctx.game.produceAsync(draft => { + const king = draft.enemies.find(e => e.id === sandwormKing.id); + if (king) { + king.hp = Math.min(king.hp + 10, king.maxHp); + } + }); + }); + + // vulture: give vultureEye when vulture deals damage + triggers.onDamage.use(async (ctx, next) => { + await next(); + + const dealt = ctx.amount - (ctx.prevented ?? 0); + if (dealt <= 0 || !ctx.sourceEntityKey) return; + + const attacker = getCombatEntity(ctx.game.value, ctx.sourceEntityKey); + if (!attacker || !("enemy" in attacker) || attacker.enemy.id !== "秃鹫") return; + + await triggers.onEffectApplied.execute(ctx.game, { + effect: findEffect("vultureEye"), + entityKey: "player", + stacks: 1, + sourceEntityKey: ctx.sourceEntityKey, + }); + }); +} diff --git a/src/samples/slay-the-spire-like/data/desert/triggers/damage.ts b/src/samples/slay-the-spire-like/data/desert/triggers/damage.ts new file mode 100644 index 0000000..1d8499c --- /dev/null +++ b/src/samples/slay-the-spire-like/data/desert/triggers/damage.ts @@ -0,0 +1,192 @@ +import { Triggers } from "@/samples/slay-the-spire-like/system/combat/triggers"; +import { + addEntityEffect, + getCombatEntity, +} from "@/samples/slay-the-spire-like/system/combat/effects"; +import { CombatGameContext } from "@/samples/slay-the-spire-like/system/combat/types"; +import { EffectData } from "@/samples/slay-the-spire-like/system/types"; +import getEffects from "../effect.csv"; + +export function addDamageTriggers(triggers: Triggers) { + const effects = getEffects(); + + function findEffect(id: string): EffectData { + const found = effects.find((e: EffectData) => e.id === id); + if (found) return found; + return { id, name: id, description: "", lifecycle: "instant" } as EffectData; + } + + // block / damage prevention + triggers.onDamage.use(async (ctx, next) => { + const entity = getCombatEntity(ctx.game.value, ctx.entityKey); + if (!entity) return; + + let preventable = ctx.amount - (ctx.prevented ?? 0); + + const blocks = entity.effects.defend?.stacks ?? 0; + const blocked = Math.min(blocks, preventable); + if (blocked) { + ctx.prevented = (ctx.prevented ?? 0) + blocked; + preventable -= blocked; + } + + const damageReduce = entity.effects.damageReduce?.stacks ?? 0; + if (damageReduce > 0) { + const reduced = Math.min(damageReduce, preventable); + ctx.prevented = (ctx.prevented ?? 0) + reduced; + preventable -= reduced; + } + + const expose = entity.effects.expose?.stacks ?? 0; + if (expose > 0) { + ctx.amount += expose; + } + + await next(); + }); + + // spike: damage attacker + triggers.onDamage.use(async (ctx, next) => { + await next(); + + if (ctx.amount - (ctx.prevented ?? 0) <= 0) return; + + const entity = getCombatEntity(ctx.game.value, ctx.entityKey); + if (!entity || !entity.isAlive) return; + + const spike = entity.effects.spike?.stacks ?? 0; + if (spike > 0 && ctx.sourceEntityKey) { + await triggers.onDamage.execute(ctx.game, { + entityKey: ctx.sourceEntityKey, + amount: spike, + sourceEntityKey: ctx.entityKey, + }); + } + }); + + // energyDrain: player loses energy when enemy takes damage + triggers.onDamage.use(async (ctx, next) => { + const entity = getCombatEntity(ctx.game.value, ctx.entityKey); + if (!entity) return; + + const energyDrain = entity.effects.energyDrain?.stacks ?? 0; + if (energyDrain > 0 && ctx.entityKey !== "player") { + const dealt = Math.min(Math.max(0, entity.hp), ctx.amount - (ctx.prevented ?? 0)); + if (dealt > 0) { + await ctx.game.produceAsync(draft => { + draft.player.energy = Math.max(0, draft.player.energy - energyDrain); + }); + } + } + + await next(); + }); + + // molt: enemy flees if molt >= maxHp + triggers.onDamage.use(async (ctx, next) => { + const entity = getCombatEntity(ctx.game.value, ctx.entityKey); + if (!entity || !entity.isAlive) { + await next(); + return; + } + + const molt = entity.effects.molt?.stacks ?? 0; + if (molt >= entity.maxHp) { + await ctx.game.produceAsync(draft => { + const e = draft.enemies.find(en => en.id === ctx.entityKey); + if (e) { + e.isAlive = false; + e.hp = 0; + } + draft.result = draft.enemies.every(en => !en.isAlive) ? "victory" : null; + }); + if (ctx.game.value.result) throw ctx.game.value; + return; + } + + await next(); + }); + + // aim: double damage, lose aim on damage + triggers.onDamage.use(async (ctx, next) => { + if (ctx.sourceEntityKey === "player") { + const player = ctx.game.value.player; + const aim = player.effects.aim?.stacks ?? 0; + if (aim > 0) { + ctx.amount *= 2; + } + } + await next(); + }); + + // roll: consume 10 roll per 10 damage + triggers.onDamage.use(async (ctx, next) => { + if (ctx.sourceEntityKey === "player") { + const player = ctx.game.value.player; + const roll = player.effects.roll?.stacks ?? 0; + if (roll >= 10) { + const rollDamage = Math.floor(roll / 10) * 10; + ctx.amount += rollDamage; + await ctx.game.produceAsync(draft => { + addEntityEffect(draft.player, findEffect("roll"), -rollDamage); + }); + } + } + await next(); + }); + + // tailSting: bonus damage on attack + triggers.onDamage.use(async (ctx, next) => { + if (ctx.sourceEntityKey && ctx.sourceEntityKey !== "player") { + const attacker = getCombatEntity(ctx.game.value, ctx.sourceEntityKey); + if (attacker) { + const tailSting = attacker.effects.tailSting?.stacks ?? 0; + if (tailSting > 0) { + ctx.amount += tailSting; + } + } + } + await next(); + }); + + // charge: double damage dealt/received, consume equal charge + triggers.onDamage.use(async (ctx, next) => { + const entity = getCombatEntity(ctx.game.value, ctx.entityKey); + if (entity) { + const charge = entity.effects.charge?.stacks ?? 0; + if (charge > 0) { + const dealt = Math.min(Math.max(0, entity.hp), ctx.amount - (ctx.prevented ?? 0)); + const consumed = Math.min(charge, dealt); + ctx.amount += dealt; + if (consumed > 0) { + await ctx.game.produceAsync(draft => { + const e = getCombatEntity(draft, ctx.entityKey); + if (e) addEntityEffect(e, findEffect("charge"), -consumed); + }); + } + } + } + + if (ctx.sourceEntityKey) { + const attacker = getCombatEntity(ctx.game.value, ctx.sourceEntityKey); + if (attacker) { + const charge = attacker.effects.charge?.stacks ?? 0; + if (charge > 0) { + const baseAmount = ctx.amount; + const targetEntity = getCombatEntity(ctx.game.value, ctx.entityKey); + const dealt = Math.min(Math.max(0, targetEntity?.hp ?? 0), baseAmount - (ctx.prevented ?? 0)); + const consumed = Math.min(charge, dealt); + ctx.amount += dealt; + if (consumed > 0) { + await ctx.game.produceAsync(draft => { + const a = getCombatEntity(draft, ctx.sourceEntityKey!); + if (a) addEntityEffect(a, findEffect("charge"), -consumed); + }); + } + } + } + } + + await next(); + }); +} diff --git a/src/samples/slay-the-spire-like/data/desert/triggers/effect.ts b/src/samples/slay-the-spire-like/data/desert/triggers/effect.ts index a2d6316..7c8bb3a 100644 --- a/src/samples/slay-the-spire-like/data/desert/triggers/effect.ts +++ b/src/samples/slay-the-spire-like/data/desert/triggers/effect.ts @@ -1,555 +1,12 @@ import { Triggers } from "@/samples/slay-the-spire-like/system/combat/triggers"; -import { - addEntityEffect, - getCombatEntity, -} from "@/samples/slay-the-spire-like/system/combat/effects"; -import { moveToRegion } from "@/core/region"; -import { CombatGameContext } from "@/samples/slay-the-spire-like/system/combat/types"; -import { EffectData } from "@/samples/slay-the-spire-like/system/types"; -import { GameCard } from "@/samples/slay-the-spire-like/system/deck"; -import { getAdjacentItems } from "@/samples/slay-the-spire-like/system/grid-inventory"; -import { GameItemMeta } from "@/samples/slay-the-spire-like/system/progress"; -import getEffects from "../effect.csv"; +import { addInstantEffectTriggers } from "./instant"; +import { addDamageTriggers } from "./damage"; +import { addTurnStartTriggers } from "./turn-start"; +import { addCardEventTriggers } from "./card-events"; export function addEffectTriggers(triggers: Triggers) { - const effects = getEffects(); - // ========== instant effects ========== - triggers.onEffectApplied.use(async (ctx, next) => { - if (ctx.effect.id === "attack") { - await triggers.onDamage.execute(ctx.game, { - entityKey: ctx.entityKey, - amount: ctx.stacks, - sourceEntityKey: ctx.sourceEntityKey ?? ctx.entityKey === "player" ? undefined : "player", - }); - } else if (ctx.effect.id === "draw") { - await triggers.onDraw.execute(ctx.game, { count: ctx.stacks }); - } else if (ctx.effect.id === "gainEnergy") { - await ctx.game.produceAsync(draft => { - draft.player.energy += ctx.stacks; - }); - } else if (ctx.effect.id === "removeWound") { - await ctx.game.produceAsync(draft => { - const { cards, regions } = draft.player.deck; - let removed = 0; - const allPileIds = [ - ...regions.drawPile.childIds, - ...regions.discardPile.childIds, - ]; - for (const cardId of allPileIds) { - if (removed >= ctx.stacks) break; - const card = cards[cardId]; - if (card && card.cardData.id === "wound") { - const sourceRegion = card.regionId === "drawPile" ? regions.drawPile : regions.discardPile; - moveToRegion(card, sourceRegion, null); - delete cards[cardId]; - removed++; - } - } - }); - } else if (ctx.effect.id === "venom") { - await ctx.game.produceAsync(draft => { - const cardId = `venom-${draft.player.deck.regions.drawPile.childIds.length}-${draft.player.deck.regions.discardPile.childIds.length}`; - const card: GameCard = { - id: cardId, - regionId: "", - position: [], - itemId: "venom", - cardData: { - id: "venom", - name: "蛇毒", - desc: "弃掉时受到3点伤害", - type: "status", - costType: "energy", - costCount: 1, - targetType: "none", - effects: [["onDiscard", "self", ctx.effect, 3]], - }, - }; - draft.player.deck.cards[cardId] = card; - moveToRegion(card, null, draft.player.deck.regions.drawPile); - }); - } else if (ctx.effect.id === "vultureEye") { - await ctx.game.produceAsync(draft => { - const cardId = `vultureEye-${draft.player.deck.regions.drawPile.childIds.length}-${draft.player.deck.regions.discardPile.childIds.length}`; - const card: GameCard = { - id: cardId, - regionId: "", - position: [], - itemId: "vultureEye", - cardData: { - id: "vultureEye", - name: "秃鹫之眼", - desc: "抓到时获得3层暴露", - type: "status", - costType: "none", - costCount: 0, - targetType: "none", - effects: [["onDraw", "self", ctx.effect, 3]], - }, - }; - draft.player.deck.cards[cardId] = card; - moveToRegion(card, null, draft.player.deck.regions.drawPile); - }); - } else if (ctx.effect.id === "static") { - await ctx.game.produceAsync(draft => { - const cardId = `static-${draft.player.deck.regions.drawPile.childIds.length}-${draft.player.deck.regions.discardPile.childIds.length}`; - const card: GameCard = { - id: cardId, - regionId: "", - position: [], - itemId: "static", - cardData: { - id: "static", - name: "静电", - desc: "在手里时受电击伤害+1", - type: "status", - costType: "none", - costCount: 0, - targetType: "none", - effects: [["onDraw", "self", ctx.effect, 1]], - }, - }; - draft.player.deck.cards[cardId] = card; - moveToRegion(card, null, draft.player.deck.regions.drawPile); - }); - } else if (ctx.effect.id === "summonMummy") { - await ctx.game.produceAsync(draft => { - for (const enemyData of getAllEnemyData(ctx.game)) { - if (enemyData.id === "木乃伊") { - const existingMummy = draft.enemies.find(e => e.enemy.id === "木乃伊" && !e.isAlive); - if (existingMummy) { - existingMummy.isAlive = true; - existingMummy.hp = existingMummy.maxHp; - return; - } - const intent = enemyData.intents.find(i => i.initialIntent) ?? enemyData.intents[0]; - const instanceId = `${enemyData.id}-${draft.enemies.length}`; - const intents: Record = {}; - for (const i of enemyData.intents) { - intents[i.id] = i; - } - draft.enemies.push({ - id: instanceId, - enemy: enemyData, - hp: ctx.stacks, - maxHp: ctx.stacks, - isAlive: true, - effects: {}, - intents, - currentIntent: intent, - }); - break; - } - } - }); - } else if (ctx.effect.id === "summonSandwormLarva") { - await ctx.game.produceAsync(draft => { - for (const enemyData of getAllEnemyData(ctx.game)) { - if (enemyData.id === "幼沙虫") { - const intent = enemyData.intents.find(i => i.initialIntent) ?? enemyData.intents[0]; - const instanceId = `${enemyData.id}-${draft.enemies.length}`; - const intents: Record = {}; - for (const i of enemyData.intents) { - intents[i.id] = i; - } - draft.enemies.push({ - id: instanceId, - enemy: enemyData, - hp: ctx.stacks, - maxHp: ctx.stacks, - isAlive: true, - effects: {}, - intents, - currentIntent: intent, - }); - break; - } - } - }); - } else if (ctx.effect.id === "reviveMummy") { - await ctx.game.produceAsync(draft => { - const deadMummy = draft.enemies.find(e => e.enemy.id === "木乃伊" && !e.isAlive); - if (deadMummy) { - deadMummy.isAlive = true; - deadMummy.hp = deadMummy.maxHp; - } - }); - } else if (ctx.effect.id === "curse") { - await ctx.game.produceAsync(draft => { - addEntityEffect(draft.player, ctx.effect, ctx.stacks); - }); - } - await next(); - }); - - // ========== block / damage prevention ========== - triggers.onDamage.use(async (ctx, next) => { - const entity = getCombatEntity(ctx.game.value, ctx.entityKey); - if (!entity) return; - - let preventable = ctx.amount - (ctx.prevented ?? 0); - - const blocks = entity.effects.defend?.stacks ?? 0; - const blocked = Math.min(blocks, preventable); - if (blocked) { - ctx.prevented = (ctx.prevented ?? 0) + blocked; - preventable -= blocked; - } - - const damageReduce = entity.effects.damageReduce?.stacks ?? 0; - if (damageReduce > 0) { - const reduced = Math.min(damageReduce, preventable); - ctx.prevented = (ctx.prevented ?? 0) + reduced; - preventable -= reduced; - } - - const expose = entity.effects.expose?.stacks ?? 0; - if (expose > 0) { - ctx.amount += expose; - } - - await next(); - }); - - // ========== spike: damage attacker ========== - triggers.onDamage.use(async (ctx, next) => { - await next(); - - if (ctx.amount - (ctx.prevented ?? 0) <= 0) return; - - const entity = getCombatEntity(ctx.game.value, ctx.entityKey); - if (!entity || !entity.isAlive) return; - - const spike = entity.effects.spike?.stacks ?? 0; - if (spike > 0 && ctx.sourceEntityKey) { - await triggers.onDamage.execute(ctx.game, { - entityKey: ctx.sourceEntityKey, - amount: spike, - sourceEntityKey: ctx.entityKey, - }); - } - }); - - // ========== storm: give static card to player when storm enemy attacks ========== - triggers.onEnemyIntent.use(async (ctx, next) => { - await next(); - - const enemy = getCombatEntity(ctx.game.value, ctx.enemyId); - if (!enemy || !enemy.isAlive) return; - - const storm = enemy.effects.storm?.stacks ?? 0; - if (storm > 0) { - for (let i = 0; i < storm; i++) { - await triggers.onEffectApplied.execute(ctx.game, { - effect: findEffect("static"), - entityKey: "player", - stacks: 1, - sourceEntityKey: ctx.enemyId, - }); - } - } - }); - - // ========== energyDrain: player loses energy when enemy takes damage ========== - triggers.onDamage.use(async (ctx, next) => { - const entity = getCombatEntity(ctx.game.value, ctx.entityKey); - if (!entity) return; - - const energyDrain = entity.effects.energyDrain?.stacks ?? 0; - if (energyDrain > 0 && ctx.entityKey !== "player") { - const dealt = Math.min(Math.max(0, entity.hp), ctx.amount - (ctx.prevented ?? 0)); - if (dealt > 0) { - await ctx.game.produceAsync(draft => { - draft.player.energy = Math.max(0, draft.player.energy - energyDrain); - }); - } - } - - await next(); - }); - - // ========== molt: enemy flees if molt >= maxHp ========== - triggers.onDamage.use(async (ctx, next) => { - const entity = getCombatEntity(ctx.game.value, ctx.entityKey); - if (!entity || !entity.isAlive) { - await next(); - return; - } - - const molt = entity.effects.molt?.stacks ?? 0; - if (molt >= entity.maxHp) { - await ctx.game.produceAsync(draft => { - const e = draft.enemies.find(en => en.id === ctx.entityKey); - if (e) { - e.isAlive = false; - e.hp = 0; - } - draft.result = draft.enemies.every(en => !en.isAlive) ? "victory" : null; - }); - if (ctx.game.value.result) throw ctx.game.value; - return; - } - - await next(); - }); - - // ========== discard: random discard at turn start ========== - triggers.onTurnStart.use(async (ctx, next) => { - if (ctx.entityKey !== "player") { - await next(); - return; - } - - const discard = ctx.game.value.player.effects.discard; - if (discard && discard.stacks > 0) { - const handIds = [...ctx.game.value.player.deck.regions.hand.childIds]; - if (handIds.length > 0) { - const randomIndex = ctx.game.rng.nextInt(handIds.length); - const randomCardId = handIds[randomIndex]; - await triggers.onCardDiscarded.execute(ctx.game, { cardId: randomCardId }); - } - } - - await next(); - }); - - // ========== defendNext: gain block next turn ========== - triggers.onTurnStart.use(async (ctx, next) => { - if (ctx.entityKey !== "player") { - await next(); - return; - } - - const defendNext = ctx.game.value.player.effects.defendNext; - if (defendNext && defendNext.stacks > 0) { - await ctx.game.produceAsync(draft => { - addEntityEffect(draft.player, findEffect("defend"), defendNext.stacks); - addEntityEffect(draft.player, defendNext.data, -defendNext.stacks); - }); - } - - await next(); - }); - - // ========== energyNext: gain energy next turn ========== - triggers.onTurnStart.use(async (ctx, next) => { - if (ctx.entityKey !== "player") { - await next(); - return; - } - - const energyNext = ctx.game.value.player.effects.energyNext; - if (energyNext && energyNext.stacks > 0) { - await ctx.game.produceAsync(draft => { - draft.player.energy += energyNext.stacks; - addEntityEffect(draft.player, energyNext.data, -energyNext.stacks); - }); - } - - await next(); - }); - - // ========== drawNext: draw extra cards next turn ========== - triggers.onTurnStart.use(async (ctx, next) => { - if (ctx.entityKey !== "player") { - await next(); - return; - } - - const drawNext = ctx.game.value.player.effects.drawNext; - if (drawNext && drawNext.stacks > 0) { - await ctx.game.produceAsync(draft => { - addEntityEffect(draft.player, drawNext.data, -drawNext.stacks); - }); - await triggers.onDraw.execute(ctx.game, { count: drawNext.stacks }); - } - - await next(); - }); - - // ========== aim: double damage, lose aim on damage ========== - triggers.onDamage.use(async (ctx, next) => { - if (ctx.sourceEntityKey === "player") { - const player = ctx.game.value.player; - const aim = player.effects.aim?.stacks ?? 0; - if (aim > 0) { - ctx.amount *= 2; - } - } - await next(); - }); - - // ========== roll: consume 10 roll per 10 damage ========== - triggers.onDamage.use(async (ctx, next) => { - if (ctx.sourceEntityKey === "player") { - const player = ctx.game.value.player; - const roll = player.effects.roll?.stacks ?? 0; - if (roll >= 10) { - const rollDamage = Math.floor(roll / 10) * 10; - ctx.amount += rollDamage; - await ctx.game.produceAsync(draft => { - addEntityEffect(draft.player, findEffect("roll"), -rollDamage); - }); - } - } - await next(); - }); - - // ========== tailSting: bonus damage on attack ========== - triggers.onDamage.use(async (ctx, next) => { - if (ctx.sourceEntityKey && ctx.sourceEntityKey !== "player") { - const attacker = getCombatEntity(ctx.game.value, ctx.sourceEntityKey); - if (attacker) { - const tailSting = attacker.effects.tailSting?.stacks ?? 0; - if (tailSting > 0) { - ctx.amount += tailSting; - } - } - } - await next(); - }); - - // ========== charge: double damage dealt/received, consume equal charge ========== - triggers.onDamage.use(async (ctx, next) => { - const entity = getCombatEntity(ctx.game.value, ctx.entityKey); - if (entity) { - const charge = entity.effects.charge?.stacks ?? 0; - if (charge > 0) { - const dealt = Math.min(Math.max(0, entity.hp), ctx.amount - (ctx.prevented ?? 0)); - const consumed = Math.min(charge, dealt); - ctx.amount += dealt; - if (consumed > 0) { - await ctx.game.produceAsync(draft => { - const e = getCombatEntity(draft, ctx.entityKey); - if (e) addEntityEffect(e, findEffect("charge"), -consumed); - }); - } - } - } - - if (ctx.sourceEntityKey) { - const attacker = getCombatEntity(ctx.game.value, ctx.sourceEntityKey); - if (attacker) { - const charge = attacker.effects.charge?.stacks ?? 0; - if (charge > 0) { - const baseAmount = ctx.amount; - const targetEntity = getCombatEntity(ctx.game.value, ctx.entityKey); - const dealt = Math.min(Math.max(0, targetEntity?.hp ?? 0), baseAmount - (ctx.prevented ?? 0)); - const consumed = Math.min(charge, dealt); - ctx.amount += dealt; - if (consumed > 0) { - await ctx.game.produceAsync(draft => { - const a = getCombatEntity(draft, ctx.sourceEntityKey!); - if (a) addEntityEffect(a, findEffect("charge"), -consumed); - }); - } - } - } - } - - await next(); - }); - - // ========== crossbow: replay other crossbows on same target ========== - triggers.onEffectApplied.use(async (ctx, next) => { - await next(); - - if (ctx.effect.id !== "crossbow" || !ctx.cardId || !ctx.targetId) return; - - const { cards, regions } = ctx.game.value.player.deck; - const handIds = [...regions.hand.childIds]; - for (const id of handIds) { - const card = cards[id]; - if (card && card.itemId === "crossbow" && id !== ctx.cardId) { - await triggers.onCardPlayed.execute(ctx.game, { - cardId: id, - targetId: ctx.targetId, - sourceEntityKey: "player", - }); - } - } - }); - - // ========== burnForEnergy: consume adjacent item, gain energy when its card is played ========== - triggers.onCardPlayed.use(async (ctx, next) => { - await next(); - - const card = ctx.game.value.player.deck.cards[ctx.cardId]; - if (!card) return; - const playedItemId = card.itemId; - - const adjacent = getAdjacentItems(ctx.game.value.inventory, playedItemId); - for (const [adjItemId] of adjacent) { - const adjEffects = ctx.game.value.player.itemEffects[adjItemId]; - if (!adjEffects) continue; - const burn = adjEffects.burnForEnergy; - if (!burn || burn.stacks <= 0) continue; - - await ctx.game.produceAsync(draft => { - const item = draft.inventory.items.get(adjItemId); - if (item) { - draft.inventory.items.delete(adjItemId); - } - draft.player.energy += burn.stacks; - delete draft.player.itemEffects[adjItemId]; - }); - break; - } - }); - - // ========== sandwormKing: heal 10 hp when player discards fatigue ========== - triggers.onCardDiscarded.use(async (ctx, next) => { - await next(); - - const card = ctx.game.value.player.deck.cards[ctx.cardId]; - if (!card || card.cardData.id !== "fatigue") return; - - const sandwormKing = ctx.game.value.enemies.find( - e => e.enemy.id === "沙虫王" && e.isAlive - ); - if (!sandwormKing) return; - - await ctx.game.produceAsync(draft => { - const king = draft.enemies.find(e => e.id === sandwormKing.id); - if (king) { - king.hp = Math.min(king.hp + 10, king.maxHp); - } - }); - }); - - // ========== vulture: give vultureEye when vulture deals damage ========== - triggers.onDamage.use(async (ctx, next) => { - await next(); - - const dealt = ctx.amount - (ctx.prevented ?? 0); - if (dealt <= 0 || !ctx.sourceEntityKey) return; - - const attacker = getCombatEntity(ctx.game.value, ctx.sourceEntityKey); - if (!attacker || !("enemy" in attacker) || attacker.enemy.id !== "秃鹫") return; - - await triggers.onEffectApplied.execute(ctx.game, { - effect: findEffect("vultureEye"), - entityKey: "player", - stacks: 1, - sourceEntityKey: ctx.sourceEntityKey, - }); - }); - - function getAllEnemyData(game: CombatGameContext) { - const seen = new Set(); - const result: typeof game.value.enemies[number]["enemy"][] = []; - for (const enemy of game.value.enemies) { - if (!seen.has(enemy.enemy.id)) { - seen.add(enemy.enemy.id); - result.push(enemy.enemy); - } - } - return result; - } - - function findEffect(id: string): EffectData { - const found = effects.find((e: EffectData) => e.id === id); - if (found) return found; - return { id, name: id, description: "", lifecycle: "instant" } as EffectData; - } + addInstantEffectTriggers(triggers); + addDamageTriggers(triggers); + addTurnStartTriggers(triggers); + addCardEventTriggers(triggers); } diff --git a/src/samples/slay-the-spire-like/data/desert/triggers/index.ts b/src/samples/slay-the-spire-like/data/desert/triggers/index.ts index 7304e90..aeb4fdd 100644 --- a/src/samples/slay-the-spire-like/data/desert/triggers/index.ts +++ b/src/samples/slay-the-spire-like/data/desert/triggers/index.ts @@ -1,6 +1 @@ -import {addEffectTriggers} from './effect'; -import {Triggers} from "@/samples/slay-the-spire-like/system/combat/triggers"; - -export function addTriggers(triggers: Triggers){ - addEffectTriggers(triggers); -} \ No newline at end of file +export { addEffectTriggers as addTriggers } from './effect'; diff --git a/src/samples/slay-the-spire-like/data/desert/triggers/instant.ts b/src/samples/slay-the-spire-like/data/desert/triggers/instant.ts new file mode 100644 index 0000000..ae3395f --- /dev/null +++ b/src/samples/slay-the-spire-like/data/desert/triggers/instant.ts @@ -0,0 +1,158 @@ +import { Triggers } from "@/samples/slay-the-spire-like/system/combat/triggers"; +import { addEntityEffect } from "@/samples/slay-the-spire-like/system/combat/effects"; +import { moveToRegion } from "@/core/region"; +import { CombatGameContext } from "@/samples/slay-the-spire-like/system/combat/types"; +import { EffectData } from "@/samples/slay-the-spire-like/system/types"; +import { GameCard } from "@/samples/slay-the-spire-like/system/deck"; +import getEffects from "../effect.csv"; +import getCards from "../card.csv"; + +export function addInstantEffectTriggers(triggers: Triggers) { + const effects = getEffects(); + const cards = getCards(); + + function findEffect(id: string): EffectData { + const found = effects.find((e: EffectData) => e.id === id); + if (found) return found; + return { id, name: id, description: "", lifecycle: "instant" } as EffectData; + } + + function findCard(id: string) { + return cards.find(c => c.id === id); + } + + function createStatusCard(draft: CombatGameContext["value"], cardId: string): void { + const cardData = findCard(cardId); + if (!cardData) return; + + const instanceId = `${cardId}-${draft.player.deck.regions.drawPile.childIds.length}-${draft.player.deck.regions.discardPile.childIds.length}`; + const card: GameCard = { + id: instanceId, + regionId: "", + position: [], + itemId: cardId, + cardData: { + id: cardData.id, + name: cardData.name, + desc: cardData.desc, + type: cardData.type, + costType: cardData.costType, + costCount: cardData.costCount, + targetType: cardData.targetType, + effects: cardData.effects, + }, + }; + draft.player.deck.cards[instanceId] = card; + moveToRegion(card, null, draft.player.deck.regions.drawPile); + } + + function getAllEnemyDataFromState(state: CombatGameContext["value"]) { + const seen = new Set(); + const result: typeof state.enemies[number]["enemy"][] = []; + for (const enemy of state.enemies) { + if (!seen.has(enemy.enemy.id)) { + seen.add(enemy.enemy.id); + result.push(enemy.enemy); + } + } + return result; + } + + function summonEnemy(draft: CombatGameContext["value"], enemyId: string, hp: number) { + for (const enemyData of getAllEnemyDataFromState(draft)) { + if (enemyData.id === enemyId) { + const existing = draft.enemies.find(e => e.enemy.id === enemyId && !e.isAlive); + if (existing) { + existing.isAlive = true; + existing.hp = existing.maxHp; + return; + } + const intent = enemyData.intents.find(i => i.initialIntent) ?? enemyData.intents[0]; + const instanceId = `${enemyData.id}-${draft.enemies.length}`; + const intents: Record = {}; + for (const i of enemyData.intents) { + intents[i.id] = i; + } + draft.enemies.push({ + id: instanceId, + enemy: enemyData, + hp, + maxHp: hp, + isAlive: true, + effects: {}, + intents, + currentIntent: intent, + }); + break; + } + } + } + + triggers.onEffectApplied.use(async (ctx, next) => { + if (ctx.effect.id === "attack") { + await triggers.onDamage.execute(ctx.game, { + entityKey: ctx.entityKey, + amount: ctx.stacks, + sourceEntityKey: ctx.sourceEntityKey ?? ctx.entityKey === "player" ? undefined : "player", + }); + } else if (ctx.effect.id === "draw") { + await triggers.onDraw.execute(ctx.game, { count: ctx.stacks }); + } else if (ctx.effect.id === "gainEnergy") { + await ctx.game.produceAsync(draft => { + draft.player.energy += ctx.stacks; + }); + } else if (ctx.effect.id === "removeWound") { + await ctx.game.produceAsync(draft => { + const { cards, regions } = draft.player.deck; + let removed = 0; + const allPileIds = [ + ...regions.drawPile.childIds, + ...regions.discardPile.childIds, + ]; + for (const cardId of allPileIds) { + if (removed >= ctx.stacks) break; + const card = cards[cardId]; + if (card && card.cardData.id === "wound") { + const sourceRegion = card.regionId === "drawPile" ? regions.drawPile : regions.discardPile; + moveToRegion(card, sourceRegion, null); + delete cards[cardId]; + removed++; + } + } + }); + } else if (ctx.effect.id === "venom") { + await ctx.game.produceAsync(draft => { + createStatusCard(draft, "venom"); + }); + } else if (ctx.effect.id === "vultureEye") { + await ctx.game.produceAsync(draft => { + createStatusCard(draft, "vultureEye"); + }); + } else if (ctx.effect.id === "static") { + await ctx.game.produceAsync(draft => { + createStatusCard(draft, "static"); + }); + } else if (ctx.effect.id === "summonMummy") { + await ctx.game.produceAsync(draft => { + summonEnemy(draft, "木乃伊", ctx.stacks); + }); + } else if (ctx.effect.id === "summonSandwormLarva") { + await ctx.game.produceAsync(draft => { + summonEnemy(draft, "幼沙虫", ctx.stacks); + }); + } else if (ctx.effect.id === "reviveMummy") { + await ctx.game.produceAsync(draft => { + const deadMummy = draft.enemies.find(e => e.enemy.id === "木乃伊" && !e.isAlive); + if (deadMummy) { + deadMummy.isAlive = true; + deadMummy.hp = deadMummy.maxHp; + } + }); + } else if (ctx.effect.id === "curse") { + await ctx.game.produceAsync(draft => { + addEntityEffect(draft.player, ctx.effect, ctx.stacks); + }); + } + await next(); + }); +} diff --git a/src/samples/slay-the-spire-like/data/desert/triggers/turn-start.ts b/src/samples/slay-the-spire-like/data/desert/triggers/turn-start.ts new file mode 100644 index 0000000..02db139 --- /dev/null +++ b/src/samples/slay-the-spire-like/data/desert/triggers/turn-start.ts @@ -0,0 +1,88 @@ +import { Triggers } from "@/samples/slay-the-spire-like/system/combat/triggers"; +import { addEntityEffect } from "@/samples/slay-the-spire-like/system/combat/effects"; +import { EffectData } from "@/samples/slay-the-spire-like/system/types"; +import getEffects from "../effect.csv"; + +export function addTurnStartTriggers(triggers: Triggers) { + const effects = getEffects(); + + function findEffect(id: string): EffectData { + const found = effects.find(e => e.id === id); + if (found) return found; + return { id, name: id, description: "", lifecycle: "instant" } as EffectData; + } + + // discard: random discard at turn start + triggers.onTurnStart.use(async (ctx, next) => { + if (ctx.entityKey !== "player") { + await next(); + return; + } + + const discard = ctx.game.value.player.effects.discard; + if (discard && discard.stacks > 0) { + const handIds = [...ctx.game.value.player.deck.regions.hand.childIds]; + if (handIds.length > 0) { + const randomIndex = ctx.game.rng.nextInt(handIds.length); + const randomCardId = handIds[randomIndex]; + await triggers.onCardDiscarded.execute(ctx.game, { cardId: randomCardId }); + } + } + + await next(); + }); + + // defendNext: gain block next turn + triggers.onTurnStart.use(async (ctx, next) => { + if (ctx.entityKey !== "player") { + await next(); + return; + } + + const defendNext = ctx.game.value.player.effects.defendNext; + if (defendNext && defendNext.stacks > 0) { + await ctx.game.produceAsync(draft => { + addEntityEffect(draft.player, findEffect("defend"), defendNext.stacks); + addEntityEffect(draft.player, defendNext.data, -defendNext.stacks); + }); + } + + await next(); + }); + + // energyNext: gain energy next turn + triggers.onTurnStart.use(async (ctx, next) => { + if (ctx.entityKey !== "player") { + await next(); + return; + } + + const energyNext = ctx.game.value.player.effects.energyNext; + if (energyNext && energyNext.stacks > 0) { + await ctx.game.produceAsync(draft => { + draft.player.energy += energyNext.stacks; + addEntityEffect(draft.player, energyNext.data, -energyNext.stacks); + }); + } + + await next(); + }); + + // drawNext: draw extra cards next turn + triggers.onTurnStart.use(async (ctx, next) => { + if (ctx.entityKey !== "player") { + await next(); + return; + } + + const drawNext = ctx.game.value.player.effects.drawNext; + if (drawNext && drawNext.stacks > 0) { + await ctx.game.produceAsync(draft => { + addEntityEffect(draft.player, drawNext.data, -drawNext.stacks); + }); + await triggers.onDraw.execute(ctx.game, { count: drawNext.stacks }); + } + + await next(); + }); +}