chore: remove old tests
This commit is contained in:
@@ -1,344 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
applyDamage,
|
||||
applyDefend,
|
||||
applyBuff,
|
||||
removeBuff,
|
||||
updateBuffs,
|
||||
canPlayCard,
|
||||
playCard,
|
||||
areAllEnemiesDead,
|
||||
isPlayerDead,
|
||||
getModifiedAttackDamage,
|
||||
getModifiedDefendAmount,
|
||||
} from '@/samples/slay-the-spire-like/combat/effects';
|
||||
import type { CombatState, PlayerCombatState, EnemyState } from '@/samples/slay-the-spire-like/combat/types';
|
||||
import {
|
||||
createCombatState,
|
||||
createEnemyInstance,
|
||||
createPlayerCombatState,
|
||||
drawCardsToHand,
|
||||
} 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 { enemyDesertData, encounterDesertData } 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 createSimpleRng() {
|
||||
return new Mulberry32RNG(42);
|
||||
}
|
||||
|
||||
describe('combat/effects', () => {
|
||||
describe('applyDamage', () => {
|
||||
it('should deal damage to player', () => {
|
||||
const state = createTestCombatState();
|
||||
applyDamage(state, 'player', 10);
|
||||
|
||||
expect(state.player.hp).toBe(40);
|
||||
expect(state.player.damageTakenThisTurn).toBe(10);
|
||||
expect(state.player.damagedThisTurn).toBe(true);
|
||||
});
|
||||
|
||||
it('should deal damage to enemy', () => {
|
||||
const state = createTestCombatState();
|
||||
const enemyId = state.enemyOrder[0];
|
||||
const enemy = state.enemies[enemyId];
|
||||
const initialHp = enemy.hp;
|
||||
|
||||
applyDamage(state, enemyId, 5);
|
||||
|
||||
expect(enemy.hp).toBe(initialHp - 5);
|
||||
});
|
||||
|
||||
it('should be absorbed by defend buff on player', () => {
|
||||
const state = createTestCombatState();
|
||||
state.player.buffs['defend'] = 3;
|
||||
|
||||
const result = applyDamage(state, 'player', 5);
|
||||
|
||||
expect(result.blockedByDefend).toBe(3);
|
||||
expect(result.damageDealt).toBe(2);
|
||||
expect(state.player.hp).toBe(48);
|
||||
});
|
||||
|
||||
it('should be fully absorbed by defend buff', () => {
|
||||
const state = createTestCombatState();
|
||||
state.player.buffs['defend'] = 10;
|
||||
|
||||
applyDamage(state, 'player', 5);
|
||||
|
||||
expect(state.player.hp).toBe(50);
|
||||
expect(state.player.buffs['defend']).toBe(5);
|
||||
});
|
||||
|
||||
it('should be absorbed by defend buff on enemy', () => {
|
||||
const state = createTestCombatState();
|
||||
const enemyId = state.enemyOrder[0];
|
||||
state.enemies[enemyId].buffs['defend'] = 4;
|
||||
|
||||
const result = applyDamage(state, enemyId, 6);
|
||||
|
||||
expect(result.blockedByDefend).toBe(4);
|
||||
expect(result.damageDealt).toBe(2);
|
||||
expect(state.enemies[enemyId].hp).toBe(state.enemies[enemyId].maxHp - 2);
|
||||
});
|
||||
|
||||
it('should mark defend broken when defend fully consumed', () => {
|
||||
const state = createTestCombatState();
|
||||
const enemyId = state.enemyOrder[0];
|
||||
state.enemies[enemyId].buffs['defend'] = 3;
|
||||
|
||||
applyDamage(state, enemyId, 5);
|
||||
|
||||
expect(state.enemies[enemyId].hadDefendBroken).toBe(true);
|
||||
});
|
||||
|
||||
it('should kill enemy when HP reaches 0', () => {
|
||||
const state = createTestCombatState();
|
||||
const enemyId = state.enemyOrder[0];
|
||||
|
||||
applyDamage(state, enemyId, state.enemies[enemyId].maxHp);
|
||||
|
||||
expect(state.enemies[enemyId].isAlive).toBe(false);
|
||||
expect(state.enemies[enemyId].hp).toBe(0);
|
||||
});
|
||||
|
||||
it('should not deal negative damage', () => {
|
||||
const state = createTestCombatState();
|
||||
|
||||
const result = applyDamage(state, 'player', -5);
|
||||
|
||||
expect(result.damageDealt).toBe(0);
|
||||
expect(state.player.hp).toBe(50);
|
||||
});
|
||||
|
||||
it('should apply damageReduce buff', () => {
|
||||
const state = createTestCombatState();
|
||||
state.player.buffs['damageReduce'] = 3;
|
||||
|
||||
applyDamage(state, 'player', 5);
|
||||
|
||||
expect(state.player.hp).toBe(48);
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyDefend', () => {
|
||||
it('should add defend stacks', () => {
|
||||
const buffs: Record<string, number> = {};
|
||||
applyDefend(buffs, 5);
|
||||
|
||||
expect(buffs['defend']).toBe(5);
|
||||
});
|
||||
|
||||
it('should stack with existing defend', () => {
|
||||
const buffs: Record<string, number> = { defend: 3 };
|
||||
applyDefend(buffs, 4);
|
||||
|
||||
expect(buffs['defend']).toBe(7);
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyBuff / removeBuff', () => {
|
||||
it('should apply buff stacks', () => {
|
||||
const buffs: Record<string, number> = {};
|
||||
applyBuff(buffs, 'aim', 'lingering', 3);
|
||||
|
||||
expect(buffs['aim']).toBe(3);
|
||||
});
|
||||
|
||||
it('should stack existing buffs', () => {
|
||||
const buffs: Record<string, number> = { aim: 2 };
|
||||
applyBuff(buffs, 'aim', 'lingering', 3);
|
||||
|
||||
expect(buffs['aim']).toBe(5);
|
||||
});
|
||||
|
||||
it('should remove buff partially', () => {
|
||||
const buffs: Record<string, number> = { aim: 5 };
|
||||
const removed = removeBuff(buffs, 'aim', 3);
|
||||
|
||||
expect(removed).toBe(3);
|
||||
expect(buffs['aim']).toBe(2);
|
||||
});
|
||||
|
||||
it('should remove buff fully when stacks exceed current', () => {
|
||||
const buffs: Record<string, number> = { aim: 2 };
|
||||
const removed = removeBuff(buffs, 'aim', 10);
|
||||
|
||||
expect(removed).toBe(2);
|
||||
expect(buffs['aim']).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateBuffs', () => {
|
||||
it('should clear temporary buffs', () => {
|
||||
const buffs: Record<string, number> = { damageReduce: 3, defendNext: 2 };
|
||||
updateBuffs(buffs);
|
||||
|
||||
expect(buffs['damageReduce']).toBeUndefined();
|
||||
expect(buffs['defendNext']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should decrement lingering buffs', () => {
|
||||
const buffs: Record<string, number> = { curse: 3, energyDrain: 1 };
|
||||
updateBuffs(buffs);
|
||||
|
||||
expect(buffs['curse']).toBe(2);
|
||||
expect(buffs['energyDrain']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not affect permanent or posture buffs', () => {
|
||||
const buffs: Record<string, number> = { defend: 5, spike: 1 };
|
||||
updateBuffs(buffs);
|
||||
|
||||
expect(buffs['defend']).toBe(5);
|
||||
expect(buffs['spike']).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('canPlayCard', () => {
|
||||
it('should allow playing card with enough energy', () => {
|
||||
const state = createTestCombatState();
|
||||
const cardId = state.player.deck.hand[0];
|
||||
|
||||
const result = canPlayCard(state, cardId);
|
||||
expect(result.canPlay).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject card not in hand', () => {
|
||||
const state = createTestCombatState();
|
||||
|
||||
const result = canPlayCard(state, 'nonexistent-card');
|
||||
expect(result.canPlay).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject card with insufficient energy', () => {
|
||||
const state = createTestCombatState();
|
||||
state.player.energy = 0;
|
||||
const cardId = state.player.deck.hand[0];
|
||||
|
||||
const card = state.player.deck.cards[cardId];
|
||||
if (card?.itemData?.costType === 'energy' && card.itemData.costCount > 0) {
|
||||
const result = canPlayCard(state, cardId);
|
||||
expect(result.canPlay).toBe(false);
|
||||
expect(result.reason).toBe('能量不足');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('playCard', () => {
|
||||
it('should deduct energy cost when playing card', () => {
|
||||
const state = createTestCombatState();
|
||||
const cardId = state.player.deck.hand[0];
|
||||
const card = state.player.deck.cards[cardId];
|
||||
const initialEnergy = state.player.energy;
|
||||
|
||||
if (card?.itemData?.costType === 'energy') {
|
||||
const ctx = { state, rng: createSimpleRng() };
|
||||
const result = playCard(ctx, cardId);
|
||||
|
||||
if (result.success) {
|
||||
expect(state.player.energy).toBe(initialEnergy - card.itemData.costCount);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should move card to discard pile after playing', () => {
|
||||
const state = createTestCombatState();
|
||||
const cardId = state.player.deck.hand[0];
|
||||
const ctx = { state, rng: createSimpleRng() };
|
||||
|
||||
const result = playCard(ctx, cardId);
|
||||
|
||||
if (result.success) {
|
||||
expect(state.player.deck.hand.includes(cardId)).toBe(false);
|
||||
expect(state.player.deck.discardPile.includes(cardId) || state.player.deck.exhaustPile.includes(cardId)).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('areAllEnemiesDead / isPlayerDead', () => {
|
||||
it('should detect all enemies dead', () => {
|
||||
const state = createTestCombatState();
|
||||
expect(areAllEnemiesDead(state)).toBe(false);
|
||||
|
||||
for (const enemyId of state.enemyOrder) {
|
||||
state.enemies[enemyId].isAlive = false;
|
||||
}
|
||||
expect(areAllEnemiesDead(state)).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect player death', () => {
|
||||
const state = createTestCombatState();
|
||||
expect(isPlayerDead(state)).toBe(false);
|
||||
|
||||
state.player.hp = 0;
|
||||
expect(isPlayerDead(state)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getModifiedAttackDamage / getModifiedDefendAmount', () => {
|
||||
it('should return base damage with no item buffs', () => {
|
||||
const state = createTestCombatState();
|
||||
expect(getModifiedAttackDamage(state, 'some-card', 5)).toBe(5);
|
||||
});
|
||||
|
||||
it('should return base defend with no item buffs', () => {
|
||||
const state = createTestCombatState();
|
||||
expect(getModifiedDefendAmount(state, 'some-card', 4)).toBe(4);
|
||||
});
|
||||
|
||||
it('should add item buff attack damage', () => {
|
||||
const state = createTestCombatState();
|
||||
state.itemBuffs.push({
|
||||
effectId: 'attackBuff',
|
||||
stacks: 3,
|
||||
timing: 'itemUntilPlayed',
|
||||
sourceItemId: 'item-1',
|
||||
targetItemId: 'item-1',
|
||||
});
|
||||
|
||||
expect(getModifiedAttackDamage(state, 'some-card', 5)).toBe(5);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,261 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { createGameHost, GameHost } from '@/core/game-host';
|
||||
import { createGameContext, createGameCommandRegistry } from '@/core/game';
|
||||
import type { CombatState, CombatGameContext } from '@/samples/slay-the-spire-like/combat/types';
|
||||
import { createCombatState } from '@/samples/slay-the-spire-like/combat/state';
|
||||
import { runCombat } from '@/samples/slay-the-spire-like/combat/procedure';
|
||||
import { prompts } from '@/samples/slay-the-spire-like/combat/prompts';
|
||||
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';
|
||||
|
||||
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 waitForPrompt(host: GameHost<CombatState>): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
const check = () => {
|
||||
if (host.activePromptSchema.value !== null) {
|
||||
resolve();
|
||||
} else {
|
||||
setTimeout(check, 10);
|
||||
}
|
||||
};
|
||||
check();
|
||||
});
|
||||
}
|
||||
|
||||
describe('combat/procedure', () => {
|
||||
describe('runCombat with GameHost', () => {
|
||||
it('should start combat and prompt for player action', async () => {
|
||||
const registry = createGameCommandRegistry<CombatState>();
|
||||
const initialState = createTestCombatState();
|
||||
const host = new GameHost(
|
||||
registry,
|
||||
() => createTestCombatState(),
|
||||
async (ctx) => {
|
||||
return await runCombat(ctx);
|
||||
},
|
||||
);
|
||||
|
||||
const combatPromise = host.start(42);
|
||||
|
||||
await waitForPrompt(host);
|
||||
expect(host.activePromptSchema.value).not.toBeNull();
|
||||
expect(host.activePromptSchema.value?.name).toBe('play-card');
|
||||
|
||||
host._context._commands._cancel();
|
||||
try { await combatPromise; } catch {}
|
||||
});
|
||||
|
||||
it('should accept play-card input', async () => {
|
||||
const registry = createGameCommandRegistry<CombatState>();
|
||||
const host = new GameHost(
|
||||
registry,
|
||||
() => createTestCombatState(),
|
||||
async (ctx) => {
|
||||
return await runCombat(ctx);
|
||||
},
|
||||
);
|
||||
|
||||
const combatPromise = host.start(42);
|
||||
await waitForPrompt(host);
|
||||
|
||||
const state = host.state.value;
|
||||
const cardId = state.player.deck.hand[0];
|
||||
|
||||
const error = host.tryAnswerPrompt(prompts.playCard, cardId, state.enemyOrder[0]);
|
||||
expect(error).toBeNull();
|
||||
|
||||
host._context._commands._cancel();
|
||||
try { await combatPromise; } catch {}
|
||||
});
|
||||
|
||||
it('should reject invalid card play', async () => {
|
||||
const registry = createGameCommandRegistry<CombatState>();
|
||||
const host = new GameHost(
|
||||
registry,
|
||||
() => createTestCombatState(),
|
||||
async (ctx) => {
|
||||
return await runCombat(ctx);
|
||||
},
|
||||
);
|
||||
|
||||
const combatPromise = host.start(42);
|
||||
await waitForPrompt(host);
|
||||
|
||||
const error = host.tryAnswerPrompt(prompts.playCard, 'nonexistent-card');
|
||||
expect(error).not.toBeNull();
|
||||
|
||||
host._context._commands._cancel();
|
||||
try { await combatPromise; } catch {}
|
||||
});
|
||||
|
||||
it('should transition to end-turn after playing cards', async () => {
|
||||
const registry = createGameCommandRegistry<CombatState>();
|
||||
const host = new GameHost(
|
||||
registry,
|
||||
() => createTestCombatState(),
|
||||
async (ctx) => {
|
||||
return await runCombat(ctx);
|
||||
},
|
||||
);
|
||||
|
||||
const combatPromise = host.start(42);
|
||||
await waitForPrompt(host);
|
||||
|
||||
const state = host.state.value;
|
||||
const cardId = state.player.deck.hand[0];
|
||||
|
||||
host.tryAnswerPrompt(prompts.playCard, cardId, state.enemyOrder[0]);
|
||||
await waitForPrompt(host);
|
||||
|
||||
expect(host.activePromptSchema.value).not.toBeNull();
|
||||
|
||||
host._context._commands._cancel();
|
||||
try { await combatPromise; } catch {}
|
||||
});
|
||||
|
||||
it('should accept end-turn', async () => {
|
||||
const registry = createGameCommandRegistry<CombatState>();
|
||||
const host = new GameHost(
|
||||
registry,
|
||||
() => createTestCombatState(),
|
||||
async (ctx) => {
|
||||
return await runCombat(ctx);
|
||||
},
|
||||
);
|
||||
|
||||
const combatPromise = host.start(42);
|
||||
await waitForPrompt(host);
|
||||
|
||||
const error = host.tryAnswerPrompt(prompts.endTurn);
|
||||
expect(error).toBeNull();
|
||||
|
||||
host._context._commands._cancel();
|
||||
try { await combatPromise; } catch {}
|
||||
});
|
||||
});
|
||||
|
||||
describe('combat outcome', () => {
|
||||
it('should return victory when all enemies are dead', async () => {
|
||||
const registry = createGameCommandRegistry<CombatState>();
|
||||
const host = new GameHost(
|
||||
registry,
|
||||
() => {
|
||||
const state = createTestCombatState();
|
||||
for (const enemyId of state.enemyOrder) {
|
||||
state.enemies[enemyId].hp = 1;
|
||||
}
|
||||
return state;
|
||||
},
|
||||
async (ctx) => {
|
||||
return await runCombat(ctx);
|
||||
},
|
||||
);
|
||||
|
||||
const combatPromise = host.start(42);
|
||||
await waitForPrompt(host);
|
||||
|
||||
let iterations = 0;
|
||||
while (host.status.value === 'running' && iterations < 100) {
|
||||
const state = host.state.value;
|
||||
if (host.activePromptSchema.value?.name === 'play-card') {
|
||||
const cardId = state.player.deck.hand[0];
|
||||
if (cardId) {
|
||||
const targetId = state.enemyOrder.find(id => state.enemies[id].isAlive);
|
||||
host.tryAnswerPrompt(prompts.playCard, cardId, targetId);
|
||||
}
|
||||
} else if (host.activePromptSchema.value?.name === 'end-turn') {
|
||||
host.tryAnswerPrompt(prompts.endTurn);
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 10));
|
||||
iterations++;
|
||||
}
|
||||
|
||||
if (host.status.value === 'running') {
|
||||
host._context._commands._cancel();
|
||||
try { await combatPromise; } catch {}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('combat state transitions', () => {
|
||||
it('should track turn number across turns', async () => {
|
||||
const registry = createGameCommandRegistry<CombatState>();
|
||||
const host = new GameHost(
|
||||
registry,
|
||||
() => createTestCombatState(),
|
||||
async (ctx) => {
|
||||
return await runCombat(ctx);
|
||||
},
|
||||
);
|
||||
|
||||
const combatPromise = host.start(42);
|
||||
await waitForPrompt(host);
|
||||
|
||||
host.tryAnswerPrompt(prompts.endTurn);
|
||||
await waitForPrompt(host);
|
||||
|
||||
host._context._commands._cancel();
|
||||
try { await combatPromise; } catch {}
|
||||
});
|
||||
|
||||
it('should reset energy at start of player turn', async () => {
|
||||
const registry = createGameCommandRegistry<CombatState>();
|
||||
const host = new GameHost(
|
||||
registry,
|
||||
() => createTestCombatState(),
|
||||
async (ctx) => {
|
||||
return await runCombat(ctx);
|
||||
},
|
||||
);
|
||||
|
||||
const combatPromise = host.start(42);
|
||||
await waitForPrompt(host);
|
||||
|
||||
const state = host.state.value;
|
||||
expect(state.player.energy).toBe(state.player.maxEnergy);
|
||||
|
||||
host._context._commands._cancel();
|
||||
try { await combatPromise; } catch {}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,303 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
createCombatState,
|
||||
createEnemyInstance,
|
||||
createPlayerCombatState,
|
||||
drawCardsToHand,
|
||||
addFatigueCards,
|
||||
discardHand,
|
||||
discardCard,
|
||||
exhaustCard,
|
||||
getEnemyCurrentIntent,
|
||||
advanceEnemyIntent,
|
||||
getEffectTiming,
|
||||
getEffectData,
|
||||
INITIAL_HAND_SIZE,
|
||||
DEFAULT_MAX_ENERGY,
|
||||
FATIGUE_CARDS_PER_SHUFFLE,
|
||||
} 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, effectDesertData } from '@/samples/slay-the-spire-like/data';
|
||||
|
||||
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 createTestPlayerState(): PlayerState {
|
||||
return { maxHp: 50, currentHp: 50, gold: 0 };
|
||||
}
|
||||
|
||||
describe('combat/state', () => {
|
||||
describe('constants', () => {
|
||||
it('should have correct default values', () => {
|
||||
expect(INITIAL_HAND_SIZE).toBe(5);
|
||||
expect(DEFAULT_MAX_ENERGY).toBe(3);
|
||||
expect(FATIGUE_CARDS_PER_SHUFFLE).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createEnemyInstance', () => {
|
||||
it('should create enemy from desert data', () => {
|
||||
const cactusData = enemyDesertData.find(e => e.id === '仙人掌怪')!;
|
||||
const enemy = createEnemyInstance('仙人掌怪', cactusData, 0, { value: 0 });
|
||||
|
||||
expect(enemy.templateId).toBe('仙人掌怪');
|
||||
expect(enemy.hp).toBe(cactusData.initHp);
|
||||
expect(enemy.maxHp).toBe(cactusData.initHp);
|
||||
expect(enemy.isAlive).toBe(true);
|
||||
expect(enemy.hadDefendBroken).toBe(false);
|
||||
});
|
||||
|
||||
it('should apply bonus HP', () => {
|
||||
const cactusData = enemyDesertData.find(e => e.id === '仙人掌怪')!;
|
||||
const enemy = createEnemyInstance('仙人掌怪', cactusData, 5, { value: 0 });
|
||||
|
||||
expect(enemy.hp).toBe(cactusData.initHp + 5);
|
||||
expect(enemy.maxHp).toBe(cactusData.initHp + 5);
|
||||
});
|
||||
|
||||
it('should initialize buffs from template', () => {
|
||||
const cactusData = enemyDesertData.find(e => e.id === '仙人掌怪')!;
|
||||
const enemy = createEnemyInstance('仙人掌怪', cactusData, 0, { value: 0 });
|
||||
|
||||
expect(enemy.buffs['spike']).toBe(1);
|
||||
});
|
||||
|
||||
it('should set initial intent', () => {
|
||||
const cactusData = enemyDesertData.find(e => e.id === '仙人掌怪')!;
|
||||
const enemy = createEnemyInstance('仙人掌怪', cactusData, 0, { value: 0 });
|
||||
|
||||
expect(enemy.currentIntentId).toBe(cactusData.initialIntent);
|
||||
});
|
||||
|
||||
it('should generate unique IDs', () => {
|
||||
const cactusData = enemyDesertData.find(e => e.id === '仙人掌怪')!;
|
||||
const e1 = createEnemyInstance('仙人掌怪', cactusData, 0, { value: 0 });
|
||||
const e2 = createEnemyInstance('仙人掌怪', cactusData, 0, { value: 0 });
|
||||
|
||||
expect(e1.id).not.toBe(e2.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createPlayerCombatState', () => {
|
||||
it('should create player state from run state and inventory', () => {
|
||||
const inv = createTestInventory();
|
||||
const playerState = createTestPlayerState();
|
||||
const combatPlayer = createPlayerCombatState(playerState, inv);
|
||||
|
||||
expect(combatPlayer.hp).toBe(50);
|
||||
expect(combatPlayer.maxHp).toBe(50);
|
||||
expect(combatPlayer.energy).toBe(DEFAULT_MAX_ENERGY);
|
||||
expect(combatPlayer.maxEnergy).toBe(DEFAULT_MAX_ENERGY);
|
||||
expect(Object.keys(combatPlayer.buffs).length).toBe(0);
|
||||
expect(combatPlayer.damagedThisTurn).toBe(false);
|
||||
expect(combatPlayer.cardsDiscardedThisTurn).toBe(0);
|
||||
});
|
||||
|
||||
it('should generate deck from inventory', () => {
|
||||
const inv = createTestInventory();
|
||||
const playerState = createTestPlayerState();
|
||||
const combatPlayer = createPlayerCombatState(playerState, inv);
|
||||
|
||||
expect(Object.keys(combatPlayer.deck.cards).length).toBeGreaterThan(0);
|
||||
expect(combatPlayer.deck.drawPile.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createCombatState', () => {
|
||||
it('should create full combat state from encounter', () => {
|
||||
const inv = createTestInventory();
|
||||
const playerState = createTestPlayerState();
|
||||
const encounter = encounterDesertData.find(e => e.name === '仙人掌怪')!;
|
||||
|
||||
const combat = createCombatState(playerState, inv, encounter);
|
||||
|
||||
expect(combat.phase).toBe('playerTurn');
|
||||
expect(combat.turnNumber).toBe(1);
|
||||
expect(combat.result).toBeNull();
|
||||
expect(combat.loot).toEqual([]);
|
||||
expect(combat.fatigueAddedCount).toBe(0);
|
||||
});
|
||||
|
||||
it('should create enemies from encounter data', () => {
|
||||
const inv = createTestInventory();
|
||||
const playerState = createTestPlayerState();
|
||||
const encounter = encounterDesertData.find(e => e.name === '仙人掌怪')!;
|
||||
|
||||
const combat = createCombatState(playerState, inv, encounter);
|
||||
|
||||
expect(combat.enemyOrder.length).toBeGreaterThan(0);
|
||||
for (const enemyId of combat.enemyOrder) {
|
||||
expect(combat.enemies[enemyId].isAlive).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('should draw initial hand', () => {
|
||||
const inv = createTestInventory();
|
||||
const playerState = createTestPlayerState();
|
||||
const encounter = encounterDesertData.find(e => e.name === '仙人掌怪')!;
|
||||
|
||||
const combat = createCombatState(playerState, inv, encounter);
|
||||
|
||||
expect(combat.player.deck.hand.length).toBe(INITIAL_HAND_SIZE);
|
||||
});
|
||||
});
|
||||
|
||||
describe('drawCardsToHand', () => {
|
||||
it('should draw cards from draw pile to hand', () => {
|
||||
const inv = createTestInventory();
|
||||
const playerState = createTestPlayerState();
|
||||
const combatPlayer = createPlayerCombatState(playerState, inv);
|
||||
|
||||
const initialDrawPile = combatPlayer.deck.drawPile.length;
|
||||
const initialHand = combatPlayer.deck.hand.length;
|
||||
drawCardsToHand(combatPlayer.deck, 3);
|
||||
|
||||
expect(combatPlayer.deck.hand.length).toBe(initialHand + 3);
|
||||
expect(combatPlayer.deck.drawPile.length).toBe(initialDrawPile - 3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addFatigueCards', () => {
|
||||
it('should add fatigue cards to draw pile', () => {
|
||||
const inv = createTestInventory();
|
||||
const playerState = createTestPlayerState();
|
||||
const combatPlayer = createPlayerCombatState(playerState, inv);
|
||||
|
||||
const initialCount = Object.keys(combatPlayer.deck.cards).length;
|
||||
const fatigueCounter = { value: 0 };
|
||||
addFatigueCards(combatPlayer.deck, FATIGUE_CARDS_PER_SHUFFLE, fatigueCounter);
|
||||
|
||||
expect(Object.keys(combatPlayer.deck.cards).length).toBe(initialCount + FATIGUE_CARDS_PER_SHUFFLE);
|
||||
expect(fatigueCounter.value).toBe(FATIGUE_CARDS_PER_SHUFFLE);
|
||||
});
|
||||
|
||||
it('should create fatigue cards with correct properties', () => {
|
||||
const inv = createTestInventory();
|
||||
const playerState = createTestPlayerState();
|
||||
const combatPlayer = createPlayerCombatState(playerState, inv);
|
||||
|
||||
addFatigueCards(combatPlayer.deck, 1, { value: 0 });
|
||||
const fatigueCard = combatPlayer.deck.cards['fatigue-1'];
|
||||
|
||||
expect(fatigueCard).toBeDefined();
|
||||
expect(fatigueCard.displayName).toBe('疲劳');
|
||||
expect(fatigueCard.sourceItemId).toBeNull();
|
||||
expect(fatigueCard.itemData).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('discardHand', () => {
|
||||
it('should move all hand cards to discard pile', () => {
|
||||
const inv = createTestInventory();
|
||||
const playerState = createTestPlayerState();
|
||||
const combatPlayer = createPlayerCombatState(playerState, inv);
|
||||
drawCardsToHand(combatPlayer.deck, 3);
|
||||
|
||||
const handCount = combatPlayer.deck.hand.length;
|
||||
discardHand(combatPlayer.deck);
|
||||
|
||||
expect(combatPlayer.deck.hand).toEqual([]);
|
||||
expect(combatPlayer.deck.discardPile.length).toBe(handCount);
|
||||
});
|
||||
});
|
||||
|
||||
describe('discardCard / exhaustCard', () => {
|
||||
it('should move a card from hand to discard pile', () => {
|
||||
const inv = createTestInventory();
|
||||
const playerState = createTestPlayerState();
|
||||
const combatPlayer = createPlayerCombatState(playerState, inv);
|
||||
drawCardsToHand(combatPlayer.deck, 3);
|
||||
|
||||
const cardId = combatPlayer.deck.hand[0];
|
||||
discardCard(combatPlayer.deck, cardId);
|
||||
|
||||
expect(combatPlayer.deck.hand.includes(cardId)).toBe(false);
|
||||
expect(combatPlayer.deck.discardPile.includes(cardId)).toBe(true);
|
||||
});
|
||||
|
||||
it('should move a card from hand to exhaust pile', () => {
|
||||
const inv = createTestInventory();
|
||||
const playerState = createTestPlayerState();
|
||||
const combatPlayer = createPlayerCombatState(playerState, inv);
|
||||
drawCardsToHand(combatPlayer.deck, 3);
|
||||
|
||||
const cardId = combatPlayer.deck.hand[0];
|
||||
exhaustCard(combatPlayer.deck, cardId);
|
||||
|
||||
expect(combatPlayer.deck.hand.includes(cardId)).toBe(false);
|
||||
expect(combatPlayer.deck.exhaustPile.includes(cardId)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('enemy intent', () => {
|
||||
it('should get current intent', () => {
|
||||
const cactusData = enemyDesertData.find(e => e.id === '仙人掌怪')!;
|
||||
const enemy = createEnemyInstance('仙人掌怪', cactusData, 0, { value: 0 });
|
||||
|
||||
const intent = getEnemyCurrentIntent(enemy);
|
||||
expect(intent).toBeDefined();
|
||||
expect(intent!.id).toBe('boost');
|
||||
});
|
||||
|
||||
it('should advance intent after action', () => {
|
||||
const cactusData = enemyDesertData.find(e => e.id === '仙人掌怪')!;
|
||||
const enemy = createEnemyInstance('仙人掌怪', cactusData, 0, { value: 0 });
|
||||
|
||||
const originalIntent = enemy.currentIntentId;
|
||||
advanceEnemyIntent(enemy);
|
||||
|
||||
expect(enemy.currentIntentId).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEffectTiming / getEffectData', () => {
|
||||
it('should return timing for known effects', () => {
|
||||
expect(getEffectTiming('attack')).toBe('instant');
|
||||
expect(getEffectTiming('defend')).toBe('posture');
|
||||
expect(getEffectTiming('spike')).toBe('permanent');
|
||||
expect(getEffectTiming('energyDrain')).toBe('lingering');
|
||||
});
|
||||
|
||||
it('should return undefined for unknown effects', () => {
|
||||
expect(getEffectTiming('nonexistent')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return effect data for known effects', () => {
|
||||
const data = getEffectData('attack');
|
||||
expect(data).toBeDefined();
|
||||
expect(data!.id).toBe('attack');
|
||||
expect(data!.name).toBe('攻击');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,254 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user