chore: tests for desert triggers
This commit is contained in:
parent
0222b66bed
commit
4f5747d81d
|
|
@ -228,22 +228,21 @@ export function addEffectTriggers(triggers: Triggers) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ========== storm: give static card to player on attack ==========
|
// ========== storm: give static card to player when storm enemy attacks ==========
|
||||||
triggers.onDamage.use(async (ctx, next) => {
|
triggers.onEnemyIntent.use(async (ctx, next) => {
|
||||||
await next();
|
await next();
|
||||||
|
|
||||||
if (ctx.amount - (ctx.prevented ?? 0) <= 0) return;
|
const enemy = getCombatEntity(ctx.game.value, ctx.enemyId);
|
||||||
|
if (!enemy || !enemy.isAlive) return;
|
||||||
|
|
||||||
const entity = getCombatEntity(ctx.game.value, ctx.entityKey);
|
const storm = enemy.effects.storm?.stacks ?? 0;
|
||||||
if (!entity || !entity.isAlive) return;
|
if (storm > 0) {
|
||||||
|
|
||||||
const storm = entity.effects.storm?.stacks ?? 0;
|
|
||||||
if (storm > 0 && ctx.entityKey !== "player") {
|
|
||||||
for (let i = 0; i < storm; i++) {
|
for (let i = 0; i < storm; i++) {
|
||||||
await triggers.onEffectApplied.execute(ctx.game, {
|
await triggers.onEffectApplied.execute(ctx.game, {
|
||||||
effect: findEffect(ctx.game, "static"),
|
effect: findEffect(ctx.game, "static"),
|
||||||
entityKey: "player",
|
entityKey: "player",
|
||||||
stacks: 1,
|
stacks: 1,
|
||||||
|
sourceEntityKey: ctx.enemyId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -269,10 +268,11 @@ export function addEffectTriggers(triggers: Triggers) {
|
||||||
|
|
||||||
// ========== molt: enemy flees if molt >= maxHp ==========
|
// ========== molt: enemy flees if molt >= maxHp ==========
|
||||||
triggers.onDamage.use(async (ctx, next) => {
|
triggers.onDamage.use(async (ctx, next) => {
|
||||||
await next();
|
|
||||||
|
|
||||||
const entity = getCombatEntity(ctx.game.value, ctx.entityKey);
|
const entity = getCombatEntity(ctx.game.value, ctx.entityKey);
|
||||||
if (!entity || !entity.isAlive) return;
|
if (!entity || !entity.isAlive) {
|
||||||
|
await next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const molt = entity.effects.molt?.stacks ?? 0;
|
const molt = entity.effects.molt?.stacks ?? 0;
|
||||||
if (molt >= entity.maxHp) {
|
if (molt >= entity.maxHp) {
|
||||||
|
|
@ -285,69 +285,84 @@ export function addEffectTriggers(triggers: Triggers) {
|
||||||
draft.result = draft.enemies.every(en => !en.isAlive) ? "victory" : null;
|
draft.result = draft.enemies.every(en => !en.isAlive) ? "victory" : null;
|
||||||
});
|
});
|
||||||
if (ctx.game.value.result) throw ctx.game.value;
|
if (ctx.game.value.result) throw ctx.game.value;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await next();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ========== discard: random discard at turn start ==========
|
// ========== discard: random discard at turn start ==========
|
||||||
triggers.onTurnStart.use(async (ctx, next) => {
|
triggers.onTurnStart.use(async (ctx, next) => {
|
||||||
await next();
|
if (ctx.entityKey !== "player") {
|
||||||
|
await next();
|
||||||
if (ctx.entityKey !== "player") return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const discard = ctx.game.value.player.effects.discard;
|
const discard = ctx.game.value.player.effects.discard;
|
||||||
if (!discard || discard.stacks <= 0) return;
|
if (discard && discard.stacks > 0) {
|
||||||
|
const handIds = [...ctx.game.value.player.deck.regions.hand.childIds];
|
||||||
|
if (handIds.length > 0) {
|
||||||
|
const randomIndex = ctx.game.rng.nextInt(handIds.length);
|
||||||
|
const randomCardId = handIds[randomIndex];
|
||||||
|
await triggers.onCardDiscarded.execute(ctx.game, { cardId: randomCardId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handIds = [...ctx.game.value.player.deck.regions.hand.childIds];
|
await next();
|
||||||
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 ==========
|
// ========== defendNext: gain block next turn ==========
|
||||||
triggers.onTurnStart.use(async (ctx, next) => {
|
triggers.onTurnStart.use(async (ctx, next) => {
|
||||||
await next();
|
if (ctx.entityKey !== "player") {
|
||||||
|
await next();
|
||||||
if (ctx.entityKey !== "player") return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const defendNext = ctx.game.value.player.effects.defendNext;
|
const defendNext = ctx.game.value.player.effects.defendNext;
|
||||||
if (!defendNext || defendNext.stacks <= 0) return;
|
if (defendNext && defendNext.stacks > 0) {
|
||||||
|
await ctx.game.produceAsync(draft => {
|
||||||
|
addEntityEffect(draft.player, findEffect(ctx.game, "defend"), defendNext.stacks);
|
||||||
|
addEntityEffect(draft.player, defendNext.data, -defendNext.stacks);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await ctx.game.produceAsync(draft => {
|
await next();
|
||||||
addEntityEffect(draft.player, findEffect(ctx.game, "defend"), defendNext.stacks);
|
|
||||||
addEntityEffect(draft.player, defendNext.data, -defendNext.stacks);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ========== energyNext: gain energy next turn ==========
|
// ========== energyNext: gain energy next turn ==========
|
||||||
triggers.onTurnStart.use(async (ctx, next) => {
|
triggers.onTurnStart.use(async (ctx, next) => {
|
||||||
await next();
|
if (ctx.entityKey !== "player") {
|
||||||
|
await next();
|
||||||
if (ctx.entityKey !== "player") return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const energyNext = ctx.game.value.player.effects.energyNext;
|
const energyNext = ctx.game.value.player.effects.energyNext;
|
||||||
if (!energyNext || energyNext.stacks <= 0) return;
|
if (energyNext && energyNext.stacks > 0) {
|
||||||
|
await ctx.game.produceAsync(draft => {
|
||||||
|
draft.player.energy += energyNext.stacks;
|
||||||
|
addEntityEffect(draft.player, energyNext.data, -energyNext.stacks);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await ctx.game.produceAsync(draft => {
|
await next();
|
||||||
draft.player.energy += energyNext.stacks;
|
|
||||||
addEntityEffect(draft.player, energyNext.data, -energyNext.stacks);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ========== drawNext: draw extra cards next turn ==========
|
// ========== drawNext: draw extra cards next turn ==========
|
||||||
triggers.onTurnStart.use(async (ctx, next) => {
|
triggers.onTurnStart.use(async (ctx, next) => {
|
||||||
await next();
|
if (ctx.entityKey !== "player") {
|
||||||
|
await next();
|
||||||
if (ctx.entityKey !== "player") return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const drawNext = ctx.game.value.player.effects.drawNext;
|
const drawNext = ctx.game.value.player.effects.drawNext;
|
||||||
if (!drawNext || drawNext.stacks <= 0) return;
|
if (drawNext && drawNext.stacks > 0) {
|
||||||
|
await ctx.game.produceAsync(draft => {
|
||||||
|
addEntityEffect(draft.player, drawNext.data, -drawNext.stacks);
|
||||||
|
});
|
||||||
|
await triggers.onDraw.execute(ctx.game, { count: drawNext.stacks });
|
||||||
|
}
|
||||||
|
|
||||||
await ctx.game.produceAsync(draft => {
|
await next();
|
||||||
addEntityEffect(draft.player, drawNext.data, -drawNext.stacks);
|
|
||||||
});
|
|
||||||
await triggers.onDraw.execute(ctx.game, { count: drawNext.stacks });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ========== aim: double damage, lose aim on damage ==========
|
// ========== aim: double damage, lose aim on damage ==========
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,592 @@
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { createGameContext, createGameCommandRegistry, IGameContext } from '@/core/game';
|
||||||
|
import { createRegion } from '@/core/region';
|
||||||
|
import { createStartWith, Triggers } from '@/samples/slay-the-spire-like/system/combat/triggers';
|
||||||
|
import { addTriggers } from '@/samples/slay-the-spire-like/data/desert/triggers';
|
||||||
|
import { CombatState, EnemyEntity } 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 { GridInventory, InventoryItem } from '@/samples/slay-the-spire-like/system/grid-inventory/types';
|
||||||
|
import { GameItemMeta } from '@/samples/slay-the-spire-like/system/progress/types';
|
||||||
|
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 { cards, effects, enemies, items } from '@/samples/slay-the-spire-like/data/desert';
|
||||||
|
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
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: 'none' as const, effects: [],
|
||||||
|
};
|
||||||
|
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<string>();
|
||||||
|
for (const item of itemsList) {
|
||||||
|
map.set(item.id, item);
|
||||||
|
occupied.add(`${item.transform.x},${item.transform.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: [],
|
||||||
|
inventory: createInventory([]),
|
||||||
|
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 {
|
||||||
|
let capturedTriggers: Triggers;
|
||||||
|
createStartWith(triggers => {
|
||||||
|
capturedTriggers = triggers;
|
||||||
|
addTriggers(triggers);
|
||||||
|
});
|
||||||
|
return capturedTriggers!;
|
||||||
|
}
|
||||||
|
|
||||||
|
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', 'posture');
|
||||||
|
|
||||||
|
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(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
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();
|
||||||
|
const stormEffect = createEffect('storm', 'permanent');
|
||||||
|
|
||||||
|
ctx._state.produce(draft => {
|
||||||
|
const enemy = draft.enemies[0];
|
||||||
|
enemy.effects.storm = { data: stormEffect, stacks: 2 };
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue