1
This commit is contained in:
@@ -1,16 +1,20 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { resolveActiveSceneActEncounterNpcIds } from '../services/customWorldSceneActRuntime';
|
||||
import {
|
||||
AnimationState,
|
||||
type Character,
|
||||
type CustomWorldProfile,
|
||||
type Encounter,
|
||||
type GameState,
|
||||
type SceneNpc,
|
||||
WorldType,
|
||||
} from '../types';
|
||||
import { getMonsterPresetsByWorld } from './hostileNpcPresets';
|
||||
import { createSceneHostileNpc } from './hostileNpcs';
|
||||
import { buildInitialNpcState } from './npcInteractions';
|
||||
import {
|
||||
createSceneEncounterPreview,
|
||||
hasAutoBattleSceneEncounter,
|
||||
resolveSceneEncounterPreview,
|
||||
} from './sceneEncounterPreviews';
|
||||
@@ -150,5 +154,345 @@ describe('sceneEncounterPreviews', () => {
|
||||
expect(monster?.encounter?.hostile).toBe(true);
|
||||
expect(monster?.encounter?.initialAffinity).toBe(-40);
|
||||
});
|
||||
});
|
||||
|
||||
it('resolves active act npc ids when runtime scene id differs from landmark id', () => {
|
||||
const profile = {
|
||||
id: 'custom-profile',
|
||||
name: '测试世界',
|
||||
settingText: '',
|
||||
subtitle: '',
|
||||
summary: '',
|
||||
tone: '',
|
||||
playerGoal: '',
|
||||
templateWorldType: WorldType.WUXIA,
|
||||
majorFactions: [],
|
||||
coreConflicts: [],
|
||||
attributeSchema: {
|
||||
attributes: [],
|
||||
},
|
||||
playableNpcs: [],
|
||||
storyNpcs: [],
|
||||
items: [],
|
||||
landmarks: [
|
||||
{
|
||||
id: 'landmark-raw-1',
|
||||
name: '旧桥',
|
||||
description: '旧桥',
|
||||
sceneNpcIds: ['npc-front', 'npc-back-1', 'npc-back-2'],
|
||||
connections: [],
|
||||
},
|
||||
],
|
||||
sceneChapterBlueprints: [
|
||||
{
|
||||
id: 'chapter-1',
|
||||
sceneId: 'landmark-raw-1',
|
||||
title: '旧桥章节',
|
||||
summary: '',
|
||||
sceneTaskDescription: '',
|
||||
linkedThreadIds: [],
|
||||
linkedLandmarkIds: ['landmark-raw-1'],
|
||||
acts: [
|
||||
{
|
||||
id: 'act-1',
|
||||
sceneId: 'landmark-raw-1',
|
||||
title: '第一幕',
|
||||
summary: '',
|
||||
stageCoverage: ['opening'],
|
||||
encounterNpcIds: ['npc-front', 'npc-back-1', 'npc-back-2'],
|
||||
primaryNpcId: 'npc-front',
|
||||
oppositeNpcId: 'npc-front',
|
||||
eventDescription: '',
|
||||
linkedThreadIds: [],
|
||||
advanceRule: 'after_primary_contact',
|
||||
actGoal: '',
|
||||
transitionHook: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
} as CustomWorldProfile;
|
||||
|
||||
expect(
|
||||
resolveActiveSceneActEncounterNpcIds({
|
||||
profile,
|
||||
sceneId: 'custom-scene-landmark-1',
|
||||
}),
|
||||
).toEqual(['npc-front', 'npc-back-1', 'npc-back-2']);
|
||||
});
|
||||
|
||||
it('resolves active act npc ids from act scene id even when chapter scene id is abstract', () => {
|
||||
const profile = {
|
||||
id: 'custom-profile',
|
||||
name: '测试世界',
|
||||
settingText: '',
|
||||
subtitle: '',
|
||||
summary: '',
|
||||
tone: '',
|
||||
playerGoal: '',
|
||||
templateWorldType: WorldType.WUXIA,
|
||||
majorFactions: [],
|
||||
coreConflicts: [],
|
||||
attributeSchema: {
|
||||
attributes: [],
|
||||
},
|
||||
playableNpcs: [],
|
||||
storyNpcs: [],
|
||||
items: [],
|
||||
landmarks: [
|
||||
{
|
||||
id: 'landmark-raw-1',
|
||||
name: '旧桥',
|
||||
description: '旧桥',
|
||||
sceneNpcIds: [],
|
||||
connections: [],
|
||||
},
|
||||
],
|
||||
sceneChapterBlueprints: [
|
||||
{
|
||||
id: 'chapter-1',
|
||||
sceneId: 'chapter-abstract-scene',
|
||||
title: '旧桥章节',
|
||||
summary: '',
|
||||
sceneTaskDescription: '',
|
||||
linkedThreadIds: [],
|
||||
linkedLandmarkIds: [],
|
||||
acts: [
|
||||
{
|
||||
id: 'act-1',
|
||||
sceneId: 'landmark-raw-1',
|
||||
title: '第一幕',
|
||||
summary: '',
|
||||
stageCoverage: ['opening'],
|
||||
encounterNpcIds: [],
|
||||
primaryNpcId: '',
|
||||
oppositeNpcId: 'npc-front',
|
||||
eventDescription: '',
|
||||
linkedThreadIds: [],
|
||||
advanceRule: 'after_primary_contact',
|
||||
actGoal: '',
|
||||
transitionHook: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
} as CustomWorldProfile;
|
||||
|
||||
expect(
|
||||
resolveActiveSceneActEncounterNpcIds({
|
||||
profile,
|
||||
sceneId: 'custom-scene-landmark-1',
|
||||
}),
|
||||
).toEqual(['npc-front']);
|
||||
});
|
||||
|
||||
it('uses the active act opposite npc as the formal scene encounter', () => {
|
||||
const state = {
|
||||
...createBaseState(),
|
||||
worldType: WorldType.CUSTOM,
|
||||
customWorldProfile: {
|
||||
id: 'custom-profile',
|
||||
name: '测试世界',
|
||||
settingText: '',
|
||||
subtitle: '',
|
||||
summary: '',
|
||||
tone: '',
|
||||
playerGoal: '',
|
||||
templateWorldType: WorldType.WUXIA,
|
||||
majorFactions: [],
|
||||
coreConflicts: [],
|
||||
attributeSchema: {
|
||||
attributes: [],
|
||||
},
|
||||
playableNpcs: [],
|
||||
storyNpcs: [],
|
||||
items: [],
|
||||
landmarks: [
|
||||
{
|
||||
id: 'landmark-raw-1',
|
||||
name: '旧桥',
|
||||
description: '旧桥',
|
||||
sceneNpcIds: ['npc-front', 'npc-back-1', 'npc-back-2'],
|
||||
connections: [],
|
||||
},
|
||||
],
|
||||
sceneChapterBlueprints: [
|
||||
{
|
||||
id: 'chapter-1',
|
||||
sceneId: 'landmark-raw-1',
|
||||
title: '旧桥章节',
|
||||
summary: '',
|
||||
sceneTaskDescription: '',
|
||||
linkedThreadIds: [],
|
||||
linkedLandmarkIds: ['landmark-raw-1'],
|
||||
acts: [
|
||||
{
|
||||
id: 'act-1',
|
||||
sceneId: 'landmark-raw-1',
|
||||
title: '第一幕',
|
||||
summary: '',
|
||||
stageCoverage: ['opening'],
|
||||
encounterNpcIds: ['npc-front', 'npc-back-1', 'npc-back-2'],
|
||||
primaryNpcId: 'npc-back-1',
|
||||
oppositeNpcId: 'npc-front',
|
||||
eventDescription: '',
|
||||
linkedThreadIds: [],
|
||||
advanceRule: 'after_primary_contact',
|
||||
actGoal: '',
|
||||
transitionHook: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
} as CustomWorldProfile,
|
||||
currentEncounter: null,
|
||||
currentScenePreset: {
|
||||
id: 'custom-scene-landmark-1',
|
||||
name: '旧桥',
|
||||
description: '旧桥',
|
||||
imageSrc: '/bridge.png',
|
||||
connectedSceneIds: [],
|
||||
npcs: [
|
||||
{
|
||||
id: 'hostile-side',
|
||||
name: '旁路敌人',
|
||||
description: '旁路敌人',
|
||||
avatar: '敌',
|
||||
role: '敌对角色',
|
||||
monsterPresetId: 'monster-01',
|
||||
initialAffinity: -40,
|
||||
hostile: true,
|
||||
},
|
||||
{
|
||||
id: 'npc-back-1',
|
||||
name: '后排甲',
|
||||
description: '后排甲',
|
||||
avatar: '甲',
|
||||
role: '同幕角色',
|
||||
},
|
||||
{
|
||||
id: 'npc-front',
|
||||
name: '主角色',
|
||||
description: '主角色',
|
||||
avatar: '主',
|
||||
role: '主角色',
|
||||
},
|
||||
{
|
||||
id: 'npc-back-2',
|
||||
name: '后排乙',
|
||||
description: '后排乙',
|
||||
avatar: '乙',
|
||||
role: '同幕角色',
|
||||
},
|
||||
] satisfies SceneNpc[],
|
||||
treasureHints: [],
|
||||
},
|
||||
} satisfies GameState;
|
||||
|
||||
const preview = createSceneEncounterPreview(state);
|
||||
|
||||
expect(preview.currentEncounter?.id).toBe('npc-front');
|
||||
expect(preview.currentEncounter?.npcName).toBe('主角色');
|
||||
});
|
||||
|
||||
it('uses active act opposite npc even when that npc is hostile', () => {
|
||||
const state = {
|
||||
...createBaseState(),
|
||||
worldType: WorldType.CUSTOM,
|
||||
customWorldProfile: {
|
||||
id: 'custom-profile',
|
||||
name: '测试世界',
|
||||
settingText: '',
|
||||
subtitle: '',
|
||||
summary: '',
|
||||
tone: '',
|
||||
playerGoal: '',
|
||||
templateWorldType: WorldType.WUXIA,
|
||||
majorFactions: [],
|
||||
coreConflicts: [],
|
||||
attributeSchema: {
|
||||
attributes: [],
|
||||
},
|
||||
playableNpcs: [],
|
||||
storyNpcs: [],
|
||||
items: [],
|
||||
landmarks: [],
|
||||
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: ['npc-hostile-opposite', 'npc-back'],
|
||||
primaryNpcId: 'npc-back',
|
||||
oppositeNpcId: 'npc-hostile-opposite',
|
||||
eventDescription: '',
|
||||
linkedThreadIds: [],
|
||||
advanceRule: 'after_primary_contact',
|
||||
actGoal: '',
|
||||
transitionHook: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
} as CustomWorldProfile,
|
||||
currentEncounter: null,
|
||||
currentScenePreset: {
|
||||
id: 'custom-scene-camp',
|
||||
name: '营地',
|
||||
description: '营地',
|
||||
imageSrc: '/camp.png',
|
||||
connectedSceneIds: [],
|
||||
npcs: [
|
||||
{
|
||||
id: 'npc-hostile-opposite',
|
||||
name: '敌意对面角色',
|
||||
description: '第一幕先开口的敌意角色',
|
||||
avatar: '敌',
|
||||
role: '第一幕对面角色',
|
||||
initialAffinity: -20,
|
||||
hostile: true,
|
||||
},
|
||||
{
|
||||
id: 'npc-back',
|
||||
name: '后排角色',
|
||||
description: '同幕后排角色',
|
||||
avatar: '后',
|
||||
role: '同幕角色',
|
||||
},
|
||||
] satisfies SceneNpc[],
|
||||
treasureHints: [],
|
||||
},
|
||||
} satisfies GameState;
|
||||
|
||||
const preview = createSceneEncounterPreview(state);
|
||||
const resolved = resolveSceneEncounterPreview({
|
||||
...state,
|
||||
...preview,
|
||||
npcStates: {
|
||||
'npc-hostile-opposite': {
|
||||
...buildInitialNpcState(
|
||||
preview.currentEncounter!,
|
||||
WorldType.CUSTOM,
|
||||
state,
|
||||
),
|
||||
affinity: -20,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(preview.currentEncounter?.id).toBe('npc-hostile-opposite');
|
||||
expect(preview.currentEncounter?.npcName).toBe('敌意对面角色');
|
||||
expect(resolved.inBattle).toBe(false);
|
||||
expect(resolved.currentEncounter?.id).toBe('npc-hostile-opposite');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user