206 lines
5.2 KiB
TypeScript
206 lines
5.2 KiB
TypeScript
/* @vitest-environment jsdom */
|
|
|
|
import { act, renderHook } from '@testing-library/react';
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import {
|
|
AnimationState,
|
|
type GameState,
|
|
type StoryMoment,
|
|
type StoryOption,
|
|
WorldType,
|
|
} from '../../types';
|
|
import { useRpgSceneTransitionModel } from './useRpgSceneTransitionModel';
|
|
|
|
function createGameState(actId: string): GameState {
|
|
return {
|
|
worldType: WorldType.WUXIA,
|
|
customWorldProfile: null,
|
|
playerCharacter: {
|
|
id: 'hero',
|
|
name: '测试主角',
|
|
title: '游侠',
|
|
description: '测试角色',
|
|
backstory: '测试背景',
|
|
avatar: '',
|
|
portrait: '',
|
|
assetFolder: '',
|
|
assetVariant: '',
|
|
attributes: {
|
|
strength: 10,
|
|
agility: 10,
|
|
intelligence: 10,
|
|
spirit: 10,
|
|
},
|
|
personality: '沉稳',
|
|
skills: [],
|
|
adventureOpenings: {},
|
|
},
|
|
runtimeStats: {
|
|
playTimeMs: 0,
|
|
lastPlayTickAt: null,
|
|
hostileNpcsDefeated: 0,
|
|
questsAccepted: 0,
|
|
itemsUsed: 0,
|
|
scenesTraveled: 0,
|
|
},
|
|
currentScene: 'Story',
|
|
storyHistory: [{ text: '旧幕', options: [] }],
|
|
storyEngineMemory: {
|
|
discoveredFactIds: [],
|
|
activeThreadIds: [],
|
|
resolvedScarIds: [],
|
|
recentCarrierIds: [],
|
|
currentSceneActState: {
|
|
sceneId: 'scene-1',
|
|
chapterId: 'chapter-1',
|
|
currentActId: actId,
|
|
currentActIndex: actId === 'act-1' ? 0 : 1,
|
|
completedActIds: actId === 'act-1' ? [] : ['act-1'],
|
|
visitedActIds: [actId],
|
|
},
|
|
},
|
|
characterChats: {},
|
|
animationState: AnimationState.IDLE,
|
|
currentEncounter: null,
|
|
npcInteractionActive: false,
|
|
currentScenePreset: {
|
|
id: 'scene-1',
|
|
name: '断桥旧哨',
|
|
description: '测试场景',
|
|
imageSrc: '/scene.png',
|
|
treasureHints: [],
|
|
npcs: [],
|
|
},
|
|
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,
|
|
};
|
|
}
|
|
|
|
function createStory(
|
|
text: string,
|
|
options: StoryOption[] = [],
|
|
deferredAutoChoice?: StoryOption,
|
|
): StoryMoment {
|
|
return {
|
|
text,
|
|
options,
|
|
deferredAutoChoice,
|
|
};
|
|
}
|
|
|
|
describe('useRpgSceneTransitionModel', () => {
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it('fires deferred auto choice only after entry and through the latest callback', () => {
|
|
vi.useFakeTimers();
|
|
const autoChoice: StoryOption = {
|
|
functionId: 'npc_preview_talk',
|
|
actionText: '与新角色交谈',
|
|
text: '与新角色交谈',
|
|
visuals: {
|
|
playerAnimation: AnimationState.IDLE,
|
|
playerMoveMeters: 0,
|
|
playerOffsetY: 0,
|
|
playerFacing: 'right',
|
|
scrollWorld: false,
|
|
monsterChanges: [],
|
|
},
|
|
};
|
|
const firstCallback = vi.fn();
|
|
const latestCallback = vi.fn();
|
|
const initialState = createGameState('act-1');
|
|
const initialStory = createStory('旧幕收束', [
|
|
{
|
|
functionId: 'story_continue_adventure',
|
|
actionText: '继续冒险',
|
|
text: '继续冒险',
|
|
visuals: autoChoice.visuals,
|
|
},
|
|
]);
|
|
const nextStory = createStory('新幕入口', [autoChoice], autoChoice);
|
|
|
|
const { result, rerender } = renderHook(
|
|
(props: {
|
|
gameState: GameState;
|
|
currentStory: StoryMoment;
|
|
onDeferredAutoChoice: (option: StoryOption) => void;
|
|
}) =>
|
|
useRpgSceneTransitionModel({
|
|
gameState: props.gameState,
|
|
currentStory: props.currentStory,
|
|
openingCampSceneId: null,
|
|
onDeferredAutoChoice: props.onDeferredAutoChoice,
|
|
}),
|
|
{
|
|
initialProps: {
|
|
gameState: initialState,
|
|
currentStory: initialStory,
|
|
onDeferredAutoChoice: firstCallback,
|
|
},
|
|
},
|
|
);
|
|
|
|
act(() => {
|
|
result.current.setSceneTransitionDurations({ exitMs: 20, entryMs: 30 });
|
|
});
|
|
act(() => {
|
|
result.current.beginSceneTransition('content-change');
|
|
});
|
|
|
|
expect(result.current.sceneTransitionPhase).toBe('exiting');
|
|
|
|
rerender({
|
|
gameState: createGameState('act-2'),
|
|
currentStory: nextStory,
|
|
onDeferredAutoChoice: latestCallback,
|
|
});
|
|
|
|
act(() => {
|
|
vi.advanceTimersByTime(20);
|
|
});
|
|
|
|
expect(result.current.sceneTransitionPhase).toBe('entering');
|
|
expect(latestCallback).not.toHaveBeenCalled();
|
|
|
|
act(() => {
|
|
vi.advanceTimersByTime(30);
|
|
});
|
|
|
|
expect(result.current.sceneTransitionPhase).toBe('idle');
|
|
expect(firstCallback).not.toHaveBeenCalled();
|
|
expect(latestCallback).toHaveBeenCalledWith(autoChoice);
|
|
});
|
|
});
|