diff --git a/src/samples/slay-the-spire-like/data/desert/effect.csv b/src/samples/slay-the-spire-like/data/desert/effect.csv index 3a73bfd..15cbdd4 100644 --- a/src/samples/slay-the-spire-like/data/desert/effect.csv +++ b/src/samples/slay-the-spire-like/data/desert/effect.csv @@ -14,7 +14,7 @@ id, name, description, lifecycle string, string, string, 'instant'|'temporary'|'lingering'|'permanent'|'posture'|'item'|'itemTemporary'|'itemUntilPlay'|'itemUntilDiscard'|'itemPermanent' attack, 攻击, 对对手造成伤害, instant -defend, 防御, 抵消下次行动前受到的伤害, posture +defend, 防御, 抵消下次行动前受到的伤害, temporary spike, 尖刺, 对攻击者造成X点伤害, permanent venom, 蛇毒, 同名状态牌/1费:打出时移除此牌。弃掉时受到3点伤害, instant curse, 诅咒, 受攻击时物品攻击-1,直到弃掉一张该物品的牌, lingering 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 index 1d8499c..e0f38bf 100644 --- a/src/samples/slay-the-spire-like/data/desert/triggers/damage.ts +++ b/src/samples/slay-the-spire-like/data/desert/triggers/damage.ts @@ -1,192 +1,213 @@ import { Triggers } from "@/samples/slay-the-spire-like/system/combat/triggers"; import { - addEntityEffect, - getCombatEntity, + 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(); + 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; + 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); + + // 1. apply damageReduce before block + const damageReduce = entity.effects.damageReduce?.stacks ?? 0; + if (damageReduce > 0) { + const reduced = Math.min(damageReduce, preventable); + ctx.prevented = (ctx.prevented ?? 0) + reduced; + preventable -= reduced; } - // block / damage prevention - triggers.onDamage.use(async (ctx, next) => { - const entity = getCombatEntity(ctx.game.value, ctx.entityKey); - if (!entity) return; + // 2. consume defend for damage prevented + const blocks = entity.effects.defend?.stacks ?? 0; + const blocked = Math.min(blocks, preventable); + if (blocked > 0) { + ctx.prevented = (ctx.prevented ?? 0) + blocked; + preventable -= blocked; + await ctx.game.produceAsync((draft) => { + const e = getCombatEntity(draft, ctx.entityKey); + if (e) addEntityEffect(e, findEffect("defend"), -blocked); + }); + } - let preventable = ctx.amount - (ctx.prevented ?? 0); + const expose = entity.effects.expose?.stacks ?? 0; + if (expose > 0) { + ctx.amount += expose; + } - const blocks = entity.effects.defend?.stacks ?? 0; - const blocked = Math.min(blocks, preventable); - if (blocked) { - ctx.prevented = (ctx.prevented ?? 0) + blocked; - preventable -= blocked; + 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; + } - const damageReduce = entity.effects.damageReduce?.stacks ?? 0; - if (damageReduce > 0) { - const reduced = Math.min(damageReduce, preventable); - ctx.prevented = (ctx.prevented ?? 0) + reduced; - preventable -= reduced; + 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(); + }); - const expose = entity.effects.expose?.stacks ?? 0; - if (expose > 0) { - ctx.amount += expose; + // 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); + }); } + } + } - 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, + 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); }); + } } - }); + } + } - // 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(); - }); + await next(); + }); } 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 dfff4a5..36958b0 100644 --- a/src/samples/slay-the-spire-like/system/combat/triggers.ts +++ b/src/samples/slay-the-spire-like/system/combat/triggers.ts @@ -51,7 +51,10 @@ type TriggerTypes = { export function createTriggers(run: IRunContext) { const triggers = { - onCombatStart: createTrigger("onCombatStart"), + 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.entityKey);