169 lines
4.8 KiB
TypeScript
169 lines
4.8 KiB
TypeScript
import {
|
|
resolveCustomWorldCampSceneImage,
|
|
resolveCustomWorldLandmarkImageMap,
|
|
} from '../data/customWorldVisuals';
|
|
import type {
|
|
CustomWorldLandmark,
|
|
CustomWorldProfile,
|
|
SceneChapterBlueprint,
|
|
} from '../types';
|
|
import { resolveCustomWorldCampScene } from './customWorldCamp';
|
|
|
|
export type CustomWorldSceneKind = 'camp' | 'landmark';
|
|
|
|
export type CustomWorldSceneActImagePreview = {
|
|
id: string;
|
|
title: string;
|
|
imageSrc: string;
|
|
};
|
|
|
|
export type CustomWorldScenePresentation = {
|
|
id: string;
|
|
kind: CustomWorldSceneKind;
|
|
name: string;
|
|
description: string;
|
|
imageSrc: string;
|
|
sceneChapters: SceneChapterBlueprint[];
|
|
actPreviews: CustomWorldSceneActImagePreview[];
|
|
};
|
|
|
|
function normalizeText(value: string | null | undefined) {
|
|
return value?.trim() ?? '';
|
|
}
|
|
|
|
export function resolveScenePresentationChapters(params: {
|
|
sceneChapters: CustomWorldProfile['sceneChapterBlueprints'];
|
|
sceneId: string;
|
|
sceneName: string;
|
|
}) {
|
|
const sceneChapters = params.sceneChapters ?? [];
|
|
const normalizedSceneId = params.sceneId.trim();
|
|
const normalizedSceneName = params.sceneName.trim();
|
|
|
|
const directMatches = sceneChapters.filter(
|
|
(chapter) => chapter.sceneId.trim() === normalizedSceneId,
|
|
);
|
|
if (directMatches.length > 0) {
|
|
return directMatches;
|
|
}
|
|
|
|
const linkedMatches = sceneChapters.filter((chapter) =>
|
|
chapter.linkedLandmarkIds.some(
|
|
(landmarkId) => landmarkId.trim() === normalizedSceneId,
|
|
),
|
|
);
|
|
if (linkedMatches.length > 0) {
|
|
return linkedMatches;
|
|
}
|
|
|
|
return sceneChapters.filter((chapter) => {
|
|
const chapterTitle = chapter.title.trim();
|
|
return (
|
|
chapterTitle === normalizedSceneName ||
|
|
chapter.summary.includes(normalizedSceneName) ||
|
|
chapter.acts.some(
|
|
(act) =>
|
|
act.title.includes(normalizedSceneName) ||
|
|
act.summary.includes(normalizedSceneName),
|
|
)
|
|
);
|
|
});
|
|
}
|
|
|
|
export function resolveScenePresentationImage(params: {
|
|
sceneImageSrc?: string | null;
|
|
sceneChapters: SceneChapterBlueprint[];
|
|
}) {
|
|
const firstActImageSrc =
|
|
params.sceneChapters
|
|
.flatMap((chapter) => chapter.acts)
|
|
.map((act) => normalizeText(act.backgroundImageSrc))
|
|
.find(Boolean) || '';
|
|
|
|
return firstActImageSrc || normalizeText(params.sceneImageSrc);
|
|
}
|
|
|
|
export function collectSceneActImagePreviews(params: {
|
|
sceneChapters: SceneChapterBlueprint[];
|
|
sceneImageSrc?: string | null;
|
|
}) {
|
|
const sceneImageSrc = normalizeText(params.sceneImageSrc);
|
|
const actPreviews = params.sceneChapters.flatMap((chapter) =>
|
|
chapter.acts
|
|
.map((act, index) => {
|
|
// 中文注释:幕预览图片必须优先读取当前幕背景;场景图只给缺图的旧数据兜底,避免开局场景和普通场景在列表、详情里显示不同图片。
|
|
const imageSrc = normalizeText(act.backgroundImageSrc) || sceneImageSrc;
|
|
return {
|
|
id: act.id.trim() || `${chapter.id}-act-${index}`,
|
|
title: act.title.trim() || `第${index + 1}幕`,
|
|
imageSrc,
|
|
};
|
|
})
|
|
.filter((act) => act.imageSrc),
|
|
);
|
|
|
|
if (actPreviews.length > 0 || !sceneImageSrc) {
|
|
return actPreviews;
|
|
}
|
|
|
|
return [1, 2, 3].map((actNumber) => ({
|
|
id: `fallback-scene-act-${actNumber}`,
|
|
title: `第${actNumber}幕`,
|
|
imageSrc: sceneImageSrc,
|
|
}));
|
|
}
|
|
|
|
export function buildScenePresentation(params: {
|
|
profile: CustomWorldProfile;
|
|
scene: CustomWorldLandmark;
|
|
kind: CustomWorldSceneKind;
|
|
sceneImageSrc?: string | null;
|
|
}) {
|
|
const sceneChapters = resolveScenePresentationChapters({
|
|
sceneChapters: params.profile.sceneChapterBlueprints,
|
|
sceneId: params.scene.id,
|
|
sceneName: params.scene.name,
|
|
});
|
|
const imageSrc = resolveScenePresentationImage({
|
|
sceneImageSrc: params.sceneImageSrc ?? params.scene.imageSrc,
|
|
sceneChapters,
|
|
});
|
|
|
|
return {
|
|
id: params.scene.id,
|
|
kind: params.kind,
|
|
name: params.scene.name,
|
|
description: params.scene.description,
|
|
imageSrc,
|
|
sceneChapters,
|
|
actPreviews: collectSceneActImagePreviews({
|
|
sceneChapters,
|
|
sceneImageSrc: imageSrc,
|
|
}),
|
|
} satisfies CustomWorldScenePresentation;
|
|
}
|
|
|
|
export function buildCustomWorldScenePresentations(profile: CustomWorldProfile) {
|
|
const landmarkImageById = resolveCustomWorldLandmarkImageMap(profile);
|
|
const campScene = resolveCustomWorldCampScene(profile);
|
|
const campPresentation = buildScenePresentation({
|
|
profile,
|
|
scene: campScene,
|
|
kind: 'camp',
|
|
sceneImageSrc: resolveCustomWorldCampSceneImage(profile),
|
|
});
|
|
const landmarkPresentations = profile.landmarks.map((landmark) =>
|
|
buildScenePresentation({
|
|
profile,
|
|
scene: landmark,
|
|
kind: 'landmark',
|
|
sceneImageSrc: landmarkImageById.get(landmark.id) || landmark.imageSrc,
|
|
}),
|
|
);
|
|
|
|
return {
|
|
camp: campPresentation,
|
|
landmarks: landmarkPresentations,
|
|
};
|
|
}
|