Use official garrysmod <tab> dashboard which closes more reliable

This commit is contained in:
2026-09-06 22:03:33 +02:00
parent dd7f7deb90
commit 566d566c64
4 changed files with 228 additions and 58 deletions
+5
View File
@@ -41,6 +41,11 @@ and recharges faster while walking; active sprinting does not receive the extra
hoverpack recharge. Its built-in looping thruster sound follows the player and
is audible to nearby players.
God Mode is a non-permanent Behaviour purchase. Each activation costs 1,000
Credits by default and immediately grants 90 seconds of server-enforced damage
immunity. It creates no ownership record. Attempting to activate it while it is
already running reports the remaining time and does not spend Credits.
The server checks all kills, balances, live prices, ownership, equipment, and
music playback. A purchase request contains only the item type and ID: clients
cannot award points, submit a purchase price, or debit their own Credits.
+143 -58
View File
@@ -2,7 +2,6 @@ if not CLIENT then return end
local Progression = CustomCoopProgression
local shopFrame
local scoreboardFrame
local shopState = {
score = 0,
credits = 0,
@@ -49,6 +48,20 @@ surface.CreateFont("CustomCoopProgressionSmall", {
weight = 400
})
-- The Base gamemode scoreboard uses these names. Recreate them after Base has
-- loaded so its standard rows, including names and ping, are easier to read.
surface.CreateFont("ScoreboardDefault", {
font = "Helvetica",
size = 24,
weight = 800
})
surface.CreateFont("ScoreboardDefaultTitle", {
font = "Helvetica",
size = 36,
weight = 800
})
local colors = {
background = Color(20, 24, 29, 245),
panel = Color(31, 37, 44, 250),
@@ -65,21 +78,45 @@ local function RequestShop()
net.SendToServer()
end
concommand.Add("coop_shop", RequestShop)
local nextShopToggle = 0
local function ToggleShop()
if RealTime() < nextShopToggle then return end
nextShopToggle = RealTime() + 0.25
if IsValid(shopFrame) then
shopFrame:Close()
return
end
RequestShop()
end
concommand.Add("coop_shop", ToggleShop)
hook.Add("ShowSpare1", "CustomCoopProgressionF3", function()
RequestShop()
ToggleShop()
return true
end)
-- Some client configurations do not route F3 through ShowSpare1. This fallback
-- keeps the advertised key reliable while the server-side rate limit absorbs a
-- possible duplicate request.
local nextF3Request = 0
hook.Add("PlayerButtonDown", "CustomCoopProgressionF3Fallback", function(ply, button)
if ply ~= LocalPlayer() or button ~= KEY_F3 or RealTime() < nextF3Request then return end
nextF3Request = RealTime() + 0.5
RequestShop()
if ply ~= LocalPlayer() or button ~= KEY_F3 then return end
ToggleShop()
end)
local escapeWasDown = false
hook.Add("Think", "CustomCoopProgressionShopEscape", function()
local escapeDown = input.IsKeyDown(KEY_ESCAPE)
if escapeDown and not escapeWasDown and IsValid(shopFrame) then
shopFrame:Close()
gui.HideGameUI()
timer.Simple(0, function()
if not IsValid(shopFrame) then gui.HideGameUI() end
end)
end
escapeWasDown = escapeDown
end)
local function SelectItem(itemType, itemID)
@@ -109,6 +146,7 @@ local function AddItemCard(layout, itemType, item)
local isWeapon = itemType == "weapon"
local isMusic = itemType == "music"
local isBehaviour = itemType == "behaviour"
local isConsumable = isBehaviour and item.consumable == true
local owned = isModel and shopState.ownedModels[item.id]
or isWeapon and shopState.ownedWeapons[item.id]
or isMusic and shopState.ownedMusic[item.id]
@@ -185,7 +223,9 @@ local function AddItemCard(layout, itemType, item)
if isMusic then
actionText = (owned or currentPrice == 0) and "Play" or "Buy"
elseif isBehaviour then
actionText = owned and (selected and "Disable" or "Enable") or "Buy & Enable"
actionText = isConsumable and "Activate"
or owned and (selected and "Disable" or "Enable")
or "Buy & Enable"
else
actionText = selected and "Equipped" or (owned or currentPrice == 0) and "Equip" or "Buy"
end
@@ -319,6 +359,9 @@ local function OpenShop()
shopFrame:SetTitle("")
shopFrame:ShowCloseButton(true)
shopFrame:MakePopup()
shopFrame.OnClose = function()
shopFrame = nil
end
shopFrame.Paint = function(_, width, height)
draw.RoundedBox(10, 0, 0, width, height, colors.background)
draw.SimpleText("CO-OP REWARDS SHOP", "CustomCoopProgressionTitle", 18, 16, colors.text)
@@ -524,7 +567,9 @@ hook.Add("HUDPaint", "CustomCoopProgressionHUD", function()
local rank = ply:GetNW2String("CustomCoopRank", Progression.GetRank(score))
local hoverpackEnabled = ply:GetNW2Bool("CustomCoopHoverpackEnabled", false)
local hoverpackThrusting = ply:GetNW2Bool("CustomCoopHoverpackThrusting", false)
local width, height = 288, hoverpackEnabled and 90 or 66
local godmodeSeconds = math.max(math.ceil(ply:GetNW2Float("CustomCoopGodmodeUntil", 0) - CurTime()), 0)
local width = 288
local height = 66 + (hoverpackEnabled and 24 or 0) + (godmodeSeconds > 0 and 24 or 0)
local x, y = ScrW() - width - 18, 18
draw.RoundedBox(8, x, y, width, height, colors.background)
@@ -534,13 +579,20 @@ hook.Add("HUDPaint", "CustomCoopProgressionHUD", function()
draw.SimpleText("Score " .. score, "CustomCoopProgressionBody", x + 12, y + 37, colors.text)
draw.SimpleText("Credits " .. credits, "CustomCoopProgressionBody", x + 151, y + 37, colors.text)
local statusY = y + 64
if hoverpackEnabled then
local suitPower = ply.GetSuitPower and math.Round(ply:GetSuitPower()) or 100
local status = hoverpackThrusting and ("THRUSTING — AUX " .. suitPower .. "%")
or suitPower <= 0 and "EMPTY — release Space to recharge"
or ("READY — AUX " .. suitPower .. "%")
draw.SimpleText("Hoverpack: " .. status, "CustomCoopProgressionSmall", x + 12, y + 64,
draw.SimpleText("Hoverpack: " .. status, "CustomCoopProgressionSmall", x + 12, statusY,
hoverpackThrusting and colors.success or colors.muted)
statusY = statusY + 24
end
if godmodeSeconds > 0 then
draw.SimpleText("God Mode: " .. godmodeSeconds .. "s remaining", "CustomCoopProgressionSmall",
x + 12, statusY, colors.success)
end
if CurTime() < gainExpires then
@@ -548,61 +600,94 @@ hook.Add("HUDPaint", "CustomCoopProgressionHUD", function()
end
end)
local function RefreshScoreboard(list)
if not IsValid(list) then return end
list:Clear()
local function AddProgressionToOfficialRow(ply, row)
if row.CustomCoopProgressionAdded then return end
row.CustomCoopProgressionAdded = true
local players = player.GetAll()
table.sort(players, function(left, right)
return left:GetNW2Int("CustomCoopScore", 0) > right:GetNW2Int("CustomCoopScore", 0)
end)
row.CustomCoopScore = row:Add("DLabel")
row.CustomCoopScore:Dock(RIGHT)
row.CustomCoopScore:SetWide(140)
row.CustomCoopScore:SetFont("ScoreboardDefault")
row.CustomCoopScore:SetTextColor(Color(75, 115, 155))
row.CustomCoopScore:SetContentAlignment(5)
row.CustomCoopScore:SetTooltip("Lifetime Score")
for _, ply in ipairs(players) do
list:AddLine(
ply:Nick(),
ply:GetNW2String("CustomCoopRank", "Newbie"),
ply:GetNW2Int("CustomCoopScore", 0),
ply:Ping()
)
row.CustomCoopRank = row:Add("DLabel")
row.CustomCoopRank:Dock(RIGHT)
row.CustomCoopRank:SetWide(165)
row.CustomCoopRank:SetFont("ScoreboardDefault")
row.CustomCoopRank:SetTextColor(Color(75, 115, 155))
row.CustomCoopRank:SetContentAlignment(5)
row.CustomCoopRank:SetTooltip("Co-op Rank")
if IsValid(row.Name) then row.Name:SetFont("ScoreboardDefault") end
row:SetTall(42)
local originalThink = row.Think
row.Think = function(self)
if originalThink then originalThink(self) end
if not IsValid(ply) then return end
local score = ply:GetNW2Int("CustomCoopScore", 0)
self.CustomCoopScore:SetText("Score " .. score)
self.CustomCoopRank:SetText(ply:GetNW2String("CustomCoopRank", "Newbie"))
-- Keep the official row behavior, but order players by lifetime score.
self:SetZPos(-math.Clamp(score, 0, 32000) + ply:EntIndex())
end
end
local function ExtendOfficialScoreboard()
if not IsValid(g_Scoreboard) then return end
if not g_Scoreboard.CustomCoopProgressionAdded then
g_Scoreboard.CustomCoopProgressionAdded = true
g_Scoreboard.PerformLayout = function(self)
local width = math.min(ScrW() - 40, 980)
local height = math.max(ScrH() - 160, 320)
self:SetSize(width, height)
self:SetPos((ScrW() - width) / 2, (ScrH() - height) / 2)
end
if IsValid(g_Scoreboard.Name) then
g_Scoreboard.Name:SetFont("ScoreboardDefaultTitle")
end
g_Scoreboard:InvalidateLayout(true)
end
for _, ply in ipairs(player.GetAll()) do
if IsValid(ply.ScoreEntry) then
AddProgressionToOfficialRow(ply, ply.ScoreEntry)
end
end
end
local scoreboardRequested = false
local scoreboardShownAt = 0
local nextScoreboardRefresh = 0
-- Returning nil lets Base's normal scoreboard create and manage g_Scoreboard.
hook.Add("ScoreboardShow", "CustomCoopProgressionScoreboardShow", function()
if IsValid(scoreboardFrame) then return true end
scoreboardFrame = vgui.Create("DFrame")
scoreboardFrame:SetSize(math.min(ScrW() - 60, 760), math.min(ScrH() - 80, 600))
scoreboardFrame:Center()
scoreboardFrame:SetTitle("CO-OP LIFETIME SCORES")
scoreboardFrame:ShowCloseButton(false)
scoreboardFrame:SetDraggable(false)
scoreboardFrame:SetKeyboardInputEnabled(false)
scoreboardFrame:SetMouseInputEnabled(false)
scoreboardFrame.Paint = function(_, width, height)
draw.RoundedBox(8, 0, 0, width, height, colors.background)
end
local list = vgui.Create("DListView", scoreboardFrame)
list:Dock(FILL)
list:DockMargin(10, 8, 10, 10)
list:AddColumn("Player")
list:AddColumn("Rank")
list:AddColumn("Lifetime Score")
list:AddColumn("Ping")
RefreshScoreboard(list)
scoreboardFrame.NextRefresh = RealTime() + 0.5
scoreboardFrame.Think = function(self)
if RealTime() < self.NextRefresh then return end
self.NextRefresh = RealTime() + 0.5
RefreshScoreboard(list)
end
return true
scoreboardRequested = true
scoreboardShownAt = RealTime()
timer.Simple(0, ExtendOfficialScoreboard)
end)
hook.Add("ScoreboardHide", "CustomCoopProgressionScoreboardHide", function()
if IsValid(scoreboardFrame) then scoreboardFrame:Remove() end
scoreboardFrame = nil
return true
scoreboardRequested = false
if IsValid(g_Scoreboard) then g_Scoreboard:Hide() end
end)
hook.Add("Think", "CustomCoopProgressionExtendOfficialScoreboard", function()
if not scoreboardRequested or not IsValid(g_Scoreboard) then return end
-- Recover if the key-release event is lost while focus or UI state changes.
if RealTime() > scoreboardShownAt + 0.15 and not input.IsKeyDown(KEY_TAB) then
scoreboardRequested = false
g_Scoreboard:Hide()
return
end
if RealTime() < nextScoreboardRefresh then return end
nextScoreboardRefresh = RealTime() + 0.25
ExtendOfficialScoreboard()
end)
+8
View File
@@ -177,6 +177,14 @@ Progression.Behaviours = {
name = "Hoverpack",
price = 500,
description = "Hold Space after jumping to hover and rise slowly. Toggle this behaviour on or off."
},
{
id = "godmode_90",
name = "God Mode — 90 Seconds",
price = 1000,
duration = 90,
consumable = true,
description = "Activate 90 seconds of complete damage immunity. Each use is purchased separately."
}
}
+72
View File
@@ -657,6 +657,65 @@ local function PlayMusic(ply, item, successMessage)
SendNotice(ply, true, successMessage or ("Playing " .. item.name .. playbackTarget))
end
local function ActivateConsumableBehaviour(ply, item)
if item.id ~= "godmode_90" then return false end
local now = CurTime()
local activeUntil = ply.CustomCoopGodmodeUntil or 0
if activeUntil > now then
SendNotice(ply, false, string.format(
"God Mode is already active for another %d seconds. No credits were spent.",
math.ceil(activeUntil - now)
))
SendShop(ply)
return true
end
local state = GetState(ply)
if not state then return true end
local price = GetShopPrice("behaviour", item)
local adminCreditBypass = state.credits < price
and Progression.AdminShopTestingBypass
and ply:IsAdmin()
if state.credits < price and not adminCreditBypass then
SendNotice(ply, false, "You need " .. (price - state.credits) .. " more credits.")
return true
end
if adminCreditBypass then
SendNotice(ply, false, string.format(
"You need %d more credits. Admin testing override: no credits will be charged.",
price - state.credits
))
else
state.credits = state.credits - price
state.profileDirty = true
end
local duration = math.max(math.floor(tonumber(item.duration) or 90), 1)
local expiresAt = now + duration
ply.CustomCoopGodmodeUntil = expiresAt
ply:SetNW2Float("CustomCoopGodmodeUntil", expiresAt)
SyncPlayer(ply, state)
local message = adminCreditBypass
and string.format("Admin test-activated God Mode for %d seconds.", duration)
or string.format("Activated God Mode for %d seconds.", duration)
SendNotice(ply, true, message)
SendShop(ply)
timer.Simple(duration, function()
if not IsValid(ply) or ply.CustomCoopGodmodeUntil ~= expiresAt then return end
ply.CustomCoopGodmodeUntil = nil
ply:SetNW2Float("CustomCoopGodmodeUntil", 0)
SendNotice(ply, false, "God Mode expired.")
end)
return true
end
local function PurchaseOrEquip(ply, itemType, itemID)
local item = GetCatalogueItem(itemType, itemID)
if not item then return end
@@ -671,6 +730,11 @@ local function PurchaseOrEquip(ply, itemType, itemID)
return
end
if itemType == "behaviour" and item.consumable then
ActivateConsumableBehaviour(ply, item)
return
end
local state = GetState(ply)
if not state then return end
@@ -887,6 +951,14 @@ hook.Add("OnNPCKilled", "CustomCoopProgressionMonsterKilled", function(npc, atta
AwardMonsterKill(ply)
end)
hook.Add("EntityTakeDamage", "CustomCoopProgressionTimedGodmode", function(target, damageInfo)
if not IsValid(target) or not target:IsPlayer() then return end
if (target.CustomCoopGodmodeUntil or 0) <= CurTime() then return end
damageInfo:SetDamage(0)
return true
end)
hook.Add("PlayerDeath", "CustomCoopProgressionDeathPenalty", function(victim)
if not IsValid(victim) or victim:IsBot() then return end
Progression.SetHoverpackThrusting(victim, false)