import { coerceWorldAttributeSchema } from '../data/attributeValidation'; import { type CreatureArchetypeProfile, type CustomWorldCompatibilityProfile, type CustomWorldExpressionProfile, type CustomWorldOwnedSettingLayers, type CustomWorldProfile, type CustomWorldReferenceProfile, type CustomWorldRuleProfile, type CustomWorldSemanticAnchor, type RoleArchetypeProfile, type SceneArchetypeBucket, WorldType, } from '../types'; import { type CustomWorldThemeMode, detectCustomWorldThemeMode, resolveCustomWorldCompatibilityTemplateWorldType, } from './customWorldTheme'; import { buildThemePackFromWorldProfile, normalizeThemePack, } from './storyEngine/themePack'; const OWNED_SETTING_LAYER_MIGRATION_VERSION = '2026-04-08-owned-setting-layers-v1'; const RESOURCE_LABEL_PRESETS: Record< CustomWorldThemeMode, CustomWorldRuleProfile['resourceLabels'] > = { mythic: { hp: '生命', mp: '心流', maxHp: '生命上限', maxMp: '心流上限', damage: '势能', guard: '防护', range: '距离', cooldown: '回整', manaCost: '心流消耗', currency: '旅券', }, martial: { hp: '气血', mp: '内力', maxHp: '气血上限', maxMp: '内力上限', damage: '招式', guard: '防御', range: '招距', cooldown: '调息', manaCost: '内力消耗', currency: '铜钱', }, arcane: { hp: '元命', mp: '灵韵', maxHp: '元命上限', maxMp: '灵韵上限', damage: '术法', guard: '护盾', range: '术距', cooldown: '回息', manaCost: '灵韵消耗', currency: '灵石', }, machina: { hp: '耐久', mp: '能量', maxHp: '耐久上限', maxMp: '能量上限', damage: '火力', guard: '护盾', range: '射程', cooldown: '充能', manaCost: '能量消耗', currency: '配给券', }, tide: { hp: '潮命', mp: '潮息', maxHp: '潮命上限', maxMp: '潮息上限', damage: '潮势', guard: '潮护', range: '潮距', cooldown: '回潮', manaCost: '潮息消耗', currency: '潮银', }, rift: { hp: '界命', mp: '裂能', maxHp: '界命上限', maxMp: '裂能上限', damage: '界势', guard: '稳界', range: '界距', cooldown: '复界', manaCost: '裂能消耗', currency: '边贸券', }, }; const INITIAL_CURRENCY_PRESETS: Record = { mythic: 160, martial: 160, arcane: 140, machina: 160, tide: 160, rift: 160, }; const SEMANTIC_ANCHOR_PRESETS: Record< CustomWorldThemeMode, Omit > = { mythic: { genreSignals: ['跨题材冒险', '未知旅境'], conflictForms: ['追查', '护送', '回收', '失踪追索'], institutionTypes: ['据点', '旅团', '档案室', '归舍'], tabooTypes: ['越界', '封存', '失约', '旧痕'], carrierTypes: ['信物', '残页', '样本', '旧钥'], forceSystemTypes: ['回响', '誓约', '遗物', '余波'], }, martial: { genreSignals: ['江湖纷争', '旧案追索'], conflictForms: ['寻仇', '围剿', '护送', '失踪追查'], institutionTypes: ['门派', '镖局', '巡司', '商号'], tabooTypes: ['旧案', '断誓', '禁脉', '失契'], carrierTypes: ['遗兵', '令牌', '残卷', '旧佩'], forceSystemTypes: ['心法', '招式', '经脉', '誓约'], }, arcane: { genreSignals: ['灵异修行', '秘境因果'], conflictForms: ['夺脉', '封印失衡', '宗门旧案', '秘境争夺'], institutionTypes: ['宗门', '法坛', '巡守司', '灵舟会'], tabooTypes: ['封印', '禁术', '残魂', '逆脉'], carrierTypes: ['法器', '灵符', '玉简', '阵核'], forceSystemTypes: ['灵脉', '术式', '契约', '神识'], }, machina: { genreSignals: ['工业前线', '失控科技'], conflictForms: ['封锁', '回收', '追查事故', '前线失守'], institutionTypes: ['财团', '工坊', '舰队', '调查局'], tabooTypes: ['过载', '失控协议', '封存日志', '污染区'], carrierTypes: ['芯片', '驱动核', '记录模组', '封存匣'], forceSystemTypes: ['科技', '协议', '驱动', '能量网'], }, tide: { genreSignals: ['海岸悬疑', '潮灾余波'], conflictForms: ['封港', '海路争夺', '追查失踪', '护送穿渡'], institutionTypes: ['港务', '巡海司', '渡船会', '潮站'], tabooTypes: ['沉船', '禁海区', '回潮夜', '失契'], carrierTypes: ['航图', '潮印', '信标', '封潮匣'], forceSystemTypes: ['潮汐', '雾潮', '海誓', '异流'], }, rift: { genreSignals: ['裂界边境', '战线余烬'], conflictForms: ['守线', '撤离', '回收异常', '追查失线'], institutionTypes: ['前哨', '巡边队', '断层站', '回收组'], tabooTypes: ['断层失守', '界外污染', '封桥令', '旧撤离线'], carrierTypes: ['界核', '锚印', '样本', '回响记录'], forceSystemTypes: ['裂界', '界压', '污染', '锚定'], }, }; const CREATURE_ARCHETYPE_PRESETS: Record< CustomWorldThemeMode, Array> > = { mythic: [ { label: '潜伏袭击者', threatStyle: '借地形潜伏后突然贴身施压。', keywords: ['潜伏', '伏击', '前探阻断'], }, { label: '群居骚扰者', threatStyle: '依靠数量与机动性反复撕扯阵线。', keywords: ['群居', '扰动', '消耗'], }, { label: '回响追猎者', threatStyle: '会追着异常痕迹与关键目标持续压迫。', keywords: ['回响', '追索', '持续压迫'], }, ], martial: [ { label: '潜伏袭击者', threatStyle: '先藏身再借速度打出首轮杀招。', keywords: ['潜袭', '伏击', '贴身爆发'], }, { label: '重甲承压者', threatStyle: '站住正面、顶着伤害强行换血。', keywords: ['承压', '守线', '正面对撞'], }, { label: '远程威胁者', threatStyle: '依靠暗器、弓弩或投掷不断压制走位。', keywords: ['远程', '压制', '封走位'], }, ], arcane: [ { label: '灵体回响体', threatStyle: '借余波与残识干扰节奏并持续追逼。', keywords: ['灵体', '回响', '术式残留'], }, { label: '异化污染体', threatStyle: '被灵潮扭曲后具备高压近身威胁。', keywords: ['异化', '污染', '近身撕咬'], }, { label: '机关守卫体', threatStyle: '围绕阵核或封印节点进行固守打击。', keywords: ['机关', '守卫', '节点压制'], }, ], machina: [ { label: '远程威胁者', threatStyle: '依靠火力、脉冲或投射装置封锁空间。', keywords: ['火力', '远程', '封锁'], }, { label: '重装阻断者', threatStyle: '借重甲和装置正面堵截推进线路。', keywords: ['重装', '阻断', '压线'], }, { label: '失控追击者', threatStyle: '高频位移并持续追杀被标记目标。', keywords: ['失控', '追击', '高机动'], }, ], tide: [ { label: '群居骚扰者', threatStyle: '借潮湿地形和数量优势消耗行进队伍。', keywords: ['群居', '潮湿', '消耗'], }, { label: '潜伏袭击者', threatStyle: '利用雾潮与死角打出突袭。', keywords: ['迷雾', '潜伏', '突袭'], }, { label: '异化污染体', threatStyle: '会沿潮灾痕迹持续扩散压迫。', keywords: ['潮灾', '异化', '扩散'], }, ], rift: [ { label: '异化污染体', threatStyle: '长期暴露在裂界环境后具备高压侵蚀性。', keywords: ['污染', '侵蚀', '裂界'], }, { label: '远程威胁者', threatStyle: '依靠界压残波或碎片投射逼迫走位。', keywords: ['界压', '残波', '远程'], }, { label: '机关守卫体', threatStyle: '围绕前哨节点和封桥设施持续守线。', keywords: ['前哨', '守线', '节点'], }, ], }; function toText(value: unknown) { return typeof value === 'string' ? value.trim() : ''; } function toStringArray(value: unknown, max = 8) { if (!Array.isArray(value)) { return []; } return value .map((item) => toText(item)) .filter(Boolean) .slice(0, max); } function dedupeStrings( values: Array, max = 8, ) { return [...new Set(values.map((value) => value?.trim() ?? '').filter(Boolean))] .slice(0, max); } function splitToneTags(tone: string) { return dedupeStrings(tone.split(/[,、,/\s]+/u), 6); } function inferInstitutionType(labels: string[]) { return dedupeStrings( labels.map((label) => { if (/[宗门宫阁派]/u.test(label)) return '宗门'; if (/[司局署院]/u.test(label)) return '调查局'; if (/[团盟会]/u.test(label)) return '同盟'; if (/[公司财团]/u.test(label)) return '公司'; if (/[港坞渡站哨]/u.test(label)) return '前哨'; if (/[学园学院书院]/u.test(label)) return '学园'; if (/[舰船坞航]/u.test(label)) return '舰队'; if (/[部族寨群落]/u.test(label)) return '部族'; return label.length <= 8 ? label : ''; }), 4, ); } function inferForceSystemTypes(profile: CustomWorldProfile) { const source = `${profile.settingText} ${profile.summary} ${profile.tone} ${profile.playerGoal}`; const detected = [ /科技|机巧|协议|工坊|脉冲|驱动/u.test(source) ? '科技' : null, /海潮|潮汐|海雾|港湾/u.test(source) ? '潮汐' : null, /裂界|断层|界桥|灰域|污染/u.test(source) ? '裂界' : null, /仙|灵|术|阵|符|秘境|宗门/u.test(source) ? '灵脉' : null, /契约|誓约|旧誓/u.test(source) ? '契约' : null, /回响|残响|余波/u.test(source) ? '回响' : null, ]; return dedupeStrings(detected, 4); } function inferRoleArchetypeLabel( role: Pick, ) { const source = `${role.role} ${role.combatStyle} ${role.tags.join(' ')}`; if (/[弓|炮|射|远程|投掷|火力]/u.test(source)) { return '远程压制型'; } if (/[盾|守|承压|阵线|重甲|护]/u.test(source)) { return '续航承压型'; } if (/[影|潜|刺|闪|遁|爆发]/u.test(source)) { return '潜行爆发型'; } if (/[法|术|控|阵|咒|机关|干扰|牵制]/u.test(source)) { return '控场解构型'; } return '正面推进型'; } function inferSceneBucketLabel( landmark: Pick, ) { const source = `${landmark.name} ${landmark.description}`; if (/[港|湾|渡口|码头|潮]/u.test(source)) return '临水渡口区'; if (/[桥|关|哨|门|入口]/u.test(source)) return '高压入口区'; if (/[殿|坛|仪式|祭]/u.test(source)) return '仪式神殿区'; if (/[塔|空桥|高空|云|崖]/u.test(source)) return '高空通路区'; if (/[厂|工坊|机库|轨|站]/u.test(source)) return '工业热区'; if (/[洞|穴|地下|遗迹|墓]/u.test(source)) return '地底遗迹区'; if (/[街|巷|城|镇|居]/u.test(source)) return '群落聚居区'; return '叙事缓冲区'; } function buildRoleArchetypes(profile: CustomWorldProfile) { return profile.playableNpcs.slice(0, 6).map((role, index) => ({ id: `role-archetype-${index + 1}`, label: inferRoleArchetypeLabel(role), combatFocus: role.combatStyle.trim() || role.role.trim() || '围绕核心职责推进战局。', narrativeFunction: role.role.trim() || role.description.trim() || '在主线推进中提供关键响应。', sourceRoleIds: [role.id], sourceTemplateCharacterIds: [], tags: dedupeStrings(role.tags, 5), })) satisfies RoleArchetypeProfile[]; } function buildSceneBuckets(profile: CustomWorldProfile) { return profile.landmarks.slice(0, 8).map((landmark, index) => ({ id: `scene-bucket-${index + 1}`, label: inferSceneBucketLabel(landmark), moodTags: dedupeStrings( splitToneTags(profile.tone), 4, ), keywords: dedupeStrings([landmark.name, landmark.description], 4), referenceLandmarkIds: [landmark.id], })) satisfies SceneArchetypeBucket[]; } function buildCreatureArchetypes(mode: CustomWorldThemeMode) { return CREATURE_ARCHETYPE_PRESETS[mode].map((creature, index) => ({ id: `creature-archetype-${index + 1}`, ...creature, })) satisfies CreatureArchetypeProfile[]; } function buildThemePackSeed(profile: CustomWorldProfile) { return buildThemePackFromWorldProfile({ settingText: profile.settingText, summary: profile.summary, tone: profile.tone, playerGoal: profile.playerGoal, templateWorldType: resolveCustomWorldCompatibilityTemplateWorldType(profile), majorFactions: profile.majorFactions, coreConflicts: profile.coreConflicts, ownedSettingLayers: null, }); } function compileSemanticAnchor( profile: CustomWorldProfile, mode: CustomWorldThemeMode, ) { const preset = SEMANTIC_ANCHOR_PRESETS[mode]; const creatorIntent = profile.creatorIntent; const institutionHints = inferInstitutionType([ ...profile.majorFactions, ...(creatorIntent?.keyFactions.map((seed) => seed.name) ?? []), ]); const forceSystemTypes = inferForceSystemTypes(profile); return { genreSignals: dedupeStrings( [...(creatorIntent?.themeKeywords ?? []), ...preset.genreSignals], 6, ), conflictForms: dedupeStrings( [...profile.coreConflicts, ...preset.conflictForms], 6, ), institutionTypes: dedupeStrings( [...institutionHints, ...preset.institutionTypes], 6, ), tabooTypes: dedupeStrings( [...profile.coreConflicts, ...preset.tabooTypes], 6, ), carrierTypes: dedupeStrings( [...(creatorIntent?.iconicElements ?? []), ...preset.carrierTypes], 6, ), forceSystemTypes: dedupeStrings( [...forceSystemTypes, ...preset.forceSystemTypes], 6, ), atmosphereTags: dedupeStrings( [...splitToneTags(profile.tone), ...preset.genreSignals], 6, ), } satisfies CustomWorldSemanticAnchor; } function compileRuleProfile( profile: CustomWorldProfile, mode: CustomWorldThemeMode, ) { return { attributeSchema: profile.attributeSchema, resourceLabels: RESOURCE_LABEL_PRESETS[mode], economyProfile: { initialCurrency: INITIAL_CURRENCY_PRESETS[mode], }, } satisfies CustomWorldRuleProfile; } function compileExpressionProfile( profile: CustomWorldProfile, semanticAnchor: CustomWorldSemanticAnchor, ) { const fallbackThemePack = buildThemePackSeed(profile); const themePack = normalizeThemePack(profile.themePack, fallbackThemePack); return { themePack, presentationTone: dedupeStrings( [profile.tone, ...semanticAnchor.atmosphereTags, ...themePack.toneRange], 8, ), namingDirectives: dedupeStrings(themePack.namingPatterns, 6), clueDirectives: dedupeStrings(themePack.clueForms, 6), revealDirectives: dedupeStrings(themePack.revealStyles, 6), } satisfies CustomWorldExpressionProfile; } function compileReferenceProfile( profile: CustomWorldProfile, mode: CustomWorldThemeMode, ) { return { roleArchetypes: buildRoleArchetypes(profile), sceneBuckets: buildSceneBuckets(profile), creatureArchetypes: buildCreatureArchetypes(mode), } satisfies CustomWorldReferenceProfile; } function compileCompatibilityProfile(profile: CustomWorldProfile) { const compatibilityTemplateWorldType = resolveCustomWorldCompatibilityTemplateWorldType(profile); return { compatibilityTemplateWorldType, legacyTemplateWorldType: compatibilityTemplateWorldType, migrationVersion: OWNED_SETTING_LAYER_MIGRATION_VERSION, } satisfies CustomWorldCompatibilityProfile; } function normalizeRoleArchetypes( value: unknown, fallback: RoleArchetypeProfile[], ) { if (!Array.isArray(value)) { return fallback; } const normalized = value .map((entry, index) => { if (!entry || typeof entry !== 'object') { return null; } const item = entry as Record; const label = toText(item.label); if (!label) { return null; } return { id: toText(item.id) || `role-archetype-${index + 1}`, label, combatFocus: toText(item.combatFocus) || fallback[index]?.combatFocus || '围绕核心职责推进战局。', narrativeFunction: toText(item.narrativeFunction) || fallback[index]?.narrativeFunction || '在主线推进中提供关键响应。', sourceRoleIds: toStringArray(item.sourceRoleIds, 4), sourceTemplateCharacterIds: toStringArray( item.sourceTemplateCharacterIds, 4, ), tags: dedupeStrings(toStringArray(item.tags, 5), 5), } satisfies RoleArchetypeProfile; }) .filter((entry): entry is RoleArchetypeProfile => Boolean(entry)); return normalized.length > 0 ? normalized : fallback; } function normalizeSceneBuckets( value: unknown, fallback: SceneArchetypeBucket[], ) { if (!Array.isArray(value)) { return fallback; } const normalized = value .map((entry, index) => { if (!entry || typeof entry !== 'object') { return null; } const item = entry as Record; const label = toText(item.label); if (!label) { return null; } return { id: toText(item.id) || `scene-bucket-${index + 1}`, label, moodTags: dedupeStrings(toStringArray(item.moodTags, 4), 4), keywords: dedupeStrings(toStringArray(item.keywords, 4), 4), referenceLandmarkIds: toStringArray(item.referenceLandmarkIds, 4), } satisfies SceneArchetypeBucket; }) .filter((entry): entry is SceneArchetypeBucket => Boolean(entry)); return normalized.length > 0 ? normalized : fallback; } function normalizeCreatureArchetypes( value: unknown, fallback: CreatureArchetypeProfile[], ) { if (!Array.isArray(value)) { return fallback; } const normalized = value .map((entry, index) => { if (!entry || typeof entry !== 'object') { return null; } const item = entry as Record; const label = toText(item.label); if (!label) { return null; } return { id: toText(item.id) || `creature-archetype-${index + 1}`, label, threatStyle: toText(item.threatStyle) || fallback[index]?.threatStyle || '围绕核心威胁方式持续施压。', keywords: dedupeStrings(toStringArray(item.keywords, 4), 4), } satisfies CreatureArchetypeProfile; }) .filter((entry): entry is CreatureArchetypeProfile => Boolean(entry)); return normalized.length > 0 ? normalized : fallback; } export function compileOwnedSettingLayersFromLegacyTemplate( profile: CustomWorldProfile, ) { const mode = detectCustomWorldThemeMode({ settingText: profile.settingText, summary: profile.summary, tone: profile.tone, playerGoal: profile.playerGoal, templateWorldType: profile.templateWorldType, compatibilityTemplateWorldType: profile.compatibilityTemplateWorldType ?? profile.templateWorldType, ownedSettingLayers: null, }); const semanticAnchor = compileSemanticAnchor(profile, mode); return { semanticAnchor, ruleProfile: compileRuleProfile(profile, mode), expressionProfile: compileExpressionProfile(profile, semanticAnchor), referenceProfile: compileReferenceProfile(profile, mode), compatibilityProfile: compileCompatibilityProfile(profile), } satisfies CustomWorldOwnedSettingLayers; } export function normalizeCustomWorldOwnedSettingLayers( value: unknown, profile: CustomWorldProfile, ) { const fallback = compileOwnedSettingLayersFromLegacyTemplate(profile); if (!value || typeof value !== 'object') { return fallback; } const item = value as Record; const semanticAnchorItem = item.semanticAnchor && typeof item.semanticAnchor === 'object' ? (item.semanticAnchor as Record) : {}; const ruleProfileItem = item.ruleProfile && typeof item.ruleProfile === 'object' ? (item.ruleProfile as Record) : {}; const resourceLabelsItem = ruleProfileItem.resourceLabels && typeof ruleProfileItem.resourceLabels === 'object' ? (ruleProfileItem.resourceLabels as Record) : {}; const expressionProfileItem = item.expressionProfile && typeof item.expressionProfile === 'object' ? (item.expressionProfile as Record) : {}; const referenceProfileItem = item.referenceProfile && typeof item.referenceProfile === 'object' ? (item.referenceProfile as Record) : {}; const compatibilityProfileItem = item.compatibilityProfile && typeof item.compatibilityProfile === 'object' ? (item.compatibilityProfile as Record) : {}; return { semanticAnchor: { genreSignals: dedupeStrings( toStringArray( semanticAnchorItem.genreSignals, fallback.semanticAnchor.genreSignals.length, ), fallback.semanticAnchor.genreSignals.length, ).length ? dedupeStrings( toStringArray( semanticAnchorItem.genreSignals, fallback.semanticAnchor.genreSignals.length, ), fallback.semanticAnchor.genreSignals.length, ) : fallback.semanticAnchor.genreSignals, conflictForms: dedupeStrings( toStringArray( semanticAnchorItem.conflictForms, fallback.semanticAnchor.conflictForms.length, ), fallback.semanticAnchor.conflictForms.length, ).length ? dedupeStrings( toStringArray( semanticAnchorItem.conflictForms, fallback.semanticAnchor.conflictForms.length, ), fallback.semanticAnchor.conflictForms.length, ) : fallback.semanticAnchor.conflictForms, institutionTypes: dedupeStrings( toStringArray( semanticAnchorItem.institutionTypes, fallback.semanticAnchor.institutionTypes.length, ), fallback.semanticAnchor.institutionTypes.length, ).length ? dedupeStrings( toStringArray( semanticAnchorItem.institutionTypes, fallback.semanticAnchor.institutionTypes.length, ), fallback.semanticAnchor.institutionTypes.length, ) : fallback.semanticAnchor.institutionTypes, tabooTypes: dedupeStrings( toStringArray( semanticAnchorItem.tabooTypes, fallback.semanticAnchor.tabooTypes.length, ), fallback.semanticAnchor.tabooTypes.length, ).length ? dedupeStrings( toStringArray( semanticAnchorItem.tabooTypes, fallback.semanticAnchor.tabooTypes.length, ), fallback.semanticAnchor.tabooTypes.length, ) : fallback.semanticAnchor.tabooTypes, carrierTypes: dedupeStrings( toStringArray( semanticAnchorItem.carrierTypes, fallback.semanticAnchor.carrierTypes.length, ), fallback.semanticAnchor.carrierTypes.length, ).length ? dedupeStrings( toStringArray( semanticAnchorItem.carrierTypes, fallback.semanticAnchor.carrierTypes.length, ), fallback.semanticAnchor.carrierTypes.length, ) : fallback.semanticAnchor.carrierTypes, forceSystemTypes: dedupeStrings( toStringArray( semanticAnchorItem.forceSystemTypes, fallback.semanticAnchor.forceSystemTypes.length, ), fallback.semanticAnchor.forceSystemTypes.length, ).length ? dedupeStrings( toStringArray( semanticAnchorItem.forceSystemTypes, fallback.semanticAnchor.forceSystemTypes.length, ), fallback.semanticAnchor.forceSystemTypes.length, ) : fallback.semanticAnchor.forceSystemTypes, atmosphereTags: dedupeStrings( toStringArray( semanticAnchorItem.atmosphereTags, fallback.semanticAnchor.atmosphereTags.length, ), fallback.semanticAnchor.atmosphereTags.length, ).length ? dedupeStrings( toStringArray( semanticAnchorItem.atmosphereTags, fallback.semanticAnchor.atmosphereTags.length, ), fallback.semanticAnchor.atmosphereTags.length, ) : fallback.semanticAnchor.atmosphereTags, }, ruleProfile: { attributeSchema: coerceWorldAttributeSchema( ruleProfileItem.attributeSchema, fallback.ruleProfile.attributeSchema, ), resourceLabels: { hp: toText(resourceLabelsItem.hp) || fallback.ruleProfile.resourceLabels.hp, mp: toText(resourceLabelsItem.mp) || fallback.ruleProfile.resourceLabels.mp, maxHp: toText(resourceLabelsItem.maxHp) || fallback.ruleProfile.resourceLabels.maxHp, maxMp: toText(resourceLabelsItem.maxMp) || fallback.ruleProfile.resourceLabels.maxMp, damage: toText(resourceLabelsItem.damage) || fallback.ruleProfile.resourceLabels.damage, guard: toText(resourceLabelsItem.guard) || fallback.ruleProfile.resourceLabels.guard, range: toText(resourceLabelsItem.range) || fallback.ruleProfile.resourceLabels.range, cooldown: toText(resourceLabelsItem.cooldown) || fallback.ruleProfile.resourceLabels.cooldown, manaCost: toText(resourceLabelsItem.manaCost) || fallback.ruleProfile.resourceLabels.manaCost, currency: toText(resourceLabelsItem.currency) || fallback.ruleProfile.resourceLabels.currency, }, economyProfile: { initialCurrency: typeof ruleProfileItem.economyProfile === 'object' && ruleProfileItem.economyProfile && typeof (ruleProfileItem.economyProfile as Record) .initialCurrency === 'number' && Number.isFinite( (ruleProfileItem.economyProfile as Record) .initialCurrency, ) ? Math.max( 0, Math.round( (ruleProfileItem.economyProfile as Record) .initialCurrency as number, ), ) : fallback.ruleProfile.economyProfile.initialCurrency, }, }, expressionProfile: { themePack: normalizeThemePack( expressionProfileItem.themePack, fallback.expressionProfile.themePack, ), presentationTone: dedupeStrings( toStringArray( expressionProfileItem.presentationTone, fallback.expressionProfile.presentationTone.length, ), fallback.expressionProfile.presentationTone.length, ).length ? dedupeStrings( toStringArray( expressionProfileItem.presentationTone, fallback.expressionProfile.presentationTone.length, ), fallback.expressionProfile.presentationTone.length, ) : fallback.expressionProfile.presentationTone, namingDirectives: dedupeStrings( toStringArray( expressionProfileItem.namingDirectives, fallback.expressionProfile.namingDirectives.length, ), fallback.expressionProfile.namingDirectives.length, ).length ? dedupeStrings( toStringArray( expressionProfileItem.namingDirectives, fallback.expressionProfile.namingDirectives.length, ), fallback.expressionProfile.namingDirectives.length, ) : fallback.expressionProfile.namingDirectives, clueDirectives: dedupeStrings( toStringArray( expressionProfileItem.clueDirectives, fallback.expressionProfile.clueDirectives.length, ), fallback.expressionProfile.clueDirectives.length, ).length ? dedupeStrings( toStringArray( expressionProfileItem.clueDirectives, fallback.expressionProfile.clueDirectives.length, ), fallback.expressionProfile.clueDirectives.length, ) : fallback.expressionProfile.clueDirectives, revealDirectives: dedupeStrings( toStringArray( expressionProfileItem.revealDirectives, fallback.expressionProfile.revealDirectives.length, ), fallback.expressionProfile.revealDirectives.length, ).length ? dedupeStrings( toStringArray( expressionProfileItem.revealDirectives, fallback.expressionProfile.revealDirectives.length, ), fallback.expressionProfile.revealDirectives.length, ) : fallback.expressionProfile.revealDirectives, }, referenceProfile: { roleArchetypes: normalizeRoleArchetypes( referenceProfileItem.roleArchetypes, fallback.referenceProfile.roleArchetypes, ), sceneBuckets: normalizeSceneBuckets( referenceProfileItem.sceneBuckets, fallback.referenceProfile.sceneBuckets, ), creatureArchetypes: normalizeCreatureArchetypes( referenceProfileItem.creatureArchetypes, fallback.referenceProfile.creatureArchetypes, ), }, compatibilityProfile: { compatibilityTemplateWorldType: compatibilityProfileItem.compatibilityTemplateWorldType === WorldType.XIANXIA ? WorldType.XIANXIA : compatibilityProfileItem.compatibilityTemplateWorldType === WorldType.WUXIA ? WorldType.WUXIA : fallback.compatibilityProfile?.compatibilityTemplateWorldType ?? null, legacyTemplateWorldType: compatibilityProfileItem.legacyTemplateWorldType === WorldType.XIANXIA ? WorldType.XIANXIA : compatibilityProfileItem.legacyTemplateWorldType === WorldType.WUXIA ? WorldType.WUXIA : fallback.compatibilityProfile?.legacyTemplateWorldType ?? null, migrationVersion: toText(compatibilityProfileItem.migrationVersion) || fallback.compatibilityProfile?.migrationVersion || OWNED_SETTING_LAYER_MIGRATION_VERSION, }, } satisfies CustomWorldOwnedSettingLayers; } export function resolveCustomWorldOwnedSettingLayers( profile: CustomWorldProfile | null | undefined, ) { if (!profile) { return null; } return profile.ownedSettingLayers ?? compileOwnedSettingLayersFromLegacyTemplate(profile); } export function resolveCustomWorldRuleProfile( profile: CustomWorldProfile | null | undefined, ) { return resolveCustomWorldOwnedSettingLayers(profile)?.ruleProfile ?? null; } export function resolveCustomWorldExpressionProfile( profile: CustomWorldProfile | null | undefined, ) { return resolveCustomWorldOwnedSettingLayers(profile)?.expressionProfile ?? null; } export function resolveCustomWorldSemanticAnchor( profile: CustomWorldProfile | null | undefined, ) { return resolveCustomWorldOwnedSettingLayers(profile)?.semanticAnchor ?? null; } export function resolveCustomWorldCompatibilityProfile( profile: CustomWorldProfile | null | undefined, ) { return resolveCustomWorldOwnedSettingLayers(profile)?.compatibilityProfile ?? null; }