210 lines
5.4 KiB
TypeScript
210 lines
5.4 KiB
TypeScript
import {beforeEach, describe, expect, it, vi} from 'vitest';
|
|
|
|
import {getCharacterById} from '../data/characterPresets';
|
|
import {AnimationState, type Character, type GameState,WorldType} from '../types';
|
|
import {buildCompanionRenderStatesForGameState} from './useNpcInteractionFlow';
|
|
|
|
vi.mock('../data/characterPresets', () => ({
|
|
getCharacterById: vi.fn(),
|
|
}));
|
|
|
|
function createTestCharacter(id: string, name: string): Character {
|
|
return {
|
|
id,
|
|
name,
|
|
title: '测试同伴',
|
|
description: '用于测试的角色',
|
|
backstory: '测试背景',
|
|
avatar: '/test-avatar.png',
|
|
portrait: '/test-portrait.png',
|
|
assetFolder: 'test-character',
|
|
assetVariant: 'default',
|
|
attributes: {
|
|
strength: 10,
|
|
agility: 10,
|
|
intelligence: 10,
|
|
spirit: 10,
|
|
},
|
|
personality: 'steady',
|
|
skills: [
|
|
{
|
|
id: 'basic-strike',
|
|
name: '试探一击',
|
|
animation: AnimationState.ATTACK,
|
|
damage: 10,
|
|
manaCost: 0,
|
|
cooldownTurns: 0,
|
|
range: 1,
|
|
style: 'steady',
|
|
},
|
|
],
|
|
adventureOpenings: {},
|
|
};
|
|
}
|
|
|
|
function createBaseState(): GameState {
|
|
return {
|
|
worldType: WorldType.WUXIA,
|
|
customWorldProfile: null,
|
|
playerCharacter: createTestCharacter('player', '主角'),
|
|
runtimeStats: {
|
|
playTimeMs: 0,
|
|
lastPlayTickAt: null,
|
|
hostileNpcsDefeated: 0,
|
|
questsAccepted: 0,
|
|
itemsUsed: 0,
|
|
scenesTraveled: 0,
|
|
},
|
|
currentScene: 'Story',
|
|
storyHistory: [],
|
|
characterChats: {},
|
|
animationState: AnimationState.IDLE,
|
|
currentEncounter: null,
|
|
npcInteractionActive: false,
|
|
currentScenePreset: null,
|
|
sceneHostileNpcs: [],
|
|
playerX: 0,
|
|
playerOffsetY: 0,
|
|
playerFacing: 'right',
|
|
playerActionMode: 'idle',
|
|
scrollWorld: false,
|
|
inBattle: false,
|
|
playerHp: 100,
|
|
playerMaxHp: 100,
|
|
playerMana: 20,
|
|
playerMaxMana: 20,
|
|
playerSkillCooldowns: {},
|
|
activeCombatEffects: [],
|
|
playerCurrency: 0,
|
|
playerInventory: [],
|
|
playerEquipment: {
|
|
weapon: null,
|
|
armor: null,
|
|
relic: null,
|
|
},
|
|
npcStates: {},
|
|
quests: [],
|
|
roster: [],
|
|
companions: [],
|
|
currentBattleNpcId: null,
|
|
currentNpcBattleMode: null,
|
|
currentNpcBattleOutcome: null,
|
|
sparReturnEncounter: null,
|
|
sparPlayerHpBefore: null,
|
|
sparPlayerMaxHpBefore: null,
|
|
sparStoryHistoryBefore: null,
|
|
};
|
|
}
|
|
|
|
describe('buildCompanionRenderStatesForGameState', () => {
|
|
beforeEach(() => {
|
|
vi.mocked(getCharacterById).mockReset();
|
|
});
|
|
|
|
it('builds render states from the provided transition snapshot', () => {
|
|
const companionCharacter = createTestCharacter('companion-a', '阿青');
|
|
vi.mocked(getCharacterById).mockImplementation((characterId: string) => {
|
|
if (characterId === companionCharacter.id || characterId === 'player') {
|
|
return companionCharacter;
|
|
}
|
|
return null;
|
|
});
|
|
|
|
const transitionSnapshot: GameState = {
|
|
...createBaseState(),
|
|
playerFacing: 'left',
|
|
animationState: AnimationState.ATTACK,
|
|
companions: [
|
|
{
|
|
npcId: 'npc-aqing',
|
|
characterId: companionCharacter.id,
|
|
joinedAtAffinity: 10,
|
|
hp: 36,
|
|
maxHp: 48,
|
|
mana: 12,
|
|
maxMana: 18,
|
|
skillCooldowns: {basicStrike: 1},
|
|
offsetX: 14,
|
|
offsetY: -6,
|
|
transitionMs: 90,
|
|
},
|
|
],
|
|
};
|
|
|
|
const renderStates = buildCompanionRenderStatesForGameState({
|
|
gameState: transitionSnapshot,
|
|
presentationByNpcId: {
|
|
'npc-aqing': {
|
|
animationState: AnimationState.ACQUIRE,
|
|
entryOffsetX: 28,
|
|
entryOffsetY: 12,
|
|
transitionMs: 240,
|
|
recruitToken: 42,
|
|
},
|
|
},
|
|
observeFacingByNpcId: {
|
|
'npc-aqing': 'right',
|
|
},
|
|
});
|
|
|
|
expect(renderStates).toHaveLength(1);
|
|
expect(renderStates[0]).toMatchObject({
|
|
npcId: 'npc-aqing',
|
|
character: companionCharacter,
|
|
hp: 36,
|
|
maxHp: 48,
|
|
mana: 12,
|
|
maxMana: 18,
|
|
animationState: AnimationState.ACQUIRE,
|
|
slot: 'upper',
|
|
facing: 'right',
|
|
entryOffsetX: 42,
|
|
entryOffsetY: 6,
|
|
transitionMs: 240,
|
|
recruitToken: 42,
|
|
});
|
|
});
|
|
|
|
it('lets callers render a visible snapshot even if the live state already changed', () => {
|
|
const companionCharacter = createTestCharacter('companion-b', '小舟');
|
|
vi.mocked(getCharacterById).mockImplementation((characterId: string) => {
|
|
if (characterId === companionCharacter.id || characterId === 'player') {
|
|
return companionCharacter;
|
|
}
|
|
return null;
|
|
});
|
|
|
|
const visibleSnapshot: GameState = {
|
|
...createBaseState(),
|
|
scrollWorld: true,
|
|
companions: [
|
|
{
|
|
npcId: 'npc-xiaozhou',
|
|
characterId: companionCharacter.id,
|
|
joinedAtAffinity: 18,
|
|
hp: 30,
|
|
maxHp: 30,
|
|
mana: 9,
|
|
maxMana: 12,
|
|
skillCooldowns: {},
|
|
},
|
|
],
|
|
};
|
|
const liveState: GameState = {
|
|
...createBaseState(),
|
|
companions: [],
|
|
};
|
|
|
|
const visibleRenderStates = buildCompanionRenderStatesForGameState({
|
|
gameState: visibleSnapshot,
|
|
});
|
|
const liveRenderStates = buildCompanionRenderStatesForGameState({
|
|
gameState: liveState,
|
|
});
|
|
|
|
expect(visibleRenderStates).toHaveLength(1);
|
|
expect(visibleRenderStates[0]?.animationState).toBe(AnimationState.RUN);
|
|
expect(liveRenderStates).toHaveLength(0);
|
|
});
|
|
});
|