refactor: reorganize trigger code
This commit is contained in:
parent
e46822b45b
commit
fa92b5d865
|
|
@ -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<GameItemMeta>(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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -1,555 +1,12 @@
|
||||||
import { Triggers } from "@/samples/slay-the-spire-like/system/combat/triggers";
|
import { Triggers } from "@/samples/slay-the-spire-like/system/combat/triggers";
|
||||||
import {
|
import { addInstantEffectTriggers } from "./instant";
|
||||||
addEntityEffect,
|
import { addDamageTriggers } from "./damage";
|
||||||
getCombatEntity,
|
import { addTurnStartTriggers } from "./turn-start";
|
||||||
} from "@/samples/slay-the-spire-like/system/combat/effects";
|
import { addCardEventTriggers } from "./card-events";
|
||||||
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";
|
|
||||||
|
|
||||||
export function addEffectTriggers(triggers: Triggers) {
|
export function addEffectTriggers(triggers: Triggers) {
|
||||||
const effects = getEffects();
|
addInstantEffectTriggers(triggers);
|
||||||
// ========== instant effects ==========
|
addDamageTriggers(triggers);
|
||||||
triggers.onEffectApplied.use(async (ctx, next) => {
|
addTurnStartTriggers(triggers);
|
||||||
if (ctx.effect.id === "attack") {
|
addCardEventTriggers(triggers);
|
||||||
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<string, typeof intent> = {};
|
|
||||||
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<string, typeof intent> = {};
|
|
||||||
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<GameItemMeta>(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<string>();
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1 @@
|
||||||
import {addEffectTriggers} from './effect';
|
export { addEffectTriggers as addTriggers } from './effect';
|
||||||
import {Triggers} from "@/samples/slay-the-spire-like/system/combat/triggers";
|
|
||||||
|
|
||||||
export function addTriggers(triggers: Triggers){
|
|
||||||
addEffectTriggers(triggers);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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<string>();
|
||||||
|
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<string, typeof intent> = {};
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue