Add coop next-map and skip voting
This commit is contained in:
+1
-1
@@ -403,7 +403,7 @@ COOP_LEVELS = {
|
||||
"ad_coop_mara_final"
|
||||
}
|
||||
|
||||
local function GetInstalledCoopLevels()
|
||||
function GetInstalledCoopLevels()
|
||||
local installedMaps = {}
|
||||
local availableLevels = {}
|
||||
|
||||
|
||||
+223
-11
@@ -12,19 +12,45 @@ local maxMapTime = CreateConVar(
|
||||
"Maximum map time in seconds before rotating; 0 disables the time limit"
|
||||
)
|
||||
|
||||
local skipVoteRatio = CreateConVar(
|
||||
"gc_mapskipvoteratio",
|
||||
"0.6",
|
||||
FCVAR_ARCHIVE,
|
||||
"Fraction of connected human players required to skip the current map"
|
||||
)
|
||||
|
||||
local nextMapVoteRatio = CreateConVar(
|
||||
"gc_nextmapvoteratio",
|
||||
"0.6",
|
||||
FCVAR_ARCHIVE,
|
||||
"Fraction of connected human players required to select the next map"
|
||||
)
|
||||
|
||||
local skipChangeDelay = CreateConVar(
|
||||
"gc_mapskipchangedelay",
|
||||
"3",
|
||||
FCVAR_ARCHIVE,
|
||||
"Seconds between a successful player skip vote and changing map"
|
||||
)
|
||||
|
||||
local changeQueued = false
|
||||
local skipVotes = {}
|
||||
local nextMapVotes = {}
|
||||
local nextMapChoices = {}
|
||||
local nextMapVoteFinished = false
|
||||
local installedMaps
|
||||
|
||||
local function RefreshInstalledMaps()
|
||||
installedMaps = {}
|
||||
for _, fileName in ipairs(file.Find("maps/*.bsp", "GAME")) do
|
||||
installedMaps[string.lower(string.StripExtension(fileName))] = true
|
||||
end
|
||||
end
|
||||
|
||||
local function MapExists(mapName)
|
||||
if not mapName or mapName == "" then return false end
|
||||
|
||||
local wanted = string.lower(string.StripExtension(mapName))
|
||||
for _, fileName in ipairs(file.Find("maps/*.bsp", "GAME")) do
|
||||
if string.lower(string.StripExtension(fileName)) == wanted then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
if not installedMaps then RefreshInstalledMaps() end
|
||||
return installedMaps[string.lower(string.StripExtension(mapName))] == true
|
||||
end
|
||||
|
||||
local function SetFreshNextLevel()
|
||||
@@ -35,6 +61,157 @@ local function SetFreshNextLevel()
|
||||
return nextLevel
|
||||
end
|
||||
|
||||
local function PlayerVoteID(ply)
|
||||
local steamID64 = ply:SteamID64()
|
||||
if steamID64 and steamID64 ~= "" then return steamID64 end
|
||||
return "user:" .. tostring(ply:UserID())
|
||||
end
|
||||
|
||||
local function RequiredVotes(ratioConVar)
|
||||
local humanCount = #player.GetHumans()
|
||||
local ratio = math.Clamp(ratioConVar:GetFloat(), 0.1, 1)
|
||||
return math.max(math.ceil(humanCount * ratio), 1)
|
||||
end
|
||||
|
||||
local function BuildNextMapChoices()
|
||||
local candidates = {}
|
||||
local currentMap = string.lower(game.GetMap())
|
||||
|
||||
for _, mapName in ipairs(GetInstalledCoopLevels()) do
|
||||
if mapName ~= currentMap then
|
||||
candidates[#candidates + 1] = mapName
|
||||
end
|
||||
end
|
||||
|
||||
for index = #candidates, 2, -1 do
|
||||
local other = math.random(index)
|
||||
candidates[index], candidates[other] = candidates[other], candidates[index]
|
||||
end
|
||||
|
||||
nextMapChoices = {}
|
||||
local plannedMap = SetFreshNextLevel()
|
||||
if plannedMap then
|
||||
nextMapChoices[1] = string.lower(plannedMap)
|
||||
end
|
||||
|
||||
for _, mapName in ipairs(candidates) do
|
||||
if #nextMapChoices >= 5 then break end
|
||||
if not table.HasValue(nextMapChoices, mapName) then
|
||||
nextMapChoices[#nextMapChoices + 1] = mapName
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function ShowNextMapChoices(ply)
|
||||
if #nextMapChoices == 0 then BuildNextMapChoices() end
|
||||
|
||||
if #nextMapChoices == 0 then
|
||||
ply:ChatPrint("[CO-OP] No alternative installed co-op map is available.")
|
||||
return
|
||||
end
|
||||
|
||||
ply:ChatPrint("[CO-OP] Vote for the map that should follow this one:")
|
||||
for index, mapName in ipairs(nextMapChoices) do
|
||||
ply:ChatPrint(string.format("[CO-OP] %d. %s", index, mapName))
|
||||
end
|
||||
ply:ChatPrint("[CO-OP] Type !votenext <number>. Type !skip if this map is stuck.")
|
||||
end
|
||||
|
||||
local function ResolveNextMapChoice(requestedChoice)
|
||||
local choiceNumber = tonumber(requestedChoice)
|
||||
if choiceNumber then
|
||||
return nextMapChoices[math.floor(choiceNumber)]
|
||||
end
|
||||
|
||||
local normalizedChoice = string.lower(string.Trim(requestedChoice or ""))
|
||||
for _, mapName in ipairs(nextMapChoices) do
|
||||
if normalizedChoice == mapName then return mapName end
|
||||
end
|
||||
end
|
||||
|
||||
local function CastNextMapVote(ply, requestedChoice)
|
||||
if nextMapVoteFinished then
|
||||
ply:ChatPrint("[CO-OP] The next map has already been selected: " .. GetConVar("nextlevel"):GetString())
|
||||
return
|
||||
end
|
||||
|
||||
local mapName = ResolveNextMapChoice(requestedChoice)
|
||||
if not mapName then
|
||||
ply:ChatPrint("[CO-OP] That is not one of the current choices.")
|
||||
ShowNextMapChoices(ply)
|
||||
return
|
||||
end
|
||||
|
||||
nextMapVotes[PlayerVoteID(ply)] = mapName
|
||||
|
||||
local votesForMap = 0
|
||||
for _, connectedPlayer in ipairs(player.GetHumans()) do
|
||||
if nextMapVotes[PlayerVoteID(connectedPlayer)] == mapName then
|
||||
votesForMap = votesForMap + 1
|
||||
end
|
||||
end
|
||||
|
||||
local required = RequiredVotes(nextMapVoteRatio)
|
||||
PrintMessage(HUD_PRINTTALK, string.format(
|
||||
"%s voted for %s as the next map (%d/%d).",
|
||||
ply:Nick(), mapName, votesForMap, required
|
||||
))
|
||||
|
||||
if votesForMap < required then return end
|
||||
|
||||
nextMapVoteFinished = true
|
||||
RunConsoleCommand("nextlevel", mapName)
|
||||
PrintMessage(HUD_PRINTTALK, "Next map selected: " .. mapName)
|
||||
end
|
||||
|
||||
local function CountCurrentSkipVotes()
|
||||
local connectedPlayers = {}
|
||||
for _, ply in ipairs(player.GetHumans()) do
|
||||
connectedPlayers[PlayerVoteID(ply)] = true
|
||||
end
|
||||
|
||||
local count = 0
|
||||
for voterID in pairs(skipVotes) do
|
||||
if connectedPlayers[voterID] then
|
||||
count = count + 1
|
||||
else
|
||||
skipVotes[voterID] = nil
|
||||
end
|
||||
end
|
||||
return count
|
||||
end
|
||||
|
||||
local function CheckSkipVote()
|
||||
if changeQueued then return end
|
||||
|
||||
local voteCount = CountCurrentSkipVotes()
|
||||
if voteCount >= RequiredVotes(skipVoteRatio) then
|
||||
QueueCoopMapChange("player skip vote")
|
||||
end
|
||||
end
|
||||
|
||||
local function CastSkipVote(ply)
|
||||
if changeQueued then
|
||||
ply:ChatPrint("[CO-OP] A map change is already on its way.")
|
||||
return
|
||||
end
|
||||
|
||||
local voterID = PlayerVoteID(ply)
|
||||
if skipVotes[voterID] then
|
||||
ply:ChatPrint("[CO-OP] You have already voted to skip this map.")
|
||||
return
|
||||
end
|
||||
|
||||
skipVotes[voterID] = true
|
||||
local voteCount = CountCurrentSkipVotes()
|
||||
local required = RequiredVotes(skipVoteRatio)
|
||||
PrintMessage(HUD_PRINTTALK, string.format(
|
||||
"%s voted to skip this map (%d/%d). Type !skip to vote.",
|
||||
ply:Nick(), voteCount, required
|
||||
))
|
||||
CheckSkipVote()
|
||||
end
|
||||
|
||||
function QueueCoopMapChange(reason, requestedMap)
|
||||
if changeQueued then return end
|
||||
|
||||
@@ -49,8 +226,10 @@ function QueueCoopMapChange(reason, requestedMap)
|
||||
end
|
||||
|
||||
changeQueued = true
|
||||
local delay = math.max(changeDelay:GetFloat(), 0)
|
||||
PrintMessage(HUD_PRINTTALK, "Map complete! Loading " .. destination .. " in " .. math.ceil(delay) .. " seconds.")
|
||||
local delay = reason == "player skip vote"
|
||||
and math.max(skipChangeDelay:GetFloat(), 0)
|
||||
or math.max(changeDelay:GetFloat(), 0)
|
||||
PrintMessage(HUD_PRINTTALK, "Loading " .. destination .. " in " .. math.ceil(delay) .. " seconds.")
|
||||
print("[custom_coop] Map change queued by " .. (reason or "unknown") .. ": " .. destination)
|
||||
|
||||
timer.Create("CustomCoopChangeCompletedMap", delay, 1, function()
|
||||
@@ -58,6 +237,38 @@ function QueueCoopMapChange(reason, requestedMap)
|
||||
end)
|
||||
end
|
||||
|
||||
hook.Add("PlayerSay", "CustomCoopMapVoting", function(ply, text)
|
||||
local command = string.lower(string.Trim(text or ""))
|
||||
|
||||
if command == "!skip" or command == "/skip" or command == "!skipmap" then
|
||||
CastSkipVote(ply)
|
||||
return ""
|
||||
end
|
||||
|
||||
if command == "!nextmap" or command == "/nextmap"
|
||||
or command == "!votenext" or command == "/votenext" then
|
||||
ShowNextMapChoices(ply)
|
||||
return ""
|
||||
end
|
||||
|
||||
local requestedChoice = string.match(command, "^[!/]votenext%s+(.+)$")
|
||||
if requestedChoice then
|
||||
CastNextMapVote(ply, requestedChoice)
|
||||
return ""
|
||||
end
|
||||
end)
|
||||
|
||||
hook.Add("PlayerInitialSpawn", "CustomCoopExplainMapVoting", function(ply)
|
||||
timer.Simple(6, function()
|
||||
if not IsValid(ply) then return end
|
||||
ply:ChatPrint("[CO-OP] Type !nextmap to vote for what comes next, or !skip if the current map is stuck.")
|
||||
end)
|
||||
end)
|
||||
|
||||
hook.Add("PlayerDisconnected", "CustomCoopRecountMapSkipVote", function()
|
||||
timer.Simple(0, CheckSkipVote)
|
||||
end)
|
||||
|
||||
hook.Add("AcceptInput", "CustomCoopDetectMapCompletion", function(ent, inputName, _, _, value)
|
||||
if not IsValid(ent) then return end
|
||||
|
||||
@@ -98,6 +309,7 @@ end)
|
||||
|
||||
hook.Add("InitPostEntity", "CustomCoopStartMapTimeLimit", function()
|
||||
SetFreshNextLevel()
|
||||
BuildNextMapChoices()
|
||||
|
||||
local limit = maxMapTime:GetFloat()
|
||||
if limit <= 0 then return end
|
||||
|
||||
Reference in New Issue
Block a user