import type {QuestGenerationContext} from '../services/aiTypes'; import { buildFallbackActorNarrativeProfile, normalizeActorNarrativeProfile, } from '../services/storyEngine/actorNarrativeProfile'; import { buildThemePackFromWorldProfile } from '../services/storyEngine/themePack'; import { buildFallbackWorldStoryGraph } from '../services/storyEngine/worldStoryGraph'; import type { EquipmentLoadout, GameState, RuntimeItemGenerationChannel, RuntimeItemGenerationContext, ScenePresetInfo, } from '../types'; import {getCharacterCombatTags, getTimedBuildBuffTags, normalizeBuildTags} from './buildTags'; type GapDefinition = { id: string; tags: string[]; }; const BUILD_GAP_DEFINITIONS: GapDefinition[] = [ {id: 'survival_gap', tags: ['守御', '护体', '回复', '续战']}, {id: 'mana_gap', tags: ['法力', '冷却', '护持', '过载']}, {id: 'finisher_gap', tags: ['爆发', '重击', '追击', '压制']}, {id: 'mobility_gap', tags: ['突进', '快袭', '风行', '游击']}, {id: 'control_gap', tags: ['控场', '符阵', '镇邪', '反击']}, ]; function dedupeStrings(values: Array) { return [...new Set( values .map(value => typeof value === 'string' ? value.trim() : '') .filter(Boolean), )]; } function collectLoadoutBuildTags(loadout: EquipmentLoadout | null | undefined) { if (!loadout) return [] as string[]; return normalizeBuildTags([ ...(loadout.weapon?.buildProfile?.tags ?? []), loadout.weapon?.buildProfile?.role ?? '', ...(loadout.armor?.buildProfile?.tags ?? []), loadout.armor?.buildProfile?.role ?? '', ...(loadout.relic?.buildProfile?.tags ?? []), loadout.relic?.buildProfile?.role ?? '', ], 6); } function buildSceneTags(scene: Pick | null) { if (!scene) return [] as string[]; const seedParts = dedupeStrings([ scene.name, ...(scene.treasureHints ?? []), ]); return seedParts .flatMap(part => part.split(/[、,。;:\s/]+/u)) .map(part => part.trim()) .filter(part => part.length >= 2) .slice(0, 8); } function buildRecentStorySummary(lines: string[]) { if (lines.length <= 0) return '最近没有形成稳定的事件线索。'; return lines.join(' / '); } function buildRecentStoryLines(storyHistory: GameState['storyHistory']) { return storyHistory .slice(-4) .map(moment => moment.text.trim()) .filter(Boolean) .slice(-3); } function derivePlayerBuildGaps(playerBuildTags: string[]) { const tagSet = new Set(playerBuildTags); return BUILD_GAP_DEFINITIONS .filter(definition => definition.tags.filter(tag => tagSet.has(tag)).length <= 0) .map(definition => definition.id) .slice(0, 3); } function resolveRelatedNpcNarrativeProfile(params: { customWorldProfile: GameState['customWorldProfile']; encounter: GameState['currentEncounter']; }) { const { customWorldProfile, encounter } = params; if (!customWorldProfile || !encounter || encounter.kind !== 'npc') { return null; } const role = customWorldProfile.storyNpcs.find((npc) => npc.id === encounter.id || npc.name === encounter.npcName, ) ?? customWorldProfile.playableNpcs.find((npc) => npc.id === encounter.id || npc.name === encounter.npcName, ); if (!role) { return encounter.narrativeProfile ?? null; } const themePack = customWorldProfile.themePack ?? buildThemePackFromWorldProfile(customWorldProfile); const storyGraph = customWorldProfile.storyGraph ?? buildFallbackWorldStoryGraph(customWorldProfile, themePack); return normalizeActorNarrativeProfile( role.narrativeProfile, buildFallbackActorNarrativeProfile(role, storyGraph, themePack), ); } function resolveActiveThreadIds(params: { customWorldProfile: GameState['customWorldProfile']; relatedNpcNarrativeProfile: RuntimeItemGenerationContext['relatedNpcNarrativeProfile']; storyEngineMemory?: GameState['storyEngineMemory'] | QuestGenerationContext['activeThreadIds']; }) { const threadSource = params.storyEngineMemory; if (Array.isArray(threadSource) && threadSource.length > 0) { return threadSource.slice(0, 4); } if ( threadSource && !Array.isArray(threadSource) && threadSource.activeThreadIds?.length ) { return threadSource.activeThreadIds.slice(0, 4); } if (params.relatedNpcNarrativeProfile?.relatedThreadIds.length) { return params.relatedNpcNarrativeProfile.relatedThreadIds.slice(0, 4); } if (!params.customWorldProfile) { return []; } const themePack = params.customWorldProfile.themePack ?? buildThemePackFromWorldProfile(params.customWorldProfile); const storyGraph = params.customWorldProfile.storyGraph ?? buildFallbackWorldStoryGraph(params.customWorldProfile, themePack); return storyGraph.visibleThreads.slice(0, 3).map((thread) => thread.id); } function buildBaseRuntimeContext(params: { worldType: GameState['worldType']; customWorldProfile: GameState['customWorldProfile']; scene: Pick | null; encounter: GameState['currentEncounter']; relatedNpcState: GameState['npcStates'][string] | null; storyEngineMemory?: GameState['storyEngineMemory'] | QuestGenerationContext['activeThreadIds']; storyHistory: GameState['storyHistory']; playerCharacterId: string; playerBuildTags: string[]; playerEquipmentTags: string[]; generationChannel: RuntimeItemGenerationChannel; }) { const { worldType, customWorldProfile, scene, encounter, relatedNpcState, storyEngineMemory, storyHistory, playerCharacterId, playerBuildTags, playerEquipmentTags, generationChannel, } = params; const recentStoryLines = buildRecentStoryLines(storyHistory); const relatedNpcNarrativeProfile = resolveRelatedNpcNarrativeProfile({ customWorldProfile, encounter, }); const activeThreadIds = resolveActiveThreadIds({ customWorldProfile, relatedNpcNarrativeProfile, storyEngineMemory, }); return { worldType, customWorldProfile, sceneId: scene?.id ?? null, sceneName: scene?.name ?? null, sceneDescription: scene?.description ?? null, sceneTags: buildSceneTags(scene), treasureHints: [...(scene?.treasureHints ?? [])], encounter: encounter ?? null, encounterNpcId: encounter?.id ?? encounter?.characterId ?? encounter?.monsterPresetId ?? encounter?.npcName ?? null, encounterNpcName: encounter?.npcName ?? null, encounterContextText: encounter?.context ?? null, relatedNpcState, relatedNpcNarrativeProfile, relatedScene: scene, recentStorySummary: buildRecentStorySummary(recentStoryLines), recentActions: recentStoryLines, activeThreadIds, playerCharacterId, playerBuildTags, playerBuildGaps: derivePlayerBuildGaps(playerBuildTags), playerEquipmentTags, generationChannel, } satisfies RuntimeItemGenerationContext; } export function buildLooseRuntimeItemGenerationContext(params: { worldType: GameState['worldType']; customWorldProfile?: GameState['customWorldProfile']; scene?: Pick | null; encounter?: GameState['currentEncounter']; relatedNpcState?: GameState['npcStates'][string] | null; storyHistory?: GameState['storyHistory']; playerCharacterId?: string; playerBuildTags?: string[]; playerEquipmentTags?: string[]; generationChannel: RuntimeItemGenerationChannel; }) { return buildBaseRuntimeContext({ worldType: params.worldType, customWorldProfile: params.customWorldProfile ?? null, scene: params.scene ?? null, encounter: params.encounter ?? null, relatedNpcState: params.relatedNpcState ?? null, storyEngineMemory: params.customWorldProfile?.storyGraph?.visibleThreads.map((thread) => thread.id) ?? [], storyHistory: params.storyHistory ?? [], playerCharacterId: params.playerCharacterId ?? 'runtime-loose-player', playerBuildTags: params.playerBuildTags ?? [], playerEquipmentTags: params.playerEquipmentTags ?? [], generationChannel: params.generationChannel, }); } export function buildRuntimeItemGenerationContext(params: { state: GameState; generationChannel: RuntimeItemGenerationChannel; encounter?: GameState['currentEncounter']; scene?: GameState['currentScenePreset']; }) { const {state, generationChannel} = params; const encounter = params.encounter ?? state.currentEncounter; const scene = params.scene ?? state.currentScenePreset; const relatedNpcState = encounter ? state.npcStates[encounter.id ?? encounter.npcName] ?? null : null; const playerBuildTags = state.playerCharacter ? normalizeBuildTags([ ...getCharacterCombatTags(state.playerCharacter), ...collectLoadoutBuildTags(state.playerEquipment), ...getTimedBuildBuffTags(state.activeBuildBuffs), ], 6) : []; return buildBaseRuntimeContext({ worldType: state.worldType, customWorldProfile: state.customWorldProfile, scene, encounter, relatedNpcState, storyEngineMemory: state.storyEngineMemory, storyHistory: state.storyHistory, playerCharacterId: state.playerCharacter?.id ?? 'unknown-player', playerBuildTags, playerEquipmentTags: collectLoadoutBuildTags(state.playerEquipment), generationChannel, }); } export function buildQuestRuntimeItemGenerationContext(params: { context: QuestGenerationContext; generationChannel?: RuntimeItemGenerationChannel; issuerNpcId: string; issuerNpcName: string; roleText: string; scene?: Pick | null; }) { const { context, issuerNpcId, issuerNpcName, roleText, scene, generationChannel = 'quest_reward', } = params; const playerBuildTags = context.playerCharacter ? normalizeBuildTags([ ...getCharacterCombatTags(context.playerCharacter), ...collectLoadoutBuildTags(context.playerEquipment), ], 6) : []; return buildBaseRuntimeContext({ worldType: context.worldType, customWorldProfile: context.customWorldProfile ?? null, scene: scene ?? ( context.currentSceneName ? { id: context.currentSceneId ?? '', name: context.currentSceneName, description: context.currentSceneDescription ?? '', treasureHints: [], } : null ), encounter: { id: issuerNpcId, kind: 'npc', npcName: issuerNpcName, npcDescription: roleText, npcAvatar: '', context: roleText, }, relatedNpcState: context.issuerAffinity == null ? null : { affinity: context.issuerAffinity, helpUsed: false, chattedCount: 0, giftsGiven: 0, inventory: [], recruited: false, revealedFacts: [], }, storyEngineMemory: context.activeThreadIds, storyHistory: context.recentStoryMoments ?? [], playerCharacterId: context.playerCharacter?.id ?? 'quest-player', playerBuildTags, playerEquipmentTags: collectLoadoutBuildTags(context.playerEquipment), generationChannel, }); }