Files
Dota-Zombie-Invasion/scripts/vscripts/arsenal/arsenalstats.lua
T
2026-05-29 15:11:31 +07:00

728 lines
22 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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