This commit is contained in:
261
src/data/attributeProfileGenerator.ts
Normal file
261
src/data/attributeProfileGenerator.ts
Normal file
@@ -0,0 +1,261 @@
|
||||
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<CharacterSkillDefinition['style'], AttributeVector> = {
|
||||
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<string>();
|
||||
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<string | null | undefined>;
|
||||
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<InventoryItem | CustomWorldItem, 'category' | 'name' | 'description'> & {
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user