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:
parent
5235ba7def
commit
2eec668851
|
|
@ -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.
|
|
|
@ -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();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue