import type { InventoryItem, ItemBuildProfile, ItemRarity, ItemStatProfile, ItemUseProfile, RuntimeItemAiIntent, RuntimeItemCompileBudget, RuntimeItemGenerationContext, RuntimeItemPlan, TimedBuildBuff, } from '../types'; import {normalizeBuildRole, normalizeBuildTags} from './buildTags'; const RARITY_ORDER: ItemRarity[] = ['common', 'uncommon', 'rare', 'epic', 'legendary']; function clampIndex(index: number) { return Math.max(0, Math.min(RARITY_ORDER.length - 1, index)); } 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 buildStructuralTags(plan: RuntimeItemPlan) { switch (plan.itemKind) { case 'equipment': return ['weapon']; case 'relic': return ['relic']; case 'material': return ['material']; case 'consumable': return ['consumable']; case 'quest': return ['relic']; default: return []; } } function resolveEquipmentSlotId(plan: RuntimeItemPlan) { if (plan.itemKind === 'equipment') { if (plan.targetBuildDirection.includes('守御') || plan.targetBuildDirection.includes('护体')) { return 'armor' as const; } return 'weapon' as const; } if (plan.itemKind === 'relic' || plan.itemKind === 'quest') { return 'relic' as const; } return null; } function resolveCategory(plan: RuntimeItemPlan) { switch (plan.itemKind) { case 'equipment': return resolveEquipmentSlotId(plan) === 'armor' ? '护甲' : '武器'; case 'consumable': return '消耗品'; case 'material': return '材料'; case 'quest': return '专属物'; default: return '稀有品'; } } function buildRuntimeBudget( context: RuntimeItemGenerationContext, plan: RuntimeItemPlan, seedKey: string, ): RuntimeItemCompileBudget { const seed = hashText(`${context.generationChannel}:${seedKey}:${plan.slot}:${plan.itemKind}`); const channelBaseIndex = context.generationChannel === 'quest_reward' ? 2 : context.generationChannel === 'treasure' ? 1 : context.generationChannel === 'monster_drop' ? 0 : 1; const slotBonus = plan.slot === 'primary' ? 1 : 0; const permanenceBonus = plan.permanence === 'permanent' ? 1 : 0; const rarityIndex = clampIndex(channelBaseIndex + slotBonus + permanenceBonus + (seed % 2)); const rarity = RARITY_ORDER[rarityIndex] ?? 'common'; return { rarity, buildTagLimit: rarity === 'legendary' ? 2 : rarity === 'epic' || rarity === 'rare' ? 2 : 1, timedBuffTagLimit: rarity === 'legendary' ? 3 : rarity === 'epic' || rarity === 'rare' ? 2 : 1, timedBuffDuration: rarity === 'legendary' ? 3 : rarity === 'epic' ? 3 : 2, statBudgetTier: rarityIndex + 1, }; } function buildTimedBuffs( itemId: string, intent: RuntimeItemAiIntent, budget: RuntimeItemCompileBudget, ): TimedBuildBuff[] { const tags = normalizeBuildTags(intent.desiredBuildTags, budget.timedBuffTagLimit); if (tags.length <= 0) return []; return [{ id: `${itemId}:buff`, sourceType: 'item', sourceId: itemId, name: `${tags[0]}增益`, tags, durationTurns: budget.timedBuffDuration, }]; } function buildUseProfile( plan: RuntimeItemPlan, intent: RuntimeItemAiIntent, budget: RuntimeItemCompileBudget, itemId: string, ): ItemUseProfile | null { if (plan.itemKind !== 'consumable' && plan.permanence === 'permanent') return null; const useProfile: ItemUseProfile = {}; if (intent.desiredFunctionalBias.includes('heal')) { useProfile.hpRestore = 16 + budget.statBudgetTier * 10; } if (intent.desiredFunctionalBias.includes('mana')) { useProfile.manaRestore = 14 + budget.statBudgetTier * 8; } if (intent.desiredFunctionalBias.includes('cooldown')) { useProfile.cooldownReduction = budget.rarity === 'legendary' || budget.rarity === 'epic' ? 1 : 0; } if (plan.permanence === 'timed' || plan.itemKind === 'consumable') { useProfile.buildBuffs = buildTimedBuffs(itemId, intent, budget); } if ( (useProfile.hpRestore ?? 0) <= 0 && (useProfile.manaRestore ?? 0) <= 0 && (useProfile.cooldownReduction ?? 0) <= 0 && (useProfile.buildBuffs?.length ?? 0) <= 0 ) { return null; } return useProfile; } function buildStatProfile( plan: RuntimeItemPlan, intent: RuntimeItemAiIntent, budget: RuntimeItemCompileBudget, ): ItemStatProfile | null { if (plan.itemKind === 'material') return null; const statProfile: ItemStatProfile = {}; const tier = budget.statBudgetTier; if (plan.itemKind === 'equipment' || plan.itemKind === 'relic' || plan.itemKind === 'quest') { if (intent.desiredFunctionalBias.includes('guard') || plan.targetBuildDirection.includes('守御')) { statProfile.maxHpBonus = 6 * tier + (plan.itemKind === 'equipment' ? 10 : 4); statProfile.incomingDamageMultiplier = Number(Math.max(0.84, 1 - tier * 0.03).toFixed(2)); } if (intent.desiredFunctionalBias.includes('mana') || plan.targetBuildDirection.includes('法力')) { statProfile.maxManaBonus = 8 * tier + (plan.itemKind === 'relic' ? 10 : 0); } if (intent.desiredFunctionalBias.includes('damage') || plan.targetBuildDirection.length > 0) { statProfile.outgoingDamageBonus = Number((0.03 * tier).toFixed(2)); } } if (Object.keys(statProfile).length <= 0) return null; return statProfile; } function buildProfile( plan: RuntimeItemPlan, intent: RuntimeItemAiIntent, budget: RuntimeItemCompileBudget, ): ItemBuildProfile | null { if (plan.permanence !== 'permanent' || (plan.itemKind !== 'equipment' && plan.itemKind !== 'relic' && plan.itemKind !== 'quest')) { return null; } const tags = normalizeBuildTags(intent.desiredBuildTags, budget.buildTagLimit); if (tags.length <= 0) return null; return { role: normalizeBuildRole(tags[0]), tags, synergy: normalizeBuildTags([ ...plan.targetBuildDirection, ...intent.desiredBuildTags, ], 3), craftTags: tags, forgeRank: 0, }; } function buildValue( budget: RuntimeItemCompileBudget, plan: RuntimeItemPlan, ) { const baseValue = { common: 18, uncommon: 32, rare: 52, epic: 80, legendary: 118, }[budget.rarity]; if (plan.itemKind === 'material') return baseValue - 8; if (plan.itemKind === 'consumable') return baseValue - 4; if (plan.itemKind === 'quest') return baseValue + 12; return baseValue; } function buildItemTags( plan: RuntimeItemPlan, intent: RuntimeItemAiIntent, budget: RuntimeItemCompileBudget, ) { const tags = [ ...buildStructuralTags(plan), ...normalizeBuildTags(intent.desiredBuildTags, budget.buildTagLimit + 1), ]; if (intent.desiredFunctionalBias.includes('heal')) tags.push('healing'); if (intent.desiredFunctionalBias.includes('mana')) tags.push('mana'); if (resolveEquipmentSlotId(plan) === 'armor') tags.push('armor'); if (resolveEquipmentSlotId(plan) === 'weapon') tags.push('weapon'); if (resolveEquipmentSlotId(plan) === 'relic') tags.push('relic'); return [...new Set(tags.filter(Boolean))]; } export function compileRuntimeItem(params: { seedKey: string; context: RuntimeItemGenerationContext; plan: RuntimeItemPlan; intent: RuntimeItemAiIntent; }) { const {seedKey, context, plan, intent} = params; const budget = buildRuntimeBudget(context, plan, seedKey); const itemId = `runtime:${context.generationChannel}:${hashText(seedKey).toString(36)}`; const runtimeMetadata = { origin: 'ai_compiled' as const, generationChannel: context.generationChannel, seedKey, relationAnchor: plan.relationAnchor, sourceReason: intent.reasonToAppear, recentEventHook: context.recentActions[0], }; return { id: itemId, category: resolveCategory(plan), name: intent.shortNameSeed || '未命名秘物', quantity: 1, rarity: budget.rarity, tags: buildItemTags(plan, intent, budget), equipmentSlotId: resolveEquipmentSlotId(plan), statProfile: buildStatProfile(plan, intent, budget), useProfile: buildUseProfile(plan, intent, budget, itemId), buildProfile: buildProfile(plan, intent, budget), value: buildValue(budget, plan), runtimeMetadata, } satisfies InventoryItem; }