From a5e2e4888e2db4eeac5df4fea02697a41f619ea2 Mon Sep 17 00:00:00 2001 From: hypercross Date: Wed, 22 Apr 2026 19:35:37 +0800 Subject: [PATCH] refactor(slay-the-spire-like): update combat targeting and effect logic Refactor combat targeting types and effect application logic in the Slay the Spire-like sample. - Update `getEffectTargets` to use `IntentEffectTarget` and more descriptive target keys (e.g., `eachEnemy`, `randomEnemy`). - Update `promptMainAction` to use `enemy` instead of `single` for card target types. - Refactor `addInstantEffectTriggers` to remove unused effect loading and improve enemy/card instantiation logic --- .../data/desert/triggers/instant.ts | 292 +++++++++--------- .../system/combat/effects.ts | 12 +- .../system/combat/prompts.ts | 4 +- .../combat/effects.test.ts | 7 +- .../combat/triggers.test.ts | 10 +- .../slay-the-spire-like/deck/factory.test.ts | 2 +- .../grid-inventory.test.ts | 3 +- 7 files changed, 170 insertions(+), 160 deletions(-) diff --git a/src/samples/slay-the-spire-like/data/desert/triggers/instant.ts b/src/samples/slay-the-spire-like/data/desert/triggers/instant.ts index ae3395f..cc26714 100644 --- a/src/samples/slay-the-spire-like/data/desert/triggers/instant.ts +++ b/src/samples/slay-the-spire-like/data/desert/triggers/instant.ts @@ -2,157 +2,167 @@ 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(); + 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(); + 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 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(); - 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); - } + 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; } - 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 = {}; - 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; - } + 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, + 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); - }); + 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++; + } } - await next(); - }); + }); + } 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(); + }); } diff --git a/src/samples/slay-the-spire-like/system/combat/effects.ts b/src/samples/slay-the-spire-like/system/combat/effects.ts index bfbda05..dc98a8d 100644 --- a/src/samples/slay-the-spire-like/system/combat/effects.ts +++ b/src/samples/slay-the-spire-like/system/combat/effects.ts @@ -10,7 +10,7 @@ import { CardData, CardEffectTarget, EffectData, - EffectTarget, + IntentEffectTarget, } from "@/samples/slay-the-spire-like/system/types"; export function addEffect( @@ -103,25 +103,25 @@ export function* getAliveEnemies(state: CombatState) { } export function* getEffectTargets( - target: CardEffectTarget | EffectTarget, + target: CardEffectTarget | IntentEffectTarget, game: CombatGameContext, targetId?: string, sourceEntityKey: "player" | string = "player", ) { - if (target === "all" || target === "team") { + if (target === "eachEnemy") { for (const enemy of getAliveEnemies(game.value)) { yield enemy; } - } else if (target === "self") { + } else if (target === "user") { const entity = getCombatEntity(game.value, sourceEntityKey); if (entity) yield entity; } else if (target === "player") { yield game.value.player; - } else if (target === "target") { + } else if (target === "eachTarget") { if (!targetId) return; const entity = getCombatEntity(game.value, targetId); if (entity) yield entity; - } else if (target === "random") { + } else if (target === "randomEnemy") { const aliveEnemies = [...getAliveEnemies(game.value)]; if (aliveEnemies.length === 0) return; const index = game.rng.nextInt(aliveEnemies.length); diff --git a/src/samples/slay-the-spire-like/system/combat/prompts.ts b/src/samples/slay-the-spire-like/system/combat/prompts.ts index bff2944..21db680 100644 --- a/src/samples/slay-the-spire-like/system/combat/prompts.ts +++ b/src/samples/slay-the-spire-like/system/combat/prompts.ts @@ -38,12 +38,12 @@ export async function promptMainAction( } const { targetType } = cardData; - if (targetType === "single") { + if (targetType === "enemy") { if (!targetId) throw `请指定目标`; const target = game.value.enemies.find((e) => e.id === targetId); if (!target) throw `目标"${targetId}"不存在`; if (!target.isAlive) throw `目标"${targetId}"已死亡`; - } else if (targetType === "none") { + } else if (targetType === "enemies" || targetType === "player") { if (targetId) throw `目标"${targetId}"无效`; } diff --git a/tests/samples/slay-the-spire-like/combat/effects.test.ts b/tests/samples/slay-the-spire-like/combat/effects.test.ts index 82a7b25..e012425 100644 --- a/tests/samples/slay-the-spire-like/combat/effects.test.ts +++ b/tests/samples/slay-the-spire-like/combat/effects.test.ts @@ -33,6 +33,7 @@ import type { import type { GameItemMeta } from "@/samples/slay-the-spire-like/system/grid-inventory/types"; import type { ParsedShape } from "@/samples/slay-the-spire-like/system/utils/parse-shape"; import type { Transform2D } from "@/samples/slay-the-spire-like/system/utils/shape-collision"; +import { CardEffect } from "@/samples/slay-the-spire-like/data/desert"; function createRunContext( items: Map>, @@ -63,7 +64,7 @@ function createEffect( id: string, lifecycle: EffectData["lifecycle"], ): EffectData { - return { id, name: id, description: "", lifecycle }; + return { id, name: id, description: "", lifecycle, emoji: "" }; } function createCard( @@ -78,8 +79,8 @@ function createCard( type: "item" as const, costType, costCount, - targetType: "none" as const, - effects: [] as const, + targetType: "player" as const, + effects: [] as CardEffect[], }; } diff --git a/tests/samples/slay-the-spire-like/combat/triggers.test.ts b/tests/samples/slay-the-spire-like/combat/triggers.test.ts index d9b486c..c3375f2 100644 --- a/tests/samples/slay-the-spire-like/combat/triggers.test.ts +++ b/tests/samples/slay-the-spire-like/combat/triggers.test.ts @@ -6,7 +6,6 @@ import { } from "@/core/game"; import { createRegion } from "@/core/region"; import { - createStartWith, createTriggers, Triggers, } from "@/samples/slay-the-spire-like/system/combat/triggers"; @@ -30,6 +29,7 @@ import { GameItemMeta } from "@/samples/slay-the-spire-like/system/grid-inventor import { ParsedShape } from "@/samples/slay-the-spire-like/system/utils/parse-shape"; import { Transform2D } from "@/samples/slay-the-spire-like/system/utils/shape-collision"; import { + CardEffect, getCards, getEffects, getEncounters, @@ -48,7 +48,7 @@ function createEffect( ): EffectData { const found = effects.find((e) => e.id === id); if (found) return found; - return { id, name: id, description: "", lifecycle }; + return { id, name: id, description: "", lifecycle, emoji: "" }; } function createDeckRegions(): DeckRegions { @@ -73,8 +73,8 @@ function createCard( type: "item" as const, costType, costCount, - targetType: "none" as const, - effects: [], + targetType: "player" as const, + effects: [] as CardEffect[], }; return { id, @@ -285,7 +285,7 @@ describe("desert triggers", () => { }), ); const triggers = getTriggers(); - const defendEffect = createEffect("defend", "posture"); + const defendEffect = createEffect("defend", "temporary"); ctx._state.produce((draft) => { draft.player.effects.defend = { data: defendEffect, stacks: 5 }; diff --git a/tests/samples/slay-the-spire-like/deck/factory.test.ts b/tests/samples/slay-the-spire-like/deck/factory.test.ts index c43d443..7d08da3 100644 --- a/tests/samples/slay-the-spire-like/deck/factory.test.ts +++ b/tests/samples/slay-the-spire-like/deck/factory.test.ts @@ -33,7 +33,7 @@ function createTestCardData(id: string, name: string, desc: string): CardData { type: "item", costType: "energy", costCount: 1, - targetType: "single", + targetType: "enemy", effects: [], }; } diff --git a/tests/samples/slay-the-spire-like/grid-inventory.test.ts b/tests/samples/slay-the-spire-like/grid-inventory.test.ts index 5ffecfa..3752716 100644 --- a/tests/samples/slay-the-spire-like/grid-inventory.test.ts +++ b/tests/samples/slay-the-spire-like/grid-inventory.test.ts @@ -12,7 +12,6 @@ import { getItemAtCell, getAdjacentItems, validatePlacement, - type GridInventory, type InventoryItem, } from "@/samples/slay-the-spire-like/system/grid-inventory"; import { createItemIn } from "@/samples/slay-the-spire-like/system/grid-inventory/factory"; @@ -36,7 +35,7 @@ function createTestCardData(id: string, name: string, desc: string): CardData { type: "item", costType: "energy", costCount: 1, - targetType: "single", + targetType: "enemy", effects: [], }; }