initial commit

This commit is contained in:
achmad
2026-05-29 15:11:31 +07:00
commit 777ee9bad8
1539 changed files with 172449 additions and 0 deletions
@@ -0,0 +1,212 @@
local ____lualib = require("lualib_bundle")
local __TS__ArrayIsArray = ____lualib.__TS__ArrayIsArray
local __TS__NumberIsFinite = ____lualib.__TS__NumberIsFinite
local ____exports = {}
local ____api_helper = require("api_helper")
local setApiHeaders = ____api_helper.setApiHeaders
local encodeApiBody = ____api_helper.encodeApiBody
local ____server_config = require("server_config")
local SERVER_CONFIG = ____server_config.SERVER_CONFIG
local ____contracts_registry = require("death_sentence.contracts_registry")
local normalizeDeathSentenceTitleIndex = ____contracts_registry.normalizeDeathSentenceTitleIndex
local normalizeDeathSentenceContractDurability = ____contracts_registry.normalizeDeathSentenceContractDurability
local normalizeDeathSentenceContractDurabilityMax = ____contracts_registry.normalizeDeathSentenceContractDurabilityMax
local getDeathSentenceComplicationPickCount = ____contracts_registry.getDeathSentenceComplicationPickCount
local LOG_PREFIX = "[DSContractsAPI]"
____exports.DEATH_SENTENCE_CONTRACT_ROSTER_CAP = 90
--- Старые записи (armor/movespeed/full_brutality) приводим к `none` — геймплей только через усложнения-абилки.
local function normalizeDeathSentenceTraitId(self, _v)
return "none"
end
local function isValidRarity(self, v)
return v == "common" or v == "rare" or v == "epic" or v == "legendary" or v == "mythic"
end
--- Разбор ответа GET / тела сохранённого JSON → массив экземпляров (0…CAP) или null при неверной структуре.
function ____exports.parseDeathSentenceContractRosterPayload(self, raw)
if not raw or type(raw) ~= "table" then
return nil
end
local root = raw
local wrap = root.death_sentence_contracts
if not wrap or type(wrap) ~= "table" then
return nil
end
local rosterRaw = wrap.roster
if not __TS__ArrayIsArray(rosterRaw) then
return nil
end
local out = {}
for ____, row in ipairs(rosterRaw) do
do
if not row or type(row) ~= "table" then
goto __continue8
end
local o = row
local instanceId = o.instanceId
local serialNum = type(o.serial) == "number" and o.serial or tonumber(o.serial)
if type(serialNum) ~= "number" or not __TS__NumberIsFinite(serialNum) then
goto __continue8
end
local rarity = o.rarity
local rmNum = type(o.rewardMultiplier) == "number" and o.rewardMultiplier or tonumber(o.rewardMultiplier)
if type(rmNum) ~= "number" or not __TS__NumberIsFinite(rmNum) then
goto __continue8
end
local compRaw = o.complicationIds
if type(instanceId) ~= "string" or #instanceId == 0 then
goto __continue8
end
if not isValidRarity(nil, rarity) then
goto __continue8
end
local complicationIds = {}
if __TS__ArrayIsArray(compRaw) then
for ____, c in ipairs(compRaw) do
if type(c) == "string" and #c > 0 then
complicationIds[#complicationIds + 1] = c
end
end
end
local maxComp = getDeathSentenceComplicationPickCount(nil, rarity)
while #complicationIds > maxComp do
table.remove(complicationIds)
end
local titleIndex = normalizeDeathSentenceTitleIndex(nil, o.titleIndex, instanceId)
local durability = normalizeDeathSentenceContractDurability(nil, o.durability, instanceId)
local ____o_durabilityMax_0 = o.durabilityMax
if ____o_durabilityMax_0 == nil then
____o_durabilityMax_0 = o.durability_max
end
local rawMax = ____o_durabilityMax_0
local durabilityMax = normalizeDeathSentenceContractDurabilityMax(nil, rawMax, durability, instanceId)
local inst = {
instanceId = instanceId,
serial = serialNum,
titleIndex = titleIndex,
rarity = rarity,
rewardMultiplier = rmNum,
traitId = normalizeDeathSentenceTraitId(nil, o.traitId),
complicationIds = complicationIds,
durability = durability,
durabilityMax = durabilityMax
}
local fav = o.favorite
if fav == true or fav == 1 then
inst.favorite = true
elseif fav == false or fav == 0 then
inst.favorite = false
end
local pin = o.pinned
if pin == true or pin == 1 then
inst.pinned = true
elseif pin == false or pin == 0 then
inst.pinned = false
end
out[#out + 1] = inst
end
::__continue8::
end
while #out > ____exports.DEATH_SENTENCE_CONTRACT_ROSTER_CAP do
table.remove(out)
end
return out
end
local function decodeJsonBody(self, body)
do
local function ____catch()
return true, nil
end
local ____try, ____hasReturned, ____returnValue = pcall(function()
return true, {json.decode(body)}
end)
if not ____try then
____hasReturned, ____returnValue = ____catch()
end
if ____hasReturned then
return ____returnValue
end
end
end
--- Извлечь объект из ответа API (массив из одного элемента или объект).
local function unwrapApiJsonObject(self, decoded)
if __TS__ArrayIsArray(decoded) and #decoded > 0 then
return decoded[1]
end
return decoded
end
--- Загрузить ростер с бэка для steam_id. В колбэке roster=null при ошибке / пусто / невалидно.
function ____exports.loadDeathSentenceContractsFromBackend(self, steamId, done)
local url = ((SERVER_CONFIG.API_URL .. "/player/") .. steamId) .. "/death_sentence_contracts"
local request = CreateHTTPRequest("GET", url)
setApiHeaders(nil, request)
request:Send(function(result)
if result.StatusCode < 200 or result.StatusCode >= 300 then
print((((LOG_PREFIX .. " GET fail steam=") .. steamId) .. " code=") .. tostring(result.StatusCode))
done(nil, nil)
return
end
local bodyStr = result.Body ~= nil and tostring(result.Body) or ""
if #bodyStr == 0 then
done(nil, nil)
return
end
local decoded = decodeJsonBody(nil, bodyStr)
local obj = unwrapApiJsonObject(nil, decoded)
local roster = ____exports.parseDeathSentenceContractRosterPayload(nil, obj)
if roster == nil then
print((LOG_PREFIX .. " GET parse invalid steam=") .. steamId)
done(nil, nil)
return
end
print((((LOG_PREFIX .. " GET ok steam=") .. steamId) .. " count=") .. tostring(#roster))
done(nil, roster)
end)
end
--- Сохранить ростер на бэк для одного steam_id.
function ____exports.saveDeathSentenceContractsToBackend(self, steamId, roster, done)
local url = ((SERVER_CONFIG.API_URL .. "/player/") .. steamId) .. "/death_sentence_contracts"
local request = CreateHTTPRequest("PUT", url)
setApiHeaders(nil, request)
local payload = {death_sentence_contracts = {roster = roster}}
request:SetHTTPRequestRawPostBody(
"application/json",
encodeApiBody(nil, payload)
)
request:Send(function(result)
local ok = result.StatusCode >= 200 and result.StatusCode < 300
if ok then
print((LOG_PREFIX .. " PUT ok steam=") .. steamId)
else
print((((LOG_PREFIX .. " PUT fail steam=") .. steamId) .. " code=") .. tostring(result.StatusCode))
end
if done then
done(nil, ok)
end
end)
end
--- Сохранить один и тот же ростер всем подключённым игрокам (одинаковая сетка лобби).
function ____exports.saveDeathSentenceContractsRosterToAllConnectedPlayers(self, roster)
do
local i = 0
while i < DOTA_MAX_PLAYERS do
do
local pid = i
if not PlayerResource:IsValidPlayerID(pid) then
goto __continue41
end
local steamId = PlayerResource:GetSteamAccountID(pid)
if not steamId or steamId == 0 then
goto __continue41
end
____exports.saveDeathSentenceContractsToBackend(
nil,
tostring(steamId),
roster
)
end
::__continue41::
i = i + 1
end
end
end
return ____exports
@@ -0,0 +1,332 @@
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