diff --git a/lua/aibrain.lua b/lua/aibrain.lua index d3bc45d430..1b664e933d 100644 --- a/lua/aibrain.lua +++ b/lua/aibrain.lua @@ -1211,6 +1211,7 @@ AIBrain = Class(AIBrainHQComponent, AIBrainStatisticsComponent, AIBrainJammerCom end, OnRecalled = function(self) + -- TODO: create a common function for `OnDefeat` and `OnRecall` self.Status = "Recalled" local army = self.Army diff --git a/lua/sim/Recall.lua b/lua/sim/Recall.lua index 16f1440490..0af1cca947 100644 --- a/lua/sim/Recall.lua +++ b/lua/sim/Recall.lua @@ -2,7 +2,7 @@ --** Shared under the MIT license --************************************************************************************************** --- import recall parameters +-- collect recall parameters (note it is not imported) doscript "/lua/shared/RecallParams.lua" -- TODO: generalize to abstract voting system, decoupled from recall @@ -64,9 +64,8 @@ function OnArmyChange() StartTime = votingThreadBrain.RecallVoteStartTime, Open = VoteTime * 0.1, Blocks = teamSize, - -- TODO: rename to `Yes` and `No` - Accept = yes, - Veto = no, + Yes = yes, + No = no, CanVote = GetArmyBrain(focus).Vote ~= nil, } end @@ -76,7 +75,7 @@ end ---@param data {From: number, To: number} function OnAllianceChange(data) local armyFrom, armyTo = data.From, data.To - local oldTeammates = 0 + local oldTeamSize = 0 local oldTeam = {} local votingThreadBrain for index, ally in ArmyBrains do @@ -84,8 +83,8 @@ function OnAllianceChange(data) and not ally:IsDefeated() and not ArmyIsCivilian(index) then - oldTeammates = oldTeammates + 1 - oldTeam[oldTeammates] = ally.Nickname + oldTeamSize = oldTeamSize + 1 + oldTeam[oldTeamSize] = ally.Nickname -- Found a voting thread. We really do need a better way to handle team data... if ally.recallVotingThread then votingThreadBrain = ally @@ -95,7 +94,7 @@ function OnAllianceChange(data) if votingThreadBrain then SPEW("Canceling recall voting for team " .. table.concat(oldTeam, ", ") .. " due to alliance break") votingThreadBrain.VoteCancelled = true - coroutine.resume(votingThreadBrain.recallVotingThread) + ResumeThread(votingThreadBrain.recallVotingThread) if IsAlly(votingThreadBrain, GetFocusArmy()) then SyncCancelRecallVote() SyncRecallStatus() @@ -112,7 +111,7 @@ end function RecallRequestCooldown(lastTeamVote, lastPlayerRequest, playerGatein) -- note that this doesn't always return the reason that currently has the longest cooldown, it -- returns the more "fundamental" one (i.e. the reason whose base cooldown is longest) - -- this is more useful in reporting the reason, and isn't a problem when put in a loop + -- this is more useful in reporting the reason, and isn't a problem as the reason checker is a loop local gametime = GetGameTick() local gateCooldown = (playerGatein or 0) + PlayerGateCooldown - gametime if gateCooldown > 0 then @@ -186,12 +185,12 @@ local function RecallVotingThread(requestingArmy) local gametick = GetGameTick() local yesVotes = 0 - local teammates = 0 + local teamSize = 0 local team = {} for index, brain in ArmyBrains do if not brain:IsDefeated() and IsAlly(requestingArmy, brain.Army) and not ArmyIsCivilian(index) then - teammates = teammates + 1 - team[teammates] = brain + teamSize = teamSize + 1 + team[teamSize] = brain if brain.RecallVote then yesVotes = yesVotes + 1 end @@ -200,7 +199,7 @@ local function RecallVotingThread(requestingArmy) end end -- this function is found in the recall params file, for those looking - local recallPassed = RecallRequestAccepted(yesVotes, teammates) + local recallPassed = RecallRequestAccepted(yesVotes, teamSize) if focus ~= -1 and IsAlly(focus, requestingArmy) then SyncCloseRecallVote(recallPassed) -- the recall UI will handle the announcement in this case @@ -212,16 +211,16 @@ local function RecallVotingThread(requestingArmy) } end local listTeam = team[1].Nickname - for i = 2, teammates do + for i = 2, teamSize do listTeam = listTeam .. ", " .. team[i].Nickname end if recallPassed then - SPEW("Recalling team " .. listTeam .. " at the request of " .. requestingBrain.Nickname .. " (vote passed " .. yesVotes .. " to " .. (teammates - yesVotes ) .. ")") + SPEW("Recalling team " .. listTeam .. " at the request of " .. requestingBrain.Nickname .. " (vote passed " .. yesVotes .. " to " .. (teamSize - yesVotes ) .. ")") for _, brain in team do brain:RecallAllCommanders() end else - SPEW("Not recalling team " .. listTeam .. " (vote failed " .. yesVotes .. " to " .. (teammates - yesVotes ) .. ")") + SPEW("Not recalling team " .. listTeam .. " (vote failed " .. yesVotes .. " to " .. (teamSize - yesVotes ) .. ")") requestingBrain.LastRecallRequestTime = gametick end if focus ~= -1 and IsAlly(requestingArmy, focus) then @@ -238,16 +237,20 @@ end ---@return boolean # if further user sync should happen local function ArmyVoteRecall(army, vote, lastVote) if lastVote then + local foundThread = false for index, ally in ArmyBrains do - if army ~= index and IsAlly(army, index) and not ally:IsDefeated() then + if army ~= index and IsAlly(army, index) then local thread = ally.recallVotingThread if thread then - -- end voting period - ResumeThread(thread) + ResumeThread(thread) -- end voting period + foundThread = true break end end end + if not foundThread then + SPEW("Unable to find recall voting thread for " .. GetArmyBrain(army).Nickname .. '!') + end end local focus = GetFocusArmy() @@ -272,9 +275,7 @@ local function ArmyRequestRecall(army, teammates) end else -- it's just us; recall our army - SPEW("Recalling " .. brain.Nickname) brain:RecallAllCommanders() - end end @@ -291,30 +292,44 @@ function SetRecallVote(data) end return end + local brain = GetArmyBrain(army) + if brain:IsDefeated() then + SyncCannotRequestRecall("observer") + SPEW("Defeated army " .. tostring(army) .. " (" .. GetArmyBrain(army).Nickname .. ") trying to vote for recall!") + return + end local vote = data.Vote and true or false -- determine team voting status local isRequest = true local lastVote = true + local likeVotes = 0 local teammates = 0 local team = {} for index, ally in ArmyBrains do - if army ~= index and not ally:IsDefeated() and IsAlly(army, index) and not ArmyIsCivilian(index) then - if ally.BrainType ~= "Human" then - if army == focus then - SyncCannotRequestRecall("ai") + if army ~= index and IsAlly(army, index) and not ArmyIsCivilian(index) then + if not ally:IsDefeated() then + if ally.BrainType ~= "Human" then + if army == focus then + SyncCannotRequestRecall("ai") + end + return + end + if ally.RecallVote == vote then + likeVotes = likeVotes + 1 end - return + + local allyHasVoted = ally.RecallVote ~= nil + lastVote = lastVote and allyHasVoted -- only the last vote if all allies have also voted + isRequest = isRequest and not allyHasVoted -- only a request if no allies have voted yet + teammates = teammates + 1 + team[teammates] = ally.Nickname + elseif ally.recallVotingThread then + isRequest = false end - local allyHasVoted = ally.RecallVote ~= nil - lastVote = lastVote and allyHasVoted -- only the last vote if all allies have also voted - isRequest = isRequest and not allyHasVoted -- only the last vote if no allies have voted - teammates = teammates + 1 - team[teammates] = ally.Nickname end end - local brain = GetArmyBrain(army) if isRequest then -- the player is making a recall request; this will reset their recall request cooldown local reason = ArmyRecallRequestCooldown(army) @@ -324,14 +339,26 @@ function SetRecallVote(data) end return end - SPEW("Army " .. tostring(army) .. " is requesting recall for " .. table.concat(team, ',')) + if teammates > 0 then + SPEW("Recall request from " .. brain.Nickname .. " for " .. table.concat(team, ',')) + else + SPEW("Recalling " .. brain.Nickname) + end brain.RecallVote = vote ArmyRequestRecall(army, teammates) else -- the player is responding to a recall request; we don't count this against their -- individual recall request cooldown - SPEW("Army " .. tostring(army) .. " recall vote: " .. (vote and "yes" or "no")) + SPEW("Recall vote for " .. brain.Nickname .. ": " .. (vote and "yes" or "no")) brain.RecallVote = vote + + -- if the vote will already be decided with this vote, close the voting session + if not lastVote and ( + vote and RecallRequestAccepted(likeVotes + 1, teammates) or -- will succeed with our vote + not vote and not RecallRequestAccepted(teammates - (likeVotes + 1), teammates) -- won't ever be able to succeed + ) then + lastVote = true + end ArmyVoteRecall(army, vote, lastVote) end end @@ -374,9 +401,9 @@ function SyncRecallVote(vote) Sync.RecallRequest = recallSync end if vote then - recallSync.Accept = (recallSync.Accept or 0) + 1 + recallSync.Yes = (recallSync.Yes or 0) + 1 else - recallSync.Veto = (recallSync.Veto or 0) + 1 + recallSync.No = (recallSync.No or 0) + 1 end end diff --git a/lua/ui/game/recall.lua b/lua/ui/game/recall.lua index 1875029e93..1cc5ed0c5d 100644 --- a/lua/ui/game/recall.lua +++ b/lua/ui/game/recall.lua @@ -42,7 +42,7 @@ function SetLayout() local Scale = LayoutHelpers.ScaleNumber local height = Scale(-4) + panel.label.Height() + Scale(5) + panel.votes.Height() -- make sure these register as a dependency - local voteHeight = panel.buttonAccept.Height() + local voteHeight = panel.buttonYes.Height() local progHeight = panel.progressBarBG.Height() local startTime = panel.startTime() if panel.canVote() then @@ -73,7 +73,7 @@ function RequestHandler(data) if data.Open then panel:StartVote(data.Blocks, data.Open, data.CanVote, data.StartTime) end - local yes, no = data.Accept, data.Veto -- TODO: rename to `Yes` and `No` + local yes, no = data.Yes, data.No if yes or no then panel:AddVotes(yes, no) end @@ -95,9 +95,8 @@ RecallPanel = ClassUI(NinePatch.NinePatch) { self.collapseArrow = UIUtil.CreateCollapseArrow(parent, "t") self.label = UIUtil.CreateText(self, "Ready for recall", 18, UIUtil.bodyFont, true) self.votes = Group(self) - -- TODO: rename to `buttonYes` and `buttonNo` - self.buttonAccept = UIUtil.CreateButtonStd(self, "/widgets02/small", "Yes", 16) - self.buttonVeto = UIUtil.CreateButtonStd(self, "/widgets02/small", "No", 16) + self.buttonYes = UIUtil.CreateButtonStd(self, "/widgets02/small", "Yes", 16) + self.buttonNo = UIUtil.CreateButtonStd(self, "/widgets02/small", "No", 16) self.progressBarBG = UIUtil.CreateBitmapColor(self, "Gray") self.progressBar = UIUtil.CreateBitmapColor(self.progressBarBG, "Yellow") @@ -135,12 +134,12 @@ RecallPanel = ClassUI(NinePatch.NinePatch) { end) :End() - local buttonYes = Layouter(self.buttonAccept) + local buttonYes = Layouter(self.buttonYes) :AtLeftIn(self, 8) :AnchorToBottom(votes, 5) :End() - local buttonNo = Layouter(self.buttonVeto) + local buttonNo = Layouter(self.buttonNo) :AtRightIn(self, 8) :AnchorToBottom(votes, 5) :End() @@ -160,9 +159,8 @@ RecallPanel = ClassUI(NinePatch.NinePatch) { :End() Tooltip.AddCheckboxTooltip(collapseArrow, "voting_collapse") - -- TODO: rename to `dip_recall_request_yes` and `dip_recall_request_no` - Tooltip.AddButtonTooltip(buttonYes, "dip_recall_request_accept") - Tooltip.AddButtonTooltip(buttonNo, "dip_recall_request_veto") + Tooltip.AddButtonTooltip(buttonYes, "dip_recall_request_yes") + Tooltip.AddButtonTooltip(buttonNo, "dip_recall_request_no") end, LayoutBlocks = function(self, blocks) @@ -243,8 +241,8 @@ RecallPanel = ClassUI(NinePatch.NinePatch) { local function ShowForVote(button, hide) return not hide and not self.canVote() end - self.buttonAccept.OnHide = ShowForVote - self.buttonAccept.OnClick = function() + self.buttonYes.OnHide = ShowForVote + self.buttonYes.OnClick = function() SimCallback({ Func = "SetRecallVote", Args = { @@ -254,8 +252,8 @@ RecallPanel = ClassUI(NinePatch.NinePatch) { }) self:SetCanVote(false) end - self.buttonVeto.OnHide = ShowForVote - self.buttonVeto.OnClick = function() + self.buttonNo.OnHide = ShowForVote + self.buttonNo.OnClick = function() SimCallback({ Func = "SetRecallVote", Args = { @@ -268,8 +266,8 @@ RecallPanel = ClassUI(NinePatch.NinePatch) { end, SetCanVote = function(self, canVote) - local buttonYes = self.buttonAccept - local buttonNo = self.buttonVeto + local buttonYes = self.buttonYes + local buttonNo = self.buttonNo self.canVote:Set(canVote) if canVote then buttonYes:Show() @@ -338,11 +336,11 @@ RecallPanel = ClassUI(NinePatch.NinePatch) { if passed then self:OnVoteAccepted() else - self:OnVoteVetoed() + self:OnVoteRejected() end end, - AddVotes = function(self, accept, veto) + AddVotes = function(self, yes, no) local votes = self.votes if votes.blocks < 3 then return end local function SetTextures(vote, filename) @@ -357,16 +355,16 @@ RecallPanel = ClassUI(NinePatch.NinePatch) { break end end - if accept then - for _ = 1, accept do + if yes then + for _ = 1, yes do local vote = votes[index] index = index + 1 vote.cast = "yes" SetTextures(vote, "/game/recall-panel/recall-accept") end end - if veto then - for _ = 1, veto do + if no then + for _ = 1, no do local vote = votes[index] index = index + 1 vote.cast = "no" @@ -431,7 +429,7 @@ RecallPanel = ClassUI(NinePatch.NinePatch) { self.label:SetText(LOC("Recalling...")) end, - OnVoteVetoed = function(self) + OnVoteRejected = function(self) import("/lua/ui/game/announcement.lua").CreateAnnouncement(LOC("The recall vote did not pass.")) self.label:SetText(LOC("Not ready for recall")) end, diff --git a/lua/ui/help/tooltips.lua b/lua/ui/help/tooltips.lua index cdb9c465db..411b0dec2b 100644 --- a/lua/ui/help/tooltips.lua +++ b/lua/ui/help/tooltips.lua @@ -1756,12 +1756,11 @@ Tooltips = { title = "Cannot Recall", description = "Your team has had a recall vote too recently.", }, - -- TODO: rename to `dip_recall_request_yes` and `dip_recall_request_no` - dip_recall_request_accept = { + dip_recall_request_yes = { title = "Yes Vote", description = "Vote yes to your team recalling from battle as a defeat.", }, - dip_recall_request_veto = { + dip_recall_request_no = { title = "No Vote", description = "Vote no to your team recalling from battle as a defeat.", },