679 lines
19 KiB
TypeScript
679 lines
19 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import {
|
|
createGameContext,
|
|
createGameCommandRegistry,
|
|
IGameContext,
|
|
} from "@/core/game";
|
|
import { createRegion } from "@/core/region";
|
|
import {
|
|
createTriggers,
|
|
Triggers,
|
|
} from "@/samples/slay-the-spire-like/system/combat/triggers";
|
|
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 {
|
|
GameCard,
|
|
DeckRegions,
|
|
} from "@/samples/slay-the-spire-like/system/deck";
|
|
import {
|
|
CellKey,
|
|
GridInventory,
|
|
InventoryItem,
|
|
} from "@/samples/slay-the-spire-like/system/grid-inventory/types";
|
|
import { GameItemMeta } from "@/samples/slay-the-spire-like/system/grid-inventory/types";
|
|
import {
|
|
CardEffect,
|
|
getCards,
|
|
getEffects,
|
|
getEnemies,
|
|
} from "@/samples/slay-the-spire-like/data/desert";
|
|
const cards = getCards();
|
|
const effects = getEffects();
|
|
const enemies = getEnemies();
|
|
|
|
function createEffect(
|
|
id: string,
|
|
lifecycle: EffectData["lifecycle"] = "instant",
|
|
): EffectData {
|
|
const found = effects.find((e) => e.id === id);
|
|
if (found) return found;
|
|
return { id, name: id, description: "", lifecycle, emoji: "" };
|
|
}
|
|
|
|
function createDeckRegions(): DeckRegions {
|
|
return {
|
|
drawPile: createRegion("drawPile", []),
|
|
hand: createRegion("hand", []),
|
|
discardPile: createRegion("discardPile", []),
|
|
exhaustPile: createRegion("exhaustPile", []),
|
|
};
|
|
}
|
|
|
|
function createCard(
|
|
id: string,
|
|
itemId: string,
|
|
costType: "energy" | "uses" | "none" = "energy",
|
|
costCount = 0,
|
|
): GameCard {
|
|
const cardData = cards.find((c) => c.id === itemId) ?? {
|
|
id: itemId,
|
|
name: itemId,
|
|
desc: "",
|
|
type: "item" as const,
|
|
costType,
|
|
costCount,
|
|
targetType: "player" as const,
|
|
effects: [] as CardEffect[],
|
|
};
|
|
return {
|
|
id,
|
|
regionId: "",
|
|
position: [0],
|
|
itemId,
|
|
cardData,
|
|
};
|
|
}
|
|
|
|
function createEnemyEntity(
|
|
enemyId: string,
|
|
hp: number,
|
|
maxHp: number,
|
|
instanceIndex = 0,
|
|
): EnemyEntity {
|
|
const enemyData = enemies.find((e) => e.id === enemyId);
|
|
if (!enemyData) throw new Error(`Enemy "${enemyId}" not found`);
|
|
const intent =
|
|
enemyData.intents.find((i) => i.initialIntent) ?? enemyData.intents[0];
|
|
const instanceId = `${enemyId}-${instanceIndex}`;
|
|
const intentMap: Record<string, typeof intent> = {};
|
|
for (const i of enemyData.intents) {
|
|
intentMap[i.id] = i;
|
|
}
|
|
return {
|
|
id: instanceId,
|
|
enemy: enemyData,
|
|
hp,
|
|
maxHp,
|
|
isAlive: true,
|
|
effects: {},
|
|
intents: intentMap,
|
|
currentIntent: intent,
|
|
};
|
|
}
|
|
|
|
function createInventory(
|
|
itemsList: InventoryItem<GameItemMeta>[],
|
|
): GridInventory<GameItemMeta> {
|
|
const map = new Map<string, InventoryItem<GameItemMeta>>();
|
|
const occupied = new Set<CellKey>();
|
|
for (const item of itemsList) {
|
|
map.set(item.id, item);
|
|
occupied.add(`${item.transform.offset.x},${item.transform.offset.y}`);
|
|
}
|
|
return { width: 6, height: 4, items: map, occupiedCells: occupied };
|
|
}
|
|
|
|
function createCombatState(overrides: Partial<CombatState> = {}): CombatState {
|
|
const regions = createDeckRegions();
|
|
return {
|
|
player: {
|
|
id: "player",
|
|
effects: {},
|
|
hp: 30,
|
|
maxHp: 30,
|
|
isAlive: true,
|
|
energy: 3,
|
|
maxEnergy: 3,
|
|
deck: { cards: {}, regions },
|
|
itemEffects: {},
|
|
},
|
|
enemies: [],
|
|
phase: "playerTurn",
|
|
turnNumber: 1,
|
|
result: null,
|
|
loot: [],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function createTestContext(state?: CombatState): IGameContext<CombatState> {
|
|
const registry = createGameCommandRegistry<CombatState>();
|
|
const ctx = createGameContext(registry, state ?? createCombatState());
|
|
ctx._rng.setSeed(42);
|
|
return ctx;
|
|
}
|
|
|
|
function getTriggers(): Triggers {
|
|
const run: IRunContext = {
|
|
getConsumedUses() {
|
|
return 0;
|
|
},
|
|
getItemData() {
|
|
return null;
|
|
},
|
|
*getAdjacentItems() {},
|
|
async setConsumedUsesAsync() {},
|
|
};
|
|
const triggers = createTriggers(run);
|
|
addTriggers(triggers, run);
|
|
return triggers;
|
|
}
|
|
|
|
function addCardToHand(ctx: IGameContext<CombatState>, card: GameCard) {
|
|
ctx._state.produce((draft) => {
|
|
draft.player.deck.cards[card.id] = card;
|
|
card.regionId = "hand";
|
|
draft.player.deck.regions.hand.childIds.push(card.id);
|
|
});
|
|
}
|
|
|
|
function addCardToDrawPile(ctx: IGameContext<CombatState>, card: GameCard) {
|
|
ctx._state.produce((draft) => {
|
|
draft.player.deck.cards[card.id] = card;
|
|
card.regionId = "drawPile";
|
|
draft.player.deck.regions.drawPile.childIds.push(card.id);
|
|
});
|
|
}
|
|
|
|
function addCardToDiscardPile(ctx: IGameContext<CombatState>, card: GameCard) {
|
|
ctx._state.produce((draft) => {
|
|
draft.player.deck.cards[card.id] = card;
|
|
card.regionId = "discardPile";
|
|
draft.player.deck.regions.discardPile.childIds.push(card.id);
|
|
});
|
|
}
|
|
|
|
function makeDummyEnemy() {
|
|
return createEnemyEntity("仙人掌怪", 999, 999);
|
|
}
|
|
|
|
describe("desert triggers", () => {
|
|
describe("instant effects", () => {
|
|
it("should apply attack effect as damage", async () => {
|
|
const ctx = createTestContext(
|
|
createCombatState({
|
|
enemies: [makeDummyEnemy()],
|
|
}),
|
|
);
|
|
const triggers = getTriggers();
|
|
const attackEffect = createEffect("attack");
|
|
|
|
await triggers.onEffectApplied.execute(ctx, {
|
|
effect: attackEffect,
|
|
entityKey: "player",
|
|
stacks: 5,
|
|
sourceEntityKey: "enemy-0",
|
|
});
|
|
|
|
expect(ctx.value.player.hp).toBe(25);
|
|
});
|
|
|
|
it("should apply draw effect", async () => {
|
|
const ctx = createTestContext();
|
|
const triggers = getTriggers();
|
|
const drawEffect = createEffect("draw");
|
|
|
|
addCardToDrawPile(ctx, createCard("card-1", "sword"));
|
|
addCardToDrawPile(ctx, createCard("card-2", "sword"));
|
|
|
|
await triggers.onEffectApplied.execute(ctx, {
|
|
effect: drawEffect,
|
|
entityKey: "player",
|
|
stacks: 2,
|
|
});
|
|
|
|
expect(ctx.value.player.deck.regions.hand.childIds.length).toBe(2);
|
|
expect(ctx.value.player.deck.regions.drawPile.childIds.length).toBe(0);
|
|
});
|
|
|
|
it("should apply gainEnergy effect", async () => {
|
|
const ctx = createTestContext();
|
|
const triggers = getTriggers();
|
|
const gainEnergyEffect = createEffect("gainEnergy");
|
|
|
|
const initialEnergy = ctx.value.player.energy;
|
|
await triggers.onEffectApplied.execute(ctx, {
|
|
effect: gainEnergyEffect,
|
|
entityKey: "player",
|
|
stacks: 2,
|
|
});
|
|
|
|
expect(ctx.value.player.energy).toBe(initialEnergy + 2);
|
|
});
|
|
|
|
it("should remove wound cards from draw and discard piles", async () => {
|
|
const ctx = createTestContext(
|
|
createCombatState({
|
|
enemies: [makeDummyEnemy()],
|
|
}),
|
|
);
|
|
const triggers = getTriggers();
|
|
const removeWoundEffect = createEffect("removeWound");
|
|
|
|
addCardToDrawPile(ctx, createCard("wound-1", "wound", "none", 0));
|
|
addCardToDiscardPile(ctx, createCard("wound-2", "wound", "none", 0));
|
|
addCardToDrawPile(ctx, createCard("sword-1", "sword"));
|
|
|
|
await triggers.onEffectApplied.execute(ctx, {
|
|
effect: removeWoundEffect,
|
|
entityKey: "player",
|
|
stacks: 2,
|
|
});
|
|
|
|
expect(ctx.value.player.deck.cards["wound-1"]).toBeUndefined();
|
|
expect(ctx.value.player.deck.cards["wound-2"]).toBeUndefined();
|
|
expect(ctx.value.player.deck.cards["sword-1"]).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe("damage pipeline", () => {
|
|
it("should prevent damage with block", async () => {
|
|
const ctx = createTestContext(
|
|
createCombatState({
|
|
enemies: [makeDummyEnemy()],
|
|
}),
|
|
);
|
|
const triggers = getTriggers();
|
|
const defendEffect = createEffect("defend", "temporary");
|
|
|
|
ctx._state.produce((draft) => {
|
|
draft.player.effects.defend = { data: defendEffect, stacks: 5 };
|
|
});
|
|
|
|
await triggers.onDamage.execute(ctx, {
|
|
entityKey: "player",
|
|
amount: 8,
|
|
sourceEntityKey: "enemy-0",
|
|
});
|
|
|
|
expect(ctx.value.player.hp).toBe(27);
|
|
expect(ctx.value.player.effects.defend?.stacks).toBe(undefined);
|
|
});
|
|
|
|
it("should reduce damage with damageReduce", async () => {
|
|
const ctx = createTestContext(
|
|
createCombatState({
|
|
enemies: [makeDummyEnemy()],
|
|
}),
|
|
);
|
|
const triggers = getTriggers();
|
|
const damageReduceEffect = createEffect("damageReduce", "temporary");
|
|
|
|
ctx._state.produce((draft) => {
|
|
draft.player.effects.damageReduce = {
|
|
data: damageReduceEffect,
|
|
stacks: 3,
|
|
};
|
|
});
|
|
|
|
await triggers.onDamage.execute(ctx, {
|
|
entityKey: "player",
|
|
amount: 8,
|
|
sourceEntityKey: "enemy-0",
|
|
});
|
|
|
|
expect(ctx.value.player.hp).toBe(25);
|
|
});
|
|
|
|
it("should increase damage with expose", async () => {
|
|
const ctx = createTestContext(
|
|
createCombatState({
|
|
enemies: [makeDummyEnemy()],
|
|
}),
|
|
);
|
|
const triggers = getTriggers();
|
|
const exposeEffect = createEffect("expose", "temporary");
|
|
|
|
ctx._state.produce((draft) => {
|
|
draft.player.effects.expose = { data: exposeEffect, stacks: 2 };
|
|
});
|
|
|
|
await triggers.onDamage.execute(ctx, {
|
|
entityKey: "player",
|
|
amount: 5,
|
|
sourceEntityKey: "enemy-0",
|
|
});
|
|
|
|
expect(ctx.value.player.hp).toBe(23);
|
|
});
|
|
});
|
|
|
|
describe("spike reflection", () => {
|
|
it("should damage attacker when entity has spike", async () => {
|
|
const ctx = createTestContext(
|
|
createCombatState({
|
|
enemies: [createEnemyEntity("仙人掌怪", 12, 12)],
|
|
}),
|
|
);
|
|
const triggers = getTriggers();
|
|
const spikeEffect = createEffect("spike", "permanent");
|
|
|
|
ctx._state.produce((draft) => {
|
|
const enemy = draft.enemies[0];
|
|
enemy.effects.spike = { data: spikeEffect, stacks: 3 };
|
|
});
|
|
|
|
await triggers.onDamage.execute(ctx, {
|
|
entityKey: "仙人掌怪-0",
|
|
amount: 5,
|
|
sourceEntityKey: "player",
|
|
});
|
|
|
|
expect(ctx.value.player.hp).toBe(27);
|
|
});
|
|
});
|
|
|
|
describe("storm static card generation", () => {
|
|
it("should give player static cards when storm enemy executes intent", async () => {
|
|
const ctx = createTestContext(
|
|
createCombatState({
|
|
enemies: [createEnemyEntity("风暴之灵", 30, 30)],
|
|
}),
|
|
);
|
|
const triggers = getTriggers();
|
|
|
|
await triggers.onEnemyIntent.execute(ctx, { enemyId: "风暴之灵-0" });
|
|
|
|
const staticCards = Object.values(ctx.value.player.deck.cards).filter(
|
|
(c: GameCard) => c.itemId === "static",
|
|
);
|
|
expect(staticCards.length).toBe(2);
|
|
});
|
|
});
|
|
|
|
describe("energyDrain", () => {
|
|
it("should drain player energy when energyDrain enemy takes damage", async () => {
|
|
const ctx = createTestContext(
|
|
createCombatState({
|
|
enemies: [createEnemyEntity("幼沙虫", 18, 18)],
|
|
}),
|
|
);
|
|
const triggers = getTriggers();
|
|
const energyDrainEffect = createEffect("energyDrain", "lingering");
|
|
|
|
ctx._state.produce((draft) => {
|
|
const enemy = draft.enemies[0];
|
|
enemy.effects.energyDrain = { data: energyDrainEffect, stacks: 1 };
|
|
});
|
|
|
|
await triggers.onDamage.execute(ctx, {
|
|
entityKey: "幼沙虫-0",
|
|
amount: 5,
|
|
sourceEntityKey: "player",
|
|
});
|
|
|
|
expect(ctx.value.player.energy).toBe(2);
|
|
});
|
|
});
|
|
|
|
describe("molt flee", () => {
|
|
it("should make enemy flee when molt >= maxHp after taking damage", async () => {
|
|
const ctx = createTestContext(
|
|
createCombatState({
|
|
enemies: [createEnemyEntity("蜥蜴", 14, 14)],
|
|
}),
|
|
);
|
|
const triggers = getTriggers();
|
|
const moltEffect = createEffect("molt", "posture");
|
|
|
|
ctx._state.produce((draft) => {
|
|
const enemy = draft.enemies[0];
|
|
enemy.effects.molt = { data: moltEffect, stacks: 14 };
|
|
});
|
|
|
|
let threw = false;
|
|
try {
|
|
await triggers.onDamage.execute(ctx, {
|
|
entityKey: "蜥蜴-0",
|
|
amount: 1,
|
|
sourceEntityKey: "player",
|
|
});
|
|
} catch (e) {
|
|
threw = true;
|
|
}
|
|
|
|
expect(threw).toBe(true);
|
|
expect(ctx.value.result).toBe("victory");
|
|
expect(ctx.value.enemies[0].isAlive).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("discard at turn start", () => {
|
|
it("should randomly discard a card when discard effect is active", async () => {
|
|
const ctx = createTestContext();
|
|
const triggers = getTriggers();
|
|
const discardEffect = createEffect("discard", "lingering");
|
|
|
|
addCardToHand(ctx, createCard("card-1", "sword"));
|
|
addCardToHand(ctx, createCard("card-2", "shield"));
|
|
addCardToHand(ctx, createCard("card-3", "dagger"));
|
|
|
|
ctx._state.produce((draft) => {
|
|
draft.player.effects.discard = { data: discardEffect, stacks: 1 };
|
|
});
|
|
|
|
await triggers.onTurnStart.execute(ctx, { entityKey: "player" });
|
|
|
|
expect(ctx.value.player.deck.regions.hand.childIds.length).toBe(2);
|
|
expect(ctx.value.player.deck.regions.discardPile.childIds.length).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe("next-turn effects", () => {
|
|
it("should gain block from defendNext at turn start", async () => {
|
|
const ctx = createTestContext();
|
|
const triggers = getTriggers();
|
|
const defendNextEffect = createEffect("defendNext", "temporary");
|
|
|
|
ctx._state.produce((draft) => {
|
|
draft.player.effects.defendNext = { data: defendNextEffect, stacks: 5 };
|
|
});
|
|
|
|
await triggers.onTurnStart.execute(ctx, { entityKey: "player" });
|
|
|
|
expect(ctx.value.player.effects.defend?.stacks).toBe(5);
|
|
expect(ctx.value.player.effects.defendNext).toBeUndefined();
|
|
});
|
|
|
|
it("should gain energy from energyNext at turn start", async () => {
|
|
const ctx = createTestContext();
|
|
const triggers = getTriggers();
|
|
const energyNextEffect = createEffect("energyNext", "temporary");
|
|
|
|
ctx._state.produce((draft) => {
|
|
draft.player.effects.energyNext = { data: energyNextEffect, stacks: 2 };
|
|
});
|
|
|
|
await triggers.onTurnStart.execute(ctx, { entityKey: "player" });
|
|
|
|
expect(ctx.value.player.energy).toBe(5);
|
|
expect(ctx.value.player.effects.energyNext).toBeUndefined();
|
|
});
|
|
|
|
it("should draw extra cards from drawNext at turn start", async () => {
|
|
const ctx = createTestContext();
|
|
const triggers = getTriggers();
|
|
const drawNextEffect = createEffect("drawNext", "temporary");
|
|
|
|
addCardToDrawPile(ctx, createCard("card-1", "sword"));
|
|
addCardToDrawPile(ctx, createCard("card-2", "sword"));
|
|
addCardToDrawPile(ctx, createCard("card-3", "sword"));
|
|
|
|
ctx._state.produce((draft) => {
|
|
draft.player.effects.drawNext = { data: drawNextEffect, stacks: 2 };
|
|
});
|
|
|
|
await triggers.onTurnStart.execute(ctx, { entityKey: "player" });
|
|
|
|
expect(ctx.value.player.deck.regions.hand.childIds.length).toBe(2);
|
|
expect(ctx.value.player.effects.drawNext).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("posture damage effects", () => {
|
|
it("should double damage with aim", async () => {
|
|
const ctx = createTestContext(
|
|
createCombatState({
|
|
enemies: [createEnemyEntity("仙人掌怪", 12, 12)],
|
|
}),
|
|
);
|
|
const triggers = getTriggers();
|
|
const aimEffect = createEffect("aim", "posture");
|
|
|
|
ctx._state.produce((draft) => {
|
|
draft.player.effects.aim = { data: aimEffect, stacks: 2 };
|
|
});
|
|
|
|
await triggers.onDamage.execute(ctx, {
|
|
entityKey: "仙人掌怪-0",
|
|
amount: 5,
|
|
sourceEntityKey: "player",
|
|
});
|
|
|
|
expect(ctx.value.enemies[0].hp).toBe(2);
|
|
});
|
|
|
|
it("should add bonus damage with roll", async () => {
|
|
const ctx = createTestContext(
|
|
createCombatState({
|
|
enemies: [createEnemyEntity("仙人掌怪", 99, 99)],
|
|
}),
|
|
);
|
|
const triggers = getTriggers();
|
|
const rollEffect = createEffect("roll", "posture");
|
|
|
|
ctx._state.produce((draft) => {
|
|
draft.player.effects.roll = { data: rollEffect, stacks: 20 };
|
|
});
|
|
|
|
await triggers.onDamage.execute(ctx, {
|
|
entityKey: "仙人掌怪-0",
|
|
amount: 5,
|
|
sourceEntityKey: "player",
|
|
});
|
|
|
|
expect(ctx.value.enemies[0].hp).toBe(74);
|
|
expect(ctx.value.player.effects.roll).toBeUndefined();
|
|
});
|
|
|
|
it("should add bonus damage with tailSting", async () => {
|
|
const ctx = createTestContext(
|
|
createCombatState({
|
|
enemies: [createEnemyEntity("沙蝎", 10, 10)],
|
|
}),
|
|
);
|
|
const triggers = getTriggers();
|
|
const tailStingEffect = createEffect("tailSting", "posture");
|
|
|
|
ctx._state.produce((draft) => {
|
|
const enemy = draft.enemies[0];
|
|
enemy.effects.tailSting = { data: tailStingEffect, stacks: 2 };
|
|
});
|
|
|
|
await triggers.onDamage.execute(ctx, {
|
|
entityKey: "player",
|
|
amount: 5,
|
|
sourceEntityKey: "沙蝎-0",
|
|
});
|
|
|
|
expect(ctx.value.player.hp).toBe(23);
|
|
});
|
|
|
|
it("should double damage with charge on attacker", async () => {
|
|
const ctx = createTestContext(
|
|
createCombatState({
|
|
enemies: [createEnemyEntity("骑马枪手", 25, 25)],
|
|
}),
|
|
);
|
|
const triggers = getTriggers();
|
|
const chargeEffect = createEffect("charge", "lingering");
|
|
|
|
ctx._state.produce((draft) => {
|
|
const enemy = draft.enemies[0];
|
|
enemy.effects.charge = { data: chargeEffect, stacks: 2 };
|
|
});
|
|
|
|
await triggers.onDamage.execute(ctx, {
|
|
entityKey: "player",
|
|
amount: 5,
|
|
sourceEntityKey: "骑马枪手-0",
|
|
});
|
|
|
|
expect(ctx.value.player.hp).toBe(20);
|
|
});
|
|
});
|
|
|
|
describe("crossbow chain", () => {
|
|
it("should replay other crossbows on same target", async () => {
|
|
const ctx = createTestContext(
|
|
createCombatState({
|
|
enemies: [createEnemyEntity("仙人掌怪", 20, 20)],
|
|
}),
|
|
);
|
|
const triggers = getTriggers();
|
|
const crossbowEffect = createEffect("crossbow");
|
|
|
|
addCardToHand(ctx, createCard("crossbow-1", "crossbow"));
|
|
addCardToHand(ctx, createCard("crossbow-2", "crossbow"));
|
|
|
|
await triggers.onEffectApplied.execute(ctx, {
|
|
effect: crossbowEffect,
|
|
entityKey: "player",
|
|
stacks: 0,
|
|
cardId: "crossbow-1",
|
|
sourceEntityKey: "player",
|
|
targetId: "仙人掌怪-0",
|
|
});
|
|
|
|
expect(ctx.value.enemies[0].hp).toBe(8);
|
|
});
|
|
});
|
|
|
|
describe("sandwormKing fatigue heal", () => {
|
|
it("should heal sandworm king when player discards fatigue", async () => {
|
|
const ctx = createTestContext(
|
|
createCombatState({
|
|
enemies: [createEnemyEntity("沙虫王", 30, 40)],
|
|
}),
|
|
);
|
|
const triggers = getTriggers();
|
|
|
|
addCardToHand(ctx, createCard("fatigue-1", "fatigue", "none", 0));
|
|
|
|
await triggers.onCardDiscarded.execute(ctx, {
|
|
cardId: "fatigue-1",
|
|
sourceEntityKey: "player",
|
|
});
|
|
|
|
expect(ctx.value.enemies[0].hp).toBe(40);
|
|
});
|
|
});
|
|
|
|
describe("vulture on-damage", () => {
|
|
it("should give player vultureEye when vulture deals damage", async () => {
|
|
const ctx = createTestContext(
|
|
createCombatState({
|
|
enemies: [createEnemyEntity("秃鹫", 12, 12)],
|
|
}),
|
|
);
|
|
const triggers = getTriggers();
|
|
|
|
await triggers.onDamage.execute(ctx, {
|
|
entityKey: "player",
|
|
amount: 5,
|
|
sourceEntityKey: "秃鹫-0",
|
|
});
|
|
|
|
const vultureEyeCards = Object.values(ctx.value.player.deck.cards).filter(
|
|
(c: GameCard) => c.itemId === "vultureEye",
|
|
);
|
|
expect(vultureEyeCards.length).toBe(1);
|
|
});
|
|
});
|
|
});
|