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 { 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 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('攻击'); }); }); });