1
This commit is contained in:
@@ -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 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user