This commit is contained in:
2026-04-27 22:50:18 +08:00
parent ded6f6ee2a
commit b6c6640548
77 changed files with 5240 additions and 833 deletions

View File

@@ -72,7 +72,9 @@ function buildBackstoryReveal(label: string) {
};
}
function buildSavedProfile() {
function buildSavedProfile(options: {
openingOppositeNpcId?: string;
} = {}) {
const profile = normalizeCustomWorldProfileRecord({
id: 'saved-runtime-profile',
settingText: '被海雾吞没的旧航路群岛',
@@ -318,9 +320,9 @@ function buildSavedProfile() {
title: '第一幕',
summary: '陆衡先开口试探玩家。',
stageCoverage: ['opening'],
encounterNpcIds: ['story-primary-only', 'story-act-only'],
primaryNpcId: 'story-primary-only',
oppositeNpcId: 'story-act-only',
encounterNpcIds: ['沈砺旧识', '陆衡'],
primaryNpcId: '沈砺旧识',
oppositeNpcId: options.openingOppositeNpcId ?? '陆衡',
eventDescription: '陆衡拿着异常账本,在开盘前拦住玩家。',
linkedThreadIds: [],
advanceRule: 'after_primary_contact',
@@ -389,6 +391,8 @@ function readSnapshot() {
isStoryLoading: boolean;
firstLandmarkResidueTitle: string | null;
playerCharacterName: string | null;
runtimeMode: string | null;
runtimePersistenceDisabled: boolean;
playerInventoryNames: string[];
playerEquipment: {
weapon: string | null;
@@ -398,8 +402,15 @@ function readSnapshot() {
};
}
function GameFlowHarness() {
const profile = useMemo(() => buildSavedProfile(), []);
function GameFlowHarness({
openingOppositeNpcId,
}: {
openingOppositeNpcId?: string;
} = {}) {
const profile = useMemo(
() => buildSavedProfile({ openingOppositeNpcId }),
[openingOppositeNpcId],
);
const playableCharacters = useMemo(
() => buildCustomWorldPlayableCharacters(profile),
[profile],
@@ -441,6 +452,8 @@ function GameFlowHarness() {
gameState.customWorldProfile?.landmarks[0]?.narrativeResidues?.[0]
?.title ?? null,
playerCharacterName: gameState.playerCharacter?.name ?? null,
runtimeMode: gameState.runtimeMode ?? null,
runtimePersistenceDisabled: gameState.runtimePersistenceDisabled === true,
playerInventoryNames: gameState.playerInventory.map((item) => item.name),
playerEquipment: {
weapon: gameState.playerEquipment.weapon?.name ?? null,
@@ -515,6 +528,8 @@ test('saved custom world result settings flow into game state after entering the
});
expect(readSnapshot().playerCharacterName).toBe('沈砺');
expect(readSnapshot().runtimeMode).toBe('test');
expect(readSnapshot().runtimePersistenceDisabled).toBe(true);
expect(readSnapshot().playerInventoryNames).toContain('旧潮短刃');
expect(readSnapshot().playerInventoryNames).toContain('旧潮图残页');
expect(readSnapshot().playerEquipment.weapon).toBe('旧潮短刃');
@@ -547,3 +562,38 @@ test('saved custom world result settings flow into game state after entering the
}),
);
});
test('custom world opening act accepts runtime npc id references and still starts configured npc chat', async () => {
const user = userEvent.setup();
render(<GameFlowHarness openingOppositeNpcId="character-npc-story-act-only" />);
await user.click(screen.getByRole('button', { name: '选择世界' }));
await waitFor(() => {
expect(readSnapshot().worldType).toBe(WorldType.CUSTOM);
});
await user.click(screen.getByRole('button', { name: '确认角色' }));
await waitFor(() => {
expect(readSnapshot().currentEncounterId).toBe('story-act-only');
});
expect(readSnapshot().currentEncounterName).toBe('陆衡');
await waitFor(() => {
expect(readSnapshot().currentStoryNpcName).toBe('陆衡');
});
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,
}),
);
});