255 lines
10 KiB
TypeScript
255 lines
10 KiB
TypeScript
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();
|
|
});
|
|
});
|
|
});
|