1575 lines
58 KiB
Lua
1575 lines
58 KiB
Lua
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
|