local ____lualib = require("lualib_bundle") local __TS__Class = ____lualib.__TS__Class local Set = ____lualib.Set local __TS__New = ____lualib.__TS__New local __TS__Spread = ____lualib.__TS__Spread local __TS__ArrayIncludes = ____lualib.__TS__ArrayIncludes local ____exports = {} local ____DayNightCycleManager = require("DayNightCycleManager") local DayNightCycleManager = ____DayNightCycleManager.DayNightCycleManager local ____player_connection_state = require("utils.player_connection_state") local DOTA_CONNECTION_STATE = ____player_connection_state.DOTA_CONNECTION_STATE local VOTE_DURATION_SEC = 30 --- Пауза после любого исхода опроса, прежде чем снова можно предложить голосование. local COOLDOWN_AFTER_SEC = 12 --- Сколько держать карточку с итогами после досрочного завершения (все ответили). local RESULT_SHOW_SEC = 3 local POLL_TIMER = "SkipNightVotePoll" --- Голосование: пропустить остаток дня и сразу начать ночь (клик по таймеру в HUD). ____exports.SkipNightVoteManager = __TS__Class() local SkipNightVoteManager = ____exports.SkipNightVoteManager SkipNightVoteManager.name = "SkipNightVoteManager" SkipNightVoteManager.____file_path = "scripts/vscripts/skip_night_vote_manager.lua" function SkipNightVoteManager.prototype.____constructor(self) self.phase = "idle" self.endGameTime = 0 self.resultPassed = false self.yesVoters = __TS__New(Set) self.noVoters = __TS__New(Set) self.cooldownUntil = 0 CustomGameEventManager:RegisterListener( "skip_night_vote_propose", function(src, data) local playerId = data and data.PlayerID if playerId == nil or playerId < 0 then return end self:onPropose(playerId) end ) CustomGameEventManager:RegisterListener( "skip_night_vote_yes", function(src, data) local playerId = data and data.PlayerID if playerId == nil or playerId < 0 then return end self:onVoteYes(playerId) end ) CustomGameEventManager:RegisterListener( "skip_night_vote_no", function(src, data) local playerId = data and data.PlayerID if playerId == nil or playerId < 0 then return end self:onVoteNo(playerId) end ) end function SkipNightVoteManager.getInstance(self) if not ____exports.SkipNightVoteManager.instance then ____exports.SkipNightVoteManager.instance = __TS__New(____exports.SkipNightVoteManager) end return ____exports.SkipNightVoteManager.instance end function SkipNightVoteManager.prototype.getVotablePlayerIds(self) local out = {} do local i = 0 while i < DOTA_MAX_PLAYERS do do local p = i if not PlayerResource:IsValidPlayerID(p) then goto __continue12 end if PlayerResource:GetConnectionState(p) ~= DOTA_CONNECTION_STATE.CONNECTED then goto __continue12 end local hero = PlayerResource:GetSelectedHeroEntity(p) if not hero or not IsValidEntity(hero) or not hero:IsRealHero() then goto __continue12 end out[#out + 1] = p end ::__continue12:: i = i + 1 end end return out end function SkipNightVoteManager.prototype.majorityNeeded(self, eligible) if eligible <= 0 then return 9999 end return math.floor(eligible / 2) + 1 end function SkipNightVoteManager.prototype.getEligibleAndPruneVotes(self) local eligible = self:getVotablePlayerIds() local valid = __TS__New(Set, eligible) for ____, p in ipairs({__TS__Spread(self.yesVoters)}) do if not valid:has(p) then self.yesVoters:delete(p) end end for ____, p in ipairs({__TS__Spread(self.noVoters)}) do if not valid:has(p) then self.noVoters:delete(p) end end return eligible end function SkipNightVoteManager.prototype.buildVotesByPlayer(self, eligible) local out = {} for ____, p in ipairs(eligible) do if self.yesVoters:has(p) then out[tostring(p)] = 1 elseif self.noVoters:has(p) then out[tostring(p)] = -1 else out[tostring(p)] = 0 end end return out end function SkipNightVoteManager.prototype.sendUpdate(self, eligibleArg) local eligible = eligibleArg or self:getEligibleAndPruneVotes() local need = self:majorityNeeded(#eligible) local votesByPlayer = self:buildVotesByPlayer(eligible) CustomGameEventManager:Send_ServerToAllClients("skip_night_vote_update", { active = self.phase == "idle" and 0 or 1, canVote = self.phase == "voting" and 1 or 0, isResult = self.phase == "result" and 1 or 0, resultPassed = self.resultPassed and 1 or 0, endGameTime = self.endGameTime, yesCount = self.yesVoters.size, noCount = self.noVoters.size, needCount = need, totalEligible = #eligible, eligiblePlayerIds = eligible, votesByPlayer = votesByPlayer }) end function SkipNightVoteManager.prototype.clearVote(self, _reason) self.phase = "idle" self.resultPassed = false self.endGameTime = 0 self.yesVoters:clear() self.noVoters:clear() Timers:RemoveTimer(POLL_TIMER) self:sendUpdate() self.cooldownUntil = GameRules:GetGameTime() + COOLDOWN_AFTER_SEC end function SkipNightVoteManager.prototype.onPropose(self, proposer) if GameRules:State_Get() ~= DOTA_GAMERULES_STATE_GAME_IN_PROGRESS then self:sendProposerError(proposer, "not_ingame") return end local d = DayNightCycleManager:getInstance() if not d:CanVoteSkipToNight() then self:sendProposerError(proposer, "not_day") return end local g = GameRules:GetGameTime() if g < self.cooldownUntil then self:sendProposerError(proposer, "cooldown") return end if self.phase ~= "idle" then self:sendProposerError(proposer, "already") return end local votable = self:getEligibleAndPruneVotes() if #votable < 1 then self:sendProposerError(proposer, "no_players") return end self.phase = "voting" self.resultPassed = false self.yesVoters:clear() self.noVoters:clear() self.endGameTime = g + VOTE_DURATION_SEC self:sendUpdate(votable) Timers:RemoveTimer(POLL_TIMER) Timers:CreateTimer( POLL_TIMER, { endTime = 0, callback = function() if self.phase == "idle" then return nil end if self.phase == "result" then if GameRules:GetGameTime() >= self.endGameTime then self:clearVote("success") return nil end self:sendUpdate() return 0.25 end local d2 = DayNightCycleManager:getInstance() if not d2:CanVoteSkipToNight() then self:clearVote("invalid") return nil end local eligible = self:getEligibleAndPruneVotes() if #eligible < 1 then self:clearVote("invalid") return nil end if GameRules:GetGameTime() >= self.endGameTime then local need = self:majorityNeeded(#eligible) local passed = self.yesVoters.size >= need if passed then local ok = d2:TryInstantSwitchDayToNightFromVote() if not ok then self:clearVote("invalid") return nil end end self:clearVote("timeout") return nil end local votedCount = self.yesVoters.size + self.noVoters.size local allVoted = votedCount >= #eligible if allVoted then local need = self:majorityNeeded(#eligible) local passed = self.yesVoters.size >= need if passed then local ok = d2:TryInstantSwitchDayToNightFromVote() if not ok then self:clearVote("invalid") return nil end end self.phase = "result" self.resultPassed = passed self.endGameTime = GameRules:GetGameTime() + RESULT_SHOW_SEC self:sendUpdate(eligible) return 0.25 end self:sendUpdate(eligible) return 0.25 end } ) end function SkipNightVoteManager.prototype.onVoteYes(self, playerId) if self.phase ~= "voting" then return end if GameRules:GetGameTime() >= self.endGameTime then return end local eligible = self:getEligibleAndPruneVotes() if not __TS__ArrayIncludes(eligible, playerId) then return end self.noVoters:delete(playerId) self.yesVoters:add(playerId) self:sendUpdate(eligible) end function SkipNightVoteManager.prototype.onVoteNo(self, playerId) if self.phase ~= "voting" then return end if GameRules:GetGameTime() >= self.endGameTime then return end local eligible = self:getEligibleAndPruneVotes() if not __TS__ArrayIncludes(eligible, playerId) then return end self.yesVoters:delete(playerId) self.noVoters:add(playerId) self:sendUpdate(eligible) end function SkipNightVoteManager.prototype.sendProposerError(self, proposer, _code) do pcall(function() local p = PlayerResource:GetPlayer(proposer) if p then CustomGameEventManager:Send_ServerToPlayer(p, "skip_night_vote_error", {code = "skip_night_err_" .. _code}) end end) end end return ____exports