This commit is contained in:
275
src/data/questFlow.test.ts
Normal file
275
src/data/questFlow.test.ts
Normal file
@@ -0,0 +1,275 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import type { QuestLogEntry, QuestStep, ScenePresetInfo } from '../types';
|
||||
import { WorldType } from '../types';
|
||||
import {
|
||||
applyQuestProgressFromHostileNpcDefeat,
|
||||
applyQuestProgressFromNpcTalk,
|
||||
buildChapterQuestForScene,
|
||||
buildQuestForEncounter,
|
||||
isQuestReadyToClaim,
|
||||
normalizeQuestLogEntries,
|
||||
} from './questFlow';
|
||||
|
||||
const TEST_SCENE = {
|
||||
id: 'forest_path',
|
||||
name: 'Forest Path',
|
||||
description: 'A narrow trail with fresh claw marks.',
|
||||
npcs: [
|
||||
{
|
||||
id: 'hostile-wolf-alpha',
|
||||
name: '狼王',
|
||||
description: 'A hostile wolf alpha.',
|
||||
avatar: '狼',
|
||||
role: '敌对角色',
|
||||
monsterPresetId: 'wolf_alpha',
|
||||
initialAffinity: -40,
|
||||
hostile: true,
|
||||
},
|
||||
],
|
||||
treasureHints: [],
|
||||
} satisfies Pick<
|
||||
ScenePresetInfo,
|
||||
'id' | 'name' | 'description' | 'npcs' | 'treasureHints'
|
||||
>;
|
||||
|
||||
const CHAPTER_SCENE = {
|
||||
id: 'palace_court',
|
||||
name: '宫苑内庭',
|
||||
description: '回廊深处静得过分,花木修得齐整,却处处像埋着王庭旧案。',
|
||||
npcs: [
|
||||
{
|
||||
id: 'npc-maid',
|
||||
name: '旧宫侍女',
|
||||
description: '她总知道哪条回廊最近不该过去。',
|
||||
avatar: '侍',
|
||||
role: '宫人',
|
||||
initialAffinity: 8,
|
||||
hostile: false,
|
||||
},
|
||||
{
|
||||
id: 'hostile-guard',
|
||||
name: '旧宫戍影',
|
||||
description: '巡行在回廊深处的敌影。',
|
||||
avatar: '戍',
|
||||
role: '敌对角色',
|
||||
monsterPresetId: 'monster-11',
|
||||
initialAffinity: -40,
|
||||
hostile: true,
|
||||
},
|
||||
],
|
||||
treasureHints: ['回廊暗格里的香囊', '花圃石座下的旧金牌'],
|
||||
} satisfies Pick<
|
||||
ScenePresetInfo,
|
||||
'id' | 'name' | 'description' | 'npcs' | 'treasureHints'
|
||||
>;
|
||||
|
||||
const OVERRIDDEN_SCENE = {
|
||||
id: 'wuxia-palace-court',
|
||||
name: '宫苑内庭',
|
||||
description: '回廊深处静得过分,花木修得齐整,却处处像埋着王庭旧案。',
|
||||
npcs: [
|
||||
{
|
||||
id: 'wuxia-npc-maid',
|
||||
name: '旧宫侍女',
|
||||
description: '嘴上说得少,却总知道哪条回廊最近不该过去。',
|
||||
avatar: '侍',
|
||||
role: '宫人',
|
||||
initialAffinity: 8,
|
||||
hostile: false,
|
||||
},
|
||||
{
|
||||
id: 'hostile-guard',
|
||||
name: '旧宫戍影',
|
||||
description: '巡行在回廊深处的敌影。',
|
||||
avatar: '戍',
|
||||
role: '敌对角色',
|
||||
monsterPresetId: 'monster-11',
|
||||
initialAffinity: -40,
|
||||
hostile: true,
|
||||
},
|
||||
],
|
||||
treasureHints: ['回廊暗格里的香囊', '花圃石座下的旧金牌'],
|
||||
} satisfies Pick<
|
||||
ScenePresetInfo,
|
||||
'id' | 'name' | 'description' | 'npcs' | 'treasureHints'
|
||||
>;
|
||||
|
||||
function requireStep(quest: QuestLogEntry, stepId: string): QuestStep {
|
||||
const step = quest.steps?.find((item) => item.id === stepId);
|
||||
expect(step).toBeTruthy();
|
||||
return step!;
|
||||
}
|
||||
|
||||
describe('questFlow', () => {
|
||||
it('builds a staged quest contract for an encounter preview', () => {
|
||||
const quest = buildQuestForEncounter({
|
||||
issuerNpcId: 'npc_scout',
|
||||
issuerNpcName: 'Scout Lin',
|
||||
roleText: 'tracker',
|
||||
scene: TEST_SCENE,
|
||||
worldType: WorldType.WUXIA,
|
||||
currentQuests: [],
|
||||
});
|
||||
|
||||
expect(quest).toBeTruthy();
|
||||
expect(quest?.steps).toHaveLength(2);
|
||||
expect(quest?.objective.kind).toBe('defeat_hostile_npc');
|
||||
expect(requireStep(quest!, 'step_primary').kind).toBe('defeat_hostile_npc');
|
||||
expect(requireStep(quest!, 'step_report_back').kind).toBe('talk_to_npc');
|
||||
expect(quest?.status).toBe('active');
|
||||
expect(quest?.reward.experience).toBeGreaterThan(0);
|
||||
expect(quest?.rewardText).toContain('经验 +');
|
||||
expect(quest?.reward.items[0]?.runtimeMetadata?.generationChannel).toBe(
|
||||
'quest_reward',
|
||||
);
|
||||
});
|
||||
|
||||
it('advances from primary objective to report-back step and then reward-ready', () => {
|
||||
const quest = buildQuestForEncounter({
|
||||
issuerNpcId: 'npc_scout',
|
||||
issuerNpcName: 'Scout Lin',
|
||||
roleText: 'tracker',
|
||||
scene: TEST_SCENE,
|
||||
worldType: WorldType.WUXIA,
|
||||
currentQuests: [],
|
||||
});
|
||||
expect(quest).toBeTruthy();
|
||||
|
||||
const afterBattle = applyQuestProgressFromHostileNpcDefeat(
|
||||
[quest!],
|
||||
TEST_SCENE.id,
|
||||
['wolf_alpha'],
|
||||
)[0];
|
||||
expect(afterBattle?.objective.kind).toBe('talk_to_npc');
|
||||
expect(afterBattle?.status).toBe('active');
|
||||
|
||||
const afterReport = applyQuestProgressFromNpcTalk(
|
||||
[afterBattle!],
|
||||
'npc_scout',
|
||||
)[0];
|
||||
expect(afterReport?.status).toBe('ready_to_turn_in');
|
||||
expect(isQuestReadyToClaim(afterReport!)).toBe(true);
|
||||
});
|
||||
|
||||
it('normalizes legacy single-objective quests into step-aware entries', () => {
|
||||
const normalized = normalizeQuestLogEntries([
|
||||
{
|
||||
id: 'legacy',
|
||||
issuerNpcId: 'npc_scout',
|
||||
issuerNpcName: 'Scout Lin',
|
||||
sceneId: TEST_SCENE.id,
|
||||
title: 'Legacy Quest',
|
||||
description: 'Legacy description',
|
||||
summary: 'Legacy summary',
|
||||
objective: {
|
||||
kind: 'inspect_treasure',
|
||||
targetSceneId: TEST_SCENE.id,
|
||||
requiredCount: 1,
|
||||
},
|
||||
progress: 1,
|
||||
status: 'completed',
|
||||
completionNotified: false,
|
||||
reward: {
|
||||
affinityBonus: 10,
|
||||
currency: 20,
|
||||
experience: 0,
|
||||
items: [],
|
||||
},
|
||||
rewardText: 'Legacy reward text',
|
||||
},
|
||||
])[0];
|
||||
|
||||
expect(normalized?.steps).toHaveLength(1);
|
||||
expect(normalized?.steps?.[0]?.kind).toBe('inspect_treasure');
|
||||
expect(normalized?.status).toBe('completed');
|
||||
expect(normalized?.progress).toBe(1);
|
||||
});
|
||||
|
||||
it('builds a scene chapter quest that reuses staged quest steps', () => {
|
||||
const quest = buildChapterQuestForScene({
|
||||
scene: CHAPTER_SCENE,
|
||||
worldType: WorldType.WUXIA,
|
||||
});
|
||||
|
||||
expect(quest).toBeTruthy();
|
||||
expect(quest?.chapterId).toBe('chapter:scene:palace_court');
|
||||
expect(quest?.sceneId).toBe('palace_court');
|
||||
expect(quest?.reward.experience).toBeGreaterThan(0);
|
||||
expect(quest?.steps?.map((step) => step.kind)).toEqual([
|
||||
'talk_to_npc',
|
||||
'defeat_hostile_npc',
|
||||
'talk_to_npc',
|
||||
]);
|
||||
});
|
||||
|
||||
it('uses sceneTaskDescription as first-entry chapter quest context', () => {
|
||||
const quest = buildChapterQuestForScene({
|
||||
scene: CHAPTER_SCENE,
|
||||
worldType: WorldType.WUXIA,
|
||||
sceneChapterContext: {
|
||||
sceneTaskDescription: '首次进入宫苑内庭时,追查旧宫戍影为何守住回廊暗格。',
|
||||
actEventDescriptions: ['旧宫侍女先指出回廊暗格。'],
|
||||
primaryNpcName: '旧宫侍女',
|
||||
},
|
||||
});
|
||||
|
||||
expect(quest?.description).toBe(
|
||||
'首次进入宫苑内庭时,追查旧宫戍影为何守住回廊暗格。',
|
||||
);
|
||||
expect(quest?.narrativeBinding.dramaticNeed).toBe(
|
||||
'首次进入宫苑内庭时,追查旧宫戍影为何守住回廊暗格。',
|
||||
);
|
||||
expect(quest?.narrativeBinding.playerHook).toContain('旧宫侍女');
|
||||
expect(quest?.narrativeBinding.followupHooks).toContain(
|
||||
'旧宫侍女先指出回廊暗格。',
|
||||
);
|
||||
});
|
||||
|
||||
it('lets scene chapter quests advance through npc talk and scene pressure steps', () => {
|
||||
const quest = buildChapterQuestForScene({
|
||||
scene: CHAPTER_SCENE,
|
||||
worldType: WorldType.WUXIA,
|
||||
});
|
||||
expect(quest).toBeTruthy();
|
||||
|
||||
const afterOpeningTalk = applyQuestProgressFromNpcTalk(
|
||||
[quest!],
|
||||
'npc-maid',
|
||||
)[0];
|
||||
expect(afterOpeningTalk?.objective.kind).toBe('defeat_hostile_npc');
|
||||
|
||||
const afterPressure = applyQuestProgressFromHostileNpcDefeat(
|
||||
[afterOpeningTalk!],
|
||||
CHAPTER_SCENE.id,
|
||||
['monster-11'],
|
||||
)[0];
|
||||
expect(afterPressure?.objective.kind).toBe('talk_to_npc');
|
||||
|
||||
const afterTurningTalk = applyQuestProgressFromNpcTalk(
|
||||
[afterPressure!],
|
||||
'npc-maid',
|
||||
)[0];
|
||||
expect(afterTurningTalk?.status).toBe('ready_to_turn_in');
|
||||
expect(isQuestReadyToClaim(afterTurningTalk!)).toBe(true);
|
||||
});
|
||||
|
||||
it('uses scene chapter overrides to prefer investigation beats on key scenes', () => {
|
||||
const quest = buildChapterQuestForScene({
|
||||
scene: OVERRIDDEN_SCENE,
|
||||
worldType: WorldType.WUXIA,
|
||||
});
|
||||
|
||||
expect(quest).toBeTruthy();
|
||||
expect(quest?.title).toBe('查清内庭旧痕');
|
||||
expect(requireStep(quest!, 'step_scene_pressure').kind).toBe(
|
||||
'inspect_treasure',
|
||||
);
|
||||
expect(requireStep(quest!, 'step_scene_pressure').title).toBe(
|
||||
'调查回廊暗格',
|
||||
);
|
||||
expect(requireStep(quest!, 'step_scene_turning').title).toBe(
|
||||
'拿旧金牌去对问侍女',
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user