333 lines
12 KiB
Lua
333 lines
12 KiB
Lua
local ____lualib = require("lualib_bundle")
|
||
local Set = ____lualib.Set
|
||
local __TS__New = ____lualib.__TS__New
|
||
local __TS__NumberIsFinite = ____lualib.__TS__NumberIsFinite
|
||
local ____exports = {}
|
||
local grantContractCreepAbility
|
||
function grantContractCreepAbility(self, unit, abilityName)
|
||
if not IsValidEntity(unit) or not unit:IsAlive() then
|
||
return
|
||
end
|
||
if unit:FindAbilityByName(abilityName) then
|
||
return
|
||
end
|
||
do
|
||
pcall(function()
|
||
local ab = unit:AddAbility(abilityName)
|
||
if ab ~= nil then
|
||
local maxLevel = ab:GetMaxLevel()
|
||
ab:SetLevel(maxLevel > 0 and maxLevel or 1)
|
||
end
|
||
end)
|
||
end
|
||
end
|
||
--- Сколько вариантов названия «приговор» в локализации (#ds_sentence_title_001 …).
|
||
____exports.DEATH_SENTENCE_TITLE_INDEX_MAX = 100
|
||
--- Восстановление прочности контракта до максимума за донат-осколки (магазин).
|
||
____exports.DEATH_SENTENCE_CONTRACT_REPAIR_DONATE_COST = 25
|
||
--- Только усложнения, дающие крипу способность из wave_creep_abilities (без чистых статов / глобальных модификаторов).
|
||
____exports.DEATH_SENTENCE_COMPLICATION_POOL = {
|
||
"ds_complication_explode",
|
||
"ds_complication_zombie_virus",
|
||
"ds_complication_ghost_evasive",
|
||
"ds_complication_phasing_march",
|
||
"ds_complication_zombie_armor_decress",
|
||
"ds_complication_toxin",
|
||
"ds_complication_full_brutality",
|
||
"ds_complication_desperate_vampirism",
|
||
"ds_complication_head_leap",
|
||
"ds_complication_bone_armor"
|
||
}
|
||
--- Сколько случайных «дебаффов» волн (усложнений) по редкости приговора.
|
||
function ____exports.getDeathSentenceComplicationPickCount(self, rarity)
|
||
if rarity == "common" then
|
||
return 0
|
||
end
|
||
if rarity == "rare" then
|
||
return 1
|
||
end
|
||
if rarity == "epic" then
|
||
return 2
|
||
end
|
||
if rarity == "legendary" then
|
||
return 3
|
||
end
|
||
return 4
|
||
end
|
||
--- Шанс (1…100), что при появлении врага на него повесится одна строка усложнения из приговора.
|
||
-- Броски по строкам независимы — одному юниту может достаться сразу несколько абилок.
|
||
____exports.DEATH_SENTENCE_CONTRACT_CREEP_ABILITY_SPAWN_CHANCE_PCT = 25
|
||
--- Юниты ивентов (виспы, рыбалка, бомбы) — без пассивок усложнения приговора при спавне.
|
||
local DEATH_SENTENCE_CONTRACT_BUFF_BLOCKED_UNITS = __TS__New(Set, {"npc_wisps", "npc_fish_1", "npc_fish_2", "npc_bomb"})
|
||
--- Усложнение приговора → пассивка крипа из wave_creep_abilities (только то, что вешается при спавне).
|
||
local DEATH_SENTENCE_COMPLICATION_CREEP_ABILITY = {
|
||
ds_complication_explode = "zombie_death_explosion",
|
||
ds_complication_zombie_virus = "zombie_virus",
|
||
ds_complication_ghost_evasive = "ghost_evasive",
|
||
ds_complication_phasing_march = "wave_phasing_march",
|
||
ds_complication_zombie_armor_decress = "zombie_armor_decress",
|
||
ds_complication_toxin = "toxin",
|
||
ds_complication_full_brutality = "wave_full_brutality",
|
||
ds_complication_desperate_vampirism = "wave_desperate_vampirism",
|
||
ds_complication_head_leap = "contract_head_leap",
|
||
ds_complication_bone_armor = "bone_armor"
|
||
}
|
||
local function roundMult2(self, x)
|
||
return math.floor(x * 100 + 0.5) / 100
|
||
end
|
||
local function rollRarity(self)
|
||
local r = RandomInt(1, 100)
|
||
if r <= 40 then
|
||
return "common"
|
||
end
|
||
if r <= 70 then
|
||
return "rare"
|
||
end
|
||
if r <= 90 then
|
||
return "epic"
|
||
end
|
||
if r <= 98 then
|
||
return "legendary"
|
||
end
|
||
return "mythic"
|
||
end
|
||
local function rollRewardMultiplierForRarity(self, r)
|
||
if r == "common" then
|
||
return roundMult2(
|
||
nil,
|
||
RandomFloat(3, 4)
|
||
)
|
||
end
|
||
if r == "rare" then
|
||
return roundMult2(
|
||
nil,
|
||
RandomFloat(4, 5)
|
||
)
|
||
end
|
||
if r == "epic" then
|
||
return roundMult2(
|
||
nil,
|
||
RandomFloat(5, 6)
|
||
)
|
||
end
|
||
if r == "legendary" then
|
||
return roundMult2(
|
||
nil,
|
||
RandomFloat(6, 7)
|
||
)
|
||
end
|
||
return roundMult2(
|
||
nil,
|
||
RandomFloat(7, 9)
|
||
)
|
||
end
|
||
--- Детерминированный индекс названия для старых записей без titleIndex.
|
||
function ____exports.inferDeathSentenceTitleIndexFromInstanceId(self, instanceId)
|
||
local h = 0
|
||
do
|
||
local i = 0
|
||
while i < #instanceId do
|
||
local ch = string.byte(instanceId, i + 1) or 0
|
||
h = (h * 31 + ch) % ____exports.DEATH_SENTENCE_TITLE_INDEX_MAX
|
||
i = i + 1
|
||
end
|
||
end
|
||
return h + 1
|
||
end
|
||
--- Детерминированная прочность 1…5 для старых записей без поля (без RNG при каждом GET).
|
||
function ____exports.inferDeathSentenceDurabilityFromInstanceId(self, instanceId)
|
||
local h = 0
|
||
do
|
||
local i = 0
|
||
while i < #instanceId do
|
||
local ch = string.byte(instanceId, i + 1) or 0
|
||
h = (h * 31 + ch) % 1000000
|
||
i = i + 1
|
||
end
|
||
end
|
||
return h % 5 + 1
|
||
end
|
||
function ____exports.rollDeathSentenceContractDurability(self)
|
||
return RandomInt(1, 5)
|
||
end
|
||
function ____exports.normalizeDeathSentenceContractDurability(self, raw, instanceId)
|
||
local n = type(raw) == "number" and raw or tonumber(raw)
|
||
if type(n) == "number" and __TS__NumberIsFinite(n) then
|
||
local f = math.floor(n)
|
||
if f >= 1 and f <= 5 then
|
||
return f
|
||
end
|
||
end
|
||
return ____exports.inferDeathSentenceDurabilityFromInstanceId(nil, instanceId)
|
||
end
|
||
--- Макс. прочность из JSON; если поля нет или битое — не ниже текущей `currentDurability`.
|
||
function ____exports.normalizeDeathSentenceContractDurabilityMax(self, rawMax, currentDurability, instanceId)
|
||
local n = type(rawMax) == "number" and rawMax or tonumber(rawMax)
|
||
local m
|
||
if type(n) == "number" and __TS__NumberIsFinite(n) then
|
||
local f = math.floor(n)
|
||
if f >= 1 and f <= 5 then
|
||
m = f
|
||
else
|
||
m = currentDurability
|
||
end
|
||
else
|
||
m = currentDurability
|
||
end
|
||
return math.max(m, currentDurability)
|
||
end
|
||
function ____exports.normalizeDeathSentenceTitleIndex(self, raw, instanceId)
|
||
local n = type(raw) == "number" and raw or tonumber(raw)
|
||
if type(n) ~= "number" or not __TS__NumberIsFinite(n) then
|
||
return ____exports.inferDeathSentenceTitleIndexFromInstanceId(nil, instanceId)
|
||
end
|
||
local f = math.floor(n)
|
||
if f < 1 or f > ____exports.DEATH_SENTENCE_TITLE_INDEX_MAX then
|
||
return ____exports.inferDeathSentenceTitleIndexFromInstanceId(nil, instanceId)
|
||
end
|
||
return f
|
||
end
|
||
--- Пыль (dust_currency) за разбор приговора.
|
||
function ____exports.getDeathSentenceDismantleShardReward(self, rarity)
|
||
local base = {
|
||
common = 15,
|
||
rare = 35,
|
||
epic = 70,
|
||
legendary = 120,
|
||
mythic = 200
|
||
}
|
||
return base[rarity] or 15
|
||
end
|
||
--- Сколько секунд вычитается из дня за один слот усложнения «короткий день».
|
||
____exports.DEATH_SENTENCE_SHORT_DAY_SEC_PER_STACK = 60
|
||
local SCARCE_LOOT_DROP_FACTOR_PER_STACK = 0.5
|
||
--- Множитель шанса выпадения предметов с юнитов (см. ItemDropSystem): каждый слот `ds_complication_scarce_loot` даёт ×0.5.
|
||
function ____exports.getDeathSentenceItemDropChanceMultiplier(self, instance)
|
||
if not instance then
|
||
return 1
|
||
end
|
||
local stacks = 0
|
||
for ____, cid in ipairs(instance.complicationIds) do
|
||
if cid == "ds_complication_scarce_loot" then
|
||
stacks = stacks + 1
|
||
end
|
||
end
|
||
if stacks <= 0 then
|
||
return 1
|
||
end
|
||
return math.pow(SCARCE_LOOT_DROP_FACTOR_PER_STACK, stacks)
|
||
end
|
||
local function rollComplicationIdsForRarity(self, rarity)
|
||
local need = ____exports.getDeathSentenceComplicationPickCount(nil, rarity)
|
||
if need <= 0 then
|
||
return {}
|
||
end
|
||
local out = {}
|
||
while #out < need do
|
||
local pool = {unpack(____exports.DEATH_SENTENCE_COMPLICATION_POOL)}
|
||
do
|
||
local i = #pool - 1
|
||
while i > 0 do
|
||
local j = RandomInt(0, i)
|
||
local tmp = pool[i + 1]
|
||
pool[i + 1] = pool[j + 1]
|
||
pool[j + 1] = tmp
|
||
i = i - 1
|
||
end
|
||
end
|
||
do
|
||
local k = 0
|
||
while k < #pool and #out < need do
|
||
out[#out + 1] = pool[k + 1]
|
||
k = k + 1
|
||
end
|
||
end
|
||
end
|
||
return out
|
||
end
|
||
--- Один экземпляр в общем ростере (слот в инвентаре).
|
||
function ____exports.generateDeathSentenceContractInstance(self, slotIndex)
|
||
local rarity = rollRarity(nil)
|
||
local d = ____exports.rollDeathSentenceContractDurability(nil)
|
||
return {
|
||
instanceId = (("ds_ci_" .. tostring(slotIndex)) .. "_") .. tostring(RandomInt(10000, 99999999)),
|
||
serial = slotIndex + 1,
|
||
titleIndex = RandomInt(1, ____exports.DEATH_SENTENCE_TITLE_INDEX_MAX),
|
||
rarity = rarity,
|
||
rewardMultiplier = rollRewardMultiplierForRarity(nil, rarity),
|
||
traitId = "none",
|
||
complicationIds = rollComplicationIdsForRarity(nil, rarity),
|
||
durability = d,
|
||
durabilityMax = d
|
||
}
|
||
end
|
||
--- Создание экземпляра контракта с принудительной редкостью (для наград конца матча).
|
||
function ____exports.generateDeathSentenceContractInstanceWithRarity(self, slotIndex, rarity)
|
||
local d = ____exports.rollDeathSentenceContractDurability(nil)
|
||
return {
|
||
instanceId = (("ds_ci_" .. tostring(slotIndex)) .. "_") .. tostring(RandomInt(10000, 99999999)),
|
||
serial = slotIndex + 1,
|
||
titleIndex = RandomInt(1, ____exports.DEATH_SENTENCE_TITLE_INDEX_MAX),
|
||
rarity = rarity,
|
||
rewardMultiplier = rollRewardMultiplierForRarity(nil, rarity),
|
||
traitId = "none",
|
||
complicationIds = rollComplicationIdsForRarity(nil, rarity),
|
||
durability = d,
|
||
durabilityMax = d
|
||
}
|
||
end
|
||
--- После настройки длины дня в лобби: каждый слот `ds_complication_short_day` у выигравшего приговора
|
||
-- уменьшает длительность следующего дня на `DEATH_SENTENCE_SHORT_DAY_SEC_PER_STACK` секунд (не ниже порога менеджера).
|
||
function ____exports.applyDeathSentenceContractDayDurationAdjustments(self, instance)
|
||
if not IsServer() or not instance then
|
||
return
|
||
end
|
||
local stacks = 0
|
||
for ____, cid in ipairs(instance.complicationIds) do
|
||
if cid == "ds_complication_short_day" then
|
||
stacks = stacks + 1
|
||
end
|
||
end
|
||
if stacks <= 0 then
|
||
return
|
||
end
|
||
do
|
||
pcall(function()
|
||
local ____require_result_0 = require("DayNightCycleManager")
|
||
local DayNightCycleManager = ____require_result_0.DayNightCycleManager
|
||
DayNightCycleManager:getInstance():adjustNextDayDurationBy(-____exports.DEATH_SENTENCE_SHORT_DAY_SEC_PER_STACK * stacks)
|
||
end)
|
||
end
|
||
end
|
||
--- После базового скейла сложности в GameMode (враги).
|
||
function ____exports.applyDeathSentenceContractOnEnemySpawn(self, unit, instance)
|
||
if not IsServer() or not IsValidEntity(unit) then
|
||
return
|
||
end
|
||
if unit:GetTeam() == DOTA_TEAM_GOODGUYS then
|
||
return
|
||
end
|
||
if not instance then
|
||
return
|
||
end
|
||
local unitName = unit:GetUnitName()
|
||
if unitName and DEATH_SENTENCE_CONTRACT_BUFF_BLOCKED_UNITS:has(unitName) then
|
||
return
|
||
end
|
||
local chance = ____exports.DEATH_SENTENCE_CONTRACT_CREEP_ABILITY_SPAWN_CHANCE_PCT
|
||
for ____, cid in ipairs(instance.complicationIds) do
|
||
do
|
||
local abilityName = DEATH_SENTENCE_COMPLICATION_CREEP_ABILITY[cid]
|
||
if not abilityName then
|
||
goto __continue60
|
||
end
|
||
if RandomInt(1, 100) > chance then
|
||
goto __continue60
|
||
end
|
||
grantContractCreepAbility(nil, unit, abilityName)
|
||
end
|
||
::__continue60::
|
||
end
|
||
end
|
||
return ____exports
|