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 { 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 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 = {}; applyDefend(buffs, 5); expect(buffs['defend']).toBe(5); }); it('should stack with existing defend', () => { const buffs: Record = { defend: 3 }; applyDefend(buffs, 4); expect(buffs['defend']).toBe(7); }); }); describe('applyBuff / removeBuff', () => { it('should apply buff stacks', () => { const buffs: Record = {}; applyBuff(buffs, 'aim', 'lingering', 3); expect(buffs['aim']).toBe(3); }); it('should stack existing buffs', () => { const buffs: Record = { aim: 2 }; applyBuff(buffs, 'aim', 'lingering', 3); expect(buffs['aim']).toBe(5); }); it('should remove buff partially', () => { const buffs: Record = { 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 = { 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 = { damageReduce: 3, defendNext: 2 }; updateBuffs(buffs); expect(buffs['damageReduce']).toBeUndefined(); expect(buffs['defendNext']).toBeUndefined(); }); it('should decrement lingering buffs', () => { const buffs: Record = { spike: 3, aim: 1 }; updateBuffs(buffs); expect(buffs['spike']).toBe(2); expect(buffs['aim']).toBeUndefined(); }); it('should not affect permanent or posture buffs', () => { const buffs: Record = { defend: 5, energyDrain: 1 }; updateBuffs(buffs); expect(buffs['defend']).toBe(5); expect(buffs['energyDrain']).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); }); }); });