From 5e172c61bbd70de6a18bca446116fe73d80a194b Mon Sep 17 00:00:00 2001 From: hypercross Date: Mon, 20 Apr 2026 11:25:52 +0800 Subject: [PATCH] refactor(slay-the-spire-like): use IRunContext in combat system Update the combat system to use `IRunContext` instead of passing raw inventory objects. This provides a cleaner abstraction for accessing item data and managing consumed uses. - Update `canPlayCard` and `payCardCost` to use `IRunContext` - Pass `IRunContext` through the trigger registration chain - Fix logic in `canPlayCard` for "uses" cost validation - Update combat effect tests to use a mock `IRunContext` --- .../data/desert/triggers/effect.ts | 11 +-- .../system/combat/effects.ts | 4 +- .../combat/effects.test.ts | 73 ++++++++++++++----- .../combat/triggers.test.ts | 16 +++- 4 files changed, 77 insertions(+), 27 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 b6ef083..d94e69a 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,12 +1,13 @@ +import { IRunContext } from "@/samples/slay-the-spire-like/system/combat/types"; import { Triggers } from "@/samples/slay-the-spire-like/system/combat/triggers"; import { addInstantEffectTriggers } from "./instant"; import { addDamageTriggers } from "./damage"; import { addTurnStartTriggers } from "./turn-start"; import { addCardEventTriggers } from "./card-events"; -export function addDesertTriggers(triggers: Triggers) { - addInstantEffectTriggers(triggers); - addDamageTriggers(triggers); - addTurnStartTriggers(triggers); - addCardEventTriggers(triggers); +export function addDesertTriggers(triggers: Triggers, run: IRunContext) { + addInstantEffectTriggers(triggers); + addDamageTriggers(triggers); + addTurnStartTriggers(triggers); + addCardEventTriggers(triggers, run); } 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 b50c95f..0d4be00 100644 --- a/src/samples/slay-the-spire-like/system/combat/effects.ts +++ b/src/samples/slay-the-spire-like/system/combat/effects.ts @@ -145,9 +145,9 @@ export function canPlayCard( if (costType === "uses") { const item = run.getItemData(itemId); if (!item) return false; - const maxUses = item?.card.costType === "energy" ? item.card.costCount : 0; + const maxUses = item?.card.costType === "uses" ? item.card.costCount : 0; const consumed = run.getConsumedUses(itemId); - return consumed + costCount <= maxUses; + return consumed < maxUses; } return true; } 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 445cae9..e03ef83 100644 --- a/tests/samples/slay-the-spire-like/combat/effects.test.ts +++ b/tests/samples/slay-the-spire-like/combat/effects.test.ts @@ -17,10 +17,14 @@ import type { CombatEntity, CombatState, EffectTable, + IRunContext, PlayerEntity, EnemyEntity, } from "@/samples/slay-the-spire-like/system/combat/types"; -import type { EffectData } from "@/samples/slay-the-spire-like/system/types"; +import type { + EffectData, + ItemData, +} from "@/samples/slay-the-spire-like/system/types"; import type { CellKey, GridInventory, @@ -30,6 +34,31 @@ import type { GameItemMeta } from "@/samples/slay-the-spire-like/system/progress 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"; +function createRunContext( + items: Map>, +): IRunContext { + return { + getItemData(id: string): ItemData | null { + const item = items.get(id); + return item?.meta.itemData ?? null; + }, + getNeighborItems(_id: string): Iterable { + return []; + }, + getConsumedUses(id: string): number { + const item = items.get(id); + return item?.meta.consumedUses ?? 0; + }, + setConsumedUsesAsync(id: string, uses: number): Promise { + const item = items.get(id); + if (item?.meta) { + item.meta.consumedUses = uses; + } + return Promise.resolve(); + }, + }; +} + function createEffect( id: string, lifecycle: EffectData["lifecycle"], @@ -517,8 +546,9 @@ describe("combat/effects", () => { const player = createPlayerEntity(); player.energy = 3; const inventory = createInventory([]); + const run = createRunContext(inventory.items); - const result = canPlayCard(player, "energy", 2, "any", inventory); + const result = canPlayCard(player, "energy", 2, "any", run); expect(result).toBe(true); }); @@ -527,8 +557,9 @@ describe("combat/effects", () => { const player = createPlayerEntity(); player.energy = 1; const inventory = createInventory([]); + const run = createRunContext(inventory.items); - const result = canPlayCard(player, "energy", 2, "any", inventory); + const result = canPlayCard(player, "energy", 2, "any", run); expect(result).toBe(false); }); @@ -537,8 +568,9 @@ describe("combat/effects", () => { const player = createPlayerEntity(); const item = createItem("potion-1", "potion-card", "uses", 3, 1); const inventory = createInventory([item]); + const run = createRunContext(inventory.items); - const result = canPlayCard(player, "uses", 3, "potion-1", inventory); + const result = canPlayCard(player, "uses", 3, "potion-1", run); expect(result).toBe(true); }); @@ -547,8 +579,9 @@ describe("combat/effects", () => { const player = createPlayerEntity(); const item = createItem("potion-1", "potion-card", "uses", 3, 3); const inventory = createInventory([item]); + const run = createRunContext(inventory.items); - const result = canPlayCard(player, "uses", 3, "potion-1", inventory); + const result = canPlayCard(player, "uses", 3, "potion-1", run); expect(result).toBe(false); }); @@ -556,8 +589,9 @@ describe("combat/effects", () => { it("should reject playing uses card when item not in inventory", () => { const player = createPlayerEntity(); const inventory = createInventory([]); + const run = createRunContext(inventory.items); - const result = canPlayCard(player, "uses", 1, "missing", inventory); + const result = canPlayCard(player, "uses", 1, "missing", run); expect(result).toBe(false); }); @@ -566,51 +600,56 @@ describe("combat/effects", () => { const player = createPlayerEntity(); player.energy = 0; const inventory = createInventory([]); + const run = createRunContext(inventory.items); - const result = canPlayCard(player, "none", 0, "any", inventory); + const result = canPlayCard(player, "none", 0, "any", run); expect(result).toBe(true); }); }); describe("payCardCost", () => { - it("should deduct energy for energy cost card", () => { + it("should deduct energy for energy cost card", async () => { const player = createPlayerEntity(); player.energy = 3; const inventory = createInventory([]); + const run = createRunContext(inventory.items); - payCardCost(player, "energy", 2, "any", inventory); + await payCardCost(player, "energy", 2, "any", run); expect(player.energy).toBe(1); }); - it("should increment depletion for uses cost card", () => { + it("should increment depletion for uses cost card", async () => { const player = createPlayerEntity(); const item = createItem("potion-1", "potion-card", "uses", 3, 1); const inventory = createInventory([item]); + const run = createRunContext(inventory.items); - payCardCost(player, "uses", 3, "potion-1", inventory); + await payCardCost(player, "uses", 3, "potion-1", run); expect(item.meta?.consumedUses).toBe(4); }); - it("should do nothing for none cost card", () => { + it("should do nothing for none cost card", async () => { const player = createPlayerEntity(); player.energy = 3; const inventory = createInventory([]); + const run = createRunContext(inventory.items); - payCardCost(player, "none", 0, "any", inventory); + await payCardCost(player, "none", 0, "any", run); expect(player.energy).toBe(3); }); - it("should handle missing item gracefully for uses cost", () => { + it("should handle missing item gracefully for uses cost", async () => { const player = createPlayerEntity(); const inventory = createInventory([]); + const run = createRunContext(inventory.items); - expect(() => - payCardCost(player, "uses", 1, "missing", inventory), - ).not.toThrow(); + await expect( + payCardCost(player, "uses", 1, "missing", run), + ).resolves.not.toThrow(); }); }); }); 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 e7815f2..54a4b22 100644 --- a/tests/samples/slay-the-spire-like/combat/triggers.test.ts +++ b/tests/samples/slay-the-spire-like/combat/triggers.test.ts @@ -14,6 +14,7 @@ import { addTriggers } from "@/samples/slay-the-spire-like/data/desert/triggers" import { CombatState, EnemyEntity, + IRunContext, } from "@/samples/slay-the-spire-like/system/combat/types"; import { EffectData } from "@/samples/slay-the-spire-like/system/types"; import { @@ -138,7 +139,6 @@ function createCombatState(overrides: Partial = {}): CombatState { itemEffects: {}, }, enemies: [], - inventory: createInventory([]), phase: "playerTurn", turnNumber: 1, result: null, @@ -155,8 +155,18 @@ function createTestContext(state?: CombatState): IGameContext { } function getTriggers(): Triggers { - const triggers = createTriggers(); - addTriggers(triggers); + const run: IRunContext = { + getConsumedUses() { + return 0; + }, + getItemData() { + return null; + }, + *getNeighborItems() {}, + async setConsumedUsesAsync() {}, + }; + const triggers = createTriggers(run); + addTriggers(triggers, run); return triggers; }