import { type AttributeVector, type Character, type CustomWorldProfile, type Encounter, type InventoryItem, type NpcPersistentState, type RoleAttributeProfile, type WorldAttributeSchema, WorldType, } from '../types'; import { buildItemAttributeResonance, buildRoleAttributeProfileFromLegacyData, } from './attributeProfileGenerator'; import { buildDefaultAxisVector, getSortedAttributeEntries, resolveAttributeSchema, resolveCharacterAttributeProfile, scoreAttributeFit, } from './attributeResolver'; import {normalizeAttributeVector} from './attributeValidation'; import {getCharacterById} from './characterPresets'; import {getRuntimeCustomWorldProfile} from './customWorldRuntime'; import {getMonsterPresetById} from './hostileNpcPresets'; export type GiftAffinityInsight = { resonanceScore: number; resonanceBonus: number; matchedAttributes: string[]; reasonText: string; }; export type ChatAffinityOutcome = { affinityGain: number; matchedAttributes: string[]; summary: string; }; export type RecruitReadinessInsight = { readinessScore: number; resonanceBonus: number; tabooPenalty: number; matchedAttributes: string[]; summary: string; }; function resolveCustomWorldProfile(profile?: CustomWorldProfile | null) { return profile ?? getRuntimeCustomWorldProfile(); } function resolveInteractionWorldType( worldType?: WorldType | null, customWorldProfile?: CustomWorldProfile | null, encounter?: Encounter | null, ) { if (worldType) { return worldType; } if (customWorldProfile || getRuntimeCustomWorldProfile()) { return WorldType.CUSTOM; } const schemaId = encounter?.attributeProfile?.schemaId ?? ''; if (schemaId.includes('xianxia')) { return WorldType.XIANXIA; } return WorldType.WUXIA; } function buildSchemaContext( worldType?: WorldType | null, customWorldProfile?: CustomWorldProfile | null, encounter?: Encounter | null, ) { const resolvedCustomWorldProfile = resolveCustomWorldProfile(customWorldProfile); const resolvedWorldType = resolveInteractionWorldType( worldType, resolvedCustomWorldProfile, encounter, ); return { worldType: resolvedWorldType, customWorldProfile: resolvedCustomWorldProfile, schema: resolveAttributeSchema(resolvedWorldType, resolvedCustomWorldProfile), }; } function resolveMonsterProfile( encounter: Encounter, worldType: WorldType, ) { if (!encounter.monsterPresetId) { return null; } return getMonsterPresetById(worldType, encounter.monsterPresetId)?.attributeProfile ?? getMonsterPresetById(WorldType.WUXIA, encounter.monsterPresetId)?.attributeProfile ?? getMonsterPresetById(WorldType.XIANXIA, encounter.monsterPresetId)?.attributeProfile ?? getMonsterPresetById(WorldType.CUSTOM, encounter.monsterPresetId)?.attributeProfile ?? null; } export function resolveEncounterAttributeProfile( encounter: Encounter, options: { worldType?: WorldType | null; customWorldProfile?: CustomWorldProfile | null; } = {}, ) { const schemaContext = buildSchemaContext( options.worldType, options.customWorldProfile, encounter, ); if (encounter.characterId) { const character = getCharacterById(encounter.characterId); if (character) { return resolveCharacterAttributeProfile( character, schemaContext.worldType, schemaContext.customWorldProfile, ) ?? encounter.attributeProfile ?? null; } } if (encounter.attributeProfile) { return encounter.attributeProfile; } const monsterProfile = resolveMonsterProfile(encounter, schemaContext.worldType); if (monsterProfile) { return monsterProfile; } return buildRoleAttributeProfileFromLegacyData({ entityId: encounter.id ?? encounter.npcName, schema: schemaContext.schema, textBlocks: [ encounter.npcName, encounter.context, encounter.npcDescription, ], }).profile; } function buildFocusVector( profile: RoleAttributeProfile | null | undefined, schema: WorldAttributeSchema, limit: number, direction: 'top' | 'bottom' = 'top', ) { const slotIds = schema.slots.map(slot => slot.slotId); const sourceEntries = getSortedAttributeEntries(profile, schema); const orderedEntries = direction === 'bottom' ? [...sourceEntries].reverse() : sourceEntries; const selectedEntries = orderedEntries.slice(0, limit); const seed = Object.fromEntries(slotIds.map(slotId => [slotId, 0])) as AttributeVector; selectedEntries.forEach(entry => { seed[entry.slot.slotId] = entry.value; }); return normalizeAttributeVector(seed, slotIds); } function getTopContributionAttributeNames( profile: RoleAttributeProfile | null | undefined, vector: AttributeVector, schema: WorldAttributeSchema, limit = 2, ) { const slotIds = schema.slots.map(slot => slot.slotId); const profileWeights = normalizeAttributeVector(profile?.values ?? {}, slotIds); const vectorWeights = normalizeAttributeVector(vector, slotIds); return schema.slots .map(slot => ({ name: slot.name, contribution: (profileWeights[slot.slotId] ?? 0) * (vectorWeights[slot.slotId] ?? 0), })) .sort((left, right) => right.contribution - left.contribution) .filter(entry => entry.contribution > 0) .slice(0, limit) .map(entry => entry.name); } function uniqueNames(names: string[]) { return [...new Set(names.filter(Boolean))]; } function buildDialogueIntentVector(actionText: string, schema: WorldAttributeSchema) { const slotIds = schema.slots.map(slot => slot.slotId); const source = actionText.trim(); const seed = buildDefaultAxisVector({}); const addWeight = (slotId: string, weight: number, pattern: RegExp) => { if (pattern.test(source)) { seed[slotId] = (seed[slotId] ?? 0) + weight; } }; addWeight('axis_a', 0.24, /顶|扛|硬闯|守住|挡住/u); addWeight('axis_b', 0.2, /快|抢|追|赶在|绕开|先一步/u); addWeight('axis_c', 0.34, /试探|追问|看穿|真假|真相|线索|判断|观察/u); addWeight('axis_d', 0.3, /直说|表态|逼|压住|摊开|决断|邀你同行|邀请/u); addWeight('axis_e', 0.34, /安抚|诚意|求助|关心|陪|同行|信|礼物|约定/u); addWeight('axis_f', 0.22, /稳|缓一缓|耐心|慢慢|调息|先稳住/u); const total = slotIds.reduce((sum, slotId) => sum + (seed[slotId] ?? 0), 0); if (total <= 0) { return normalizeAttributeVector( buildDefaultAxisVector({ axis_c: 0.34, axis_e: 0.38, axis_f: 0.28, }), slotIds, ); } return normalizeAttributeVector(seed, slotIds); } export function buildEncounterAttributeRumors( encounter: Encounter, options: { worldType?: WorldType | null; customWorldProfile?: CustomWorldProfile | null; limit?: number; } = {}, ) { const schemaContext = buildSchemaContext( options.worldType, options.customWorldProfile, encounter, ); const profile = resolveEncounterAttributeProfile(encounter, options); const evidence = profile?.evidence?.slice(0, options.limit ?? 2).map(entry => entry.reason) ?? []; if (evidence.length > 0) { return evidence; } return getSortedAttributeEntries(profile, schemaContext.schema) .slice(0, options.limit ?? 2) .map(entry => entry.slot.name); } export function buildGiftAffinityInsight( item: InventoryItem, encounter: Encounter, options: { worldType?: WorldType | null; customWorldProfile?: CustomWorldProfile | null; } = {}, ): GiftAffinityInsight { const schemaContext = buildSchemaContext( options.worldType, options.customWorldProfile, encounter, ); const npcProfile = resolveEncounterAttributeProfile(encounter, options); const attributeResonance = item.attributeResonance ?? buildItemAttributeResonance(item); const resonanceScore = scoreAttributeFit( npcProfile, attributeResonance.resonanceVector, schemaContext.schema, ); const resonanceBonus = Math.max(0, Math.min(6, Math.round(resonanceScore * 18) - 1)); const matchedAttributes = getTopContributionAttributeNames( npcProfile, attributeResonance.resonanceVector, schemaContext.schema, ); return { resonanceScore, resonanceBonus, matchedAttributes, reasonText: matchedAttributes.length > 0 ? `${item.name}更对上对方看重的${matchedAttributes.join('、')}。` : attributeResonance.explanation ?? `${item.name}带来的属性共振暂时不够明显。`, }; } export function buildChatAffinityOutcome(params: { playerCharacter: Character; encounter: Encounter; npcState: NpcPersistentState; actionText: string; worldType?: WorldType | null; customWorldProfile?: CustomWorldProfile | null; }): ChatAffinityOutcome { const schemaContext = buildSchemaContext( params.worldType, params.customWorldProfile, params.encounter, ); const playerProfile = resolveCharacterAttributeProfile( params.playerCharacter, schemaContext.worldType, schemaContext.customWorldProfile, ); const npcProfile = resolveEncounterAttributeProfile(params.encounter, { worldType: schemaContext.worldType, customWorldProfile: schemaContext.customWorldProfile, }); const intentVector = buildDialogueIntentVector(params.actionText, schemaContext.schema); const actorFit = scoreAttributeFit(playerProfile, intentVector, schemaContext.schema); const targetFit = scoreAttributeFit(npcProfile, intentVector, schemaContext.schema); const sharedFocusVector = buildFocusVector(npcProfile, schemaContext.schema, 2); const sharedResonance = scoreAttributeFit(playerProfile, sharedFocusVector, schemaContext.schema); const baseGain = Math.max(4, 8 - params.npcState.chattedCount); const bonusGain = Math.max( 0, Math.min(4, Math.round(actorFit * 8 + targetFit * 6 + sharedResonance * 8) - 2), ); const matchedAttributes = uniqueNames([ ...getTopContributionAttributeNames(playerProfile, intentVector, schemaContext.schema), ...getTopContributionAttributeNames(npcProfile, intentVector, schemaContext.schema), ]).slice(0, 2); return { affinityGain: baseGain + bonusGain, matchedAttributes, summary: matchedAttributes.length > 0 ? `你这番说法更对上了对方在${matchedAttributes.join('、')}上的判断方式。` : '这轮交谈让对方更愿意继续观察你的来意。', }; } export function buildRecruitmentInsight(params: { encounter: Encounter; npcState: NpcPersistentState; playerCharacter: Character; worldType?: WorldType | null; customWorldProfile?: CustomWorldProfile | null; }): RecruitReadinessInsight { const schemaContext = buildSchemaContext( params.worldType, params.customWorldProfile, params.encounter, ); const playerProfile = resolveCharacterAttributeProfile( params.playerCharacter, schemaContext.worldType, schemaContext.customWorldProfile, ); const npcProfile = resolveEncounterAttributeProfile(params.encounter, { worldType: schemaContext.worldType, customWorldProfile: schemaContext.customWorldProfile, }); const preferenceVector = buildFocusVector(npcProfile, schemaContext.schema, 2); const tabooVector = buildFocusVector(npcProfile, schemaContext.schema, 1, 'bottom'); const resonanceScore = scoreAttributeFit(playerProfile, preferenceVector, schemaContext.schema); const tabooScore = scoreAttributeFit(playerProfile, tabooVector, schemaContext.schema); const resonanceBonus = Math.max(0, Math.min(8, Math.round(resonanceScore * 24) - 2)); const tabooPenalty = Math.max(0, Math.min(4, Math.round(tabooScore * 12) - 5)); const matchedAttributes = getTopContributionAttributeNames( playerProfile, preferenceVector, schemaContext.schema, ); const tabooAttributeName = getTopContributionAttributeNames( playerProfile, tabooVector, schemaContext.schema, 1, )[0]; return { readinessScore: params.npcState.affinity + resonanceBonus - tabooPenalty, resonanceBonus, tabooPenalty, matchedAttributes, summary: matchedAttributes.length > 0 ? tabooPenalty > 0 && tabooAttributeName ? `你在${matchedAttributes.join('、')}上的做派更容易被对方视为可同行,但${tabooAttributeName}这一路数仍让他保留几分。` : `你在${matchedAttributes.join('、')}上的做派,正是对方愿意并肩同行的那类信号。` : '对方主要还是在看你们当前积累下来的信任。', }; }