Files
Genarrative/src/services/customWorldScenePresentation.ts
kdletters cbc27bad4a
Some checks failed
CI / verify (push) Has been cancelled
init with react+axum+spacetimedb
2026-04-26 18:06:23 +08:00

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,
};
}