This commit is contained in:
2026-04-28 02:05:12 +08:00
parent 271db02e4a
commit 1eb090e4a5
39 changed files with 2671 additions and 165 deletions

View File

@@ -14,6 +14,7 @@ import { getMonsterPresetsByWorld } from './hostileNpcPresets';
import { createSceneHostileNpc } from './hostileNpcs';
import { buildInitialNpcState } from './npcInteractions';
import {
buildNpcBattleFormationFromEncounter,
createSceneEncounterPreview,
hasAutoBattleSceneEncounter,
resolveSceneEncounterPreview,
@@ -495,4 +496,371 @@ describe('sceneEncounterPreviews', () => {
expect(resolved.inBattle).toBe(false);
expect(resolved.currentEncounter?.id).toBe('npc-hostile-opposite');
});
it('builds active act npc battle formations with stable back-row slots', () => {
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: [
{
id: 'npc-front',
name: '正面对手',
title: '刀客',
description: '正面对手',
initialAffinity: -30,
imageSrc: '',
role: '敌对角色',
backstory: '',
personality: '',
motivation: '',
combatStyle: '',
relationshipHooks: [],
tags: [],
skills: [],
initialItems: [],
},
{
id: 'npc-back-1',
name: '后排甲',
title: '弓手',
description: '后排甲',
initialAffinity: -25,
imageSrc: '',
role: '敌对角色',
backstory: '',
personality: '',
motivation: '',
combatStyle: '',
relationshipHooks: [],
tags: [],
skills: [],
initialItems: [],
},
{
id: 'npc-back-2',
name: '后排乙',
title: '术士',
description: '后排乙',
initialAffinity: -20,
imageSrc: '',
role: '敌对角色',
backstory: '',
personality: '',
motivation: '',
combatStyle: '',
relationshipHooks: [],
tags: [],
skills: [],
initialItems: [],
},
],
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,
currentScenePreset: {
id: 'landmark-raw-1',
name: '旧桥',
description: '旧桥',
imageSrc: '/bridge.png',
connectedSceneIds: [],
treasureHints: [],
npcs: [
{
id: 'npc-front',
name: '正面对手',
description: '正面对手',
avatar: '正',
role: '敌对角色',
initialAffinity: -30,
hostile: true,
attributeProfile: {
attributes: {},
combat: {
maxHp: 96,
attack: 12,
defense: 8,
speed: 10,
},
} as SceneNpc['attributeProfile'],
},
{
id: 'npc-back-1',
name: '后排甲',
description: '后排甲',
avatar: '甲',
role: '敌对角色',
initialAffinity: -25,
hostile: true,
attributeProfile: {
attributes: {},
combat: {
maxHp: 82,
attack: 10,
defense: 6,
speed: 9,
},
} as SceneNpc['attributeProfile'],
},
{
id: 'npc-back-2',
name: '后排乙',
description: '后排乙',
avatar: '乙',
role: '敌对角色',
initialAffinity: -20,
hostile: true,
attributeProfile: {
attributes: {},
combat: {
maxHp: 78,
attack: 9,
defense: 5,
speed: 11,
},
} as SceneNpc['attributeProfile'],
},
] satisfies SceneNpc[],
},
currentEncounter: {
id: 'npc-front',
kind: 'npc',
npcName: '正面对手',
npcDescription: '正面对手',
npcAvatar: '正',
context: '敌对角色',
hostile: true,
initialAffinity: -30,
xMeters: 3.2,
},
npcStates: {
'npc-front': {
...buildInitialNpcState(createEncounter(), WorldType.CUSTOM),
affinity: -30,
},
'npc-back-1': {
...buildInitialNpcState(createEncounter(), WorldType.CUSTOM),
affinity: -25,
},
'npc-back-2': {
...buildInitialNpcState(createEncounter(), WorldType.CUSTOM),
affinity: -20,
},
},
} satisfies GameState;
const formation = buildNpcBattleFormationFromEncounter({
state,
encounter: state.currentEncounter!,
});
expect(formation).toHaveLength(3);
expect(formation.map((monster) => monster.encounter?.id)).toEqual([
'npc-front',
'npc-back-1',
'npc-back-2',
]);
expect(
formation.map((monster) => ({
id: monster.encounter?.id,
xMeters: monster.xMeters,
yOffset: monster.yOffset,
})),
).toEqual([
{ id: 'npc-front', xMeters: 3.2, yOffset: 0 },
{ id: 'npc-back-1', xMeters: 4.28, yOffset: 62 },
{ id: 'npc-back-2', xMeters: 4.28, yOffset: -46 },
]);
});
it('keeps scene-act formation order even when the clicked encounter comes from the back row', () => {
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-front',
oppositeNpcId: 'npc-front',
eventDescription: '',
linkedThreadIds: [],
advanceRule: 'after_primary_contact',
actGoal: '',
transitionHook: '',
},
],
},
],
} as CustomWorldProfile,
currentScenePreset: {
id: 'landmark-raw-1',
name: '海底遗址',
description: '海底遗址',
imageSrc: '/underwater.png',
connectedSceneIds: [],
treasureHints: [],
npcs: [
{
id: 'npc-front',
name: '珊瑚祭司',
description: '前排祭司',
avatar: '祭',
role: '敌对角色',
initialAffinity: -20,
hostile: true,
},
{
id: 'npc-back-1',
name: '赤发护卫',
description: '后排护卫',
avatar: '卫',
role: '敌对角色',
initialAffinity: -20,
hostile: true,
},
{
id: 'npc-back-2',
name: '潮歌侍从',
description: '后排侍从',
avatar: '侍',
role: '敌对角色',
initialAffinity: -20,
hostile: true,
},
] satisfies SceneNpc[],
},
currentEncounter: {
id: 'npc-back-1',
kind: 'npc',
npcName: '赤发护卫',
npcDescription: '后排护卫',
npcAvatar: '卫',
context: '敌对角色',
hostile: true,
initialAffinity: -20,
xMeters: 4.28,
},
npcStates: {
'npc-front': {
...buildInitialNpcState(createEncounter(), WorldType.CUSTOM),
affinity: -20,
},
'npc-back-1': {
...buildInitialNpcState(createEncounter(), WorldType.CUSTOM),
affinity: -20,
},
'npc-back-2': {
...buildInitialNpcState(createEncounter(), WorldType.CUSTOM),
affinity: -20,
},
},
} satisfies GameState;
const formation = buildNpcBattleFormationFromEncounter({
state,
encounter: state.currentEncounter!,
});
expect(
formation.map((monster) => ({
id: monster.encounter?.id,
xMeters: monster.xMeters,
yOffset: monster.yOffset,
})),
).toEqual([
{ id: 'npc-front', xMeters: 3.2, yOffset: 0 },
{ id: 'npc-back-1', xMeters: 4.28, yOffset: 62 },
{ id: 'npc-back-2', xMeters: 4.28, yOffset: -46 },
]);
});
});