Implement scene-based chapter quest progression
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-08 11:58:47 +08:00
parent 9d2fc9e4b8
commit bd9fdcbe31
170 changed files with 18259 additions and 1049 deletions

View File

@@ -1,6 +1,15 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { AFFINITY_BACKSTORY_CHAPTER_THRESHOLDS } from '../data/affinityLevels';
import { getScenePresetsByWorld } from '../data/scenePresets';
import type {
Character,
Encounter,
SceneHostileNpc,
StoryMoment,
StoryOption,
} from '../types';
import { AnimationState, WorldType } from '../types';
const {
connectivityError,
@@ -27,19 +36,12 @@ vi.mock('./llmClient', () => ({
streamPlainTextCompletion: streamPlainTextCompletionMock,
}));
import type {
Character,
Encounter,
SceneHostileNpc,
StoryMoment,
StoryOption,
} from '../types';
import { AnimationState, WorldType } from '../types';
import {
generateCharacterPanelChatSuggestions,
generateCustomWorldProfile,
generateCustomWorldSceneImage,
generateInitialStory,
generateNextStep,
streamCharacterPanelChatReply,
streamNpcRecruitDialogue,
} from './ai';
@@ -393,6 +395,123 @@ describe('ai orchestration fallbacks', () => {
expect(response.storyText.length).toBeGreaterThan(0);
});
it('repairs mixed-language story text before returning the story response', async () => {
const availableOptions = [
createStoryOption({
functionId: 'idle_explore_forward',
actionText: '继续沿山道探路。',
text: '继续沿山道探路。',
}),
];
requestChatMessageContentMock
.mockResolvedValueOnce(
JSON.stringify({
storyText: 'The forest is quiet. 你听见远处的风声。',
encounter: null,
options: [
{
functionId: 'idle_explore_forward',
actionText: 'Move forward carefully.',
},
],
}),
)
.mockResolvedValueOnce(
JSON.stringify({
storyText: '林间重新安静下来,你听见远处的风声。',
encounter: null,
options: [
{
functionId: 'idle_explore_forward',
actionText: '继续沿山道探路。',
},
],
}),
);
const response = await generateInitialStory(
WorldType.WUXIA,
playerCharacter,
monsters,
context,
{ availableOptions },
);
expect(response.storyText).toBe('林间重新安静下来,你听见远处的风声。');
expect(response.options[0]?.actionText).toBe('继续沿山道探路。');
expect(requestChatMessageContentMock).toHaveBeenCalledTimes(2);
expect(requestChatMessageContentMock.mock.calls[1]?.[2]).toEqual(
expect.objectContaining({
debugLabel: 'story-language-repair',
}),
);
});
it('ignores generated encounter payloads during post-battle continuations when no new scene encounter is pending', async () => {
const availableOptions = [
createStoryOption({
functionId: 'idle_explore_forward',
actionText: '先稳住呼吸,再看看前面的动静。',
text: '先稳住呼吸,再看看前面的动静。',
}),
];
const sceneWithNpc = getScenePresetsByWorld(WorldType.WUXIA).find(
(scene) => (scene.npcs?.length ?? 0) > 0,
);
const targetNpcId = sceneWithNpc?.npcs?.[0]?.id;
if (!sceneWithNpc || !targetNpcId) {
throw new Error('Expected a wuxia scene with at least one npc preset.');
}
requestChatMessageContentMock.mockResolvedValue(
JSON.stringify({
storyText: '山道总算安静下来,你收住气息,重新判断前路。',
encounter: {
kind: 'npc',
npcId: targetNpcId,
},
options: [
{
functionId: 'idle_explore_forward',
actionText: '先稳住呼吸,再看看前面的动静。',
},
],
}),
);
const response = await generateNextStep(
WorldType.WUXIA,
playerCharacter,
[],
[
{
text: '挥刀抢攻',
options: [],
historyRole: 'action',
},
{
text: '山道客已经败下阵来。',
options: [],
historyRole: 'result',
},
],
'挥刀抢攻',
createContext({
sceneId: sceneWithNpc.id,
sceneName: sceneWithNpc.name,
sceneDescription: sceneWithNpc.description,
pendingSceneEncounter: false,
}),
{ availableOptions },
);
expect(response.encounter).toBeUndefined();
expect(response.options).toEqual(availableOptions);
const userPrompt = requestChatMessageContentMock.mock.calls.at(-1)?.[1];
expect(userPrompt).toContain('encounter 必须为 null');
expect(userPrompt).toContain('战斗结束后的续写');
});
it('returns offline character chat suggestions when the plain-text client reports connectivity errors', async () => {
requestPlainTextCompletionMock.mockRejectedValue(connectivityError);