Files
Genarrative/src/services/customWorldOwnedSettingLayers.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

982 lines
31 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<CustomWorldThemeMode, number> = {
mythic: 160,
martial: 160,
arcane: 140,
machina: 160,
tide: 160,
rift: 160,
};
const SEMANTIC_ANCHOR_PRESETS: Record<
CustomWorldThemeMode,
Omit<CustomWorldSemanticAnchor, 'atmosphereTags'>
> = {
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<Omit<CreatureArchetypeProfile, 'id'>>
> = {
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<string | null | undefined>,
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<CustomWorldProfile['playableNpcs'][number], 'combatStyle' | 'role' | 'tags'>,
) {
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<CustomWorldProfile['landmarks'][number], 'name' | 'description'>,
) {
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<string, unknown>;
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<string, unknown>;
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<string, unknown>;
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<string, unknown>;
const semanticAnchorItem =
item.semanticAnchor && typeof item.semanticAnchor === 'object'
? (item.semanticAnchor as Record<string, unknown>)
: {};
const ruleProfileItem =
item.ruleProfile && typeof item.ruleProfile === 'object'
? (item.ruleProfile as Record<string, unknown>)
: {};
const resourceLabelsItem =
ruleProfileItem.resourceLabels &&
typeof ruleProfileItem.resourceLabels === 'object'
? (ruleProfileItem.resourceLabels as Record<string, unknown>)
: {};
const expressionProfileItem =
item.expressionProfile && typeof item.expressionProfile === 'object'
? (item.expressionProfile as Record<string, unknown>)
: {};
const referenceProfileItem =
item.referenceProfile && typeof item.referenceProfile === 'object'
? (item.referenceProfile as Record<string, unknown>)
: {};
const compatibilityProfileItem =
item.compatibilityProfile && typeof item.compatibilityProfile === 'object'
? (item.compatibilityProfile as Record<string, unknown>)
: {};
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<string, unknown>)
.initialCurrency === 'number' &&
Number.isFinite(
(ruleProfileItem.economyProfile as Record<string, unknown>)
.initialCurrency,
)
? Math.max(
0,
Math.round(
(ruleProfileItem.economyProfile as Record<string, unknown>)
.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;
}