import type { CustomWorldCoverProfile, CustomWorldCoverSourceType, CustomWorldItem, CustomWorldPlayableNpc, } from '../runtimeTypes.js'; /** * 工作包 G: * 把 runtime profile 编译流程里的通用常量、基础文本归一和通用小型归一逻辑收口到共享模块, * 让角色、地点、场景章节和主编译入口都不再重复维护这些基础能力。 */ const MIN_CUSTOM_WORLD_AFFINITY = -40; const MAX_CUSTOM_WORLD_AFFINITY = 90; const CUSTOM_WORLD_RARITIES = [ 'common', 'uncommon', 'rare', 'epic', 'legendary', ] as const; const CUSTOM_WORLD_ROLE_ITEM_CATEGORIES = [ '武器', '护甲', '饰品', '消耗品', '材料', '稀有品', '专属物品', '专属物', ] as const; export const MIN_CUSTOM_WORLD_PLAYABLE_NPC_COUNT = 5; export const MIN_CUSTOM_WORLD_TOTAL_NPC_COUNT = 30; export const MIN_CUSTOM_WORLD_LANDMARK_COUNT = 10; export const MIN_CUSTOM_WORLD_STORY_NPC_COUNT = Math.max( 0, MIN_CUSTOM_WORLD_TOTAL_NPC_COUNT - MIN_CUSTOM_WORLD_PLAYABLE_NPC_COUNT, ); export const PLAYABLE_TEMPLATE_CHARACTER_IDS = [ 'sword-princess', 'archer-hero', 'girl-hero', 'punch-hero', 'fighter-4', ] as const; export function toText(value: unknown) { return typeof value === 'string' ? value.trim() : ''; } export function toFiniteInteger(value: unknown) { return typeof value === 'number' && Number.isFinite(value) ? Math.round(value) : undefined; } export function toRecord(value: unknown) { return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record) : null; } export function toRecordArray(value: unknown) { return Array.isArray(value) ? (value.filter((item) => item && typeof item === 'object') as Array< Record >) : []; } export function toStringArray(value: unknown, nestedKey?: string) { if (!Array.isArray(value)) { return []; } return value .map((item) => { if (typeof item === 'string') { return item.trim(); } if (nestedKey && item && typeof item === 'object') { return toText((item as Record)[nestedKey]); } return ''; }) .filter(Boolean); } export function normalizeTags(value: unknown, fallbackTags: string[] = []) { const tags = Array.isArray(value) ? value.map((item) => toText(item)).filter(Boolean) : []; return [ ...new Set((tags.length > 0 ? tags : fallbackTags).filter(Boolean)), ].slice(0, 5); } export function clampText(value: string, maxLength: number) { const normalized = value.trim().replace(/\s+/g, ' '); if (!normalized) { return ''; } if (normalized.length <= maxLength) { return normalized; } return `${normalized.slice(0, Math.max(0, maxLength - 1)).trim()}…`; } export function slugify(value: string) { const ascii = value .toLowerCase() .replace(/[^a-z0-9\u4e00-\u9fa5]+/g, '-') .replace(/^-+|-+$/g, ''); return ascii ? ascii.slice(0, 24) : 'entry'; } export function createEntryId(prefix: string, label: string, index: number) { return `${prefix}-${slugify(label || `${prefix}-${index + 1}`)}-${index + 1}`; } export function clampCustomWorldAffinity(value: number) { return Math.max( MIN_CUSTOM_WORLD_AFFINITY, Math.min(MAX_CUSTOM_WORLD_AFFINITY, Math.round(value)), ); } export function normalizeInitialAffinity(value: unknown, fallback: number) { return typeof value === 'number' && Number.isFinite(value) ? clampCustomWorldAffinity(value) : fallback; } export function normalizeRarity( value: unknown, fallback: (typeof CUSTOM_WORLD_RARITIES)[number] = 'rare', ) { const rarity = toText(value).toLowerCase(); return CUSTOM_WORLD_RARITIES.includes( rarity as (typeof CUSTOM_WORLD_RARITIES)[number], ) ? (rarity as (typeof CUSTOM_WORLD_RARITIES)[number]) : fallback; } export function normalizeRoleItemCategory(value: unknown, fallback = '材料') { const category = toText(value); if ( (CUSTOM_WORLD_ROLE_ITEM_CATEGORIES as readonly string[]).includes(category) ) { return category === '专属物' ? '专属物品' : category; } if (/武器|刀|剑|弓|枪|炮|锤/u.test(category)) return '武器'; if (/甲|护|盾|衣|袍/u.test(category)) return '护甲'; if (/饰|符|佩|坠|珠|印/u.test(category)) return '饰品'; if (/药|食|补给|瓶|卷轴/u.test(category)) return '消耗品'; if (/材|矿|木|石|鳞|骨/u.test(category)) return '材料'; if (/秘|钥|图|册|录|卷/u.test(category)) return '稀有品'; if (/信物|遗物|核心|母印|真符/u.test(category)) return '专属物品'; return fallback; } export function normalizeCustomWorldCoverCharacterRoleIds( value: unknown, playableNpcs: Array>, ) { const availableIds = new Set( playableNpcs.map((entry) => entry.id.trim()).filter(Boolean), ); const selectedIds = Array.isArray(value) ? [ ...new Set( value .map((entry) => toText(entry)) .filter((entry) => entry && availableIds.has(entry)), ), ].slice(0, 3) : []; if (selectedIds.length > 0) { return selectedIds; } return playableNpcs .map((entry) => entry.id.trim()) .filter(Boolean) .slice(0, 3); } export function buildDefaultCustomWorldCover( playableNpcs: Array>, ): CustomWorldCoverProfile { return { sourceType: 'default' as const, imageSrc: null, characterRoleIds: normalizeCustomWorldCoverCharacterRoleIds( undefined, playableNpcs, ), }; } export function normalizeCustomWorldCover( value: unknown, playableNpcs: Array>, ): CustomWorldCoverProfile { if (!value || typeof value !== 'object' || Array.isArray(value)) { return buildDefaultCustomWorldCover(playableNpcs); } const item = value as Record; const sourceType: CustomWorldCoverSourceType = item.sourceType === 'uploaded' || item.sourceType === 'generated' ? item.sourceType : 'default'; const imageSrc = toText(item.imageSrc) || null; if (sourceType !== 'default' && imageSrc) { return { sourceType, imageSrc, characterRoleIds: [], }; } return buildDefaultCustomWorldCover(playableNpcs); } export function normalizeItemList(value: unknown) { return toRecordArray(value) .map((item, index) => { const name = toText(item.name); const category = toText(item.category); return { id: toText(item.id) || createEntryId('item', name, index), name, category, rarity: normalizeRarity(item.rarity, 'rare'), description: toText(item.description), tags: normalizeTags(item.tags), } satisfies CustomWorldItem; }) .filter((entry) => entry.name && entry.category); }