import type { CustomWorldProfile, ThemePack, WorldTemplateType, WorldType, } from '../../types'; import { detectCustomWorldThemeMode } from '../customWorldTheme'; type ThemePackPreset = Omit & { displayName: string; }; const THEME_PACK_PRESETS: Record = { mythic: { displayName: '自定义回响', toneRange: ['未知', '克制', '余波未定', '局势待开'], institutionLexicon: ['据点', '同盟', '旅团', '档案室', '哨站', '归舍'], tabooLexicon: ['失约', '旧痕', '越界', '封存', '误触', '回响'], artifactClasses: ['信物', '残页', '封匣', '样本', '旧钥', '印记'], actorArchetypes: ['见证者', '守望人', '异乡来客', '带路人', '失序幸存者'], conflictForms: ['追查', '护送', '回收', '分歧对峙', '失踪追索'], clueForms: ['痕迹', '记录', '口供', '残片', '旧图'], namingPatterns: ['地点+余痕+器类', '势力+旧称+用途', '事件+残响+物件'], revealStyles: ['循序松口', '线索回指', '保留一层', '让事实自己浮出'], }, martial: { displayName: '江湖旧事', toneRange: ['冷峻', '克制', '刀锋般紧绷', '旧案余震'], institutionLexicon: ['门派', '镖局', '巡司', '商号', '关隘', '行旅'], tabooLexicon: ['旧案', '灭门', '失契', '私印', '禁脉', '断誓'], artifactClasses: ['遗兵', '信物', '令牌', '残卷', '旧佩', '封匣'], actorArchetypes: ['游侠', '守路人', '旧案见证者', '带路人', '避祸者'], conflictForms: ['寻仇', '护送', '围剿', '失踪追查', '门派角力'], clueForms: ['刀痕', '旧誓', '口供', '渡口风声', '残页'], namingPatterns: ['旧称+伤痕+器类', '地标+余痕+器类', '势力+制式+用途'], revealStyles: ['试探', '旁敲侧击', '旧事回响', '迟疑松口'], }, arcane: { displayName: '灵脉秘闻', toneRange: ['清冷', '玄异', '高压', '因果牵引'], institutionLexicon: ['宗门', '法坛', '巡守司', '灵舟会', '洞府', '云阙'], tabooLexicon: ['封印', '逆脉', '禁术', '残魂', '天契', '旧誓'], artifactClasses: ['法器', '灵符', '玉简', '残卷', '封匣', '阵核'], actorArchetypes: ['巡守使', '隐修者', '守秘人', '失格弟子', '旧阵幸存者'], conflictForms: ['夺脉', '追索', '封印失衡', '宗门旧案', '秘境争夺'], clueForms: ['灵痕', '阵纹', '残识', '旧契', '法简'], namingPatterns: ['云阙+残痕+器类', '灵脉+封痕+器类', '誓约+余烬+器类'], revealStyles: ['碎片式透露', '借象示意', '以术遮掩', '因果回指'], }, machina: { displayName: '机巧裂域', toneRange: ['冷硬', '压迫', '机械失序', '余温未散'], institutionLexicon: ['财团', '工坊', '舰队', '前哨站', '调查局', '机库'], tabooLexicon: ['过载', '黑匣', '失控协议', '封存日志', '污染区'], artifactClasses: ['芯片', '驱动核', '制式装置', '记录模组', '封存匣'], actorArchetypes: ['技师', '巡检员', '失联驾驶员', '前线回收者', '见证者'], conflictForms: ['封锁', '回收', '追查事故', '公司内斗', '前线失守'], clueForms: ['烧蚀痕', '日志', '封条', '校准码', '脉冲残波'], namingPatterns: ['编号+余痕+器类', '站点+制式+用途', '事故+残片+器类'], revealStyles: ['日志碎片', '术语遮掩', '延迟承认', '故障回声'], }, tide: { displayName: '潮痕旧闻', toneRange: ['潮湿', '迷雾', '迟滞', '暗潮涌动'], institutionLexicon: ['港务', '巡海司', '渡船会', '哨塔', '湾区营地', '潮站'], tabooLexicon: ['沉船', '失契', '潮誓', '禁海区', '回潮夜'], artifactClasses: ['潮印', '旧锚', '航图', '信标', '封潮匣', '雾骨遗物'], actorArchetypes: ['渡口守更人', '巡潮者', '失船幸存者', '采录员', '岸线猎人'], conflictForms: ['封港', '追查失踪', '海路争夺', '潮灾余波', '护送穿渡'], clueForms: ['潮痕', '盐霜', '航线残页', '旧锚印', '失物清单'], namingPatterns: ['潮名+伤痕+器类', '港口+遗痕+器类', '誓约+余潮+器类'], revealStyles: ['传闻递进', '潮汐比喻', '回避本名', '隔雾指认'], }, rift: { displayName: '裂界前线', toneRange: ['焦灼', '边境压力', '失序', '战线余烬'], institutionLexicon: ['前哨', '巡边队', '断层站', '界桥营', '回收组', '观察哨'], tabooLexicon: ['断层失守', '回响名单', '界外污染', '封桥令', '旧撤离线'], artifactClasses: ['界核', '锚印', '边潮样本', '回响记录', '裂缝制式物'], actorArchetypes: ['边巡者', '失线幸存者', '回收员', '名单记录人', '封桥见证者'], conflictForms: ['守线', '撤离', '回收异常', '追查失线', '前线补给争夺'], clueForms: ['裂痕', '界压残波', '名单残页', '旧封条', '警示标记'], namingPatterns: ['裂界+旧痕+器类', '前哨+制式+用途', '名单+残响+器类'], revealStyles: ['压力主导', '证词错位', '名单回响', '旧事倒灌'], }, }; function dedupeStrings(values: Array, limit = 8) { return [...new Set((values ?? []).map((value) => value?.trim() ?? '').filter(Boolean))] .slice(0, limit); } function collectProfileLexicon(profile: Pick< CustomWorldProfile, 'majorFactions' | 'coreConflicts' | 'summary' | 'tone' | 'playerGoal' >) { return dedupeStrings([ ...(profile.majorFactions ?? []), ...(profile.coreConflicts ?? []), profile.summary, profile.tone, profile.playerGoal, ]); } function cloneThemePack(mode: string, preset: ThemePackPreset): ThemePack { return { id: `theme-pack:${mode}`, displayName: preset.displayName, toneRange: [...preset.toneRange], institutionLexicon: [...preset.institutionLexicon], tabooLexicon: [...preset.tabooLexicon], artifactClasses: [...preset.artifactClasses], actorArchetypes: [...preset.actorArchetypes], conflictForms: [...preset.conflictForms], clueForms: [...preset.clueForms], namingPatterns: [...preset.namingPatterns], revealStyles: [...preset.revealStyles], }; } function collectSemanticAnchorLexicon( profile: Pick, ) { const semanticAnchor = profile.ownedSettingLayers?.semanticAnchor; const expressionProfile = profile.ownedSettingLayers?.expressionProfile; if (!semanticAnchor && !expressionProfile) { return []; } return dedupeStrings([ ...(semanticAnchor?.genreSignals ?? []), ...(semanticAnchor?.conflictForms ?? []), ...(semanticAnchor?.institutionTypes ?? []), ...(semanticAnchor?.tabooTypes ?? []), ...(semanticAnchor?.carrierTypes ?? []), ...(semanticAnchor?.forceSystemTypes ?? []), ...(semanticAnchor?.atmosphereTags ?? []), ...(expressionProfile?.presentationTone ?? []), ...(expressionProfile?.namingDirectives ?? []), ...(expressionProfile?.clueDirectives ?? []), ...(expressionProfile?.revealDirectives ?? []), ]); } function resolveThemeModeFromWorldType( worldType: WorldTemplateType | WorldType | null | undefined, ) { if (worldType === 'XIANXIA') { return 'arcane'; } return 'mythic'; } export function resolveFallbackThemePack( worldType: WorldTemplateType | WorldType | null | undefined, ) { const mode = resolveThemeModeFromWorldType(worldType); return cloneThemePack(mode, THEME_PACK_PRESETS[mode]!); } export function normalizeThemePack( value: unknown, fallback: ThemePack, ): ThemePack { if (!value || typeof value !== 'object') { return fallback; } const item = value as Partial; const readList = (candidate: unknown, fallbackValue: string[]) => { const next = dedupeStrings(candidate as string[], fallbackValue.length); return next.length > 0 ? next : fallbackValue; }; return { id: typeof item.id === 'string' && item.id.trim() ? item.id.trim() : fallback.id, displayName: typeof item.displayName === 'string' && item.displayName.trim() ? item.displayName.trim() : fallback.displayName, toneRange: readList(item.toneRange, fallback.toneRange), institutionLexicon: readList( item.institutionLexicon, fallback.institutionLexicon, ), tabooLexicon: readList(item.tabooLexicon, fallback.tabooLexicon), artifactClasses: readList(item.artifactClasses, fallback.artifactClasses), actorArchetypes: readList(item.actorArchetypes, fallback.actorArchetypes), conflictForms: readList(item.conflictForms, fallback.conflictForms), clueForms: readList(item.clueForms, fallback.clueForms), namingPatterns: readList(item.namingPatterns, fallback.namingPatterns), revealStyles: readList(item.revealStyles, fallback.revealStyles), }; } export function buildThemePackFromWorldProfile( profile: Pick< CustomWorldProfile, | 'settingText' | 'summary' | 'tone' | 'playerGoal' | 'templateWorldType' | 'majorFactions' | 'coreConflicts' | 'ownedSettingLayers' > & { templateWorldType: WorldTemplateType | WorldType; }, ) { const mode = detectCustomWorldThemeMode(profile); const base = cloneThemePack(mode, THEME_PACK_PRESETS[mode]!); const ownedThemePack = profile.ownedSettingLayers?.expressionProfile?.themePack; if (ownedThemePack) { return normalizeThemePack(ownedThemePack, base); } const lexicon = collectProfileLexicon(profile); const semanticLexicon = collectSemanticAnchorLexicon(profile); const semanticAnchor = profile.ownedSettingLayers?.semanticAnchor; const expressionProfile = profile.ownedSettingLayers?.expressionProfile; return normalizeThemePack( { ...base, institutionLexicon: dedupeStrings([ ...base.institutionLexicon, ...(semanticAnchor?.institutionTypes ?? []), ...lexicon.filter((item) => item.length >= 2), ]), tabooLexicon: dedupeStrings([ ...base.tabooLexicon, ...(semanticAnchor?.tabooTypes ?? []), ...(profile.coreConflicts ?? []).slice(0, 4), ]), artifactClasses: dedupeStrings([ ...base.artifactClasses, ...(semanticAnchor?.carrierTypes ?? []), ]), conflictForms: dedupeStrings([ ...base.conflictForms, ...(semanticAnchor?.conflictForms ?? []), ...(profile.coreConflicts ?? []).slice(0, 3), ]), clueForms: dedupeStrings([ ...base.clueForms, ...(expressionProfile?.clueDirectives ?? []), ...(profile.majorFactions ?? []).slice(0, 3), ]), namingPatterns: dedupeStrings([ ...base.namingPatterns, ...(expressionProfile?.namingDirectives ?? []), ]), revealStyles: dedupeStrings([ ...base.revealStyles, ...(expressionProfile?.revealDirectives ?? []), ]), toneRange: dedupeStrings([ profile.tone, ...(expressionProfile?.presentationTone ?? []), ...base.toneRange, ]), actorArchetypes: dedupeStrings([ ...base.actorArchetypes, ...semanticLexicon.filter((item) => item.length >= 2), ]), }, base, ); }