Use official garrysmod <tab> dashboard which closes more reliable
This commit is contained in:
@@ -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
@@ -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)
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user