133 lines
3.6 KiB
TypeScript
133 lines
3.6 KiB
TypeScript
import type {
|
|
CustomWorldCoverProfile,
|
|
CustomWorldPlayableNpc,
|
|
CustomWorldProfile,
|
|
} from '../types';
|
|
|
|
export type CustomWorldCoverRenderMode = 'image' | 'scene_with_roles';
|
|
|
|
export type CustomWorldCoverPresentation = {
|
|
imageSrc: string | null;
|
|
renderMode: CustomWorldCoverRenderMode;
|
|
characterImageSrcs: string[];
|
|
sourceType: CustomWorldCoverProfile['sourceType'];
|
|
};
|
|
|
|
function resolveOpeningSceneFirstActImageSrc(profile: CustomWorldProfile) {
|
|
return profile.sceneChapterBlueprints?.[0]?.acts?.[0]?.backgroundImageSrc?.trim() || null;
|
|
}
|
|
|
|
function resolveOpeningSceneImageSrc(profile: CustomWorldProfile) {
|
|
// 默认封面优先取开局场景第一幕图,避免草稿页与作品库继续沿用旧的营地兜底策略。
|
|
const firstActImageSrc = resolveOpeningSceneFirstActImageSrc(profile);
|
|
if (firstActImageSrc) {
|
|
return firstActImageSrc;
|
|
}
|
|
|
|
const campImageSrc = profile.camp?.imageSrc?.trim() || '';
|
|
if (campImageSrc) {
|
|
return campImageSrc;
|
|
}
|
|
|
|
return (
|
|
profile.landmarks
|
|
.map((landmark) => landmark.imageSrc?.trim() || '')
|
|
.find(Boolean) || null
|
|
);
|
|
}
|
|
|
|
function resolvePlayableCoverImageSrc(role: CustomWorldPlayableNpc) {
|
|
const explicitImageSrc = role.imageSrc?.trim() || '';
|
|
if (explicitImageSrc) {
|
|
return explicitImageSrc;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function normalizeCoverCharacterRoleIds(
|
|
profile: Pick<CustomWorldProfile, 'playableNpcs'>,
|
|
roleIds?: string[] | null,
|
|
) {
|
|
const availableIds = new Set(
|
|
profile.playableNpcs.map((role) => role.id.trim()).filter(Boolean),
|
|
);
|
|
const selectedIds = Array.isArray(roleIds)
|
|
? [
|
|
...new Set(
|
|
roleIds
|
|
.map((roleId) => roleId.trim())
|
|
.filter((roleId) => roleId && availableIds.has(roleId)),
|
|
),
|
|
].slice(0, 3)
|
|
: [];
|
|
|
|
if (selectedIds.length > 0) {
|
|
return selectedIds;
|
|
}
|
|
|
|
return profile.playableNpcs
|
|
.map((role) => role.id.trim())
|
|
.filter(Boolean)
|
|
.slice(0, 3);
|
|
}
|
|
|
|
export function buildDefaultCustomWorldCoverProfile(
|
|
profile: Pick<CustomWorldProfile, 'playableNpcs'>,
|
|
): CustomWorldCoverProfile {
|
|
return {
|
|
sourceType: 'default',
|
|
imageSrc: null,
|
|
characterRoleIds: normalizeCoverCharacterRoleIds(profile),
|
|
};
|
|
}
|
|
|
|
export function resolveCustomWorldCoverPresentation(
|
|
profile: CustomWorldProfile,
|
|
): CustomWorldCoverPresentation {
|
|
const cover = profile.cover;
|
|
const sourceType =
|
|
cover?.sourceType === 'uploaded' || cover?.sourceType === 'generated'
|
|
? cover.sourceType
|
|
: 'default';
|
|
const explicitImageSrc = cover?.imageSrc?.trim() || '';
|
|
|
|
if (sourceType !== 'default' && explicitImageSrc) {
|
|
return {
|
|
imageSrc: explicitImageSrc,
|
|
renderMode: 'image',
|
|
characterImageSrcs: [],
|
|
sourceType,
|
|
};
|
|
}
|
|
|
|
const openingSceneImageSrc = resolveOpeningSceneImageSrc(profile);
|
|
const roleById = new Map(
|
|
profile.playableNpcs.map((role) => [role.id.trim(), role] as const),
|
|
);
|
|
const characterImageSrcs = normalizeCoverCharacterRoleIds(
|
|
profile,
|
|
cover?.characterRoleIds,
|
|
)
|
|
.map((roleId) => roleById.get(roleId))
|
|
.map((role) => (role ? resolvePlayableCoverImageSrc(role) : null))
|
|
.filter((imageSrc): imageSrc is string => Boolean(imageSrc));
|
|
const leadPlayableImageSrc =
|
|
profile.playableNpcs
|
|
.map((role) => resolvePlayableCoverImageSrc(role))
|
|
.find(Boolean) || null;
|
|
|
|
return {
|
|
imageSrc: openingSceneImageSrc || leadPlayableImageSrc,
|
|
renderMode:
|
|
openingSceneImageSrc && characterImageSrcs.length > 0
|
|
? 'scene_with_roles'
|
|
: 'image',
|
|
characterImageSrcs:
|
|
openingSceneImageSrc && characterImageSrcs.length > 0
|
|
? characterImageSrcs
|
|
: [],
|
|
sourceType: 'default',
|
|
};
|
|
}
|