feat: implmenet some effects for the design

This commit is contained in:
hypercross 2026-04-17 16:57:29 +08:00
parent 02c159f8ae
commit 131af2c0bb
2 changed files with 454 additions and 28 deletions

View File

@ -1,33 +1,455 @@
import {Triggers} from "@/samples/slay-the-spire-like/system/combat/triggers"; import { Triggers } from "@/samples/slay-the-spire-like/system/combat/triggers";
import {getCombatEntity} from "@/samples/slay-the-spire-like/system/combat/effects"; import {
addEntityEffect,
getCombatEntity,
} 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";
export function addEffectTriggers(triggers: Triggers) { export function addEffectTriggers(triggers: Triggers) {
// instant effects // ========== instant effects ==========
triggers.onEffectApplied.use(async (ctx, next) => { triggers.onEffectApplied.use(async (ctx, next) => {
if (ctx.effect.id === "attack") { if (ctx.effect.id === "attack") {
await triggers.onDamage.execute(ctx.game, { await triggers.onDamage.execute(ctx.game, {
entityKey: ctx.entityKey, entityKey: ctx.entityKey,
amount: ctx.stacks amount: ctx.stacks,
sourceEntityKey: ctx.sourceEntityKey ?? ctx.entityKey === "player" ? undefined : "player",
}); });
} else if (ctx.effect.id === "draw") { } else if (ctx.effect.id === "draw") {
await triggers.onDraw.execute(ctx.game, { await triggers.onDraw.execute(ctx.game, { count: ctx.stacks });
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: 14,
maxHp: 14,
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: 18,
maxHp: 18,
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(); await next();
}); });
// blocks // ========== block / damage prevention ==========
triggers.onDamage.use(async (ctx, next) => { triggers.onDamage.use(async (ctx, next) => {
const entity = getCombatEntity(ctx.game.value, ctx.entityKey); const entity = getCombatEntity(ctx.game.value, ctx.entityKey);
if (!entity) return; if (!entity) return;
const preventable = (ctx.amount - (ctx.prevented ?? 0)); let preventable = ctx.amount - (ctx.prevented ?? 0);
const blocks = entity.effects.block?.stacks ?? 0;
const blocks = entity.effects.defend?.stacks ?? 0;
const blocked = Math.min(blocks, preventable); const blocked = Math.min(blocks, preventable);
if (blocked) { if (blocked) {
ctx.prevented = (ctx.prevented ?? 0) + 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 on attack ==========
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 storm = entity.effects.storm?.stacks ?? 0;
if (storm > 0 && ctx.entityKey !== "player") {
for (let i = 0; i < storm; i++) {
await triggers.onEffectApplied.execute(ctx.game, {
effect: findEffect(ctx.game, "static"),
entityKey: "player",
stacks: 1,
});
}
}
});
// ========== 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) => {
await next();
const entity = getCombatEntity(ctx.game.value, ctx.entityKey);
if (!entity || !entity.isAlive) 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;
}
});
// ========== discard: random discard at turn start ==========
triggers.onTurnStart.use(async (ctx, next) => {
await next();
if (ctx.entityKey !== "player") return;
const discard = ctx.game.value.player.effects.discard;
if (!discard || discard.stacks <= 0) return;
const handIds = [...ctx.game.value.player.deck.regions.hand.childIds];
if (handIds.length === 0) return;
const randomIndex = ctx.game.rng.nextInt(handIds.length);
const randomCardId = handIds[randomIndex];
await triggers.onCardDiscarded.execute(ctx.game, { cardId: randomCardId });
});
// ========== defendNext: gain block next turn ==========
triggers.onTurnStart.use(async (ctx, next) => {
await next();
if (ctx.entityKey !== "player") return;
const defendNext = ctx.game.value.player.effects.defendNext;
if (!defendNext || defendNext.stacks <= 0) return;
await ctx.game.produceAsync(draft => {
addEntityEffect(draft.player, findEffect(ctx.game, "defend"), defendNext.stacks);
addEntityEffect(draft.player, defendNext.data, -defendNext.stacks);
});
});
// ========== energyNext: gain energy next turn ==========
triggers.onTurnStart.use(async (ctx, next) => {
await next();
if (ctx.entityKey !== "player") return;
const energyNext = ctx.game.value.player.effects.energyNext;
if (!energyNext || energyNext.stacks <= 0) return;
await ctx.game.produceAsync(draft => {
draft.player.energy += energyNext.stacks;
addEntityEffect(draft.player, energyNext.data, -energyNext.stacks);
});
});
// ========== drawNext: draw extra cards next turn ==========
triggers.onTurnStart.use(async (ctx, next) => {
await next();
if (ctx.entityKey !== "player") return;
const drawNext = ctx.game.value.player.effects.drawNext;
if (!drawNext || drawNext.stacks <= 0) return;
await ctx.game.produceAsync(draft => {
addEntityEffect(draft.player, drawNext.data, -drawNext.stacks);
});
await triggers.onDraw.execute(ctx.game, { count: drawNext.stacks });
});
// ========== 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(); 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(ctx.game, "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(ctx.game, "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(ctx.game, "charge"), -consumed);
});
}
}
}
}
await next();
});
}
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(game: CombatGameContext | { value: CombatGameContext["value"] }, id: string): EffectData {
const value = "value" in game ? game.value : game;
const dataModule = (globalThis as any).__desertEffects;
if (dataModule) {
const found = dataModule.find((e: EffectData) => e.id === id);
if (found) return found;
}
return { id, name: id, description: "", lifecycle: "instant" } as EffectData;
} }

View File

@ -18,14 +18,14 @@ type TriggerTypes = {
onTurnStart: { entityKey: "player" | string, }, onTurnStart: { entityKey: "player" | string, },
onTurnEnd: { entityKey: "player" | string, }, onTurnEnd: { entityKey: "player" | string, },
onShuffle: {}, onShuffle: {},
onCardPlayed: { cardId: string, targetId?: string }, onCardPlayed: { cardId: string, targetId?: string, sourceEntityKey?: "player" | string },
onCardDiscarded: { cardId: string, }, onCardDiscarded: { cardId: string, sourceEntityKey?: "player" | string },
onCardDrawn: { cardId: string, }, onCardDrawn: { cardId: string, sourceEntityKey?: "player" | string },
onDraw: {count: number}, onDraw: {count: number},
onEffectApplied: { effect: EffectData, entityKey: "player" | string, stacks: number, cardId?: string }, onEffectApplied: { effect: EffectData, entityKey: "player" | string, stacks: number, cardId?: string, sourceEntityKey?: "player" | string },
onHpChange: { entityKey: "player" | string, amount: number}, onHpChange: { entityKey: "player" | string, amount: number},
onDamage: { entityKey: "player" | string, amount: number, prevented?: number}, onDamage: { entityKey: "player" | string, amount: number, prevented?: number, sourceEntityKey?: "player" | string},
onEnemyIntent: { enemyId: string }, onEnemyIntent: { enemyId: string, sourceEntityKey?: "player" | string },
onIntentUpdate: { enemyId: string }, onIntentUpdate: { enemyId: string },
} }
@ -67,10 +67,11 @@ function createTriggers(){
}); });
const {cards, regions} = ctx.game.value.player.deck; const {cards, regions} = ctx.game.value.player.deck;
const card = cards[ctx.cardId]; const card = cards[ctx.cardId];
const source = ctx.sourceEntityKey ?? "player";
for(const [trigger, target, effect, stacks] of card.cardData.effects){ for(const [trigger, target, effect, stacks] of card.cardData.effects){
if(trigger !== 'onPlay') continue; if(trigger !== 'onPlay') continue;
for(const entity of getEffectTargets(target, ctx.game, ctx.targetId)) for(const entity of getEffectTargets(target, ctx.game, ctx.targetId))
await triggers.onEffectApplied.execute(ctx.game,{effect, entityKey: entity.id, stacks, cardId: ctx.cardId}); await triggers.onEffectApplied.execute(ctx.game,{effect, entityKey: entity.id, stacks, cardId: ctx.cardId, sourceEntityKey: source});
} }
}), }),
onCardDiscarded: createTrigger("onCardDiscarded", async ctx => { onCardDiscarded: createTrigger("onCardDiscarded", async ctx => {
@ -81,10 +82,11 @@ function createTriggers(){
}); });
const {cards, regions} = ctx.game.value.player.deck; const {cards, regions} = ctx.game.value.player.deck;
const card = cards[ctx.cardId]; const card = cards[ctx.cardId];
const source = ctx.sourceEntityKey ?? "player";
for(const [trigger, target, effect, stacks] of card.cardData.effects){ for(const [trigger, target, effect, stacks] of card.cardData.effects){
if(trigger !== 'onDiscard') continue; if(trigger !== 'onDiscard') continue;
for(const entity of getEffectTargets(target, ctx.game)) for(const entity of getEffectTargets(target, ctx.game))
await triggers.onEffectApplied.execute(ctx.game,{effect, entityKey: entity.id, stacks, cardId: ctx.cardId}); await triggers.onEffectApplied.execute(ctx.game,{effect, entityKey: entity.id, stacks, cardId: ctx.cardId, sourceEntityKey: source});
} }
}), }),
onCardDrawn: createTrigger("onCardDrawn", async ctx => { onCardDrawn: createTrigger("onCardDrawn", async ctx => {
@ -94,10 +96,11 @@ function createTriggers(){
}); });
const {cards, regions} = ctx.game.value.player.deck; const {cards, regions} = ctx.game.value.player.deck;
const card = cards[ctx.cardId]; const card = cards[ctx.cardId];
const source = ctx.sourceEntityKey ?? "player";
for(const [trigger, target, effect, stacks] of card.cardData.effects){ for(const [trigger, target, effect, stacks] of card.cardData.effects){
if(trigger !== 'onDraw') continue; if(trigger !== 'onDraw') continue;
for(const entity of getEffectTargets(target, ctx.game)) for(const entity of getEffectTargets(target, ctx.game))
await triggers.onEffectApplied.execute(ctx.game,{effect, entityKey: entity.id, stacks, cardId: ctx.cardId}); await triggers.onEffectApplied.execute(ctx.game,{effect, entityKey: entity.id, stacks, cardId: ctx.cardId, sourceEntityKey: source});
} }
}), }),
onDraw: createTrigger("onDraw", async ctx => { onDraw: createTrigger("onDraw", async ctx => {
@ -162,9 +165,10 @@ function createTriggers(){
const intent = enemy.currentIntent; const intent = enemy.currentIntent;
if(!intent) return; if(!intent) return;
const source = ctx.sourceEntityKey ?? enemy.id;
for(const [target, effect, stacks] of intent.effects){ for(const [target, effect, stacks] of intent.effects){
for(const entity of getEffectTargets(target, ctx.game)) for(const entity of getEffectTargets(target, ctx.game))
await triggers.onEffectApplied.execute(ctx.game, { effect, entityKey: entity.id, stacks, }); await triggers.onEffectApplied.execute(ctx.game, { effect, entityKey: entity.id, stacks, sourceEntityKey: source });
} }
}), }),
onIntentUpdate: createTrigger("onIntentUpdate", async ctx => { onIntentUpdate: createTrigger("onIntentUpdate", async ctx => {