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 { const inv = createGridInventory(6, 4); const meta1 = createTestMeta('短刀', 'oe'); const item1: InventoryItem = { 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(); }); }); });