refactor: update damage triggers and combat start logic

- Update `defend` effect lifecycle to `temporary` in desert data
- Refactor `onDamage` triggers to improve readability and logic flow
- Implement shuffle and draw actions on `onCombatStart` trigger
This commit is contained in:
hypercross 2026-04-22 11:06:19 +08:00
parent 5235ba7def
commit 2eec668851
3 changed files with 196 additions and 172 deletions

View File

@ -14,7 +14,7 @@
id, name, description, lifecycle id, name, description, lifecycle
string, string, string, 'instant'|'temporary'|'lingering'|'permanent'|'posture'|'item'|'itemTemporary'|'itemUntilPlay'|'itemUntilDiscard'|'itemPermanent' string, string, string, 'instant'|'temporary'|'lingering'|'permanent'|'posture'|'item'|'itemTemporary'|'itemUntilPlay'|'itemUntilDiscard'|'itemPermanent'
attack, 攻击, 对对手造成伤害, instant attack, 攻击, 对对手造成伤害, instant
defend, 防御, 抵消下次行动前受到的伤害, posture defend, 防御, 抵消下次行动前受到的伤害, temporary
spike, 尖刺, 对攻击者造成X点伤害, permanent spike, 尖刺, 对攻击者造成X点伤害, permanent
venom, 蛇毒, 同名状态牌/1费打出时移除此牌。弃掉时受到3点伤害, instant venom, 蛇毒, 同名状态牌/1费打出时移除此牌。弃掉时受到3点伤害, instant
curse, 诅咒, 受攻击时物品攻击-1直到弃掉一张该物品的牌, lingering curse, 诅咒, 受攻击时物品攻击-1直到弃掉一张该物品的牌, lingering

Can't render this file because it has a wrong number of fields in line 14.

View File

@ -1,192 +1,213 @@
import { Triggers } from "@/samples/slay-the-spire-like/system/combat/triggers"; import { Triggers } from "@/samples/slay-the-spire-like/system/combat/triggers";
import { import {
addEntityEffect, addEntityEffect,
getCombatEntity, getCombatEntity,
} from "@/samples/slay-the-spire-like/system/combat/effects"; } 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 { EffectData } from "@/samples/slay-the-spire-like/system/types";
import getEffects from "../effect.csv"; import getEffects from "../effect.csv";
export function addDamageTriggers(triggers: Triggers) { export function addDamageTriggers(triggers: Triggers) {
const effects = getEffects(); const effects = getEffects();
function findEffect(id: string): EffectData { function findEffect(id: string): EffectData {
const found = effects.find((e: EffectData) => e.id === id); const found = effects.find((e: EffectData) => e.id === id);
if (found) return found; if (found) return found;
return { id, name: id, description: "", lifecycle: "instant" } as EffectData; 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 // 2. consume defend for damage prevented
triggers.onDamage.use(async (ctx, next) => { const blocks = entity.effects.defend?.stacks ?? 0;
const entity = getCombatEntity(ctx.game.value, ctx.entityKey); const blocked = Math.min(blocks, preventable);
if (!entity) return; 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; await next();
const blocked = Math.min(blocks, preventable); });
if (blocked) {
ctx.prevented = (ctx.prevented ?? 0) + blocked; // spike: damage attacker
preventable -= blocked; 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; await next();
if (damageReduce > 0) { });
const reduced = Math.min(damageReduce, preventable);
ctx.prevented = (ctx.prevented ?? 0) + reduced; // aim: double damage, lose aim on damage
preventable -= reduced; 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; // charge: double damage dealt/received, consume equal charge
if (expose > 0) { triggers.onDamage.use(async (ctx, next) => {
ctx.amount += expose; 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(); if (ctx.sourceEntityKey) {
}); const attacker = getCombatEntity(ctx.game.value, ctx.sourceEntityKey);
if (attacker) {
// spike: damage attacker const charge = attacker.effects.charge?.stacks ?? 0;
triggers.onDamage.use(async (ctx, next) => { if (charge > 0) {
await next(); const baseAmount = ctx.amount;
const targetEntity = getCombatEntity(ctx.game.value, ctx.entityKey);
if (ctx.amount - (ctx.prevented ?? 0) <= 0) return; const dealt = Math.min(
Math.max(0, targetEntity?.hp ?? 0),
const entity = getCombatEntity(ctx.game.value, ctx.entityKey); baseAmount - (ctx.prevented ?? 0),
if (!entity || !entity.isAlive) return; );
const consumed = Math.min(charge, dealt);
const spike = entity.effects.spike?.stacks ?? 0; ctx.amount += dealt;
if (spike > 0 && ctx.sourceEntityKey) { if (consumed > 0) {
await triggers.onDamage.execute(ctx.game, { await ctx.game.produceAsync((draft) => {
entityKey: ctx.sourceEntityKey, const a = getCombatEntity(draft, ctx.sourceEntityKey!);
amount: spike, if (a) addEntityEffect(a, findEffect("charge"), -consumed);
sourceEntityKey: ctx.entityKey,
}); });
}
} }
}); }
}
// energyDrain: player loses energy when enemy takes damage await next();
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();
});
} }

View File

@ -51,7 +51,10 @@ type TriggerTypes = {
export function createTriggers(run: IRunContext) { export function createTriggers(run: IRunContext) {
const triggers = { 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) => { onTurnStart: createTrigger("onTurnStart", async (ctx) => {
await ctx.game.produceAsync((draft) => { await ctx.game.produceAsync((draft) => {
const entity = getCombatEntity(draft, ctx.entityKey); const entity = getCombatEntity(draft, ctx.entityKey);