From 131af2c0bb6dda8d6a26a3348ca3233af158cd59 Mon Sep 17 00:00:00 2001 From: hypercross Date: Fri, 17 Apr 2026 16:57:29 +0800 Subject: [PATCH] feat: implmenet some effects for the design --- .../data/desert/triggers/effect.ts | 458 +++++++++++++++++- .../system/combat/triggers.ts | 24 +- 2 files changed, 454 insertions(+), 28 deletions(-) diff --git a/src/samples/slay-the-spire-like/data/desert/triggers/effect.ts b/src/samples/slay-the-spire-like/data/desert/triggers/effect.ts index 1c91345..ce493ff 100644 --- a/src/samples/slay-the-spire-like/data/desert/triggers/effect.ts +++ b/src/samples/slay-the-spire-like/data/desert/triggers/effect.ts @@ -1,33 +1,455 @@ -import {Triggers} from "@/samples/slay-the-spire-like/system/combat/triggers"; -import {getCombatEntity} from "@/samples/slay-the-spire-like/system/combat/effects"; +import { Triggers } from "@/samples/slay-the-spire-like/system/combat/triggers"; +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){ - // instant effects - triggers.onEffectApplied.use(async (ctx, next) => { - if(ctx.effect.id === "attack") { +export function addEffectTriggers(triggers: Triggers) { + // ========== instant effects ========== + triggers.onEffectApplied.use(async (ctx, next) => { + if (ctx.effect.id === "attack") { await triggers.onDamage.execute(ctx.game, { entityKey: ctx.entityKey, - amount: ctx.stacks + 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 === "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 = {}; + 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 = {}; + 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(); }); - - // blocks + + // ========== block / damage prevention ========== triggers.onDamage.use(async (ctx, next) => { const entity = getCombatEntity(ctx.game.value, ctx.entityKey); - if(!entity) return; - - const preventable = (ctx.amount - (ctx.prevented ?? 0)); - const blocks = entity.effects.block?.stacks ?? 0; + 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){ + 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 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(); }); -} \ No newline at end of file + + // ========== 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(); + 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; +} diff --git a/src/samples/slay-the-spire-like/system/combat/triggers.ts b/src/samples/slay-the-spire-like/system/combat/triggers.ts index 837fe9e..60feab7 100644 --- a/src/samples/slay-the-spire-like/system/combat/triggers.ts +++ b/src/samples/slay-the-spire-like/system/combat/triggers.ts @@ -18,14 +18,14 @@ type TriggerTypes = { onTurnStart: { entityKey: "player" | string, }, onTurnEnd: { entityKey: "player" | string, }, onShuffle: {}, - onCardPlayed: { cardId: string, targetId?: string }, - onCardDiscarded: { cardId: string, }, - onCardDrawn: { cardId: string, }, + onCardPlayed: { cardId: string, targetId?: string, sourceEntityKey?: "player" | string }, + onCardDiscarded: { cardId: string, sourceEntityKey?: "player" | string }, + onCardDrawn: { cardId: string, sourceEntityKey?: "player" | string }, 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}, - onDamage: { entityKey: "player" | string, amount: number, prevented?: number}, - onEnemyIntent: { enemyId: string }, + onDamage: { entityKey: "player" | string, amount: number, prevented?: number, sourceEntityKey?: "player" | string}, + onEnemyIntent: { enemyId: string, sourceEntityKey?: "player" | string }, onIntentUpdate: { enemyId: string }, } @@ -67,10 +67,11 @@ function createTriggers(){ }); const {cards, regions} = ctx.game.value.player.deck; const card = cards[ctx.cardId]; + const source = ctx.sourceEntityKey ?? "player"; for(const [trigger, target, effect, stacks] of card.cardData.effects){ if(trigger !== 'onPlay') continue; 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 => { @@ -81,10 +82,11 @@ function createTriggers(){ }); const {cards, regions} = ctx.game.value.player.deck; const card = cards[ctx.cardId]; + const source = ctx.sourceEntityKey ?? "player"; for(const [trigger, target, effect, stacks] of card.cardData.effects){ if(trigger !== 'onDiscard') continue; 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 => { @@ -94,10 +96,11 @@ function createTriggers(){ }); const {cards, regions} = ctx.game.value.player.deck; const card = cards[ctx.cardId]; + const source = ctx.sourceEntityKey ?? "player"; for(const [trigger, target, effect, stacks] of card.cardData.effects){ if(trigger !== 'onDraw') continue; 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 => { @@ -162,9 +165,10 @@ function createTriggers(){ const intent = enemy.currentIntent; if(!intent) return; + const source = ctx.sourceEntityKey ?? enemy.id; for(const [target, effect, stacks] of intent.effects){ 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 => {