1
This commit is contained in:
@@ -13,6 +13,44 @@ function toSet(values: string[]) {
|
||||
return new Set(values.map((value) => value.trim()).filter(Boolean));
|
||||
}
|
||||
|
||||
function resolveCustomWorldRuntimeSceneAliases(
|
||||
profile: CustomWorldProfile,
|
||||
sceneId: string,
|
||||
) {
|
||||
const aliases = toSet([sceneId]);
|
||||
const campId = profile.camp?.id?.trim() || 'custom-scene-camp';
|
||||
if (sceneId === 'custom-scene-camp' || sceneId === campId) {
|
||||
aliases.add(campId);
|
||||
aliases.add('custom-scene-camp');
|
||||
}
|
||||
|
||||
// 中文注释:部分单元测试和旧快照会传入精简 profile,运行态解析不能假设 landmarks 始终存在。
|
||||
(profile.landmarks ?? []).forEach((landmark, index) => {
|
||||
const runtimeSceneId = `custom-scene-landmark-${index + 1}`;
|
||||
if (sceneId === runtimeSceneId || sceneId === landmark.id) {
|
||||
aliases.add(runtimeSceneId);
|
||||
aliases.add(landmark.id);
|
||||
}
|
||||
});
|
||||
|
||||
return aliases;
|
||||
}
|
||||
|
||||
function doesSceneMatchChapter(
|
||||
profile: CustomWorldProfile,
|
||||
sceneId: string,
|
||||
chapter: SceneChapterBlueprint,
|
||||
) {
|
||||
const sceneAliases = resolveCustomWorldRuntimeSceneAliases(profile, sceneId);
|
||||
const chapterSceneIds = toSet([
|
||||
chapter.sceneId,
|
||||
...(chapter.linkedLandmarkIds ?? []),
|
||||
...(chapter.acts ?? []).map((act) => act.sceneId),
|
||||
]);
|
||||
|
||||
return [...sceneAliases].some((id) => chapterSceneIds.has(id));
|
||||
}
|
||||
|
||||
export function resolveSceneChapterBlueprint(
|
||||
profile: CustomWorldProfile | null | undefined,
|
||||
sceneId: string | null | undefined,
|
||||
@@ -22,8 +60,8 @@ export function resolveSceneChapterBlueprint(
|
||||
}
|
||||
|
||||
return (
|
||||
profile.sceneChapterBlueprints?.find(
|
||||
(entry) => entry.sceneId === sceneId || entry.linkedLandmarkIds.includes(sceneId),
|
||||
profile.sceneChapterBlueprints?.find((entry) =>
|
||||
doesSceneMatchChapter(profile, sceneId, entry),
|
||||
) ?? null
|
||||
);
|
||||
}
|
||||
@@ -33,15 +71,24 @@ export function resolveActiveSceneActBlueprint(params: {
|
||||
sceneId: string | null | undefined;
|
||||
storyEngineMemory?: StoryEngineMemoryState | null;
|
||||
}): SceneActBlueprint | null {
|
||||
const chapter = resolveSceneChapterBlueprint(params.profile, params.sceneId);
|
||||
const runtimeState = params.storyEngineMemory?.currentSceneActState;
|
||||
const runtimeChapter =
|
||||
params.profile && runtimeState?.chapterId
|
||||
? params.profile.sceneChapterBlueprints?.find(
|
||||
(entry) =>
|
||||
entry.id === runtimeState.chapterId &&
|
||||
Boolean(params.sceneId) &&
|
||||
doesSceneMatchChapter(params.profile!, params.sceneId!, entry),
|
||||
) ?? null
|
||||
: null;
|
||||
const chapter =
|
||||
runtimeChapter ?? resolveSceneChapterBlueprint(params.profile, params.sceneId);
|
||||
if (!chapter || chapter.acts.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const runtimeState = params.storyEngineMemory?.currentSceneActState;
|
||||
if (
|
||||
runtimeState &&
|
||||
runtimeState.sceneId === chapter.sceneId &&
|
||||
runtimeState.chapterId === chapter.id
|
||||
) {
|
||||
const matchedAct = chapter.acts.find((entry) => entry.id === runtimeState.currentActId);
|
||||
@@ -132,15 +179,23 @@ export function buildInitialSceneActRuntimeState(params: {
|
||||
sceneId: string | null | undefined;
|
||||
storyEngineMemory?: StoryEngineMemoryState | null;
|
||||
}): SceneActRuntimeState | null {
|
||||
const chapter = resolveSceneChapterBlueprint(params.profile, params.sceneId);
|
||||
const runtimeState = params.storyEngineMemory?.currentSceneActState;
|
||||
const runtimeChapter =
|
||||
params.profile && params.sceneId && runtimeState?.chapterId
|
||||
? params.profile.sceneChapterBlueprints?.find(
|
||||
(entry) =>
|
||||
entry.id === runtimeState.chapterId &&
|
||||
doesSceneMatchChapter(params.profile!, params.sceneId!, entry),
|
||||
) ?? null
|
||||
: null;
|
||||
const chapter =
|
||||
runtimeChapter ?? resolveSceneChapterBlueprint(params.profile, params.sceneId);
|
||||
if (!chapter || chapter.acts.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const runtimeState = params.storyEngineMemory?.currentSceneActState;
|
||||
if (
|
||||
runtimeState &&
|
||||
runtimeState.sceneId === chapter.sceneId &&
|
||||
runtimeState.chapterId === chapter.id &&
|
||||
chapter.acts.some((entry) => entry.id === runtimeState.currentActId)
|
||||
) {
|
||||
@@ -167,11 +222,22 @@ export function resolveActiveSceneActEncounterNpcIds(params: {
|
||||
sceneId: string | null | undefined;
|
||||
storyEngineMemory?: StoryEngineMemoryState | null;
|
||||
}) {
|
||||
return (
|
||||
resolveActiveSceneActBlueprint(params)?.encounterNpcIds
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean) ?? []
|
||||
);
|
||||
const activeAct = resolveActiveSceneActBlueprint(params);
|
||||
if (!activeAct) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
...new Set(
|
||||
[
|
||||
activeAct.primaryNpcId,
|
||||
activeAct.oppositeNpcId,
|
||||
...activeAct.encounterNpcIds,
|
||||
]
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
export function resolveActiveSceneActPrimaryNpcId(params: {
|
||||
@@ -182,6 +248,28 @@ export function resolveActiveSceneActPrimaryNpcId(params: {
|
||||
return resolveActiveSceneActBlueprint(params)?.primaryNpcId?.trim() || null;
|
||||
}
|
||||
|
||||
export function resolveActiveSceneActOppositeNpcId(params: {
|
||||
profile: CustomWorldProfile | null | undefined;
|
||||
sceneId: string | null | undefined;
|
||||
storyEngineMemory?: StoryEngineMemoryState | null;
|
||||
}) {
|
||||
return resolveActiveSceneActBlueprint(params)?.oppositeNpcId?.trim() || null;
|
||||
}
|
||||
|
||||
export function resolveActiveSceneActEncounterFocusNpcId(params: {
|
||||
profile: CustomWorldProfile | null | undefined;
|
||||
sceneId: string | null | undefined;
|
||||
storyEngineMemory?: StoryEngineMemoryState | null;
|
||||
}) {
|
||||
const activeAct = resolveActiveSceneActBlueprint(params);
|
||||
return (
|
||||
activeAct?.oppositeNpcId?.trim() ||
|
||||
activeAct?.primaryNpcId?.trim() ||
|
||||
activeAct?.encounterNpcIds[0]?.trim() ||
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveActiveSceneActBackgroundImage(params: {
|
||||
profile: CustomWorldProfile | null | undefined;
|
||||
sceneId: string | null | undefined;
|
||||
@@ -201,6 +289,22 @@ export function canUseLimitedPrimaryNpcChat(params: {
|
||||
return false;
|
||||
}
|
||||
|
||||
const activeAct = resolveActiveSceneActBlueprint({
|
||||
profile: params.profile,
|
||||
sceneId: params.sceneId,
|
||||
storyEngineMemory: params.storyEngineMemory,
|
||||
});
|
||||
|
||||
const limitedChatNpcIds = toSet([
|
||||
activeAct?.primaryNpcId ?? '',
|
||||
activeAct?.oppositeNpcId ?? '',
|
||||
]);
|
||||
|
||||
// 中文注释:第一幕对面角色即使是负好感,也必须先进入剧情对话;普通敌人仍按战斗处理。
|
||||
if (limitedChatNpcIds.has(params.npcId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
resolveActiveSceneActPrimaryNpcId({
|
||||
profile: params.profile,
|
||||
|
||||
Reference in New Issue
Block a user