import type { AttributeMigrationTrace, AttributeVector, Character, CharacterSkillDefinition, CustomWorldItem, CustomWorldNpc, CustomWorldPlayableNpc, InventoryItem, ItemAttributeResonance, LegacyAttributeSet, RoleAttributeEvidence, WorldAttributeSchema, } from '../types'; import {WORLD_ATTRIBUTE_SLOT_IDS} from '../types'; import {buildDefaultAxisVector} from './attributeResolver'; import {ensureRoleAttributeProfile} from './attributeValidation'; const AXIS_KEYWORD_RULES: Array<{slotId: string; patterns: RegExp[]; weight: number; reason: string}> = [ { slotId: 'axis_a', patterns: [/骨|甲|壳|岩|重|守|镇|顶|硬|锋|体/u], weight: 16, reason: '文本表现出强承压与硬碰硬倾向。' }, { slotId: 'axis_b', patterns: [/身法|迅|影|风|闪|游|机动|追|步|轻灵/u], weight: 16, reason: '文本强调速度、位移或换位能力。' }, { slotId: 'axis_c', patterns: [/识|眼|谋|算|阵|符|术|察|局|禁制/u], weight: 16, reason: '文本强调洞察、术理或破局能力。' }, { slotId: 'axis_d', patterns: [/心|焰|胆|威|压|怒|决|破|强推|意志/u], weight: 16, reason: '文本强调意志、压迫与决断。' }, { slotId: 'axis_e', patterns: [/缘|契|情|盟|商|医|助|信|交|誓/u], weight: 16, reason: '文本强调关系、共鸣或交换。' }, { slotId: 'axis_f', patterns: [/息|稳|续|守|调|回|养|持久|回复|韧/u], weight: 16, reason: '文本强调稳态、续战或恢复。' }, ]; const SKILL_STYLE_VECTORS: Record = { burst: buildDefaultAxisVector({ axis_a: 0.18, axis_c: 0.2, axis_d: 0.46, axis_f: 0.16 }), steady: buildDefaultAxisVector({ axis_a: 0.16, axis_c: 0.18, axis_e: 0.14, axis_f: 0.52 }), mobility: buildDefaultAxisVector({ axis_b: 0.52, axis_c: 0.12, axis_d: 0.16, axis_f: 0.2 }), finisher: buildDefaultAxisVector({ axis_a: 0.3, axis_b: 0.22, axis_c: 0.2, axis_d: 0.28 }), projectile: buildDefaultAxisVector({ axis_b: 0.26, axis_c: 0.34, axis_d: 0.1, axis_f: 0.3 }), }; function applyKeywordWeights( seed: AttributeVector, sourceText: string, evidence: RoleAttributeEvidence[], schema: WorldAttributeSchema, ) { AXIS_KEYWORD_RULES.forEach(rule => { const matches = rule.patterns.reduce((count, pattern) => count + (pattern.test(sourceText) ? 1 : 0), 0); if (matches <= 0) return; seed[rule.slotId] = (seed[rule.slotId] ?? 0) + rule.weight * matches; const slot = schema.slots.find(item => item.slotId === rule.slotId); if (slot) { evidence.push({ slotId: slot.slotId, reason: `${slot.name}:${rule.reason}`, }); } }); } function buildLegacyAttributeSeed(attributes: LegacyAttributeSet) { return buildDefaultAxisVector({ axis_a: attributes.strength * 8 + attributes.spirit * 2, axis_b: attributes.agility * 9 + attributes.intelligence * 1, axis_c: attributes.intelligence * 8 + attributes.agility * 2, axis_d: attributes.spirit * 5 + attributes.strength * 4 + attributes.agility * 1, axis_e: attributes.spirit * 4 + attributes.intelligence * 4 + attributes.agility * 1, axis_f: attributes.spirit * 7 + attributes.strength * 3, }); } function uniqueEvidence(evidence: RoleAttributeEvidence[]) { const seen = new Set(); return evidence.filter(entry => { const key = `${entry.slotId}:${entry.reason}`; if (seen.has(key)) return false; seen.add(key); return true; }); } export function buildRoleAttributeProfileFromLegacyData({ entityId, schema, legacyAttributes, textBlocks, extraWeights, }: { entityId: string; schema: WorldAttributeSchema; legacyAttributes?: LegacyAttributeSet | null; textBlocks?: Array; extraWeights?: AttributeVector; }) { const evidence: RoleAttributeEvidence[] = []; const seed = legacyAttributes ? buildLegacyAttributeSeed(legacyAttributes) : buildDefaultAxisVector({ axis_a: 58, axis_b: 58, axis_c: 58, axis_d: 58, axis_e: 58, axis_f: 58, }); const sourceText = (textBlocks ?? []).filter(Boolean).join(' '); if (sourceText) { applyKeywordWeights(seed, sourceText, evidence, schema); } WORLD_ATTRIBUTE_SLOT_IDS.forEach(slotId => { seed[slotId] = (seed[slotId] ?? 0) + (extraWeights?.[slotId] ?? 0); }); const fallbackEvidence = uniqueEvidence(evidence).slice(0, 4); const profile = ensureRoleAttributeProfile( { schemaId: schema.id, values: seed, }, schema, { fallbackValues: seed, fallbackEvidence, }, ); const trace: AttributeMigrationTrace = { sourceCharacterId: entityId, schemaId: schema.id, oldAttributes: legacyAttributes ?? undefined, inferredReasons: fallbackEvidence.map(entry => entry.reason), fallbackUsed: false, }; return { profile, trace, }; } export function buildCharacterAttributeProfile(character: Character, schema: WorldAttributeSchema) { return buildRoleAttributeProfileFromLegacyData({ entityId: character.id, schema, legacyAttributes: character.attributes, textBlocks: [ character.title, character.description, character.backstory, character.personality, ...(character.combatTags ?? []), ...character.skills.map(skill => `${skill.name} ${skill.style} ${skill.delivery ?? ''}`), ], }).profile; } export function buildCustomWorldPlayableNpcAttributeProfile( npc: CustomWorldPlayableNpc, schema: WorldAttributeSchema, templateAttributes?: LegacyAttributeSet, ) { return buildRoleAttributeProfileFromLegacyData({ entityId: npc.id, schema, legacyAttributes: templateAttributes, textBlocks: [ npc.title, npc.role, npc.description, npc.backstory, npc.personality, npc.motivation, npc.combatStyle, ...(npc.relationshipHooks ?? []), ...(npc.tags ?? []), ], }).profile; } export function buildCustomWorldStoryNpcAttributeProfile(npc: CustomWorldNpc, schema: WorldAttributeSchema) { return buildRoleAttributeProfileFromLegacyData({ entityId: npc.id, schema, textBlocks: [ npc.title, npc.role, npc.description, npc.backstory, npc.personality, npc.motivation, npc.combatStyle, ...(npc.relationshipHooks ?? []), ...(npc.tags ?? []), ], }).profile; } export function buildMonsterAttributeProfile( monster: { id: string; name: string; description: string; introAction: string; combatTags?: string[]; habitatTags?: string[]; baseStats: { attackRange: number; speed: number; maxHp: number }; }, schema: WorldAttributeSchema, ) { return buildRoleAttributeProfileFromLegacyData({ entityId: monster.id, schema, textBlocks: [ monster.name, monster.description, monster.introAction, ...(monster.combatTags ?? []), ...(monster.habitatTags ?? []), ], extraWeights: buildDefaultAxisVector({ axis_a: monster.baseStats.maxHp >= 150 ? 24 : 0, axis_b: monster.baseStats.speed >= 7 ? 22 : 0, axis_d: monster.baseStats.attackRange >= 1.5 ? 18 : 6, axis_f: monster.baseStats.maxHp >= 180 ? 26 : 10, }), }).profile; } export function buildItemAttributeResonance( item: Pick & { tags?: string[]; buildProfile?: { resonanceVector?: AttributeVector | null } | null; }, ): ItemAttributeResonance { const directVector = item.buildProfile?.resonanceVector; if (directVector) { return { resonanceVector: directVector, explanation: `${item.name}显式声明了属性共振向量。`, }; } const source = `${item.category} ${item.name} ${item.description ?? ''} ${(item.tags ?? []).join(' ')}`; const vector = buildDefaultAxisVector({ axis_a: /甲|盾|锤|骨|锋|刃|护/u.test(source) ? 0.28 : 0.08, axis_b: /弓|箭|靴|影|风|迅|游/u.test(source) ? 0.26 : 0.08, axis_c: /卷|符|阵|镜|策|识|图/u.test(source) ? 0.26 : 0.08, axis_d: /雷|爆|怒|杀|破|冲/u.test(source) ? 0.24 : 0.08, axis_e: /礼|契|印|信|药|护符|盟/u.test(source) ? 0.22 : 0.08, axis_f: /药|丹|露|稳|护|续|回/u.test(source) ? 0.24 : 0.08, }); return { resonanceVector: vector, explanation: `${item.name}的共振由品类与文本语义推断。`, }; } export function buildSkillAttributeProfile(skill: CharacterSkillDefinition) { return { intentVector: SKILL_STYLE_VECTORS[skill.style] ?? SKILL_STYLE_VECTORS.steady, }; }