1
This commit is contained in:
@@ -73,6 +73,51 @@ describe('normalizeCustomWorldProfileRecord role asset descriptions', () => {
|
||||
expect(profile?.sceneChapterBlueprints?.[0]?.acts).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('把幕配置里的角色名归一到真实角色 id', () => {
|
||||
const profile = normalizeCustomWorldProfileRecord({
|
||||
name: '雾港归航',
|
||||
settingText: '海雾旧案',
|
||||
playableNpcs: [
|
||||
{
|
||||
id: 'playable-cendeng',
|
||||
name: '岑灯',
|
||||
title: '返乡守灯人',
|
||||
role: '主角代理',
|
||||
},
|
||||
],
|
||||
storyNpcs: [
|
||||
{
|
||||
id: 'story-luheng',
|
||||
name: '陆衡',
|
||||
title: '航运公会审计员',
|
||||
role: '第一幕主NPC',
|
||||
},
|
||||
],
|
||||
sceneChapterBlueprints: [
|
||||
{
|
||||
id: 'scene-chapter-1',
|
||||
sceneId: 'custom-scene-camp',
|
||||
title: '开局章节',
|
||||
acts: [
|
||||
{
|
||||
id: 'act-1',
|
||||
title: '第一幕',
|
||||
summary: '陆衡先拦住玩家。',
|
||||
encounterNpcIds: ['陆衡'],
|
||||
primaryNpcId: '航运公会审计员',
|
||||
oppositeNpcId: '陆衡',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const act = profile?.sceneChapterBlueprints?.[0]?.acts[0];
|
||||
expect(act?.encounterNpcIds).toEqual(['story-luheng']);
|
||||
expect(act?.primaryNpcId).toBe('story-luheng');
|
||||
expect(act?.oppositeNpcId).toBe('story-luheng');
|
||||
});
|
||||
|
||||
it('直接读取 Rust 草稿角色字段和形象资源', () => {
|
||||
const profile = normalizeCustomWorldProfileRecord({
|
||||
name: '雾港归航',
|
||||
@@ -121,4 +166,3 @@ describe('normalizeCustomWorldProfileRecord role asset descriptions', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
normalizeCustomWorldLockState,
|
||||
} from '../services/customWorldCreatorIntent';
|
||||
import { normalizeCustomWorldOwnedSettingLayers } from '../services/customWorldOwnedSettingLayers';
|
||||
import { resolveCustomWorldRoleIdReference } from '../services/customWorldRoleReferences';
|
||||
import {
|
||||
AnimationState,
|
||||
CharacterAnimationConfig,
|
||||
@@ -971,18 +972,30 @@ function normalizeSceneActBlueprint(
|
||||
value: unknown,
|
||||
index: number,
|
||||
sceneId: string,
|
||||
profileRoles?: {
|
||||
playableNpcs: CustomWorldPlayableNpc[];
|
||||
storyNpcs: CustomWorldNpc[];
|
||||
} | null,
|
||||
): SceneActBlueprint | null {
|
||||
if (!isRecord(value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const encounterNpcIds = toStringArray(value.encounterNpcIds);
|
||||
const encounterNpcIds = toStringArray(value.encounterNpcIds).map((npcId) =>
|
||||
resolveCustomWorldRoleIdReference(profileRoles, npcId),
|
||||
);
|
||||
const stageCoverage = normalizeSceneActStageCoverage(value.stageCoverage);
|
||||
const advanceRule = toText(value.advanceRule);
|
||||
const title = toText(value.title);
|
||||
const summary = toText(value.summary);
|
||||
const primaryNpcId = toText(value.primaryNpcId, encounterNpcIds[0] ?? '');
|
||||
const oppositeNpcId = toText(value.oppositeNpcId, primaryNpcId);
|
||||
const primaryNpcId = resolveCustomWorldRoleIdReference(
|
||||
profileRoles,
|
||||
toText(value.primaryNpcId, encounterNpcIds[0] ?? ''),
|
||||
);
|
||||
const oppositeNpcId = resolveCustomWorldRoleIdReference(
|
||||
profileRoles,
|
||||
toText(value.oppositeNpcId, primaryNpcId),
|
||||
);
|
||||
|
||||
if (!title && !summary && encounterNpcIds.length === 0) {
|
||||
return null;
|
||||
@@ -1020,7 +1033,13 @@ function normalizeSceneActBlueprint(
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeSceneChapterBlueprints(value: unknown) {
|
||||
function normalizeSceneChapterBlueprints(
|
||||
value: unknown,
|
||||
profileRoles?: {
|
||||
playableNpcs: CustomWorldPlayableNpc[];
|
||||
storyNpcs: CustomWorldNpc[];
|
||||
} | null,
|
||||
) {
|
||||
if (!Array.isArray(value)) {
|
||||
return null;
|
||||
}
|
||||
@@ -1036,7 +1055,12 @@ function normalizeSceneChapterBlueprints(value: unknown) {
|
||||
const acts = Array.isArray(entry.acts)
|
||||
? entry.acts
|
||||
.map((act, actIndex) =>
|
||||
normalizeSceneActBlueprint(act, actIndex, sceneId),
|
||||
normalizeSceneActBlueprint(
|
||||
act,
|
||||
actIndex,
|
||||
sceneId,
|
||||
profileRoles,
|
||||
),
|
||||
)
|
||||
.filter((act): act is SceneActBlueprint => Boolean(act))
|
||||
: [];
|
||||
@@ -1126,6 +1150,11 @@ function normalizeProfile(value: unknown): CustomWorldProfile | null {
|
||||
.filter((entry): entry is CustomWorldLandmarkDraft => Boolean(entry))
|
||||
: [];
|
||||
|
||||
const playableNpcs = Array.isArray(value.playableNpcs)
|
||||
? value.playableNpcs
|
||||
.map((entry, index) => normalizePlayableNpc(entry, index))
|
||||
.filter((entry): entry is CustomWorldPlayableNpc => Boolean(entry))
|
||||
: [];
|
||||
const normalizedProfile = {
|
||||
id: toText(value.id, `saved-custom-world-${Date.now().toString(36)}`),
|
||||
settingText,
|
||||
@@ -1144,11 +1173,7 @@ function normalizeProfile(value: unknown): CustomWorldProfile | null {
|
||||
value.attributeSchema,
|
||||
generatedAttributeSchema,
|
||||
),
|
||||
playableNpcs: Array.isArray(value.playableNpcs)
|
||||
? value.playableNpcs
|
||||
.map((entry, index) => normalizePlayableNpc(entry, index))
|
||||
.filter((entry): entry is CustomWorldPlayableNpc => Boolean(entry))
|
||||
: [],
|
||||
playableNpcs,
|
||||
storyNpcs,
|
||||
items: Array.isArray(value.items)
|
||||
? value.items
|
||||
@@ -1168,6 +1193,10 @@ function normalizeProfile(value: unknown): CustomWorldProfile | null {
|
||||
preserveStructuredRecordArray<ThreadContract>(value.threadContracts),
|
||||
sceneChapterBlueprints: normalizeSceneChapterBlueprints(
|
||||
value.sceneChapterBlueprints,
|
||||
{
|
||||
playableNpcs,
|
||||
storyNpcs,
|
||||
},
|
||||
),
|
||||
anchorContent: preserveStructuredRecord<EightAnchorContent>(
|
||||
value.anchorContent,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { resolveCustomWorldRoleIdReference } from '../services/customWorldRoleReferences';
|
||||
import {
|
||||
canUseLimitedPrimaryNpcChat,
|
||||
resolveActiveSceneActEncounterFocusNpcId,
|
||||
@@ -145,9 +146,16 @@ function getAvailableActiveSceneActNpcs(state: GameState) {
|
||||
|
||||
return (state.currentScenePreset?.npcs ?? [])
|
||||
.filter(candidate => {
|
||||
const candidateIds = [candidate.id, candidate.characterId].filter(
|
||||
(value): value is string => Boolean(value),
|
||||
);
|
||||
const candidateIds = [
|
||||
candidate.id,
|
||||
candidate.characterId,
|
||||
candidate.name,
|
||||
candidate.title,
|
||||
]
|
||||
.map((value) =>
|
||||
resolveCustomWorldRoleIdReference(state.customWorldProfile, value),
|
||||
)
|
||||
.filter(Boolean);
|
||||
return candidateIds.some(id => activeActNpcIdSet.has(id));
|
||||
})
|
||||
.filter(candidate => candidate.characterId !== state.playerCharacter?.id)
|
||||
@@ -180,8 +188,19 @@ function pickFriendlySceneNpcForActiveAct(state: GameState, npcs: SceneNpc[]) {
|
||||
return (
|
||||
npcs.find(
|
||||
(npc) =>
|
||||
npc.id === focusNpcId ||
|
||||
(npc.characterId ? npc.characterId === focusNpcId : false),
|
||||
resolveCustomWorldRoleIdReference(state.customWorldProfile, npc.id) === focusNpcId ||
|
||||
resolveCustomWorldRoleIdReference(
|
||||
state.customWorldProfile,
|
||||
npc.characterId,
|
||||
) === focusNpcId ||
|
||||
resolveCustomWorldRoleIdReference(
|
||||
state.customWorldProfile,
|
||||
npc.name,
|
||||
) === focusNpcId ||
|
||||
resolveCustomWorldRoleIdReference(
|
||||
state.customWorldProfile,
|
||||
npc.title,
|
||||
) === focusNpcId,
|
||||
) ?? pickRandomItem(npcs)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { resolveCustomWorldCampScene } from '../services/customWorldCamp';
|
||||
import { buildCustomCampSceneName } from '../services/customWorldPresentation';
|
||||
import { resolveCustomWorldRoleIdReferences } from '../services/customWorldRoleReferences';
|
||||
import {
|
||||
buildFallbackActorNarrativeProfile,
|
||||
normalizeActorNarrativeProfile,
|
||||
@@ -406,9 +407,11 @@ function collectSceneActNpcIdsForScene(
|
||||
}
|
||||
|
||||
chapter.acts.forEach((act) => {
|
||||
pushNpcId(act.primaryNpcId);
|
||||
pushNpcId(act.oppositeNpcId);
|
||||
act.encounterNpcIds.forEach(pushNpcId);
|
||||
resolveCustomWorldRoleIdReferences(profile, [
|
||||
act.primaryNpcId,
|
||||
act.oppositeNpcId,
|
||||
...act.encounterNpcIds,
|
||||
]).forEach(pushNpcId);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user