Files
Genarrative/src/data/attributeProfileGenerator.ts
T
kdletters cbc27bad4a
CI / verify (push) Has been cancelled
init with react+axum+spacetimedb
2026-04-26 18:06:23 +08:00

262 lines
8.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
};
}