local ____lualib = require("lualib_bundle") local __TS__NumberIsFinite = ____lualib.__TS__NumberIsFinite local ____exports = {} local ARSENAL_RARITY_ORDER = { "common", "rare", "epic", "legendary", "mythic" } local ASCENDED_STAT_QUALITY = "ascended" local ARSENAL_RARITY_MULTIPLIER = { common = 1, rare = 1.2, epic = 1.45, legendary = 1.75, mythic = 2.1, ascended = 2.55 } local BASE_STAT_COUNT = 2 local ADDITIONAL_STAT_COUNT = 4 local HIDDEN_FINAL_STAT_INDEX = 6 local HIDDEN_FINAL_UNLOCK_LEVEL = 1 local MAX_UPGRADE_LEVEL = 5 local PER_LEVEL_GROWTH = 0.12 local MYTHIC_BEST_LINE_CHANCE_PCT = 24 local MYTHIC_WEAK_LINE_CHANCE_PCT = 10 --- С вероятностью N% новая строка копирует ключ уже существующего (ниже — реже дубли одного и того же стата на предмете). local ARSENAL_REPEAT_EXISTING_STAT_CHANCE_PCT = 26 local MAX_RARITY_INDEX = #ARSENAL_RARITY_ORDER - 1 local MAX_RARITY_CAP_APPROACH_PER_STEP = 0.22 local ADDITIONAL_SLOTS_BY_ITEM_QUALITY = { common = 0, rare = 1, epic = 2, legendary = 3, mythic = 4 } --- Диапазоны с учётом 6 слотов экипировки и роста от редкости/уровня (см. Panorama `getTemplateForStatKey`). -- -- `upgradeGrowthMin` / `upgradeGrowthMax` — диапазон для **`k`** в формуле итога: -- `значение ∝ effectiveBase × rarityMult × (1 + k × lineBonus)`. -- Это **не** «+N к тултипу» и **не** проценты вида «15 = 15%»; типичные **`k`** — **доли 0.08…0.14** -- (при `lineBonus = 5` множитель прокачки около **1.4…1.7**). local ARSENAL_STAT_POOL = { { key = "bonus_damage", min = 12, max = 64, upgradeGrowthMin = 0.1, upgradeGrowthMax = 0.14 }, { key = "attack_speed", min = 5, max = 22, upgradeGrowthMin = 0.09, upgradeGrowthMax = 0.13 }, { key = "bonus_armor", min = 2, max = 8, upgradeGrowthMin = 0.1, upgradeGrowthMax = 0.14 }, { key = "max_health", min = 70, max = 240, upgradeGrowthMin = 0.09, upgradeGrowthMax = 0.12 }, { key = "max_mana", min = 50, max = 180, upgradeGrowthMin = 0.09, upgradeGrowthMax = 0.12 }, { key = "health_regen", min = 0.4, max = 3.2, decimals = 1, upgradeGrowthMin = 0.09, upgradeGrowthMax = 0.12 }, { key = "mana_regen", min = 0.2, max = 1.8, decimals = 2, upgradeGrowthMin = 0.09, upgradeGrowthMax = 0.12 }, { key = "move_speed", min = 6, max = 12, upgradeGrowthMin = 0.08, upgradeGrowthMax = 0.12 }, { key = "magic_resist", min = 0.2, max = 1.2, decimals = 1, upgradeGrowthMin = 0.04, upgradeGrowthMax = 0.1 }, { key = "spell_amp", min = 1, max = 5, decimals = 1, upgradeGrowthMin = 0.08, upgradeGrowthMax = 0.13 }, { key = "all_stats", min = 2, max = 5, upgradeGrowthMin = 0.045, upgradeGrowthMax = 0.07 }, { key = "bonus_strength", min = 4, max = 16, upgradeGrowthMin = 0.09, upgradeGrowthMax = 0.13 }, { key = "bonus_agility", min = 4, max = 16, upgradeGrowthMin = 0.09, upgradeGrowthMax = 0.13 }, { key = "bonus_intellect", min = 4, max = 16, upgradeGrowthMin = 0.09, upgradeGrowthMax = 0.13 }, { key = "all_stats_pct", min = 0.2, max = 1.1, decimals = 1, upgradeGrowthMin = 0.04, upgradeGrowthMax = 0.065 }, { key = "strength_pct", min = 0.8, max = 10, decimals = 1, upgradeGrowthMin = 0.05, upgradeGrowthMax = 0.08 }, { key = "agility_pct", min = 0.8, max = 10, decimals = 1, upgradeGrowthMin = 0.05, upgradeGrowthMax = 0.08 }, { key = "intellect_pct", min = 0.8, max = 10, decimals = 1, upgradeGrowthMin = 0.05, upgradeGrowthMax = 0.08 }, { key = "luck", min = 0.5, max = 2.5, decimals = 1, upgradeGrowthMin = 0.08, upgradeGrowthMax = 0.14 }, { key = "outgoing_damage_pct", min = 0.8, max = 3.5, decimals = 1, upgradeGrowthMin = 0.06, upgradeGrowthMax = 0.1 }, { key = "incoming_damage_reduction_pct", min = 0.5, max = 2, decimals = 1, upgradeGrowthMin = 0.06, upgradeGrowthMax = 0.1 }, { key = "attack_speed_pct", min = 1.5, max = 6, decimals = 1, upgradeGrowthMin = 0.07, upgradeGrowthMax = 0.11 }, { key = "move_speed_pct", min = 1, max = 4, decimals = 1, upgradeGrowthMin = 0.07, upgradeGrowthMax = 0.11 }, { key = "base_damage_pct", min = 0.8, max = 3.5, decimals = 1, upgradeGrowthMin = 0.07, upgradeGrowthMax = 0.11 }, { key = "crit_mult", min = 1, max = 8, decimals = 1, upgradeGrowthMin = 0.07, upgradeGrowthMax = 0.11 }, {key = "battle_level", min = 1, max = 1, decimals = 0} } local function roundTo(self, value, decimals) if decimals == nil then decimals = 0 end if decimals <= 0 then return math.floor(value + 0.5) end local mul = math.pow(10, decimals) return math.floor(value * mul + 0.5) / mul end local function randomTemplateIndex(self) return RandomInt(1, #ARSENAL_STAT_POOL) - 1 end local function inferSlotFromItemName(self, itemName) local n = string.lower(itemName) if (string.find(n, "_weapon_", nil, true) or 0) - 1 >= 0 then return "weapon" end if (string.find(n, "_armor_", nil, true) or 0) - 1 >= 0 then return "armor" end if (string.find(n, "_helmet_", nil, true) or 0) - 1 >= 0 then return "helmet" end if (string.find(n, "_boots_", nil, true) or 0) - 1 >= 0 then return "boots" end if (string.find(n, "_necklace_", nil, true) or 0) - 1 >= 0 then return "necklace" end return "ring" end local function getSlotWeightForStat(self, slot, key) if slot == "ring" then if key == "max_mana" or key == "magic_resist" or key == "spell_amp" then return 2.5 end if key == "mana_regen" then return 2.2 end if key == "max_health" then return 1.4 end if key == "all_stats_pct" then return 1.55 end if key == "all_stats" then return 1.15 end if key == "intellect_pct" or key == "strength_pct" or key == "agility_pct" or key == "bonus_intellect" or key == "bonus_strength" or key == "bonus_agility" then return 1.3 end elseif slot == "weapon" then if key == "bonus_damage" or key == "attack_speed" or key == "base_damage_pct" then return 2.6 end if key == "attack_speed_pct" or key == "outgoing_damage_pct" or key == "crit_mult" then return 1.8 end if key == "strength_pct" or key == "agility_pct" or key == "bonus_strength" or key == "bonus_agility" then return 1.5 end if key == "all_stats_pct" then return 1.25 end elseif slot == "armor" then if key == "health_regen" or key == "max_health" then return 1.85 end if key == "mana_regen" or key == "bonus_armor" then return 1.55 end if key == "all_stats_pct" then return 1.55 end if key == "all_stats" then return 1.2 end if key == "strength_pct" or key == "bonus_strength" then return 1.6 end elseif slot == "helmet" then if key == "health_regen" then return 1.65 end if key == "all_stats_pct" then return 1.72 end if key == "all_stats" then return 1.25 end if key == "intellect_pct" or key == "bonus_intellect" then return 1.8 end elseif slot == "boots" then if key == "all_stats_pct" then return 1.38 end if key == "agility_pct" or key == "bonus_agility" then return 1.6 end elseif slot == "necklace" then if key == "mana_regen" or key == "spell_amp" then return 1.75 end if key == "all_stats_pct" then return 1.62 end if key == "all_stats" then return 1.2 end if key == "intellect_pct" or key == "bonus_intellect" then return 1.5 end end return 1 end local function randomTemplateIndexForSlot(self, slot) local totalWeight = 0 for ____, t in ipairs(ARSENAL_STAT_POOL) do totalWeight = totalWeight + getSlotWeightForStat(nil, slot, t.key) end if totalWeight <= 0 then return randomTemplateIndex(nil) end local roll = RandomFloat(0, totalWeight) local passed = 0 do local i = 0 while i < #ARSENAL_STAT_POOL do passed = passed + getSlotWeightForStat(nil, slot, ARSENAL_STAT_POOL[i + 1].key) if roll <= passed then return i end i = i + 1 end end return #ARSENAL_STAT_POOL - 1 end local function findTemplateIndexByKey(self, key) do local i = 0 while i < #ARSENAL_STAT_POOL do if ARSENAL_STAT_POOL[i + 1].key == key then return i end i = i + 1 end end return nil end --- Реже, чем раньше: часть строк может взять тот же стат, что уже выпал (случайная из набранных). -- Иначе — обычный взвешенный ролл по слоту. local function randomTemplateIndexForSlotWithStackBias(self, slot, existingLines) if #existingLines == 0 or RandomInt(1, 100) > ARSENAL_REPEAT_EXISTING_STAT_CHANCE_PCT then return randomTemplateIndexForSlot(nil, slot) end local linePick = RandomInt(1, #existingLines) - 1 local key = existingLines[linePick + 1].key local idx = findTemplateIndexByKey(nil, key) if idx == nil then return randomTemplateIndexForSlot(nil, slot) end return idx end local function rollStatBase(self, template, slotIndex, contractDropStatBias) if contractDropStatBias == nil then contractDropStatBias = 0 end local roll01 = RandomFloat(0, 1) local exponent = slotIndex < BASE_STAT_COUNT and 1.35 or 1.75 if contractDropStatBias > 0 then exponent = exponent * (1 - 0.45 * contractDropStatBias) end local biased = math.pow(roll01, exponent) local raw = template.min + (template.max - template.min) * biased return roundTo(nil, raw, template.decimals or 0) end local function rollStatBaseInBand(self, template, fromRatio, toRatio) local clampedFrom = math.max( 0, math.min(1, fromRatio) ) local clampedTo = math.max( clampedFrom, math.min(1, toRatio) ) local ratio = RandomFloat(clampedFrom, clampedTo) local raw = template.min + (template.max - template.min) * ratio return roundTo(nil, raw, template.decimals or 0) end --- Случайный множитель роста строки по шаблону; без min/max в шаблоне — undefined (берётся PER_LEVEL_GROWTH). local function rollUpgradeGrowthForTemplate(self, template) local minG = template.upgradeGrowthMin local maxG = template.upgradeGrowthMax if minG == nil or maxG == nil then return nil end if not __TS__NumberIsFinite(minG) or not __TS__NumberIsFinite(maxG) then return nil end local lo = math.min(minG, maxG) local hi = math.max(minG, maxG) return roundTo( nil, RandomFloat(lo, hi), 4 ) end local function clampRarityIndex(self, index) if index < 0 then return 0 end if index >= #ARSENAL_RARITY_ORDER then return #ARSENAL_RARITY_ORDER - 1 end return index end local function getRarityIndex(self, quality) do local i = 0 while i < #ARSENAL_RARITY_ORDER do if ARSENAL_RARITY_ORDER[i + 1] == quality then return i end i = i + 1 end end return 0 end local function elevateRarityBySteps(self, baseQuality, steps) if steps <= 0 then return baseQuality end local nextIndex = clampRarityIndex( nil, getRarityIndex(nil, baseQuality) + steps ) return ARSENAL_RARITY_ORDER[nextIndex + 1] end local function capStatQualityByItemQuality(self, statQuality, itemQuality) local statIndex = getRarityIndex(nil, statQuality) local itemIndex = getRarityIndex(nil, itemQuality) return ARSENAL_RARITY_ORDER[math.min(statIndex, itemIndex) + 1] end local function getTemplateByKey(self, key) for ____, t in ipairs(ARSENAL_STAT_POOL) do if t.key == key then return t end end return nil end local function getStatQualityByBaseValue(self, stat) local tpl = getTemplateByKey(nil, stat.key) if not tpl then return "common" end local span = tpl.max - tpl.min if span <= 0 then return "common" end local ratio = (stat.base - tpl.min) / span local clamped = math.max( 0, math.min(1, ratio) ) local rawTier = math.floor(clamped * #ARSENAL_RARITY_ORDER) return ARSENAL_RARITY_ORDER[clampRarityIndex(nil, rawTier) + 1] end local function getEffectiveStatQuality(self, item, statIndex, baseQuality) if item.quality == "mythic" and statIndex == HIDDEN_FINAL_STAT_INDEX and math.floor(item.upgradeLevel) >= MAX_UPGRADE_LEVEL then return ASCENDED_STAT_QUALITY end return capStatQualityByItemQuality(nil, baseQuality, item.quality) end local function getBaseAfterCapApproach(self, stat, extraMaxRaritySteps) if extraMaxRaritySteps <= 0 then return stat.base end local tpl = getTemplateByKey(nil, stat.key) if not tpl or tpl.max <= stat.base then return stat.base end local approachFactor = 1 - math.pow(1 - MAX_RARITY_CAP_APPROACH_PER_STEP, extraMaxRaritySteps) local shifted = stat.base + (tpl.max - stat.base) * approachFactor return roundTo(nil, shifted, tpl.decimals or 0) end local function getActiveAdditionalCount(self, itemQuality) return ADDITIONAL_SLOTS_BY_ITEM_QUALITY[itemQuality] or 0 end local function getAdditionalUpgradeTargetIndex(self, item, levelStep, activeAdditional) if levelStep <= 0 then return nil end if levelStep == HIDDEN_FINAL_UNLOCK_LEVEL then return HIDDEN_FINAL_STAT_INDEX end local poolSize = math.min(ADDITIONAL_STAT_COUNT, activeAdditional) + 1 if poolSize <= 0 then return nil end local seed = levelStep * 97 + poolSize * 31 do local offset = 0 while offset < poolSize do local idx = offset == poolSize - 1 and HIDDEN_FINAL_STAT_INDEX or BASE_STAT_COUNT + offset local line = item.stats[idx + 1] local baseInt = math.floor((line and line.base or 0) * 100 + 0.5) seed = seed + ((idx + 1) * 17 + baseInt) offset = offset + 1 end end local offset = math.floor(math.abs(seed)) % poolSize return offset == poolSize - 1 and HIDDEN_FINAL_STAT_INDEX or BASE_STAT_COUNT + offset end function ____exports.contractRewardMultToDropStatBias01(self, rewardMultiplier) local minM = 3 local maxM = 10 local span = maxM - minM if span <= 0 then return 0 end return math.max( 0, math.min(1, (rewardMultiplier - minM) / span) ) end function ____exports.createInitialItemState(self, itemName, quality, context) local bias = math.max( 0, math.min(1, context and context.contractDropStatBias or 0) ) local mythicBandLift = bias * 0.035 local slot = inferSlotFromItemName(nil, itemName) local stats = {} local totalStatLines = BASE_STAT_COUNT + ADDITIONAL_STAT_COUNT + 1 local mythicBestLines = 0 if quality == "mythic" then mythicBestLines = 1 if RandomInt(1, 100) <= MYTHIC_BEST_LINE_CHANCE_PCT then mythicBestLines = 2 end end do local i = 0 while i < BASE_STAT_COUNT + ADDITIONAL_STAT_COUNT + 1 do local templateIndex = randomTemplateIndexForSlotWithStackBias(nil, slot, stats) local template = ARSENAL_STAT_POOL[templateIndex + 1] local rolledBase if quality == "mythic" then local guaranteedBestCut = totalStatLines - mythicBestLines if i >= guaranteedBestCut then rolledBase = rollStatBaseInBand( nil, template, 0.8, math.min(1, 0.97 + mythicBandLift) ) elseif RandomInt(1, 100) <= MYTHIC_WEAK_LINE_CHANCE_PCT then rolledBase = rollStatBaseInBand( nil, template, 0.05, math.min(1, 0.59 + mythicBandLift * 0.5) ) else rolledBase = rollStatBaseInBand( nil, template, 0.6, math.min(1, 0.79 + mythicBandLift) ) end else rolledBase = rollStatBase(nil, template, i, bias) end local rolledGrowth = rollUpgradeGrowthForTemplate(nil, template) local row = {key = template.key, base = rolledBase} if rolledGrowth ~= nil then row.upgradeGrowth = rolledGrowth end stats[#stats + 1] = row i = i + 1 end end return {itemName = itemName, quality = quality, upgradeLevel = 0, stats = stats} end function ____exports.createArsenalItemInstance(self, instanceId, serial, itemName, quality, rollContext) local core = ____exports.createInitialItemState(nil, itemName, quality, rollContext) return { instanceId = instanceId, serial = serial, itemName = core.itemName, quality = core.quality, upgradeLevel = core.upgradeLevel, stats = core.stats } end function ____exports.getEffectiveItemStats(self, item) local result = {} local level = math.max( 0, math.min( MAX_UPGRADE_LEVEL, math.floor(item.upgradeLevel) ) ) local activeAdditional = getActiveAdditionalCount(nil, item.quality) local lineLevelBonus = {} do local i = 0 while i < #item.stats do lineLevelBonus[i + 1] = 0 i = i + 1 end end do local i = 0 while i < BASE_STAT_COUNT and i < #item.stats do lineLevelBonus[i + 1] = level i = i + 1 end end do local step = 1 while step <= level do local target = getAdditionalUpgradeTargetIndex(nil, item, step, activeAdditional) if target ~= nil and target < #item.stats then lineLevelBonus[target + 1] = (lineLevelBonus[target + 1] or 0) + 1 end step = step + 1 end end do local i = 0 while i < #item.stats do do if i >= BASE_STAT_COUNT and i < BASE_STAT_COUNT + ADDITIONAL_STAT_COUNT then local localIndex = i - BASE_STAT_COUNT if localIndex >= activeAdditional then goto __continue106 end end if i == HIDDEN_FINAL_STAT_INDEX and level < HIDDEN_FINAL_UNLOCK_LEVEL then goto __continue106 end local rolledQuality = getStatQualityByBaseValue(nil, item.stats[i + 1]) local rarityBonusSteps = lineLevelBonus[i + 1] or 0 local upgradedQuality = elevateRarityBySteps(nil, rolledQuality, rarityBonusSteps) local maxRarityStepsSpent = math.max( 0, rarityBonusSteps - (MAX_RARITY_INDEX - getRarityIndex(nil, rolledQuality)) ) local statQuality = getEffectiveStatQuality(nil, item, i, upgradedQuality) local effectiveBase = getBaseAfterCapApproach(nil, item.stats[i + 1], maxRarityStepsSpent) local qualityMult = ARSENAL_RARITY_MULTIPLIER[statQuality] local lineBonus = lineLevelBonus[i + 1] or 0 local statKey = item.stats[i + 1].key if statKey == "battle_level" then result[#result + 1] = { key = statKey, value = math.max( 0, math.floor(item.stats[i + 1].base) + lineBonus ) } goto __continue106 end local tpl = getTemplateByKey(nil, statKey) local outDecimals = tpl and tpl.decimals or 0 local perLevelGrowth = item.stats[i + 1].upgradeGrowth or PER_LEVEL_GROWTH local growthMult = 1 + perLevelGrowth * lineBonus result[#result + 1] = { key = statKey, value = roundTo(nil, effectiveBase * qualityMult * growthMult, outDecimals) } end ::__continue106:: i = i + 1 end end return result end function ____exports.getItemVisibleStatsCount(self, item) local activeAdditional = getActiveAdditionalCount(nil, item.quality) local hasHidden = math.floor(item.upgradeLevel) >= HIDDEN_FINAL_UNLOCK_LEVEL and 1 or 0 return BASE_STAT_COUNT + activeAdditional + hasHidden end return ____exports