262 lines
8.5 KiB
TypeScript
262 lines
8.5 KiB
TypeScript
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,
|
||
};
|
||
}
|