import type { EquipmentSlotId, InventoryItem, ItemBuildProfile, ItemRarity, ItemStatProfile, WorldType, } from '../types'; import { getSimilarBuildTags, normalizeBuildRole, normalizeBuildTags } from './buildTags'; import { formatCurrency } from './economy'; import { getEquipmentSlotFromItem } from './equipmentEffects'; import { addInventoryItems, removeInventoryItem } from './npcInteractions'; export type ForgeRecipeKind = 'synthesis' | 'forge'; type ForgeRequirement = { id: string; label: string; quantity: number; matches: (item: InventoryItem) => boolean; }; type ForgeRecipeDefinition = { id: string; name: string; kind: ForgeRecipeKind; description: string; resultLabel: string; currencyCost: number; requirements: ForgeRequirement[]; createResult: (worldType: WorldType | null) => InventoryItem; }; export type ForgeRecipeView = { id: string; name: string; kind: ForgeRecipeKind; description: string; resultLabel: string; currencyCost: number; currencyText: string; requirements: Array<{ id: string; label: string; quantity: number; owned: number; }>; canCraft: boolean; }; export type ForgeExecutionResult = { inventory: InventoryItem[]; currency: number; createdItem: InventoryItem; }; export type DismantleExecutionResult = { inventory: InventoryItem[]; outputs: InventoryItem[]; }; export type ReforgeExecutionResult = { inventory: InventoryItem[]; reforgedItem: InventoryItem; currencyCost: number; }; function createItemId(prefix: string) { return `${prefix}:${Date.now().toString(36)}:${Math.random().toString(36).slice(2, 8)}`; } function normalizeQuantity(quantity: number) { return Math.max(1, Math.floor(quantity)); } function buildMaterialItem( name: string, quantity: number, tags: string[], rarity: ItemRarity = 'uncommon', description?: string, ): InventoryItem { return { id: createItemId(`forge-material:${name}`), category: '材料', name, quantity: normalizeQuantity(quantity), rarity, tags: ['material', ...normalizeBuildTags(tags)], description, buildProfile: { role: normalizeBuildRole('工巧'), tags: normalizeBuildTags(tags), craftTags: normalizeBuildTags(tags), forgeRank: 0, }, }; } function buildEquipmentItem(params: { name: string; slot: EquipmentSlotId; rarity: ItemRarity; description: string; role: string; tags: string[]; setId: string; setName: string; pieceName: string; synergy: string[]; statProfile: ItemStatProfile; }) { return { id: createItemId(`forge-equip:${params.name}`), category: params.slot === 'weapon' ? '武器' : params.slot === 'armor' ? '护甲' : '饰品', name: params.name, quantity: 1, rarity: params.rarity, tags: [ params.slot === 'weapon' ? 'weapon' : params.slot === 'armor' ? 'armor' : 'relic', ...normalizeBuildTags(params.tags), ], description: params.description, equipmentSlotId: params.slot, statProfile: params.statProfile, buildProfile: { role: normalizeBuildRole(params.role), tags: normalizeBuildTags(params.tags), setId: params.setId, setName: params.setName, pieceName: params.pieceName, synergy: params.synergy, craftTags: normalizeBuildTags(params.tags), forgeRank: 1, } satisfies ItemBuildProfile, } satisfies InventoryItem; } function buildRefinedIngot() { return buildMaterialItem('精炼锭材', 1, ['工巧', '守御'], 'rare', '经过二次锻压的通用金属锭材,可用于武器与护甲锻造。'); } function buildCondensedSilk() { return buildMaterialItem('凝光纱', 1, ['工巧', '法力'], 'rare', '适合饰品与法器类配方的高阶纤维材料。'); } function buildTagEssence(tag: string) { return buildMaterialItem(`${tag}精粹`, 1, [tag, '工巧'], 'rare', `从旧装备中提炼出的 ${tag} 构筑精粹。`); } function buildAnyMaterialRequirement(id: string, label: string, quantity: number): ForgeRequirement { return { id, label, quantity, matches: item => item.tags.includes('material') || item.category.includes('材料'), }; } function buildNamedMaterialRequirement(name: string, quantity: number): ForgeRequirement { return { id: `name:${name}`, label: name, quantity, matches: item => item.name === name, }; } const FORGE_RECIPES: ForgeRecipeDefinition[] = [ { id: 'synthesis-refined-ingot', name: '压炼锭材', kind: 'synthesis', description: '把零散残片和基础材料压成稳定可用的金属锭材。', resultLabel: '精炼锭材', currencyCost: 18, requirements: [ buildAnyMaterialRequirement('material:any', '任意材料', 3), ], createResult: () => buildRefinedIngot(), }, { id: 'synthesis-condensed-silk', name: '凝光纺丝', kind: 'synthesis', description: '用灵性残材与粉末纺出适合饰品锻造的凝光纱。', resultLabel: '凝光纱', currencyCost: 24, requirements: [ buildAnyMaterialRequirement('material:any', '任意材料', 2), { id: 'tag:mana', label: '含法力标签材料', quantity: 1, matches: item => (item.tags.includes('material') || item.category.includes('材料')) && item.tags.includes('mana'), }, ], createResult: () => buildCondensedSilk(), }, { id: 'forge-duelist-blade', name: '锻造 百炼追风剑', kind: 'forge', description: '围绕快剑、突进、追击构筑的轻灵主武器。', resultLabel: '百炼追风剑', currencyCost: 72, requirements: [ buildNamedMaterialRequirement('精炼锭材', 2), buildNamedMaterialRequirement('快剑精粹', 1), buildNamedMaterialRequirement('突进精粹', 1), ], createResult: () => buildEquipmentItem({ name: '百炼追风剑', slot: 'weapon', rarity: 'epic', description: '为快剑与追身构筑准备的锻造兵刃,挥动时更容易连续压进对手空门。', role: '快剑', tags: ['快剑', '突进', '追击'], setId: 'forge-set-duelist', setName: '追风连锋', pieceName: 'weapon', synergy: ['快剑', '突进', '追击'], statProfile: { maxManaBonus: 10, outgoingDamageBonus: 0.2, }, }), }, { id: 'forge-ward-armor', name: '锻造 镇岳护甲', kind: 'forge', description: '面向前排承压的护甲,适合守御与护体构筑。', resultLabel: '镇岳护甲', currencyCost: 78, requirements: [ buildNamedMaterialRequirement('精炼锭材', 2), buildNamedMaterialRequirement('守御精粹', 1), buildNamedMaterialRequirement('护体精粹', 1), ], createResult: () => buildEquipmentItem({ name: '镇岳护甲', slot: 'armor', rarity: 'epic', description: '厚重但稳定的护甲套件,适合顶住正面压力后再伺机反打。', role: '守御', tags: ['守御', '护体', '先锋'], setId: 'forge-set-ward', setName: '镇岳守阵', pieceName: 'armor', synergy: ['守御', '护体', '先锋'], statProfile: { maxHpBonus: 56, maxManaBonus: 8, outgoingDamageBonus: 0.08, incomingDamageMultiplier: 0.84, }, }), }, { id: 'forge-thunder-relic', name: '锻造 雷纹灵坠', kind: 'forge', description: '为法修、雷法、过载 build 提供资源与爆发补强。', resultLabel: '雷纹灵坠', currencyCost: 88, requirements: [ buildNamedMaterialRequirement('凝光纱', 2), buildNamedMaterialRequirement('法力精粹', 1), buildNamedMaterialRequirement('雷法精粹', 1), ], createResult: () => buildEquipmentItem({ name: '雷纹灵坠', slot: 'relic', rarity: 'epic', description: '内封雷纹与灵引回路的饰品,能在短窗口内快速放大法术节奏。', role: '法修', tags: ['法修', '雷法', '过载'], setId: 'forge-set-thunder', setName: '雷纹御法', pieceName: 'relic', synergy: ['法修', '雷法', '过载'], statProfile: { maxHpBonus: 8, maxManaBonus: 42, outgoingDamageBonus: 0.14, incomingDamageMultiplier: 0.92, }, }), }, ]; function countMatchingItems(inventory: InventoryItem[], requirement: ForgeRequirement) { return inventory .filter(item => requirement.matches(item)) .reduce((sum, item) => sum + item.quantity, 0); } function consumeRequirement(inventory: InventoryItem[], requirement: ForgeRequirement) { let remaining = requirement.quantity; let nextInventory = [...inventory]; for (const item of inventory) { if (remaining <= 0) break; if (!requirement.matches(item)) continue; const consumed = Math.min(item.quantity, remaining); nextInventory = removeInventoryItem(nextInventory, item.id, consumed); remaining -= consumed; } return remaining === 0 ? nextInventory : null; } function applyRequirementsIfPossible(inventory: InventoryItem[], requirements: ForgeRequirement[]) { let nextInventory = [...inventory]; for (const requirement of requirements) { const consumedInventory = consumeRequirement(nextInventory, requirement); if (!consumedInventory) return null; nextInventory = consumedInventory; } return nextInventory; } function buildDismantleBaseMaterials(item: InventoryItem, slot: EquipmentSlotId | null) { const rarityScale: Record = { common: 1, uncommon: 2, rare: 3, epic: 4, legendary: 5, }; const amount = rarityScale[item.rarity]; if (slot === 'weapon') { return [buildMaterialItem('武器残片', amount, ['工巧', '重击'], item.rarity === 'common' ? 'common' : 'uncommon')]; } if (slot === 'armor') { return [buildMaterialItem('甲片', amount, ['工巧', '守御'], item.rarity === 'common' ? 'common' : 'uncommon')]; } if (slot === 'relic') { return [buildMaterialItem('灵饰碎片', amount, ['工巧', '法力'], item.rarity === 'common' ? 'common' : 'uncommon')]; } return [buildMaterialItem('零散材料', Math.max(1, Math.ceil(amount / 2)), ['工巧'], 'common')]; } function buildDismantleEssences(item: InventoryItem) { const buildTags = normalizeBuildTags([ ...(item.buildProfile?.tags ?? []), item.buildProfile?.role ?? '', ]).slice(0, item.rarity === 'legendary' ? 3 : 2); return buildTags.map(tag => buildTagEssence(tag)); } function enhanceStatProfile(statProfile: ItemStatProfile | null | undefined, slot: EquipmentSlotId | null) { const nextProfile = { ...(statProfile ?? {}) }; nextProfile.maxHpBonus = (nextProfile.maxHpBonus ?? 0) + (slot === 'armor' ? 10 : 4); nextProfile.maxManaBonus = (nextProfile.maxManaBonus ?? 0) + (slot === 'relic' ? 10 : 4); nextProfile.outgoingDamageBonus = Number(((nextProfile.outgoingDamageBonus ?? 0) + 0.03).toFixed(3)); if (typeof nextProfile.incomingDamageMultiplier === 'number') { nextProfile.incomingDamageMultiplier = Number(Math.max(0.72, nextProfile.incomingDamageMultiplier - 0.03).toFixed(3)); } else if (slot === 'armor' || slot === 'relic') { nextProfile.incomingDamageMultiplier = slot === 'armor' ? 0.94 : 0.97; } return nextProfile; } function buildReforgedItem(item: InventoryItem) { const slot = getEquipmentSlotFromItem(item); if (!slot || !item.buildProfile) return null; const currentTags = normalizeBuildTags(item.buildProfile.tags); const primaryTag = currentTags[0]; const replacement = primaryTag ? getSimilarBuildTags(primaryTag, 0.6).find(tag => !currentTags.includes(tag)) ?? primaryTag : null; const nextTags = normalizeBuildTags([ ...(replacement ? [replacement] : []), ...currentTags.slice(replacement && replacement !== primaryTag ? 1 : 0), ]).slice(0, 3); return { ...item, id: createItemId(`reforge:${item.name}`), name: item.name.includes('重铸') ? item.name : `${item.name}·重铸`, statProfile: enhanceStatProfile(item.statProfile, slot), buildProfile: { ...item.buildProfile, role: normalizeBuildRole(item.buildProfile.role), tags: nextTags, forgeRank: (item.buildProfile.forgeRank ?? 0) + 1, synergy: nextTags, }, } satisfies InventoryItem; } function getReforgeCost(slot: EquipmentSlotId | null) { if (slot === 'relic') { return { requirements: [buildNamedMaterialRequirement('凝光纱', 1)], currencyCost: 52, }; } return { requirements: [buildNamedMaterialRequirement('精炼锭材', 1)], currencyCost: 46, }; } export function getForgeRecipeViews( inventory: InventoryItem[], playerCurrency = 0, worldType: WorldType | null = null, ) { return FORGE_RECIPES.map(recipe => ({ id: recipe.id, name: recipe.name, kind: recipe.kind, description: recipe.description, resultLabel: recipe.resultLabel, currencyCost: recipe.currencyCost, currencyText: formatCurrency(recipe.currencyCost, worldType), requirements: recipe.requirements.map(requirement => ({ id: requirement.id, label: requirement.label, quantity: requirement.quantity, owned: countMatchingItems(inventory, requirement), })), canCraft: playerCurrency >= recipe.currencyCost && recipe.requirements.every(requirement => countMatchingItems(inventory, requirement) >= requirement.quantity), })) satisfies ForgeRecipeView[]; } export function executeForgeRecipe( inventory: InventoryItem[], recipeId: string, worldType: WorldType | null, playerCurrency: number, ): ForgeExecutionResult | null { const recipe = FORGE_RECIPES.find(candidate => candidate.id === recipeId); if (!recipe || playerCurrency < recipe.currencyCost) return null; const consumedInventory = applyRequirementsIfPossible(inventory, recipe.requirements); if (!consumedInventory) return null; const createdItem = recipe.createResult(worldType); return { inventory: addInventoryItems(consumedInventory, [createdItem]), currency: playerCurrency - recipe.currencyCost, createdItem, }; } export function executeDismantleItem(inventory: InventoryItem[], itemId: string): DismantleExecutionResult | null { const targetItem = inventory.find(item => item.id === itemId); if (!targetItem || targetItem.quantity <= 0) return null; const slot = getEquipmentSlotFromItem(targetItem); if (!slot && !targetItem.buildProfile) return null; const outputs = [ ...buildDismantleBaseMaterials(targetItem, slot), ...buildDismantleEssences(targetItem), ]; return { inventory: addInventoryItems(removeInventoryItem(inventory, itemId, 1), outputs), outputs, }; } export function executeReforgeItem( inventory: InventoryItem[], itemId: string, playerCurrency: number, ): ReforgeExecutionResult | null { const targetItem = inventory.find(item => item.id === itemId); if (!targetItem || targetItem.quantity <= 0) return null; const slot = getEquipmentSlotFromItem(targetItem); const reforgedItem = buildReforgedItem(targetItem); const reforgeCost = getReforgeCost(slot); if (!reforgedItem || playerCurrency < reforgeCost.currencyCost) return null; const consumedInventory = applyRequirementsIfPossible( removeInventoryItem(inventory, itemId, 1), reforgeCost.requirements, ); if (!consumedInventory) return null; return { inventory: addInventoryItems(consumedInventory, [reforgedItem]), reforgedItem, currencyCost: reforgeCost.currencyCost, }; } export function getReforgeCostView(item: InventoryItem, worldType: WorldType | null) { const slot = getEquipmentSlotFromItem(item); const cost = getReforgeCost(slot); return { currencyCost: cost.currencyCost, currencyText: formatCurrency(cost.currencyCost, worldType), requirements: cost.requirements.map(requirement => ({ id: requirement.id, label: requirement.label, quantity: requirement.quantity, })), }; } export function buildForgeSuccessText(action: 'craft' | 'dismantle' | 'reforge', params: { sourceItemName?: string; recipeName?: string; createdItemName?: string; outputNames?: string[]; currencyText?: string; }) { if (action === 'craft') { return `你在工坊中完成了${params.recipeName},获得了${params.createdItemName}${params.currencyText ? `,并支付了${params.currencyText}` : ''}。`; } if (action === 'reforge') { return `你消耗材料重新淬炼了${params.sourceItemName},最终得到${params.createdItemName}${params.currencyText ? `,并支付了${params.currencyText}` : ''}。`; } return `你拆解了${params.sourceItemName},回收出${(params.outputNames ?? []).join('、')}。`; }