From 566d566c64161bf534299611e357a9cdd8eaac64 Mon Sep 17 00:00:00 2001 From: 0xFEEDC0DE64 Date: Sun, 6 Sep 2026 22:03:33 +0200 Subject: [PATCH] Use official garrysmod dashboard which closes more reliable --- README.md | 5 + gamemode/cl_progression.lua | 201 +++++++++++++++++++++++--------- gamemode/progression_shared.lua | 8 ++ gamemode/sv_progression.lua | 72 ++++++++++++ 4 files changed, 228 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index f6c0721..3e6abfb 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/gamemode/cl_progression.lua b/gamemode/cl_progression.lua index 5a5cb56..14f700a 100644 --- a/gamemode/cl_progression.lua +++ b/gamemode/cl_progression.lua @@ -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) diff --git a/gamemode/progression_shared.lua b/gamemode/progression_shared.lua index cc9ee46..e49b047 100644 --- a/gamemode/progression_shared.lua +++ b/gamemode/progression_shared.lua @@ -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." } } diff --git a/gamemode/sv_progression.lua b/gamemode/sv_progression.lua index 5ff73f7..4a94da1 100644 --- a/gamemode/sv_progression.lua +++ b/gamemode/sv_progression.lua @@ -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)