boardgame-core/tests/samples/slay-the-spire-like/combat/triggers.test.ts

255 lines
10 KiB
TypeScript
Raw Normal View History

import { describe, it, expect } from 'vitest';
import {
createCombatTriggerRegistry,
dispatchTrigger,
dispatchAttackedTrigger,
dispatchDamageTrigger,
dispatchOutgoingDamageTrigger,
dispatchIncomingDamageTrigger,
} from '@/samples/slay-the-spire-like/combat/triggers';
import type { TriggerContext, CombatTriggerRegistry } from '@/samples/slay-the-spire-like/combat/triggers';
import type { CombatState } from '@/samples/slay-the-spire-like/combat/types';
import { createCombatState } from '@/samples/slay-the-spire-like/combat/state';
import { createGridInventory, placeItem } from '@/samples/slay-the-spire-like/grid-inventory';
import type { GridInventory, InventoryItem } from '@/samples/slay-the-spire-like/grid-inventory';
import type { GameItemMeta, PlayerState } from '@/samples/slay-the-spire-like/progress/types';
import { IDENTITY_TRANSFORM } from '@/samples/slay-the-spire-like/utils/shape-collision';
import { parseShapeString } from '@/samples/slay-the-spire-like/utils/parse-shape';
import { encounterDesertData, enemyDesertData } from '@/samples/slay-the-spire-like/data';
import { Mulberry32RNG } from '@/utils/rng';
function createTestMeta(name: string, shapeStr: string): GameItemMeta {
const shape = parseShapeString(shapeStr);
return {
itemData: {
type: 'weapon',
name,
shape: shapeStr,
costType: 'energy',
costCount: 1,
targetType: 'single',
price: 10,
desc: '测试',
},
shape,
};
}
function createTestInventory(): GridInventory<GameItemMeta> {
const inv = createGridInventory<GameItemMeta>(6, 4);
const meta1 = createTestMeta('短刀', 'oe');
const item1: InventoryItem<GameItemMeta> = {
id: 'item-1',
shape: meta1.shape,
transform: { ...IDENTITY_TRANSFORM, offset: { x: 0, y: 0 } },
meta: meta1,
};
placeItem(inv, item1);
return inv;
}
function createTestCombatState(): CombatState {
const inv = createTestInventory();
const playerState: PlayerState = { maxHp: 50, currentHp: 50, gold: 0 };
const encounter = encounterDesertData.find(e => e.name === '仙人掌怪')!;
return createCombatState(playerState, inv, encounter);
}
function createTestTriggerCtx(state: CombatState): TriggerContext {
return { state, rng: new Mulberry32RNG(42) };
}
describe('combat/triggers', () => {
describe('createCombatTriggerRegistry', () => {
it('should create registry with desert zone triggers', () => {
const registry = createCombatTriggerRegistry();
expect(registry['spike']).toBeDefined();
expect(registry['aim']).toBeDefined();
expect(registry['charge']).toBeDefined();
expect(registry['roll']).toBeDefined();
expect(registry['tailSting']).toBeDefined();
expect(registry['energyDrain']).toBeDefined();
expect(registry['molt']).toBeDefined();
expect(registry['storm']).toBeDefined();
expect(registry['vultureEye']).toBeDefined();
expect(registry['venom']).toBeDefined();
expect(registry['static']).toBeDefined();
expect(registry['curse']).toBeDefined();
expect(registry['discard']).toBeDefined();
});
});
describe('spike trigger', () => {
it('should deal damage to attacker when enemy is attacked', () => {
const state = createTestCombatState();
const registry = createCombatTriggerRegistry();
const ctx = createTestTriggerCtx(state);
const enemyId = state.enemyOrder[0];
state.enemies[enemyId].buffs['spike'] = 2;
const initialPlayerHp = state.player.hp;
dispatchAttackedTrigger(ctx, 'player', enemyId, 5, registry);
expect(state.player.hp).toBeLessThan(initialPlayerHp);
});
});
describe('aim trigger', () => {
it('should double outgoing damage with aim stacks', () => {
const state = createTestCombatState();
const registry = createCombatTriggerRegistry();
const ctx = createTestTriggerCtx(state);
const enemyId = state.enemyOrder[0];
state.enemies[enemyId].buffs['aim'] = 3;
const modified = dispatchOutgoingDamageTrigger(ctx, enemyId, 5, registry);
expect(modified).toBe(10);
});
it('should not double damage with 0 aim stacks', () => {
const state = createTestCombatState();
const registry = createCombatTriggerRegistry();
const ctx = createTestTriggerCtx(state);
const enemyId = state.enemyOrder[0];
state.enemies[enemyId].buffs['aim'] = 0;
const modified = dispatchOutgoingDamageTrigger(ctx, enemyId, 5, registry);
expect(modified).toBe(5);
});
it('should lose aim stacks on damage', () => {
const state = createTestCombatState();
const registry = createCombatTriggerRegistry();
const ctx = createTestTriggerCtx(state);
const enemyId = state.enemyOrder[0];
state.enemies[enemyId].buffs['aim'] = 5;
dispatchDamageTrigger(ctx, enemyId, 3, registry);
expect(state.enemies[enemyId].buffs['aim']).toBe(2);
});
});
describe('charge trigger', () => {
it('should double outgoing and incoming damage', () => {
const state = createTestCombatState();
const registry = createCombatTriggerRegistry();
const ctx = createTestTriggerCtx(state);
const enemyId = state.enemyOrder[0];
state.enemies[enemyId].buffs['charge'] = 2;
const outDmg = dispatchOutgoingDamageTrigger(ctx, enemyId, 6, registry);
expect(outDmg).toBe(12);
const inDmg = dispatchIncomingDamageTrigger(ctx, enemyId, 6, registry);
expect(inDmg).toBe(12);
});
it('should lose charge stacks on damage', () => {
const state = createTestCombatState();
const registry = createCombatTriggerRegistry();
const ctx = createTestTriggerCtx(state);
const enemyId = state.enemyOrder[0];
state.enemies[enemyId].buffs['charge'] = 5;
dispatchDamageTrigger(ctx, enemyId, 3, registry);
expect(state.enemies[enemyId].buffs['charge']).toBe(2);
});
});
describe('roll trigger', () => {
it('should increase damage when roll >= 10', () => {
const state = createTestCombatState();
const registry = createCombatTriggerRegistry();
const ctx = createTestTriggerCtx(state);
const enemyId = state.enemyOrder[0];
state.enemies[enemyId].buffs['roll'] = 20;
const modified = dispatchOutgoingDamageTrigger(ctx, enemyId, 5, registry);
expect(modified).toBe(7);
expect(state.enemies[enemyId].buffs['roll']).toBe(0);
});
it('should not modify damage when roll < 10', () => {
const state = createTestCombatState();
const registry = createCombatTriggerRegistry();
const ctx = createTestTriggerCtx(state);
const enemyId = state.enemyOrder[0];
state.enemies[enemyId].buffs['roll'] = 5;
const modified = dispatchOutgoingDamageTrigger(ctx, enemyId, 5, registry);
expect(modified).toBe(5);
});
});
describe('tailSting trigger', () => {
it('should deal damage to player at turn end', () => {
const state = createTestCombatState();
const registry = createCombatTriggerRegistry();
const ctx = createTestTriggerCtx(state);
const enemyId = state.enemyOrder[0];
state.enemies[enemyId].buffs['tailSting'] = 3;
const initialHp = state.player.hp;
dispatchTrigger(ctx, 'onTurnEnd', enemyId, registry);
expect(state.player.hp).toBeLessThan(initialHp);
});
});
describe('static trigger', () => {
it('should increase incoming damage to player', () => {
const state = createTestCombatState();
const registry = createCombatTriggerRegistry();
const ctx = createTestTriggerCtx(state);
state.player.buffs['static'] = 2;
const modified = dispatchIncomingDamageTrigger(ctx, 'player', 5, registry);
expect(modified).toBe(7);
});
});
describe('molt trigger', () => {
it('should cause enemy to flee when molt stacks >= maxHp', () => {
const state = createTestCombatState();
const registry = createCombatTriggerRegistry();
const ctx = createTestTriggerCtx(state);
const enemyId = state.enemyOrder[0];
state.enemies[enemyId].buffs['molt'] = state.enemies[enemyId].maxHp;
dispatchDamageTrigger(ctx, enemyId, 1, registry);
expect(state.enemies[enemyId].isAlive).toBe(false);
expect(state.result).toBe('fled');
});
it('should not cause flee when molt stacks < maxHp', () => {
const state = createTestCombatState();
const registry = createCombatTriggerRegistry();
const ctx = createTestTriggerCtx(state);
const enemyId = state.enemyOrder[0];
state.enemies[enemyId].buffs['molt'] = 1;
dispatchDamageTrigger(ctx, enemyId, 1, registry);
expect(state.enemies[enemyId].isAlive).toBe(true);
});
});
describe('dispatchTrigger with missing handler', () => {
it('should be a no-op for unknown buff', () => {
const state = createTestCombatState();
const registry = createCombatTriggerRegistry();
const ctx = createTestTriggerCtx(state);
const enemyId = state.enemyOrder[0];
state.enemies[enemyId].buffs['nonexistentBuff'] = 5;
expect(() => {
dispatchTrigger(ctx, 'onTurnStart', enemyId, registry);
}).not.toThrow();
});
});
});