|
|
|
@@ -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();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|