local ____lualib = require("lualib_bundle") local __TS__Class = ____lualib.__TS__Class local __TS__New = ____lualib.__TS__New local __TS__ArrayFindIndex = ____lualib.__TS__ArrayFindIndex local __TS__ObjectAssign = ____lualib.__TS__ObjectAssign local Set = ____lualib.Set local __TS__ArraySort = ____lualib.__TS__ArraySort local __TS__ArrayFind = ____lualib.__TS__ArrayFind local __TS__Iterator = ____lualib.__TS__Iterator local __TS__ArrayFilter = ____lualib.__TS__ArrayFilter local __TS__ArrayIsArray = ____lualib.__TS__ArrayIsArray local __TS__Number = ____lualib.__TS__Number local __TS__ObjectKeys = ____lualib.__TS__ObjectKeys local __TS__ArraySplice = ____lualib.__TS__ArraySplice local __TS__ArrayMap = ____lualib.__TS__ArrayMap local __TS__ObjectEntries = ____lualib.__TS__ObjectEntries local __TS__ArraySetLength = ____lualib.__TS__ArraySetLength local __TS__Delete = ____lualib.__TS__Delete local __TS__ArrayIncludes = ____lualib.__TS__ArrayIncludes local __TS__ArraySome = ____lualib.__TS__ArraySome local ____exports = {} local ____contracts_registry = require("death_sentence.contracts_registry") local getDeathSentenceDismantleShardReward = ____contracts_registry.getDeathSentenceDismantleShardReward local applyDeathSentenceContractDayDurationAdjustments = ____contracts_registry.applyDeathSentenceContractDayDurationAdjustments local generateDeathSentenceContractInstanceWithRarity = ____contracts_registry.generateDeathSentenceContractInstanceWithRarity local normalizeDeathSentenceContractDurability = ____contracts_registry.normalizeDeathSentenceContractDurability local normalizeDeathSentenceContractDurabilityMax = ____contracts_registry.normalizeDeathSentenceContractDurabilityMax local DEATH_SENTENCE_CONTRACT_REPAIR_DONATE_COST = ____contracts_registry.DEATH_SENTENCE_CONTRACT_REPAIR_DONATE_COST local ____contracts_backend = require("death_sentence.contracts_backend") local loadDeathSentenceContractsFromBackend = ____contracts_backend.loadDeathSentenceContractsFromBackend local saveDeathSentenceContractsToBackend = ____contracts_backend.saveDeathSentenceContractsToBackend local ____real_lobby_player = require("utils.real_lobby_player") local isRealLobbyPlayer = ____real_lobby_player.isRealLobbyPlayer local countRealLobbyPlayers = ____real_lobby_player.countRealLobbyPlayers local ____player_connection_state = require("utils.player_connection_state") local DOTA_CONNECTION_STATE = ____player_connection_state.DOTA_CONNECTION_STATE local ____store_manager = require("store_manager") local StoreManager = ____store_manager.StoreManager local DEATH_SENTENCE_EMPTY_CONTRACT_VOTE = "__empty_death_sentence_contract__" --- Дебаг: фиктивный «чужой» контракт во 2-й колонке превью при **одном** живом игроке. -- Не трогает бэкенд. Только для локалки — в репозитории держи `false`. local DEATH_SENTENCE_DEBUG_INJECT_SOLO_PEER_CONTRACT = false local DifficultyManager = __TS__Class() DifficultyManager.name = "DifficultyManager" DifficultyManager.____file_path = "scripts/vscripts/difficulty_manager.lua" function DifficultyManager.prototype.____constructor(self) self.diffs = { easy = 0, normal = 0, hard = 0, impossible = 0, death_sentence = 0 } self.players = {} self.contractVotes = {} self.contractColumnOfferByPlayer = {} self.contractInventoryByPlayer = {} self.leader = "normal" self.selectionEnd = false self.listenersRegistered = false self.winningContractId = nil self.winningContractSnapshot = nil self.sharedContractRoster = {} self.sharedContractRosterBuilt = false self.deathSentenceRosterHydrated = false self.deathSentenceHydrateStarted = false self.deathSentenceHydrateWaiting = false self.deathSentenceHydratedByPlayer = {} self.deathSentenceHydrateWaitingByPlayer = {} self.deathSentenceRosterMutationEpoch = 0 self.debugSoloPeerContractInstance = nil end function DifficultyManager.prototype.areAllRealLobbyPlayersFinishedLoading(self) local need = countRealLobbyPlayers(nil) if need <= 0 then return false end local ready = 0 do local i = 0 while i < DOTA_MAX_PLAYERS do do local pid = i if not isRealLobbyPlayer(nil, pid) then goto __continue5 end if PlayerResource:GetConnectionState(pid) == DOTA_CONNECTION_STATE.CONNECTED then ready = ready + 1 end end ::__continue5:: i = i + 1 end end return ready == need end function DifficultyManager.prototype.tickDeathSentenceRosterHydrationWait(self) if self.deathSentenceHydrateStarted then return nil end local state = GameRules:State_Get() if state ~= DOTA_GAMERULES_STATE_CUSTOM_GAME_SETUP then self:beginDeathSentenceRosterHydration() return nil end if self:areAllRealLobbyPlayersFinishedLoading() then self:beginDeathSentenceRosterHydration() return nil end return 0.25 end function DifficultyManager.getInstance(self) if not DifficultyManager.instance then DifficultyManager.instance = __TS__New(DifficultyManager) end return DifficultyManager.instance end function DifficultyManager.prototype.Init(self) if not self.listenersRegistered then if type(CustomGameEventManager) == "nil" then return end CustomGameEventManager:RegisterListener( "invasion_select_difficulty", function(_, data) self:Select({PlayerID = data.PlayerID, diff = data.diff}) end ) CustomGameEventManager:RegisterListener( "invasion_vote_death_sentence_contract", function(_, data) self:SelectContractVote({PlayerID = data.PlayerID, contractId = data.contractId}) end ) CustomGameEventManager:RegisterListener( "invasion_set_death_sentence_column_offer", function(_, data) self:SetDeathSentenceColumnOffer({PlayerID = data.PlayerID, contractId = data.contractId}) end ) CustomGameEventManager:RegisterListener( "invasion_pick_own_death_sentence_contract", function(_, data) self:PickOwnDeathSentenceFromInventory({PlayerID = data.PlayerID, contractId = data.contractId}) end ) CustomGameEventManager:RegisterListener( "invasion_dismantle_death_sentence_contract", function(_, data) self:DismantleDeathSentenceContract({PlayerID = data.PlayerID, instanceId = data.instanceId}) end ) CustomGameEventManager:RegisterListener( "invasion_death_sentence_contract_dismantle_batch", function(_, data) self:DismantleDeathSentenceContractsBatch({PlayerID = data.PlayerID, instanceIds = data.instanceIds}) end ) CustomGameEventManager:RegisterListener( "invasion_death_sentence_contract_toggle_favorite", function(_, data) self:ToggleDeathSentenceContractFavorite({PlayerID = data.PlayerID, instanceId = data.instanceId}) end ) CustomGameEventManager:RegisterListener( "invasion_death_sentence_contract_toggle_pin", function(_, data) self:ToggleDeathSentenceContractPin({PlayerID = data.PlayerID, instanceId = data.instanceId}) end ) CustomGameEventManager:RegisterListener( "invasion_request_death_sentence_contracts_sync", function(_, data) self:RequestDeathSentenceContractsSync(data.PlayerID) end ) CustomGameEventManager:RegisterListener( "invasion_repair_death_sentence_contract_durability", function(_, data) self:RepairDeathSentenceContractDurability({PlayerID = data.PlayerID, instanceId = data.instanceId}) end ) ListenToGameEvent( "player_connect_full", function(event) local playerId = event.PlayerID if playerId ~= nil then self:RequestDeathSentenceContractsSync(playerId) self:sendUpdateToAllClients() end end, nil ) self.listenersRegistered = true end self:recalculateLeader() Timers:CreateTimer( 0.25, function() return self:tickDeathSentenceRosterHydrationWait() end ) Timers:CreateTimer( 0.75, function() self:sendUpdateToAllClients() return nil end ) end function DifficultyManager.prototype.Select(self, data) if self.selectionEnd then return end local playerId = data.PlayerID local newDiff = data.diff local previousVote = self.players[playerId] if newDiff ~= "death_sentence" then self.contractVotes[playerId] = nil end if previousVote == newDiff then if previousVote ~= nil and previousVote ~= nil then local ____self_diffs_0, ____previousVote_1 = self.diffs, previousVote ____self_diffs_0[____previousVote_1] = ____self_diffs_0[____previousVote_1] - 1 end self.players[playerId] = nil else if previousVote and previousVote ~= nil then local ____self_diffs_2, ____previousVote_3 = self.diffs, previousVote ____self_diffs_2[____previousVote_3] = ____self_diffs_2[____previousVote_3] - 1 end self.players[playerId] = newDiff local ____self_diffs_4, ____newDiff_5 = self.diffs, newDiff ____self_diffs_4[____newDiff_5] = ____self_diffs_4[____newDiff_5] + 1 end self:recalculateLeader() self:sendUpdateToAllClients() end function DifficultyManager.prototype.SelectContractVote(self, data) if self.selectionEnd then return end local playerId = data.PlayerID self:ensurePlayerContractInventory(playerId) self:sanitizePlayerContractInventory(playerId) local newContract = data.contractId if newContract == nil or newContract == "" then newContract = nil end local prev = self.contractVotes[playerId] if newContract ~= nil and newContract ~= DEATH_SENTENCE_EMPTY_CONTRACT_VOTE and not self:contractInstanceIdInLobbySharedRoster(newContract) then return end if prev ~= nil and prev ~= nil and prev == newContract then self.contractVotes[playerId] = nil else self.contractVotes[playerId] = newContract end if self.contractVotes[playerId] ~= nil then self:forceDifficultyVote(playerId, "death_sentence") end self:sendUpdateToAllClients() end function DifficultyManager.prototype.PickOwnDeathSentenceFromInventory(self, data) if self.selectionEnd then return end local playerId = data.PlayerID self:ensurePlayerContractInventory(playerId) self:sanitizePlayerContractInventory(playerId) local id = data.contractId if id == nil or id == "" then id = nil end if id == nil or id == DEATH_SENTENCE_EMPTY_CONTRACT_VOTE then self.contractColumnOfferByPlayer[playerId] = nil self.contractVotes[playerId] = nil self:sendUpdateToAllClients() return end if not self:contractInstanceInPlayerInventory(playerId, id) then return end self.contractColumnOfferByPlayer[playerId] = id self.contractVotes[playerId] = id self:forceDifficultyVote(playerId, "death_sentence") self:sendUpdateToAllClients() end function DifficultyManager.prototype.SetDeathSentenceColumnOffer(self, data) if self.selectionEnd then return end local playerId = data.PlayerID if not PlayerResource:IsValidPlayerID(playerId) or PlayerResource:IsFakeClient(playerId) then return end local nid = data.contractId if nid == nil or nid == "" then nid = nil end if nid ~= nil and not self:contractInstanceInPlayerInventory(playerId, nid) then return end self.contractColumnOfferByPlayer[playerId] = nid self:sendUpdateToAllClients() end function DifficultyManager.prototype.contractInstanceInPlayerInventory(self, playerId, instanceId) local inv = self.contractInventoryByPlayer[playerId] if not inv then return false end for ____, row in ipairs(inv) do if row.instanceId == instanceId then return true end end return false end function DifficultyManager.prototype.sanitizeColumnOfferAfterInventoryChange(self, playerId) local sid = self.contractColumnOfferByPlayer[playerId] if sid == nil or sid == nil then return end if not self:contractInstanceInPlayerInventory(playerId, sid) then self.contractColumnOfferByPlayer[playerId] = nil end end function DifficultyManager.prototype.sendDeathSentenceDismantleResult(self, playerId, ok, shards, meta) local player = PlayerResource:GetPlayer(playerId) if not player then return end CustomGameEventManager:Send_ServerToPlayer(player, "death_sentence_contract_dismantle_result", {ok = ok, shards = shards, batch_count = meta and meta.batchCount or 0, skipped_pinned = meta and meta.skippedPinned or 0}) end function DifficultyManager.prototype.sendDeathSentenceRepairResult(self, playerId, ok, reason, success) local player = PlayerResource:GetPlayer(playerId) if not player then return end local payload = {ok = ok, reason = reason or ""} if ok and success then payload.instance_id = success.instanceId payload.durability = success.durability payload.durability_max = success.durabilityMax end CustomGameEventManager:Send_ServerToPlayer(player, "death_sentence_contract_repair_result", payload) end function DifficultyManager.prototype.RepairDeathSentenceContractDurability(self, data) local playerId = data.PlayerID local instanceId = data.instanceId local function fail(____, reason) return self:sendDeathSentenceRepairResult(playerId, false, reason) end if not PlayerResource:IsValidPlayerID(playerId) or PlayerResource:IsFakeClient(playerId) then fail(nil, "invalid") return end if self.selectionEnd or type(instanceId) ~= "string" or #instanceId == 0 then fail(nil, "state") return end if GameRules:State_Get() ~= DOTA_GAMERULES_STATE_CUSTOM_GAME_SETUP then fail(nil, "state") return end if not self.deathSentenceHydratedByPlayer[playerId] then fail(nil, "state") return end local inv = self.contractInventoryByPlayer[playerId] or ({}) local idx = __TS__ArrayFindIndex( inv, function(____, x) return x.instanceId == instanceId end ) if idx < 0 then fail(nil, "not_found") return end local inst = inv[idx + 1] local cur = normalizeDeathSentenceContractDurability(nil, inst.durability, inst.instanceId) local max = normalizeDeathSentenceContractDurabilityMax(nil, inst.durabilityMax, cur, inst.instanceId) inst.durability = cur inst.durabilityMax = max if cur >= max then fail(nil, "full") return end local store = StoreManager:getInstance() local cost = DEATH_SENTENCE_CONTRACT_REPAIR_DONATE_COST if store:getDonateCurrency(playerId) < cost then fail(nil, "funds") return end local prevDur = cur inst.durability = max if not store:tryConsumeDonateCurrency(playerId, cost) then inst.durability = prevDur fail(nil, "funds") return end self.deathSentenceRosterMutationEpoch = self.deathSentenceRosterMutationEpoch + 1 self:sortContractRosterForDisplay(inv) self:rebuildSharedContractRosterFromPlayerInventories() self:syncContractInventoryToClient(playerId) self:saveContractRosterForPlayer( playerId, function(____, okSave) if not okSave then inst.durability = prevDur store:addDonateCurrency(playerId, cost) self:syncContractInventoryToClient(playerId) self:sendUpdateToAllClients() fail(nil, "save") return end self:sendDeathSentenceRepairResult( playerId, true, "", { instanceId = inst.instanceId, durability = max, durabilityMax = normalizeDeathSentenceContractDurabilityMax(nil, inst.durabilityMax, max, inst.instanceId) } ) self:sendUpdateToAllClients() end ) end function DifficultyManager.prototype.shouldInjectDebugSoloPeerContract(self) return DEATH_SENTENCE_DEBUG_INJECT_SOLO_PEER_CONTRACT and countRealLobbyPlayers(nil) == 1 end function DifficultyManager.prototype.getOrCreateDebugSoloPeerContract(self) if self.debugSoloPeerContractInstance then return self.debugSoloPeerContractInstance end local base = generateDeathSentenceContractInstanceWithRarity(nil, 1, "legendary") self.debugSoloPeerContractInstance = __TS__ObjectAssign({}, base, { instanceId = "ds_ci__debug_solo_peer__", serial = 2, titleIndex = 42, pinned = false, favorite = false }) return self.debugSoloPeerContractInstance end function DifficultyManager.prototype.rebuildSharedContractRosterFromPlayerInventories(self) local seen = __TS__New(Set) local next = {} do local i = 0 while i < DOTA_MAX_PLAYERS do do local pid = i local inv = self.contractInventoryByPlayer[pid] if not inv then goto __continue85 end for ____, inst in ipairs(inv) do do if not inst or seen:has(inst.instanceId) then goto __continue87 end seen:add(inst.instanceId) next[#next + 1] = inst end ::__continue87:: end end ::__continue85:: i = i + 1 end end if self:shouldInjectDebugSoloPeerContract() then local dbg = self:getOrCreateDebugSoloPeerContract() if not seen:has(dbg.instanceId) then seen:add(dbg.instanceId) next[#next + 1] = dbg end end self.sharedContractRoster = next self.sharedContractRosterBuilt = true self:sortSharedContractRosterForDisplay() self:invalidateContractVotesNotInRoster() end function DifficultyManager.prototype.sortContractRosterForDisplay(self, roster) __TS__ArraySort( roster, function(____, a, b) local pinA = a.pinned and 1 or 0 local pinB = b.pinned and 1 or 0 if pinA ~= pinB then return pinB - pinA end local favA = a.favorite and 1 or 0 local favB = b.favorite and 1 or 0 if favA ~= favB then return favB - favA end return a.serial - b.serial end ) end function DifficultyManager.prototype.sortSharedContractRosterForDisplay(self) self:sortContractRosterForDisplay(self.sharedContractRoster) end function DifficultyManager.prototype.syncContractInventoryToClient(self, playerId) if not PlayerResource:IsValidPlayerID(playerId) then return end CustomNetTables:SetTableValue( "death_sentence_contracts", tostring(playerId), {roster = self.contractInventoryByPlayer[playerId] or ({})} ) end function DifficultyManager.prototype.ToggleDeathSentenceContractFavorite(self, data) local playerId = data.PlayerID local instanceId = data.instanceId if not PlayerResource:IsValidPlayerID(playerId) or PlayerResource:IsFakeClient(playerId) then return end if self.selectionEnd or type(instanceId) ~= "string" or #instanceId == 0 then return end if GameRules:State_Get() ~= DOTA_GAMERULES_STATE_CUSTOM_GAME_SETUP then return end if not self.deathSentenceHydratedByPlayer[playerId] then return end local inv = self.contractInventoryByPlayer[playerId] or ({}) local inst = __TS__ArrayFind( inv, function(____, x) return x.instanceId == instanceId end ) if not inst then return end inst.favorite = not inst.favorite self.deathSentenceRosterMutationEpoch = self.deathSentenceRosterMutationEpoch + 1 self:sortContractRosterForDisplay(inv) self:rebuildSharedContractRosterFromPlayerInventories() self:syncContractInventoryToClient(playerId) self:saveContractRosterForPlayer( playerId, function() end ) self:sendUpdateToAllClients() end function DifficultyManager.prototype.ToggleDeathSentenceContractPin(self, data) local playerId = data.PlayerID local instanceId = data.instanceId if not PlayerResource:IsValidPlayerID(playerId) or PlayerResource:IsFakeClient(playerId) then return end if self.selectionEnd or type(instanceId) ~= "string" or #instanceId == 0 then return end if GameRules:State_Get() ~= DOTA_GAMERULES_STATE_CUSTOM_GAME_SETUP then return end if not self.deathSentenceHydratedByPlayer[playerId] then return end local inv = self.contractInventoryByPlayer[playerId] or ({}) local inst = __TS__ArrayFind( inv, function(____, x) return x.instanceId == instanceId end ) if not inst then return end inst.pinned = not inst.pinned self.deathSentenceRosterMutationEpoch = self.deathSentenceRosterMutationEpoch + 1 self:sortContractRosterForDisplay(inv) self:rebuildSharedContractRosterFromPlayerInventories() self:syncContractInventoryToClient(playerId) self:saveContractRosterForPlayer( playerId, function() end ) self:sendUpdateToAllClients() end function DifficultyManager.prototype.DismantleDeathSentenceContractsBatch(self, data) local playerId = data.PlayerID local rawIds = self:normalizeContractBatchIds(data.instanceIds) if not PlayerResource:IsValidPlayerID(playerId) then return end if PlayerResource:IsFakeClient(playerId) then self:sendDeathSentenceDismantleResult(playerId, false, 0) return end if self.selectionEnd or #rawIds == 0 then self:sendDeathSentenceDismantleResult(playerId, false, 0) return end if GameRules:State_Get() ~= DOTA_GAMERULES_STATE_CUSTOM_GAME_SETUP then self:sendDeathSentenceDismantleResult(playerId, false, 0) return end if not self.deathSentenceHydratedByPlayer[playerId] then self:sendDeathSentenceDismantleResult(playerId, false, 0) return end local inv = self.contractInventoryByPlayer[playerId] or ({}) local want = __TS__New(Set) for ____, id in ipairs(rawIds) do if type(id) == "string" and #id > 0 then want:add(id) end end if want.size == 0 then self:sendDeathSentenceDismantleResult(playerId, false, 0) return end local skippedPinned = 0 local totalShards = 0 local removed = 0 local toRemove = {} for ____, id in __TS__Iterator(want) do do local inst = __TS__ArrayFind( inv, function(____, x) return x.instanceId == id end ) if not inst then goto __continue125 end if inst.pinned == true then skippedPinned = skippedPinned + 1 goto __continue125 end toRemove[#toRemove + 1] = id totalShards = totalShards + getDeathSentenceDismantleShardReward(nil, inst.rarity) end ::__continue125:: end if #toRemove == 0 then self:sendDeathSentenceDismantleResult(playerId, false, 0, {batchCount = 0, skippedPinned = skippedPinned}) return end local removeSet = __TS__New(Set, toRemove) self.contractInventoryByPlayer[playerId] = __TS__ArrayFilter( inv, function(____, x) return not removeSet:has(x.instanceId) end ) removed = #toRemove self.deathSentenceRosterMutationEpoch = self.deathSentenceRosterMutationEpoch + 1 self:sortContractRosterForDisplay(self.contractInventoryByPlayer[playerId]) self:rebuildSharedContractRosterFromPlayerInventories() self:sanitizeColumnOfferAfterInventoryChange(playerId) self:syncContractInventoryToClient(playerId) self:invalidateContractVotesNotInRoster() self:saveContractRosterForPlayer( playerId, function() end ) StoreManager:getInstance():grantDustCurrencyMatchEndReward(playerId, totalShards) self:sendDeathSentenceDismantleResult(playerId, true, totalShards, {batchCount = removed, skippedPinned = skippedPinned}) self:sendUpdateToAllClients() end function DifficultyManager.prototype.normalizeContractBatchIds(self, raw) if not raw then return {} end if __TS__ArrayIsArray(raw) then local out = {} for ____, id in ipairs(raw) do if type(id) == "string" and #id > 0 then out[#out + 1] = id end end return out end if type(raw) == "table" then local obj = raw local out = {} local keys = __TS__ArraySort( __TS__ObjectKeys(obj), function(____, a, b) return __TS__Number(a) - __TS__Number(b) end ) for ____, key in ipairs(keys) do local val = obj[key] if type(val) == "string" and #val > 0 then out[#out + 1] = val end end return out end return {} end function DifficultyManager.prototype.DismantleDeathSentenceContract(self, data) local playerId = data.PlayerID local instanceId = data.instanceId if not PlayerResource:IsValidPlayerID(playerId) then return end if PlayerResource:IsFakeClient(playerId) then self:sendDeathSentenceDismantleResult(playerId, false, 0) return end if self.selectionEnd or type(instanceId) ~= "string" or #instanceId == 0 then self:sendDeathSentenceDismantleResult(playerId, false, 0) return end if GameRules:State_Get() ~= DOTA_GAMERULES_STATE_CUSTOM_GAME_SETUP then self:sendDeathSentenceDismantleResult(playerId, false, 0) return end if not self.deathSentenceHydratedByPlayer[playerId] then self:sendDeathSentenceDismantleResult(playerId, false, 0) return end local inv = self.contractInventoryByPlayer[playerId] or ({}) local idx = __TS__ArrayFindIndex( inv, function(____, x) return x.instanceId == instanceId end ) if idx < 0 then self:sendDeathSentenceDismantleResult(playerId, false, 0) return end local inst = inv[idx + 1] if inst.pinned == true then self:sendDeathSentenceDismantleResult(playerId, false, 0) return end local shards = getDeathSentenceDismantleShardReward(nil, inst.rarity) __TS__ArraySplice(inv, idx, 1) self.deathSentenceRosterMutationEpoch = self.deathSentenceRosterMutationEpoch + 1 self:sortContractRosterForDisplay(inv) self:rebuildSharedContractRosterFromPlayerInventories() self:sanitizeColumnOfferAfterInventoryChange(playerId) self:syncContractInventoryToClient(playerId) self:invalidateContractVotesNotInRoster() self:saveContractRosterForPlayer( playerId, function() end ) StoreManager:getInstance():grantDustCurrencyMatchEndReward(playerId, shards) self:sendDeathSentenceDismantleResult(playerId, true, shards) self:sendUpdateToAllClients() end function DifficultyManager.prototype.getSteamIdForContracts(self, playerId) if not PlayerResource:IsValidPlayerID(playerId) or PlayerResource:IsFakeClient(playerId) then return nil end local steamId = PlayerResource:GetSteamAccountID(playerId) if not steamId or steamId == 0 then return nil end return tostring(steamId) end function DifficultyManager.prototype.invalidateContractVotesNotInRoster(self) local valid = __TS__New( Set, __TS__ArrayMap( self.sharedContractRoster, function(____, x) return x.instanceId end ) ) do local i = 0 while i < DOTA_MAX_PLAYERS do local pid = i local v = self.contractVotes[pid] if v and not valid:has(v) then self.contractVotes[pid] = nil end i = i + 1 end end end function DifficultyManager.prototype.applyPlayerContractRosterFromBackend(self, playerId, roster) local next = roster ~= nil and ({unpack(roster)}) or ({}) while #next > DifficultyManager.CONTRACT_INVENTORY_CAP do table.remove(next) end self:sortContractRosterForDisplay(next) self.contractInventoryByPlayer[playerId] = next self.deathSentenceHydratedByPlayer[playerId] = true self:syncContractInventoryToClient(playerId) self:rebuildSharedContractRosterFromPlayerInventories() self:sanitizeColumnOfferAfterInventoryChange(playerId) print(((("[DifficultyManager] Death Sentence: ростер игрока pid=" .. tostring(playerId)) .. " с бэка, шт.=") .. tostring(#next)) .. ".") end function DifficultyManager.prototype.loadDeathSentenceRosterForPlayer(self, playerId, done) if not PlayerResource:IsValidPlayerID(playerId) or PlayerResource:IsFakeClient(playerId) then if done ~= nil then done(nil) end return end if self.deathSentenceHydratedByPlayer[playerId] then if done ~= nil then done(nil) end return end if self.deathSentenceHydrateWaitingByPlayer[playerId] then return end local steam = self:getSteamIdForContracts(playerId) if not steam then print(("[DifficultyManager] Death Sentence: SteamID для pid=" .. tostring(playerId)) .. " пока недоступен, повторим позже.") if done ~= nil then done(nil) end return end self.deathSentenceHydrateWaitingByPlayer[playerId] = true local mutationEpochAtFetch = self.deathSentenceRosterMutationEpoch loadDeathSentenceContractsFromBackend( nil, steam, function(____, roster) self.deathSentenceHydrateWaitingByPlayer[playerId] = false if self.deathSentenceRosterMutationEpoch ~= mutationEpochAtFetch and self.deathSentenceHydratedByPlayer[playerId] then print(("[DifficultyManager] Death Sentence: ответ GET игрока pid=" .. tostring(playerId)) .. " проигнорирован (локально уже меняли ростер после старта загрузки).") if done ~= nil then done(nil) end return end self:applyPlayerContractRosterFromBackend(playerId, roster) if done ~= nil then done(nil) end end ) end function DifficultyManager.prototype.beginDeathSentenceRosterHydration(self, preferredPlayerId) if self.deathSentenceHydrateStarted then return end self.deathSentenceHydrateStarted = true local players = {} if preferredPlayerId ~= nil and PlayerResource:IsValidPlayerID(preferredPlayerId) and not PlayerResource:IsFakeClient(preferredPlayerId) then players[#players + 1] = preferredPlayerId else do local i = 0 while i < DOTA_MAX_PLAYERS do do local pid = i if not isRealLobbyPlayer(nil, pid) then goto __continue174 end if PlayerResource:GetConnectionState(pid) ~= DOTA_CONNECTION_STATE.CONNECTED then goto __continue174 end players[#players + 1] = pid end ::__continue174:: i = i + 1 end end end if #players <= 0 then self.deathSentenceHydrateStarted = false self.deathSentenceHydrateWaiting = false print("[DifficultyManager] Death Sentence: нет игроков для загрузки ростеров, повторим позже.") return end self.deathSentenceHydrateWaiting = true local pending = #players local function onDone() pending = pending - 1 if pending > 0 then return end self.deathSentenceHydrateWaiting = false self:finishDeathSentenceRosterHydration() end for ____, playerId in ipairs(players) do self:loadDeathSentenceRosterForPlayer(playerId, onDone) end end function DifficultyManager.prototype.RequestDeathSentenceContractsSync(self, playerId) if not PlayerResource:IsValidPlayerID(playerId) or PlayerResource:IsFakeClient(playerId) then return end if not self.deathSentenceHydratedByPlayer[playerId] then self:loadDeathSentenceRosterForPlayer( playerId, function() self:finishDeathSentenceRosterHydration() end ) return end self:sanitizePlayerContractInventory(playerId) self:syncContractInventoryToClient(playerId) self:sendUpdateToAllClients() end function DifficultyManager.prototype.finishDeathSentenceRosterHydration(self) self.deathSentenceRosterHydrated = true self:sendUpdateToAllClients() end function DifficultyManager.prototype.ensureSharedContractRoster(self) if self.sharedContractRosterBuilt then return end if self.deathSentenceHydrateWaiting then return end self:rebuildSharedContractRosterFromPlayerInventories() end function DifficultyManager.prototype.ensurePlayerContractInventory(self, playerId) if not self.deathSentenceHydratedByPlayer[playerId] then return end local inv = self.contractInventoryByPlayer[playerId] if not inv then self.contractInventoryByPlayer[playerId] = {} end end function DifficultyManager.prototype.sanitizePlayerContractInventory(self, playerId) if not self.deathSentenceHydratedByPlayer[playerId] then return end local listRef = self.contractInventoryByPlayer[playerId] if not listRef then self.contractInventoryByPlayer[playerId] = {} return end local first = listRef[1] if type(first) == "string" then self.contractInventoryByPlayer[playerId] = {} return end local next = {} for ____, row in ipairs(listRef) do do local inst = row if not inst or type(inst.instanceId) ~= "string" then goto __continue197 end inst.durability = normalizeDeathSentenceContractDurability(nil, inst.durability, inst.instanceId) inst.durabilityMax = normalizeDeathSentenceContractDurabilityMax(nil, inst.durabilityMax, inst.durability, inst.instanceId) next[#next + 1] = inst if #next >= DifficultyManager.CONTRACT_INVENTORY_CAP then break end end ::__continue197:: end self:sortContractRosterForDisplay(next) self.contractInventoryByPlayer[playerId] = next end function DifficultyManager.prototype.contractInstanceIdInLobbySharedRoster(self, contractInstanceId) self:ensureSharedContractRoster() for ____, inst in ipairs(self.sharedContractRoster) do if inst.instanceId == contractInstanceId then return true end end return false end function DifficultyManager.prototype.forceDifficultyVote(self, playerId, newDiff) local previousVote = self.players[playerId] if previousVote == newDiff then return end if previousVote ~= nil and previousVote ~= nil then local ____self_diffs_20, ____previousVote_21 = self.diffs, previousVote ____self_diffs_20[____previousVote_21] = ____self_diffs_20[____previousVote_21] - 1 end self.players[playerId] = newDiff local ____self_diffs_22, ____newDiff_23 = self.diffs, newDiff ____self_diffs_22[____newDiff_23] = ____self_diffs_22[____newDiff_23] + 1 self:recalculateLeader() end function DifficultyManager.prototype.recalculateLeader(self) local maxVotes = 0 local newLeader = "normal" for ____, ____value in ipairs(__TS__ObjectEntries(self.diffs)) do local diff = ____value[1] local votes = ____value[2] if votes > maxVotes then maxVotes = votes newLeader = diff end end if maxVotes == 0 then newLeader = "normal" end self.leader = newLeader end function DifficultyManager.prototype.resolveWinningContract(self) local ids = __TS__ArrayMap( self.sharedContractRoster, function(____, x) return x.instanceId end ) if #ids == 0 then return nil end local counts = {} for ____, id in ipairs(ids) do counts[id] = 0 end for ____, ____value in ipairs(__TS__ObjectEntries(self.contractVotes)) do local pidStr = ____value[1] local contractId = ____value[2] do local pid = tonumber(pidStr) if self.players[pid] ~= "death_sentence" then goto __continue218 end if not contractId or contractId == "" or contractId == DEATH_SENTENCE_EMPTY_CONTRACT_VOTE then goto __continue218 end if counts[contractId] == nil then goto __continue218 end counts[contractId] = (counts[contractId] or 0) + 1 end ::__continue218:: end local max = 0 local leaders = {} for ____, id in ipairs(ids) do local c = counts[id] or 0 if c > max then max = c __TS__ArraySetLength(leaders, 0) leaders[#leaders + 1] = id elseif c == max and c > 0 then leaders[#leaders + 1] = id end end if max == 0 then return nil end return leaders[RandomInt(0, #leaders - 1) + 1] end function DifficultyManager.prototype.sendUpdateToAllClients(self) if type(CustomGameEventManager) == "nil" then return end if self.deathSentenceRosterHydrated then do local i = 0 while i < DOTA_MAX_PLAYERS do do local playerId = i if not PlayerResource:IsValidPlayerID(playerId) then goto __continue231 end if not self.deathSentenceHydratedByPlayer[playerId] then goto __continue231 end self:ensurePlayerContractInventory(playerId) self:sanitizePlayerContractInventory(playerId) self:syncContractInventoryToClient(playerId) end ::__continue231:: i = i + 1 end end end local updateData = {votes = self.diffs, currentLeader = self.leader, playerVotes = self.players} if self.deathSentenceRosterHydrated then updateData.contractVotes = self.contractVotes if self:shouldInjectDebugSoloPeerContract() then updateData.debugSoloPeerContract = self:getOrCreateDebugSoloPeerContract() end local details = {} do local i = 0 while i < DOTA_MAX_PLAYERS do do local pid = i local contractId = self.contractVotes[pid] if not contractId or contractId == DEATH_SENTENCE_EMPTY_CONTRACT_VOTE then details[pid] = nil goto __continue236 end local ____opt_24 = self.contractInventoryByPlayer[pid] local own = ____opt_24 and __TS__ArrayFind( self.contractInventoryByPlayer[pid], function(____, x) return x.instanceId == contractId end ) or nil local ____own_27 = own if ____own_27 == nil then local ____table_sharedContractRosterBuilt_26 if self.sharedContractRosterBuilt then ____table_sharedContractRosterBuilt_26 = __TS__ArrayFind( self.sharedContractRoster, function(____, x) return x.instanceId == contractId end ) or nil else ____table_sharedContractRosterBuilt_26 = nil end ____own_27 = ____table_sharedContractRosterBuilt_26 end details[pid] = ____own_27 end ::__continue236:: i = i + 1 end end updateData.contractVoteDetails = details local columnOffer = {} do local j = 0 while j < DOTA_MAX_PLAYERS do do local pOffer = j if not PlayerResource:IsValidPlayerID(pOffer) then goto __continue240 end local c = self.contractColumnOfferByPlayer[pOffer] columnOffer[pOffer] = c ~= nil and c ~= nil and c or nil end ::__continue240:: j = j + 1 end end updateData.contractColumnOffer = columnOffer end CustomGameEventManager:Send_ServerToAllClients("update_difficulty_selections", updateData) end function DifficultyManager.prototype.ensureHeroSelectionResolvedForMatchStart(self) if self.selectionEnd then return end self:OnHeroSelectionState() end function DifficultyManager.prototype.getDeathSentenceContractPayloadForLossPenalty(self, playerId) if self.leader ~= "death_sentence" then return nil end local active = self:getActiveDeathSentenceContractPayload() if active then return active end local vote = self.contractVotes[playerId] if not vote or vote == DEATH_SENTENCE_EMPTY_CONTRACT_VOTE then return nil end local ____opt_28 = self.contractInventoryByPlayer[playerId] local inst = ____opt_28 and __TS__ArrayFind( self.contractInventoryByPlayer[playerId], function(____, x) return x.instanceId == vote end ) or __TS__ArrayFind( self.sharedContractRoster, function(____, x) return x.instanceId == vote end ) or nil if not inst then return nil end local durability = normalizeDeathSentenceContractDurability(nil, inst.durability, inst.instanceId) return { instanceId = inst.instanceId, serial = inst.serial, titleIndex = inst.titleIndex, rarity = inst.rarity, rewardMultiplier = inst.rewardMultiplier, traitId = inst.traitId, complicationIds = {unpack(inst.complicationIds)}, durability = durability, durabilityMax = normalizeDeathSentenceContractDurabilityMax(nil, inst.durabilityMax, durability, inst.instanceId) } end function DifficultyManager.prototype.forceReloadDeathSentenceContractsFromBackend(self, playerId) if not PlayerResource:IsValidPlayerID(playerId) or PlayerResource:IsFakeClient(playerId) then return end self.deathSentenceHydratedByPlayer[playerId] = false __TS__Delete(self.deathSentenceHydrateWaitingByPlayer, playerId) self:loadDeathSentenceRosterForPlayer( playerId, function() self:finishDeathSentenceRosterHydration() end ) end function DifficultyManager.prototype.OnHeroSelectionState(self) self.selectionEnd = true if not self.sharedContractRosterBuilt and not self.deathSentenceHydrateWaiting then self:rebuildSharedContractRosterFromPlayerInventories() end if not self.deathSentenceRosterHydrated then self.deathSentenceRosterHydrated = true end local ____temp_30 if self.leader == "death_sentence" then ____temp_30 = self:resolveWinningContract() else ____temp_30 = nil end self.winningContractId = ____temp_30 local ____temp_31 if self.winningContractId ~= nil then ____temp_31 = __TS__ArrayFind( self.sharedContractRoster, function(____, x) return x.instanceId == self.winningContractId end ) or nil else ____temp_31 = nil end self.winningContractSnapshot = ____temp_31 if type(CustomGameEventManager) == "nil" then return end CustomGameEventManager:Send_ServerToAllClients( "difficulty_selected", { difficulty = self.leader, death_sentence_contract = self.winningContractId, death_sentence_contract_data = self.winningContractSnapshot and ({ instanceId = self.winningContractSnapshot.instanceId, serial = self.winningContractSnapshot.serial, titleIndex = self.winningContractSnapshot.titleIndex, rarity = self.winningContractSnapshot.rarity, rewardMultiplier = self.winningContractSnapshot.rewardMultiplier, traitId = self.winningContractSnapshot.traitId, complicationIds = {unpack(self.winningContractSnapshot.complicationIds)}, durability = normalizeDeathSentenceContractDurability(nil, self.winningContractSnapshot.durability, self.winningContractSnapshot.instanceId), durabilityMax = normalizeDeathSentenceContractDurabilityMax( nil, self.winningContractSnapshot.durabilityMax, normalizeDeathSentenceContractDurability(nil, self.winningContractSnapshot.durability, self.winningContractSnapshot.instanceId), self.winningContractSnapshot.instanceId ) }) or nil } ) if __TS__ArrayIncludes({ "easy", "normal", "hard", "impossible", "death_sentence" }, self.leader) then local units = FindUnitsInRadius( DOTA_TEAM_GOODGUYS, Vector(0, 0, 0), nil, -1, DOTA_UNIT_TARGET_TEAM_ENEMY, DOTA_UNIT_TARGET_ALL, DOTA_UNIT_TARGET_FLAG_MAGIC_IMMUNE_ENEMIES, FIND_ANY_ORDER, false ) _G.Difficulter = self:getNpcStatScale() for ____, unit in ipairs(units) do self:NPC(unit) end local dayLength = 0 local NightLength = 0 repeat local ____switch262 = self.leader local ____cond262 = ____switch262 == "easy" if ____cond262 then dayLength = -60 NightLength = -60 break end ____cond262 = ____cond262 or ____switch262 == "normal" if ____cond262 then dayLength = -120 NightLength = -120 break end ____cond262 = ____cond262 or ____switch262 == "hard" if ____cond262 then dayLength = -60 NightLength = -60 break end ____cond262 = ____cond262 or (____switch262 == "impossible" or ____switch262 == "death_sentence") if ____cond262 then dayLength = -120 NightLength = -120 break end until true do pcall(function() local ____require_result_32 = require("DayNightCycleManager") local DayNightCycleManager = ____require_result_32.DayNightCycleManager local dayManager = DayNightCycleManager:getInstance() local oldDayDuration = dayManager:GetDayDuration() local oldNightDuration = dayManager:GetNightDuration() dayManager:SetDayDuration(oldDayDuration + dayLength) dayManager:SetNightDuration(oldNightDuration + NightLength) if self.leader == "death_sentence" then applyDeathSentenceContractDayDurationAdjustments(nil, self.winningContractSnapshot) end end) end end self:sendUpdateToAllClients() end function DifficultyManager.prototype.getNpcStatScale(self) repeat local ____switch266 = self.leader local ____cond266 = ____switch266 == "easy" if ____cond266 then return 0.5 end ____cond266 = ____cond266 or ____switch266 == "normal" if ____cond266 then return 1 end ____cond266 = ____cond266 or ____switch266 == "hard" if ____cond266 then return 2 end ____cond266 = ____cond266 or ____switch266 == "impossible" if ____cond266 then return 4 end ____cond266 = ____cond266 or ____switch266 == "death_sentence" if ____cond266 then local ____opt_33 = self.winningContractSnapshot return ____opt_33 and ____opt_33.rewardMultiplier or 6 end do return 1 end until true end function DifficultyManager.prototype.getWinningContractSnapshot(self) return self.winningContractSnapshot end function DifficultyManager.prototype.getActiveDeathSentenceContract(self) if not self.selectionEnd then return nil end return self.winningContractId end function DifficultyManager.prototype.getActiveDeathSentenceContractPayload(self) if not self.selectionEnd or not self.winningContractSnapshot then return nil end local w = self.winningContractSnapshot local durability = normalizeDeathSentenceContractDurability(nil, w.durability, w.instanceId) return { instanceId = w.instanceId, serial = w.serial, titleIndex = w.titleIndex, rarity = w.rarity, rewardMultiplier = w.rewardMultiplier, traitId = w.traitId, complicationIds = {unpack(w.complicationIds)}, durability = durability, durabilityMax = normalizeDeathSentenceContractDurabilityMax(nil, w.durabilityMax, durability, w.instanceId) } end function DifficultyManager.prototype.getDeathSentenceContractRewardMultiplier(self) local ____opt_35 = self.winningContractSnapshot return ____opt_35 and ____opt_35.rewardMultiplier or 3.5 end function DifficultyManager.prototype.getRarityIndex(self, rarity) local order = { "common", "rare", "epic", "legendary", "mythic" } do local i = 0 while i < #order do if order[i + 1] == rarity then return i end i = i + 1 end end return 0 end function DifficultyManager.prototype.rarityByIndex(self, index) local order = { "common", "rare", "epic", "legendary", "mythic" } local clamped = math.max( 0, math.min( #order - 1, math.floor(index) ) ) return order[clamped + 1] end function DifficultyManager.prototype.roundContractMultiplier(self, value) return math.floor(value * 100 + 0.5) / 100 end function DifficultyManager.prototype.rollMatchEndContractRarity(self) if self.leader ~= "death_sentence" then local r = RandomInt(1, 100) if r <= 70 then return "rare" end if r <= 95 then return "epic" end return "legendary" end local ____opt_37 = self.winningContractSnapshot local base = ____opt_37 and ____opt_37.rarity or "epic" local baseIdx = self:getRarityIndex(base) local roll = RandomInt(1, 100) if roll <= 75 then return base end return self:rarityByIndex(baseIdx + 1) end function DifficultyManager.prototype.grantMatchEndContractIfEligible(self, playerId) if self.leader ~= "impossible" and self.leader ~= "death_sentence" then return nil end if not PlayerResource:IsValidPlayerID(playerId) or PlayerResource:IsFakeClient(playerId) then return nil end if not self.deathSentenceHydratedByPlayer[playerId] then print(("[DifficultyManager] grantMatchEndContractIfEligible: pid=" .. tostring(playerId)) .. " пропуск — ростер не подтянут с бэка (hyd=false).") return nil end self:ensurePlayerContractInventory(playerId) self:sanitizePlayerContractInventory(playerId) local inv = self.contractInventoryByPlayer[playerId] or ({}) if #inv >= DifficultyManager.CONTRACT_INVENTORY_CAP then print(((((("[DifficultyManager] grantMatchEndContractIfEligible: pid=" .. tostring(playerId)) .. " пропуск — лимит ростера ") .. tostring(DifficultyManager.CONTRACT_INVENTORY_CAP)) .. " (сейчас ") .. tostring(#inv)) .. ").") return nil end local rarity = self:rollMatchEndContractRarity() local serial = #inv + 1 local created = generateDeathSentenceContractInstanceWithRarity(nil, serial - 1, rarity) if self.leader == "death_sentence" then local passed = self.winningContractSnapshot if passed then local delta = RandomFloat(0.25, 1) created.rewardMultiplier = self:roundContractMultiplier(passed.rewardMultiplier + delta) end end inv[#inv + 1] = created self:sortContractRosterForDisplay(inv) self.deathSentenceRosterMutationEpoch = self.deathSentenceRosterMutationEpoch + 1 self:rebuildSharedContractRosterFromPlayerInventories() self:syncContractInventoryToClient(playerId) self:sendUpdateToAllClients() return created end function DifficultyManager.prototype.applyDeathSentenceContractDurabilityOnLoss(self, done) if self.leader ~= "death_sentence" then done(nil) return end local cid = self.winningContractId if not cid or cid == DEATH_SENTENCE_EMPTY_CONTRACT_VOTE then done(nil) return end local ownerPid do local i = 0 while i < DOTA_MAX_PLAYERS do do local pid = i if not PlayerResource:IsValidPlayerID(pid) or PlayerResource:IsFakeClient(pid) then goto __continue293 end local inv = self.contractInventoryByPlayer[pid] if not inv then goto __continue293 end if __TS__ArraySome( inv, function(____, x) return x.instanceId == cid end ) then ownerPid = pid break end end ::__continue293:: i = i + 1 end end if ownerPid == nil then print(("[DifficultyManager] DS durability loss: экземпляр " .. cid) .. " не найден ни в одном ростере") done(nil) return end local inv = self.contractInventoryByPlayer[ownerPid] local idx = __TS__ArrayFindIndex( inv, function(____, x) return x.instanceId == cid end ) if idx < 0 then done(nil) return end local inst = inv[idx + 1] local cur = normalizeDeathSentenceContractDurability(nil, inst.durability, inst.instanceId) inst.durability = cur local next = cur - 1 if next <= 0 then __TS__ArraySplice(inv, idx, 1) do local j = 0 while j < DOTA_MAX_PLAYERS do local pid = j if self.contractVotes[pid] == cid then self.contractVotes[pid] = nil end if self.contractColumnOfferByPlayer[pid] == cid then self.contractColumnOfferByPlayer[pid] = nil end j = j + 1 end end print(((("[DifficultyManager] DS durability: приговор " .. cid) .. " сломан (было ") .. tostring(cur)) .. "→0), удалён из инвентаря") else inst.durability = next print((((("[DifficultyManager] DS durability: приговор " .. cid) .. " ") .. tostring(cur)) .. "→") .. tostring(next)) end self:sortContractRosterForDisplay(inv) self.deathSentenceRosterMutationEpoch = self.deathSentenceRosterMutationEpoch + 1 self:rebuildSharedContractRosterFromPlayerInventories() self:saveContractRosterForPlayer( ownerPid, function() self:sendUpdateToAllClients() done(nil) end ) end function DifficultyManager.prototype.saveContractRosterForPlayer(self, playerId, done) if not PlayerResource:IsValidPlayerID(playerId) or PlayerResource:IsFakeClient(playerId) then done(nil, false) return end local steamId = PlayerResource:GetSteamAccountID(playerId) if not steamId or steamId == 0 then done(nil, false) return end saveDeathSentenceContractsToBackend( nil, tostring(steamId), self.contractInventoryByPlayer[playerId] or ({}), done ) end function DifficultyManager.prototype.NPC(self, npc) if npc:GetTeam() == DOTA_TEAM_GOODGUYS then return end local s = self:getNpcStatScale() local result = s npc:SetBaseMaxHealth(npc:GetMaxHealth() * result) npc:SetMaxHealth(npc:GetMaxHealth() * result) npc:SetHealth(npc:GetMaxHealth()) npc:SetBaseHealthRegen(npc:GetBaseHealthRegen() * result) npc:SetBaseDamageMin(npc:GetBaseDamageMin() * result) npc:SetBaseDamageMax(npc:GetBaseDamageMax() * result) _G.Difficulter = s end DifficultyManager.CONTRACT_INVENTORY_CAP = 30 ____exports.Difficulty = DifficultyManager:getInstance() return ____exports