Files
Genarrative/src/data/questFlow.test.ts
kdletters cbc27bad4a
Some checks failed
CI / verify (push) Has been cancelled
init with react+axum+spacetimedb
2026-04-26 18:06:23 +08:00

276 lines
8.3 KiB
TypeScript

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(
'拿旧金牌去对问侍女',
);
});
});