1
This commit is contained in:
@@ -3,17 +3,34 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { useMemo } from 'react';
|
||||
import { afterEach, expect, test } from 'vitest';
|
||||
import { afterEach, beforeEach, expect, test, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
buildCustomWorldPlayableCharacters,
|
||||
setRuntimeCharacterOverrides,
|
||||
} from '../data/characterPresets';
|
||||
import { setRuntimeCustomWorldProfile } from '../data/customWorldRuntime';
|
||||
import { normalizeCustomWorldProfileRecord } from '../data/customWorldLibrary';
|
||||
import { setRuntimeCustomWorldProfile } from '../data/customWorldRuntime';
|
||||
import { WorldType } from '../types';
|
||||
import { useRpgRuntimeStory } from './rpg-runtime-story/useRpgRuntimeStory';
|
||||
import { useRpgSessionBootstrap } from './rpg-session';
|
||||
|
||||
const aiServiceMocks = vi.hoisted(() => ({
|
||||
streamNpcChatTurn: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../services/aiService', async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import('../services/aiService')>(
|
||||
'../services/aiService',
|
||||
);
|
||||
|
||||
return {
|
||||
...actual,
|
||||
streamNpcChatTurn: aiServiceMocks.streamNpcChatTurn,
|
||||
};
|
||||
});
|
||||
|
||||
function buildBackstoryReveal(label: string) {
|
||||
return {
|
||||
publicSummary: `${label}的公开背景`,
|
||||
@@ -208,6 +225,40 @@ function buildSavedProfile() {
|
||||
reactionHooks: ['原始灯册', '封灯令'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'story-primary-only',
|
||||
name: '沈砺旧识',
|
||||
title: '旧潮案记录员',
|
||||
role: '第一幕主线记录者',
|
||||
description: '负责整理旧潮案脉络的人。',
|
||||
backstory: '他知道异常账本的来源,但不会第一时间正面对话。',
|
||||
personality: '沉默、谨慎。',
|
||||
motivation: '保住旧案原始记录。',
|
||||
combatStyle: '以防守和牵制为主。',
|
||||
initialAffinity: 8,
|
||||
relationshipHooks: ['旧案记录'],
|
||||
tags: ['记录', '主线'],
|
||||
backstoryReveal: buildBackstoryReveal('沈砺旧识'),
|
||||
skills: [],
|
||||
initialItems: [],
|
||||
},
|
||||
{
|
||||
id: 'story-act-only',
|
||||
name: '陆衡',
|
||||
title: '航运公会审计员',
|
||||
role: '第一幕主NPC',
|
||||
description: '正在交易所大厅核对异常账本的人。',
|
||||
backstory: '他掌握着旧航路资金流向的第一份实证。',
|
||||
personality: '克制、警惕,习惯先观察再开口。',
|
||||
motivation: '确认谁在开盘前转移了旧案资金。',
|
||||
combatStyle: '用短杖和账册压制对手节奏。',
|
||||
initialAffinity: 6,
|
||||
relationshipHooks: ['异常账本'],
|
||||
tags: ['审计', '第一幕'],
|
||||
backstoryReveal: buildBackstoryReveal('陆衡'),
|
||||
skills: [],
|
||||
initialItems: [],
|
||||
},
|
||||
],
|
||||
items: [],
|
||||
camp: {
|
||||
@@ -219,7 +270,7 @@ function buildSavedProfile() {
|
||||
id: 'landmark-1',
|
||||
name: '回潮旧灯塔',
|
||||
description: '被海雾啃旧的石塔仍在夜里维持着微弱灯火。',
|
||||
sceneNpcIds: ['story-1'],
|
||||
sceneNpcIds: [],
|
||||
connections: [
|
||||
{
|
||||
targetLandmarkId: 'landmark-2',
|
||||
@@ -251,6 +302,60 @@ function buildSavedProfile() {
|
||||
],
|
||||
},
|
||||
],
|
||||
sceneChapterBlueprints: [
|
||||
{
|
||||
id: 'chapter-1',
|
||||
sceneId: 'custom-scene-camp',
|
||||
title: '交易所第一幕',
|
||||
summary: '玩家在交易大厅被异常账本牵住。',
|
||||
sceneTaskDescription: '查清异常账本指向谁。',
|
||||
linkedThreadIds: [],
|
||||
linkedLandmarkIds: [],
|
||||
acts: [
|
||||
{
|
||||
id: 'act-1',
|
||||
sceneId: 'custom-scene-camp',
|
||||
title: '第一幕',
|
||||
summary: '陆衡先开口试探玩家。',
|
||||
stageCoverage: ['opening'],
|
||||
encounterNpcIds: ['story-primary-only', 'story-act-only'],
|
||||
primaryNpcId: 'story-primary-only',
|
||||
oppositeNpcId: 'story-act-only',
|
||||
eventDescription: '陆衡拿着异常账本,在开盘前拦住玩家。',
|
||||
linkedThreadIds: [],
|
||||
advanceRule: 'after_primary_contact',
|
||||
actGoal: '确认异常账本的第一条线索。',
|
||||
transitionHook: '账本指向旧灯塔的潮痕。',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'chapter-late',
|
||||
sceneId: 'landmark-2',
|
||||
title: '雾栈后续幕',
|
||||
summary: '后续场景不应抢走开局。',
|
||||
sceneTaskDescription: '处理雾栈尽头的后续问题。',
|
||||
linkedThreadIds: [],
|
||||
linkedLandmarkIds: ['landmark-2'],
|
||||
acts: [
|
||||
{
|
||||
id: 'act-late',
|
||||
sceneId: 'landmark-2',
|
||||
title: '后续幕',
|
||||
summary: '雾栈里有人影闪过。',
|
||||
stageCoverage: ['aftermath'],
|
||||
encounterNpcIds: ['story-1'],
|
||||
primaryNpcId: 'story-1',
|
||||
oppositeNpcId: 'story-1',
|
||||
eventDescription: '后续角色在雾栈尽头等待。',
|
||||
linkedThreadIds: [],
|
||||
advanceRule: 'after_active_step_complete',
|
||||
actGoal: '后续推进。',
|
||||
transitionHook: '继续深入雾栈。',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
scenarioPackId: 'scenario-pack:tide',
|
||||
campaignPackId: 'campaign-pack:tide',
|
||||
generationMode: 'full',
|
||||
@@ -275,6 +380,13 @@ function readSnapshot() {
|
||||
currentScenePresetId: string | null;
|
||||
currentScenePresetName: string | null;
|
||||
currentSceneConnectedIds: string[];
|
||||
currentSceneActId: string | null;
|
||||
currentEncounterId: string | null;
|
||||
currentEncounterName: string | null;
|
||||
currentStoryDisplayMode: string | null;
|
||||
currentStoryNpcName: string | null;
|
||||
currentStoryDialogueTexts: string[];
|
||||
isStoryLoading: boolean;
|
||||
firstLandmarkResidueTitle: string | null;
|
||||
playerCharacterName: string | null;
|
||||
playerInventoryNames: string[];
|
||||
@@ -293,8 +405,19 @@ function GameFlowHarness() {
|
||||
[profile],
|
||||
);
|
||||
const selectedCharacter = playableCharacters[0] ?? null;
|
||||
const { gameState, handleCustomWorldSelect, handleCharacterSelect } =
|
||||
const {
|
||||
gameState,
|
||||
setGameState,
|
||||
handleCustomWorldSelect,
|
||||
handleCharacterSelect,
|
||||
} =
|
||||
useRpgSessionBootstrap();
|
||||
const story = useRpgRuntimeStory({
|
||||
gameState,
|
||||
setGameState,
|
||||
buildResolvedChoiceState: () => ({}) as never,
|
||||
playResolvedChoice: async (state) => state,
|
||||
});
|
||||
|
||||
const snapshot = {
|
||||
worldType: gameState.worldType,
|
||||
@@ -305,6 +428,15 @@ function GameFlowHarness() {
|
||||
currentScenePresetId: gameState.currentScenePreset?.id ?? null,
|
||||
currentScenePresetName: gameState.currentScenePreset?.name ?? null,
|
||||
currentSceneConnectedIds: gameState.currentScenePreset?.connectedSceneIds ?? [],
|
||||
currentSceneActId:
|
||||
gameState.storyEngineMemory?.currentSceneActState?.currentActId ?? null,
|
||||
currentEncounterId: gameState.currentEncounter?.id ?? null,
|
||||
currentEncounterName: gameState.currentEncounter?.npcName ?? null,
|
||||
currentStoryDisplayMode: story.currentStory?.displayMode ?? null,
|
||||
currentStoryNpcName: story.currentStory?.npcChatState?.npcName ?? null,
|
||||
currentStoryDialogueTexts:
|
||||
story.currentStory?.dialogue?.map((entry) => entry.text) ?? [],
|
||||
isStoryLoading: story.isLoading,
|
||||
firstLandmarkResidueTitle:
|
||||
gameState.customWorldProfile?.landmarks[0]?.narrativeResidues?.[0]
|
||||
?.title ?? null,
|
||||
@@ -345,6 +477,16 @@ afterEach(() => {
|
||||
setRuntimeCharacterOverrides(null);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
aiServiceMocks.streamNpcChatTurn.mockReset();
|
||||
aiServiceMocks.streamNpcChatTurn.mockResolvedValue({
|
||||
affinityDelta: 0,
|
||||
affinityText: '这轮对话暂时没有带来明显关系变化。',
|
||||
npcReply: '开盘前别靠近账本。你先告诉我,是谁让你来查这笔异常资金?',
|
||||
suggestions: ['我先说明来意', '你先说账本哪里异常', '我不是来抢账本的'],
|
||||
});
|
||||
});
|
||||
|
||||
test('saved custom world result settings flow into game state after entering the world', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
@@ -378,4 +520,30 @@ test('saved custom world result settings flow into game state after entering the
|
||||
expect(readSnapshot().playerEquipment.weapon).toBe('旧潮短刃');
|
||||
expect(readSnapshot().playerEquipment.relic).toBe('旧潮图残页');
|
||||
expect(readSnapshot().playerEquipment.armor).toBeTruthy();
|
||||
expect(readSnapshot().currentScenePresetId).toBe('custom-scene-camp');
|
||||
expect(readSnapshot().currentSceneActId).toBe('act-1');
|
||||
expect(readSnapshot().currentEncounterId).toBe('story-act-only');
|
||||
expect(readSnapshot().currentEncounterName).toBe('陆衡');
|
||||
expect(readSnapshot().currentEncounterId).not.toBe('story-primary-only');
|
||||
await waitFor(() => {
|
||||
expect(readSnapshot().currentStoryNpcName).toBe('陆衡');
|
||||
});
|
||||
expect(readSnapshot().currentStoryDisplayMode).toBe('dialogue');
|
||||
expect(readSnapshot().currentStoryDialogueTexts).toContain(
|
||||
'开盘前别靠近账本。你先告诉我,是谁让你来查这笔异常资金?',
|
||||
);
|
||||
expect(aiServiceMocks.streamNpcChatTurn).toHaveBeenCalledWith(
|
||||
WorldType.CUSTOM,
|
||||
expect.objectContaining({ name: '沈砺' }),
|
||||
expect.objectContaining({ id: 'story-act-only', npcName: '陆衡' }),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
[],
|
||||
'',
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
npcInitiatesConversation: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user