This commit is contained in:
272
src/services/storyEngine/themePack.ts
Normal file
272
src/services/storyEngine/themePack.ts
Normal file
@@ -0,0 +1,272 @@
|
||||
import type {
|
||||
CustomWorldProfile,
|
||||
ThemePack,
|
||||
WorldTemplateType,
|
||||
WorldType,
|
||||
} from '../../types';
|
||||
import { detectCustomWorldThemeMode } from '../customWorldTheme';
|
||||
|
||||
type ThemePackPreset = Omit<ThemePack, 'id' | 'displayName'> & {
|
||||
displayName: string;
|
||||
};
|
||||
|
||||
const THEME_PACK_PRESETS: Record<string, ThemePackPreset> = {
|
||||
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<string | null | undefined>, 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<CustomWorldProfile, 'ownedSettingLayers'>,
|
||||
) {
|
||||
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<ThemePack>;
|
||||
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,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user