import type { DirectedRuntimeReward, InventoryItem, RuntimeItemGenerationChannel, RuntimeItemGenerationContext, RuntimeItemKind, RuntimeItemPermanence, RuntimeItemPlan, RuntimeRelationAnchor, } from '../types'; import {generateRuntimeItemAiIntents} from '../services/runtimeItemAiDirector'; import {compileRuntimeItem} from './runtimeItemCompiler'; import { applyRuntimeItemNarrative, buildRuntimeItemAiIntent, buildRuntimeRewardStoryHint, flattenDirectedRuntimeRewardItems, } from './runtimeItemNarrative'; type RuntimeRewardOptions = { seedKey: string; variant?: string; itemCount?: number; fixedKinds?: RuntimeItemKind[]; fixedPermanence?: RuntimeItemPermanence[]; baseHp?: number; baseMana?: number; baseCurrency?: number; storyHint?: string; }; const GAP_TO_TAGS: Record = { survival_gap: ['守御', '护体', '回复', '续战'], mana_gap: ['法力', '冷却', '过载'], finisher_gap: ['爆发', '重击', '追击'], mobility_gap: ['突进', '快袭', '风行', '游击'], control_gap: ['控场', '符阵', '镇邪'], }; function hashText(value: string) { let hash = 0; for (let index = 0; index < value.length; index += 1) { hash = ((hash << 5) - hash) + value.charCodeAt(index); hash |= 0; } return Math.abs(hash); } function resolveGapDrivenTags(context: RuntimeItemGenerationContext) { const gapTags = context.playerBuildGaps.flatMap(gap => GAP_TO_TAGS[gap] ?? []); return [...new Set([ ...gapTags, ...context.playerBuildTags, ])].slice(0, 3); } function resolveRelationAnchor(context: RuntimeItemGenerationContext): RuntimeRelationAnchor { if (context.encounter?.monsterPresetId) { return { type: 'monster', monsterId: context.encounter.monsterPresetId, monsterName: context.encounter.npcName, }; } if (context.encounterNpcName) { return { type: 'npc', npcId: context.encounterNpcId ?? undefined, npcName: context.encounterNpcName, roleText: context.encounterContextText ?? undefined, }; } if (context.sceneName) { return { type: 'scene', sceneId: context.sceneId ?? undefined, sceneName: context.sceneName, }; } return { type: 'landmark', landmarkName: context.customWorldProfile?.name ?? '无名之地', }; } function getChannelTemplate(channel: RuntimeItemGenerationChannel) { switch (channel) { case 'treasure': return { kinds: ['relic', 'consumable', 'material'] as RuntimeItemKind[], permanence: ['permanent', 'timed', 'resource'] as RuntimeItemPermanence[], defaultCount: 2, }; case 'npc_trade': return { kinds: ['consumable', 'material', 'relic', 'equipment'] as RuntimeItemKind[], permanence: ['timed', 'resource', 'permanent', 'permanent'] as RuntimeItemPermanence[], defaultCount: 4, }; case 'npc_reward': return { kinds: ['consumable', 'relic', 'equipment'] as RuntimeItemKind[], permanence: ['timed', 'permanent', 'permanent'] as RuntimeItemPermanence[], defaultCount: 1, }; case 'monster_drop': return { kinds: ['material', 'consumable', 'relic'] as RuntimeItemKind[], permanence: ['resource', 'timed', 'permanent'] as RuntimeItemPermanence[], defaultCount: 2, }; case 'quest_reward': return { kinds: ['equipment', 'relic', 'consumable'] as RuntimeItemKind[], permanence: ['permanent', 'permanent', 'timed'] as RuntimeItemPermanence[], defaultCount: 2, }; default: return { kinds: ['consumable', 'material', 'relic'] as RuntimeItemKind[], permanence: ['timed', 'resource', 'permanent'] as RuntimeItemPermanence[], defaultCount: 2, }; } } function planRuntimeItems( context: RuntimeItemGenerationContext, options: RuntimeRewardOptions, ): RuntimeItemPlan[] { const template = getChannelTemplate(context.generationChannel); const count = Math.max(1, options.itemCount ?? template.defaultCount); const relationAnchor = resolveRelationAnchor(context); const targetBuildDirection = resolveGapDrivenTags(context); const seed = hashText(`${options.seedKey}:${options.variant ?? 'default'}:${context.generationChannel}`); return Array.from({length: count}, (_, index) => { const kinds = options.fixedKinds?.length ? options.fixedKinds : template.kinds; const permanences = options.fixedPermanence?.length ? options.fixedPermanence : template.permanence; const itemKind = kinds[(seed + index) % kinds.length] ?? 'consumable'; const permanence = permanences[(seed + index) % permanences.length] ?? 'timed'; return { slot: index === 0 ? 'primary' : index === 1 ? 'secondary' : 'support', itemKind, permanence, narrativeWeight: index === 0 ? 'heavy' : 'medium', targetBuildDirection: targetBuildDirection.length > 0 ? targetBuildDirection : ['均衡'], relationAnchor, } satisfies RuntimeItemPlan; }); } function compilePlannedItem( context: RuntimeItemGenerationContext, plan: RuntimeItemPlan, seedKey: string, intentOverride?: ReturnType, ) { const intent = intentOverride ?? buildRuntimeItemAiIntent(context, plan); const compiled = compileRuntimeItem({ seedKey, context, plan, intent, }); return applyRuntimeItemNarrative({ item: compiled, context, plan, intent, }); } function buildDirectedRewardFromItems( compiledItems: InventoryItem[], options: RuntimeRewardOptions, ): DirectedRuntimeReward { const reward: DirectedRuntimeReward = { primaryItem: compiledItems[0] ?? null, supportItems: compiledItems.slice(1), hp: options.baseHp, mana: options.baseMana, currency: options.baseCurrency, storyHint: options.storyHint, }; return { ...reward, storyHint: buildRuntimeRewardStoryHint(reward), }; } export function buildDirectedRuntimeReward( context: RuntimeItemGenerationContext, options: RuntimeRewardOptions, ): DirectedRuntimeReward { const plans = planRuntimeItems(context, options); const compiledItems = plans.map((plan, index) => compilePlannedItem(context, plan, `${options.seedKey}:${plan.slot}:${index}`), ); return buildDirectedRewardFromItems(compiledItems, options); } export async function generateDirectedRuntimeReward( context: RuntimeItemGenerationContext, options: RuntimeRewardOptions, ): Promise { const plans = planRuntimeItems(context, options); try { const aiIntents = await generateRuntimeItemAiIntents({ context, plans, }); const compiledItems = plans.map((plan, index) => compilePlannedItem( context, plan, `${options.seedKey}:${plan.slot}:${index}`, aiIntents[index], ), ); return buildDirectedRewardFromItems(compiledItems, options); } catch (error) { console.warn('[RuntimeItemDirector] falling back to deterministic item intent', error); return buildDirectedRuntimeReward(context, options); } } export function buildRuntimeInventoryStock( context: RuntimeItemGenerationContext, options: RuntimeRewardOptions, ): InventoryItem[] { return flattenDirectedRuntimeRewardItems(buildDirectedRuntimeReward(context, options)); }