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

304 lines
12 KiB
TypeScript

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('lingering');
expect(getEffectTiming('energyDrain')).toBe('permanent');
});
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('攻击');
});
});
});