import type { CreatureArchetypeProfile, CustomWorldNpc, CustomWorldPlayableNpc, CustomWorldProfile, RoleArchetypeProfile, SceneArchetypeBucket, } from '../types'; type SceneBucketSignalPreset = { keywords: string[]; }; type CreatureArchetypeSignalPreset = { keywords: string[]; combatTags: string[]; habitatTags: string[]; }; type RoleArchetypeSignalPreset = { keywords: string[]; templateCharacterIds: string[]; }; const SCENE_BUCKET_SIGNAL_PRESETS: Record = { 高压入口区: { keywords: ['入口', '关口', '哨站', '桥口', '门廊', '边关'], }, 临水渡口区: { keywords: ['渡口', '码头', '港口', '岸线', '船坞', '水路'], }, 仪式神殿区: { keywords: ['祭坛', '神殿', '仪式', '法坛', '庙宇', '圣所'], }, 高空通路区: { keywords: ['高空', '悬桥', '云阶', '塔顶', '崖道', '飞桥'], }, 工业热区: { keywords: ['工坊', '轨道', '机库', '熔炉', '工场', '锅炉'], }, 地底遗迹区: { keywords: ['地宫', '矿道', '遗迹', '洞窟', '墓道', '地底'], }, 群落聚居区: { keywords: ['街巷', '聚落', '城镇', '营地', '居所', '市集'], }, 高压交汇区: { keywords: ['险地', '封锁', '交汇', '前线', '险关', '断层'], }, 叙事缓冲区: { keywords: ['归处', '栖居', '缓冲', '休整', '据点', '落脚'], }, }; const CREATURE_ARCHETYPE_SIGNAL_PRESETS: Record< string, CreatureArchetypeSignalPreset > = { 潜伏袭击者: { keywords: ['潜伏', '伏击', '突袭', '暗影', '贴身'], combatTags: ['快袭', '突进', '机动'], habitatTags: ['雾林', '断垣', '妖雾', '崖壁'], }, 重甲承压者: { keywords: ['重甲', '承压', '守线', '堵截', '厚重'], combatTags: ['重甲', '守御', '护体', '堡垒'], habitatTags: ['矿道', '废城', '边关', '地宫'], }, 群居骚扰者: { keywords: ['群居', '骚扰', '游窜', '围猎', '消耗'], combatTags: ['机动', '追击', '控场'], habitatTags: ['竹林', '雾林', '荒野', '月湖'], }, 远程威胁者: { keywords: ['远程', '投射', '压制', '炮击', '凝视'], combatTags: ['远射', '法修', '雷法'], habitatTags: ['长街', '仙门', '星舟', '祭坛'], }, 异化污染体: { keywords: ['异化', '污染', '腐化', '潮灾', '侵蚀'], combatTags: ['法力', '回复', '重甲'], habitatTags: ['洞天', '谷地', '秘境', '灵泉'], }, 灵体回响体: { keywords: ['灵体', '回响', '残魂', '旧痕', '幽灵'], combatTags: ['镇邪', '控场', '法修'], habitatTags: ['遗迹', '祭坛', '古迹', '废寺'], }, 机关守卫体: { keywords: ['机关', '守卫', '节点', '封印', '装置'], combatTags: ['守御', '压制', '符阵'], habitatTags: ['铸坊', '工场', '前哨', '长廊'], }, 回响追猎者: { keywords: ['追猎', '回响', '索敌', '追索', '名单'], combatTags: ['追击', '压制', '机动'], habitatTags: ['前线', '断层', '渡口', '雾港'], }, }; const ROLE_ARCHETYPE_SIGNAL_PRESETS: Record = { 正面推进型: { keywords: ['推进', '压前', '正面', '先锋', '破阵'], templateCharacterIds: ['sword-princess', 'punch-hero'], }, 远程压制型: { keywords: ['远程', '弓', '射击', '投掷', '炮击'], templateCharacterIds: ['archer-hero'], }, 控场解构型: { keywords: ['控场', '阵', '法', '机关', '解构', '牵制'], templateCharacterIds: ['fighter-4', 'girl-hero'], }, 续航承压型: { keywords: ['承压', '护体', '守御', '续航', '稳阵'], templateCharacterIds: ['fighter-4', 'punch-hero'], }, 潜行爆发型: { keywords: ['潜行', '爆发', '影袭', '突进', '追击'], templateCharacterIds: ['girl-hero', 'sword-princess'], }, }; type ReferenceRoleSource = Pick< CustomWorldPlayableNpc | CustomWorldNpc, 'id' | 'name' | 'title' | 'role' | 'description' | 'personality' | 'combatStyle' | 'tags' >; type ReferenceCreatureSource = Partial< Pick< CustomWorldPlayableNpc & CustomWorldNpc, | 'id' | 'name' | 'title' | 'role' | 'description' | 'backstory' | 'personality' | 'motivation' | 'combatStyle' | 'relationshipHooks' | 'tags' > >; function toText(value: unknown) { return typeof value === 'string' ? value.trim() : ''; } function dedupeStrings( values: Array, max = 12, ) { return [...new Set(values.map((value) => value?.trim() ?? '').filter(Boolean))] .slice(0, max); } function hashText(value: string) { let hash = 0; for (let index = 0; index < value.length; index += 1) { hash = (hash * 31 + value.charCodeAt(index)) >>> 0; } return hash >>> 0; } function buildRoleSourceText(role: ReferenceRoleSource) { return dedupeStrings([ role.name, role.title, role.role, role.description, role.personality, role.combatStyle, ...(role.tags ?? []), ]).join(' '); } function buildCreatureSourceText(source: ReferenceCreatureSource) { return dedupeStrings([ source.name, source.title, source.role, source.description, source.backstory, source.personality, source.motivation, source.combatStyle, ...(source.relationshipHooks ?? []), ...(source.tags ?? []), ]).join(' '); } function scoreTextMatches(sourceText: string, keywords: string[]) { return keywords.reduce((score, keyword) => { if (!keyword || !sourceText.includes(keyword)) { return score; } if (keyword.length >= 4) { return score + 8; } if (keyword.length === 3) { return score + 6; } return score + 4; }, 0); } function getReferenceProfile(profile: CustomWorldProfile | null | undefined) { return profile?.ownedSettingLayers?.referenceProfile ?? null; } export function collectSceneBucketSignalKeywords( bucket: Pick, ) { const preset = SCENE_BUCKET_SIGNAL_PRESETS[bucket.label]; return dedupeStrings([ bucket.label, ...bucket.keywords, ...bucket.moodTags, ...(preset?.keywords ?? []), ]); } export function resolveSceneBucketForLandmark( profile: CustomWorldProfile | null | undefined, landmark: Pick, ) { const sceneBuckets = getReferenceProfile(profile)?.sceneBuckets ?? []; if (sceneBuckets.length === 0) { return null; } const explicitBucket = sceneBuckets.find((bucket) => bucket.referenceLandmarkIds.includes(landmark.id), ); if (explicitBucket) { return explicitBucket; } const sourceText = dedupeStrings([landmark.name, landmark.description]).join(' '); const scoredBuckets = sceneBuckets .map((bucket) => ({ bucket, score: scoreTextMatches(sourceText, collectSceneBucketSignalKeywords(bucket)), })) .sort((left, right) => right.score - left.score); return (scoredBuckets[0]?.score ?? 0) > 0 ? scoredBuckets[0]?.bucket ?? null : null; } export function collectCreatureArchetypeSignals( archetype: Pick, ) { const preset = CREATURE_ARCHETYPE_SIGNAL_PRESETS[archetype.label]; return { keywords: dedupeStrings([ archetype.label, archetype.threatStyle, ...archetype.keywords, ...(preset?.keywords ?? []), ]), combatTags: dedupeStrings(preset?.combatTags ?? [], 6), habitatTags: dedupeStrings(preset?.habitatTags ?? [], 6), }; } export function resolveCreatureArchetypeForSource( profile: CustomWorldProfile | null | undefined, source: ReferenceCreatureSource, ) { const creatureArchetypes = getReferenceProfile(profile)?.creatureArchetypes ?? []; if (creatureArchetypes.length === 0) { return null; } const sourceText = buildCreatureSourceText(source); const scoredArchetypes = creatureArchetypes .map((archetype) => ({ archetype, score: scoreTextMatches( sourceText, collectCreatureArchetypeSignals(archetype).keywords, ), })) .sort((left, right) => right.score - left.score); return (scoredArchetypes[0]?.score ?? 0) > 0 ? scoredArchetypes[0]?.archetype ?? null : creatureArchetypes[0] ?? null; } function collectRoleArchetypeSignals( archetype: Pick< RoleArchetypeProfile, | 'label' | 'combatFocus' | 'narrativeFunction' | 'tags' | 'sourceTemplateCharacterIds' >, ) { const preset = ROLE_ARCHETYPE_SIGNAL_PRESETS[archetype.label]; return { keywords: dedupeStrings([ archetype.label, archetype.combatFocus, archetype.narrativeFunction, ...archetype.tags, ...(preset?.keywords ?? []), ]), templateCharacterIds: archetype.sourceTemplateCharacterIds.length > 0 ? archetype.sourceTemplateCharacterIds : preset?.templateCharacterIds ?? [], }; } export function resolveRoleArchetypeForRole( profile: CustomWorldProfile | null | undefined, role: ReferenceRoleSource, ) { const roleArchetypes = getReferenceProfile(profile)?.roleArchetypes ?? []; if (roleArchetypes.length === 0) { return null; } const explicitArchetype = roleArchetypes.find((archetype) => archetype.sourceRoleIds.includes(role.id), ); if (explicitArchetype) { return explicitArchetype; } const sourceText = buildRoleSourceText(role); const scoredArchetypes = roleArchetypes .map((archetype) => ({ archetype, score: scoreTextMatches(sourceText, collectRoleArchetypeSignals(archetype).keywords), })) .sort((left, right) => right.score - left.score); return (scoredArchetypes[0]?.score ?? 0) > 0 ? scoredArchetypes[0]?.archetype ?? null : roleArchetypes[0] ?? null; } export function resolveRoleTemplateCharacterIdFromReferenceProfile( profile: CustomWorldProfile | null | undefined, role: ReferenceRoleSource, ) { const archetype = resolveRoleArchetypeForRole(profile, role); if (!archetype) { return null; } const templateCharacterIds = collectRoleArchetypeSignals(archetype).templateCharacterIds; if (templateCharacterIds.length === 0) { return null; } const seedSource = toText(role.id) || buildRoleSourceText(role); return templateCharacterIds[hashText(seedSource) % templateCharacterIds.length] ?? null; }