import type {AttributeVector, WorldAttributeSchema, WorldAttributeSlot} from '../types'; import {normalizeAttributeVector} from './attributeValidation'; type BuildTagAttributeAffinityMap = Record; type SemanticAxisRule = { axisId: string; patterns: RegExp[]; }; const SEMANTIC_SLOT_RULES: SemanticAxisRule[] = [ { axisId: 'axis_a', patterns: [/骨|躯|甲|壳|体|锋|承压|抗压|结构|根基|底子|扛住|稳固|硬碰|机锋|潮骨|界躯|道骨|骨势/u], }, { axisId: 'axis_b', patterns: [/步|身法|位移|换位|机动|迅|闪|轻灵|抢位|转场|穿梭|步准|浪步|裂步|灵行/u], }, { axisId: 'axis_c', patterns: [/识|察|算|谋|阵|符|术|洞察|解析|看穿|辨认|因果|规律|算识|舟识|界识|识海|眼脉/u], }, { axisId: 'axis_d', patterns: [/压|魄|焰|胆|威|决|推进|强推|压迫|定调|逼出|逆转|潮压|潮魄|界压|劫纹|心焰/u], }, { axisId: 'axis_e', patterns: [/缘|契|盟|信|交|助|协|共鸣|共振|联结|牵引|交换|安抚|协频|契汐|缚契|尘缘|心契/u], }, { axisId: 'axis_f', patterns: [/息|稳|续|回|养|持久|恢复|调息|循环|续航|回稳|稳态|续载|回澜|回脉|玄息/u], }, ]; function roundNumber(value: number, digits = 4) { const factor = 10 ** digits; return Math.round(value * factor) / factor; } function affinity( strength: number, agility: number, intelligence: number, spirit: number, ): AttributeVector { return { axis_a: roundNumber(strength * 0.72 + spirit * 0.28), axis_b: roundNumber(agility * 0.88 + intelligence * 0.12), axis_c: roundNumber(intelligence * 0.78 + agility * 0.22), axis_d: roundNumber(strength * 0.62 + agility * 0.18 + intelligence * 0.2), axis_e: roundNumber(spirit * 0.72 + intelligence * 0.28), axis_f: roundNumber(spirit * 0.74 + strength * 0.26), }; } function buildSlotSemanticVector(slot: WorldAttributeSlot, index: number) { const sourceText = [ slot.slotId, slot.name, slot.definition, slot.combatUseText, slot.socialUseText, slot.explorationUseText, ...(slot.positiveSignals ?? []), ...(slot.negativeSignals ?? []), ].join(' '); const semanticVector: AttributeVector = {}; SEMANTIC_SLOT_RULES.forEach((rule, ruleIndex) => { let score = 0; if (slot.slotId === rule.axisId) { score += 2.2; } else if (slot.slotId === `axis_${String.fromCharCode(97 + ruleIndex)}`) { score += 1.2; } score += rule.patterns.reduce((sum, pattern) => sum + (pattern.test(sourceText) ? 1 : 0), 0); if (score > 0) { semanticVector[rule.axisId] = roundNumber(score, 4); } }); if (Object.keys(semanticVector).length === 0) { const fallbackAxisId = SEMANTIC_SLOT_RULES[index]?.axisId ?? slot.slotId; semanticVector[fallbackAxisId] = 1; } return normalizeAttributeVector( semanticVector, SEMANTIC_SLOT_RULES.map(rule => rule.axisId), ); } function calculateVectorSimilarity(left: AttributeVector, right: AttributeVector) { return roundNumber( Object.keys({...left, ...right}).reduce( (sum, key) => sum + ((left[key] ?? 0) * (right[key] ?? 0)), 0, ), 4, ); } function resolveSchemaAwareAffinity(tagAffinity: AttributeVector, schema: WorldAttributeSchema) { const rawSimilarity = Object.fromEntries( schema.slots.map((slot, index) => [ slot.slotId, calculateVectorSimilarity(tagAffinity, buildSlotSemanticVector(slot, index)), ]), ); return { rawSimilarity, normalizedSimilarity: normalizeAttributeVector( rawSimilarity, schema.slots.map(slot => slot.slotId), ), }; } export const BUILD_TAG_ATTRIBUTE_AFFINITY: BuildTagAttributeAffinityMap = { quickblade: affinity(0.35, 1, 0.1, 0.05), combo: affinity(0.3, 0.92, 0.18, 0.08), dash: affinity(0.45, 0.95, 0, 0), pursuit: affinity(0.38, 0.88, 0.08, 0.02), swiftstrike: affinity(0.22, 0.98, 0.12, 0.04), ranged: affinity(0.18, 0.82, 0.34, 0.08), guerrilla: affinity(0.24, 0.9, 0.28, 0.12), mobility: affinity(0.18, 1, 0.08, 0.08), windrun: affinity(0.08, 1, 0.1, 0.1), heavyhit: affinity(1, 0.28, 0.02, 0.04), burst: affinity(0.72, 0.58, 0.36, 0.08), armorbreak: affinity(0.92, 0.28, 0.08, 0.02), pressure: affinity(0.62, 0.64, 0.1, 0.08), bloodrush: affinity(0.84, 0.54, 0.04, 0.18), guard: affinity(0.7, 0.18, 0.04, 0.72), barrier: affinity(0.48, 0.08, 0.2, 0.92), heavyarmor: affinity(0.88, 0.04, 0.02, 0.54), counter: affinity(0.66, 0.46, 0.14, 0.36), banish: affinity(0.24, 0.06, 0.54, 0.88), caster: affinity(0, 0.1, 1, 0.6), mana: affinity(0.02, 0.08, 0.94, 0.74), thunder: affinity(0.06, 0.24, 0.96, 0.42), formation: affinity(0.08, 0.12, 0.82, 0.96), control: affinity(0.12, 0.34, 0.78, 0.72), overload: affinity(0.14, 0.18, 0.92, 0.38), heal: affinity(0.02, 0.08, 0.56, 1), support: affinity(0.14, 0.14, 0.58, 0.98), sustain: affinity(0.34, 0.18, 0.22, 0.9), fate: affinity(0.08, 0.22, 0.72, 0.84), fortune: affinity(0.06, 0.34, 0.7, 0.78), cooldown: affinity(0.04, 0.46, 0.82, 0.4), command: affinity(0.38, 0.26, 0.72, 0.82), balanced: affinity(0.58, 0.58, 0.58, 0.58), craft: affinity(0.24, 0.16, 0.74, 0.5), alchemy: affinity(0.08, 0.16, 0.84, 0.76), vanguard: affinity(0.82, 0.44, 0.08, 0.34), berserk: affinity(0.98, 0.42, 0, 0.22), spellblade: affinity(0.42, 0.42, 0.88, 0.38), paladin: affinity(0.58, 0.12, 0.42, 0.96), fortress: affinity(0.94, 0.04, 0.08, 0.82), starter: affinity(0.42, 0.42, 0.42, 0.42), }; export function getBuildTagAttributeAffinity(tagId: string, schema?: WorldAttributeSchema) { const semanticAffinity = BUILD_TAG_ATTRIBUTE_AFFINITY[tagId] ?? affinity(0.4, 0.4, 0.4, 0.4); if (!schema) { return semanticAffinity; } return resolveSchemaAwareAffinity(semanticAffinity, schema).normalizedSimilarity; } export function getBuildTagAttributeSimilarityProfile(tagId: string, schema: WorldAttributeSchema) { const semanticAffinity = BUILD_TAG_ATTRIBUTE_AFFINITY[tagId] ?? affinity(0.4, 0.4, 0.4, 0.4); return resolveSchemaAwareAffinity(semanticAffinity, schema); }