179 lines
5.9 KiB
TypeScript
179 lines
5.9 KiB
TypeScript
import type {AttributeVector, WorldAttributeSchema, WorldAttributeSlot} from '../types';
|
|
import {normalizeAttributeVector} from './attributeValidation';
|
|
|
|
type BuildTagAttributeAffinityMap = Record<string, AttributeVector>;
|
|
|
|
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,
|
|
].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);
|
|
}
|