Files
Genarrative/src/data/buildTagAttributeAffinity.ts
2026-04-28 20:25:37 +08:00

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);
}