Files

772 lines
27 KiB
Lua

if not CLIENT then return end
local Progression = CustomCoopProgression
local shopFrame
local shopState = {
score = 0,
credits = 0,
playtime = 0,
equippedModel = Progression.DefaultModelID,
equippedWeapon = Progression.DefaultWeaponID,
equippedTrail = Progression.DefaultTrailID,
ownedModels = {},
ownedWeapons = {},
ownedTrails = {},
ownedMusic = {},
ownedBehaviours = {},
enabledBehaviours = {},
prices = {},
isAdmin = false
}
local activeMusicChannel
local activeMusicSource
local activeMusicIsSpatial = false
local previousMusicVolume = 0.7
local musicVolume = CreateClientConVar(
"coop_music_volume", "0.7", true, false,
"Volume for music bought in the custom co-op shop (0 to 1).", 0, 1
)
surface.CreateFont("CustomCoopProgressionTitle", {
font = "Roboto",
size = 28,
weight = 700
})
surface.CreateFont("CustomCoopProgressionHeading", {
font = "Roboto",
size = 20,
weight = 700
})
surface.CreateFont("CustomCoopProgressionBody", {
font = "Roboto",
size = 17,
weight = 500
})
surface.CreateFont("CustomCoopProgressionSmall", {
font = "Roboto",
size = 15,
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),
panelHover = Color(39, 47, 56, 250),
accent = Color(75, 176, 255),
success = Color(92, 205, 122),
warning = Color(244, 190, 74),
text = Color(240, 244, 248),
muted = Color(170, 181, 191)
}
local function RequestShop()
net.Start("CustomCoopProgressionRequestShop")
net.SendToServer()
end
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()
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.
hook.Add("PlayerButtonDown", "CustomCoopProgressionF3Fallback", function(ply, button)
if ply ~= LocalPlayer() or button ~= KEY_F3 then return end
ToggleShop()
end)
local escapeWasDown = false
local f3WasDown = false
hook.Add("Think", "CustomCoopProgressionShopEscape", function()
local escapeDown = input.IsKeyDown(KEY_ESCAPE)
local f3Down = input.IsKeyDown(KEY_F3)
if f3Down and not f3WasDown and IsValid(shopFrame) then
ToggleShop()
end
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
f3WasDown = f3Down
end)
local function SelectItem(itemType, itemID)
net.Start("CustomCoopProgressionSelectItem")
net.WriteString(itemType)
net.WriteString(itemID)
net.SendToServer()
end
local function CurrentPrice(itemType, item)
local price = shopState.prices[itemType .. ":" .. item.id]
if price == nil then price = item.price or 0 end
return math.max(math.floor(tonumber(price) or 0), 0)
end
local function AddText(parent, text, font, color)
local label = vgui.Create("DLabel", parent)
label:SetText(text)
label:SetFont(font)
label:SetTextColor(color)
label:SetWrap(true)
return label
end
local function AddItemCard(layout, itemType, item)
local isModel = itemType == "model"
local isWeapon = itemType == "weapon"
local isMusic = itemType == "music"
local isBehaviour = itemType == "behaviour"
local isTrail = itemType == "trail"
local isEntity = itemType == "entity"
local isHelper = itemType == "helper"
local isSpawnPurchase = isEntity or isHelper
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]
or isBehaviour and shopState.ownedBehaviours[item.id]
or isTrail and shopState.ownedTrails[item.id]
local selected = isModel and shopState.equippedModel == item.id
or isWeapon and shopState.equippedWeapon == item.id
or isBehaviour and shopState.enabledBehaviours[item.id]
or isTrail and shopState.equippedTrail == item.id
local currentPrice = CurrentPrice(itemType, item)
local card = layout:Add("DPanel")
card:SetSize(252, 292)
card.Paint = function(self, width, height)
local color = self:IsHovered() and colors.panelHover or colors.panel
draw.RoundedBox(8, 0, 0, width, height, color)
if selected then
surface.SetDrawColor(colors.success)
surface.DrawOutlinedRect(0, 0, width, height, 2)
end
end
local previewModel = item.model
if isWeapon and item.weaponClass ~= "" then
local storedWeapon = weapons.GetStored(item.weaponClass)
previewModel = storedWeapon and storedWeapon.WorldModel
end
if previewModel and previewModel ~= "" then
local preview = vgui.Create("DModelPanel", card)
preview:SetPos(8, 8)
preview:SetSize(236, 166)
preview:SetModel(previewModel)
local humanoidPreview = isModel or (isHelper and item.id ~= "rebel_turret")
preview:SetFOV(humanoidPreview and 30 or 38)
if humanoidPreview then
preview:SetCamPos(Vector(68, 0, 64))
preview:SetLookAt(Vector(0, 0, 64))
else
preview:SetCamPos(Vector(45, 45, 28))
preview:SetLookAt(Vector(0, 0, 0))
end
preview.LayoutEntity = function(_, entity)
if IsValid(entity) then entity:SetAngles(Angle(0, RealTime() * 18 % 360, 0)) end
end
else
local previewText = isMusic and "SERVER MUSIC"
or isBehaviour and "PLAYER ABILITY"
or isTrail and "PLAYER TRAIL"
or isEntity and "DEPLOYABLE ENTITY"
or isHelper and "FRIENDLY NPC"
or isWeapon and "MAP LOADOUT"
or "NO PREVIEW"
local noPreview = AddText(card, previewText, "CustomCoopProgressionHeading", colors.muted)
noPreview:SetContentAlignment(5)
noPreview:SetPos(8, 8)
noPreview:SetSize(236, 166)
end
local title = AddText(card, item.name, "CustomCoopProgressionHeading", colors.text)
title:SetPos(12, 177)
title:SetSize(228, 25)
local description = AddText(card, item.description, "CustomCoopProgressionSmall", colors.muted)
description:SetPos(12, 202)
description:SetSize(228, 38)
local priceText = currentPrice == 0 and "FREE" or (currentPrice .. " credits")
local price = AddText(card, priceText, "CustomCoopProgressionBody", currentPrice <= shopState.credits and colors.success or colors.warning)
price:SetPos(12, 239)
price:SetSize(100, 22)
local button = vgui.Create("DButton", card)
button:SetPos(112, 244)
button:SetSize(128, 36)
button:SetFont("CustomCoopProgressionBody")
local actionText
if isMusic then
actionText = (owned or currentPrice == 0) and "Play" or "Buy"
elseif isSpawnPurchase then
actionText = "Buy & Spawn"
elseif isBehaviour then
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
button:SetText(actionText)
button:SetEnabled(isBehaviour or not selected)
button.DoClick = function()
button:SetEnabled(false)
button:SetText("Please wait...")
SelectItem(itemType, item.id)
timer.Simple(1, function()
if IsValid(button) then
button:SetEnabled(isBehaviour or not selected)
button:SetText(actionText)
end
end)
end
end
local function CreateCataloguePage(sheet, title, itemType, items)
local page = vgui.Create("DPanel", sheet)
page:DockPadding(10, 10, 10, 10)
page.Paint = function(_, width, height)
draw.RoundedBox(6, 0, 0, width, height, Color(24, 29, 35))
end
local scroll = vgui.Create("DScrollPanel", page)
scroll:Dock(FILL)
local layout = vgui.Create("DIconLayout", scroll)
layout:Dock(TOP)
layout:SetSpaceX(12)
layout:SetSpaceY(12)
layout:SetTall(math.ceil(#items / 3) * 304)
for _, item in ipairs(items) do
AddItemCard(layout, itemType, item)
end
sheet:AddSheet(title, page)
end
local function SendPriceChange(itemType, itemID, price)
local cleanPrice = math.Clamp(math.floor(tonumber(price) or 0), 0, Progression.MaximumShopPrice)
net.Start("CustomCoopProgressionSetPrice")
net.WriteString(itemType)
net.WriteString(itemID)
net.WriteUInt(cleanPrice, 32)
net.SendToServer()
end
local function CreateAdminPage(sheet)
local page = vgui.Create("DPanel", sheet)
page:DockPadding(10, 10, 10, 10)
page.Paint = function(_, width, height)
draw.RoundedBox(6, 0, 0, width, height, Color(24, 29, 35))
end
local controls = vgui.Create("DPanel", page)
controls:Dock(BOTTOM)
controls:SetTall(70)
controls:DockMargin(0, 8, 0, 0)
controls.Paint = nil
local instruction = AddText(
controls,
"Select an item, enter its new Credit price, then save. All connected clients refresh immediately.",
"CustomCoopProgressionSmall",
colors.muted
)
instruction:Dock(TOP)
instruction:SetTall(24)
local priceInput = vgui.Create("DNumberWang", controls)
priceInput:Dock(LEFT)
priceInput:SetWide(180)
priceInput:SetMinMax(0, Progression.MaximumShopPrice)
priceInput:SetDecimals(0)
priceInput:SetValue(0)
local save = vgui.Create("DButton", controls)
save:Dock(LEFT)
save:DockMargin(8, 0, 0, 0)
save:SetWide(180)
save:SetText("Save selected price")
save:SetFont("CustomCoopProgressionBody")
save:SetEnabled(false)
local list = vgui.Create("DListView", page)
list:Dock(FILL)
list:AddColumn("Type"):SetFixedWidth(85)
list:AddColumn("Item ID"):SetFixedWidth(190)
list:AddColumn("Item")
list:AddColumn("Credits"):SetFixedWidth(100)
for _, catalogue in ipairs({
{ itemType = "model", items = Progression.Models },
{ itemType = "weapon", items = Progression.Weapons },
{ itemType = "music", items = Progression.Music },
{ itemType = "behaviour", items = Progression.Behaviours },
{ itemType = "trail", items = Progression.Trails },
{ itemType = "entity", items = Progression.Entities },
{ itemType = "helper", items = Progression.Helpers }
}) do
for _, item in ipairs(catalogue.items) do
local row = list:AddLine(catalogue.itemType, item.id, item.name, CurrentPrice(catalogue.itemType, item))
row.CustomCoopItemType = catalogue.itemType
row.CustomCoopItemID = item.id
end
end
local selectedRow
list.OnRowSelected = function(_, _, row)
selectedRow = row
priceInput:SetValue(tonumber(row:GetColumnText(4)) or 0)
save:SetEnabled(true)
end
save.DoClick = function()
if not IsValid(selectedRow) then return end
SendPriceChange(selectedRow.CustomCoopItemType, selectedRow.CustomCoopItemID, priceInput:GetValue())
save:SetEnabled(false)
save:SetText("Saving...")
end
sheet:AddSheet("Admin Prices", page)
end
local function OpenShop()
if IsValid(shopFrame) then shopFrame:Remove() end
shopFrame = vgui.Create("DFrame")
shopFrame:SetSize(math.min(ScrW() - 40, 850), math.min(ScrH() - 40, 650))
shopFrame:Center()
shopFrame:SetTitle("")
shopFrame:ShowCloseButton(true)
shopFrame:MakePopup()
shopFrame.OnKeyCodePressed = function(self, keyCode)
if keyCode == KEY_F3 then
ToggleShop()
elseif keyCode == KEY_ESCAPE then
self:Close()
gui.HideGameUI()
end
end
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)
end
local summary = AddText(
shopFrame,
string.format(
"Lifetime Score: %d Credits: %d Rank: %s Played: %s",
shopState.score,
shopState.credits,
Progression.GetRank(shopState.score),
Progression.FormatPlaytime(shopState.playtime)
),
"CustomCoopProgressionBody",
colors.accent
)
summary:SetPos(19, 52)
summary:SetSize(shopFrame:GetWide() - 38, 25)
summary.Think = function(self)
local ply = LocalPlayer()
if not IsValid(ply) then return end
self:SetText(string.format(
"Lifetime Score: %d Credits: %d Rank: %s Played: %s",
ply:GetNW2Int("CustomCoopScore", shopState.score),
ply:GetNW2Int("CustomCoopCredits", shopState.credits),
ply:GetNW2String("CustomCoopRank", Progression.GetRank(shopState.score)),
Progression.FormatPlaytime(ply:GetNW2Int("CustomCoopPlaytime", shopState.playtime))
))
end
local help = AddText(
shopFrame,
string.format(
"Harder enemies pay more. Playing %s earns %d Credits. Purchases and deaths spend Credits only.",
Progression.FormatPlaytime(Progression.PlaytimeRewardInterval),
Progression.PlaytimeCreditsPerInterval
),
"CustomCoopProgressionSmall",
colors.muted
)
help:SetPos(19, 77)
help:SetSize(shopFrame:GetWide() - 38, 20)
local sheet = vgui.Create("DPropertySheet", shopFrame)
sheet:SetPos(14, 104)
sheet:SetSize(shopFrame:GetWide() - 28, shopFrame:GetTall() - 118)
CreateCataloguePage(sheet, "Player Models", "model", Progression.Models)
CreateCataloguePage(sheet, "Bonus Weapons", "weapon", Progression.Weapons)
CreateCataloguePage(sheet, "Music", "music", Progression.Music)
CreateCataloguePage(sheet, "Behaviours", "behaviour", Progression.Behaviours)
CreateCataloguePage(sheet, "Player Trails", "trail", Progression.Trails)
CreateCataloguePage(sheet, "Entities", "entity", Progression.Entities)
CreateCataloguePage(sheet, "Helping NPCs", "helper", Progression.Helpers)
if shopState.isAdmin then CreateAdminPage(sheet) end
end
local function ReadShopPrices()
local prices = {}
for _ = 1, net.ReadUInt(8) do
local itemType = net.ReadString()
local itemID = net.ReadString()
prices[itemType .. ":" .. itemID] = net.ReadUInt(32)
end
return prices
end
net.Receive("CustomCoopProgressionOpenShop", function()
shopState.score = net.ReadUInt(32)
shopState.credits = net.ReadUInt(32)
shopState.playtime = net.ReadUInt(32)
shopState.equippedModel = net.ReadString()
shopState.equippedWeapon = net.ReadString()
shopState.equippedTrail = net.ReadString()
shopState.ownedModels = { [Progression.DefaultModelID] = true }
shopState.ownedWeapons = { [Progression.DefaultWeaponID] = true }
shopState.ownedTrails = { [Progression.DefaultTrailID] = true }
shopState.ownedMusic = {}
shopState.ownedBehaviours = {}
shopState.enabledBehaviours = {}
for _ = 1, net.ReadUInt(8) do
local itemType = net.ReadString()
local itemID = net.ReadString()
if itemType == "model" then
shopState.ownedModels[itemID] = true
elseif itemType == "weapon" then
shopState.ownedWeapons[itemID] = true
elseif itemType == "music" then
shopState.ownedMusic[itemID] = true
elseif itemType == "behaviour" then
shopState.ownedBehaviours[itemID] = true
elseif itemType == "trail" then
shopState.ownedTrails[itemID] = true
end
end
for _ = 1, net.ReadUInt(8) do
shopState.enabledBehaviours[net.ReadString()] = true
end
shopState.isAdmin = net.ReadBool()
shopState.prices = ReadShopPrices()
OpenShop()
end)
net.Receive("CustomCoopProgressionPricesChanged", function()
shopState.prices = ReadShopPrices()
if IsValid(shopFrame) then OpenShop() end
notification.AddLegacy("Shop prices were updated by an administrator.", NOTIFY_GENERIC, 4)
end)
net.Receive("CustomCoopProgressionNotice", function()
local success = net.ReadBool()
local message = net.ReadString()
notification.AddLegacy(message, success and NOTIFY_GENERIC or NOTIFY_ERROR, 4)
surface.PlaySound(success and "buttons/button15.wav" or "buttons/button10.wav")
end)
local function ToggleMusicMute()
local current = musicVolume:GetFloat()
if current > 0.001 then
previousMusicVolume = current
RunConsoleCommand("coop_music_volume", "0")
notification.AddLegacy("Shop music muted. Use !musicmute to turn it back on.", NOTIFY_GENERIC, 4)
else
RunConsoleCommand("coop_music_volume", tostring(math.max(previousMusicVolume, 0.1)))
notification.AddLegacy("Shop music enabled.", NOTIFY_GENERIC, 4)
end
end
concommand.Add("coop_music_mute", ToggleMusicMute)
cvars.AddChangeCallback("coop_music_volume", function(_, _, newValue)
if IsValid(activeMusicChannel) then
activeMusicChannel:SetVolume(math.Clamp(tonumber(newValue) or 0, 0, 1))
end
end, "CustomCoopProgressionMusicVolume")
net.Receive("CustomCoopProgressionToggleMusicMute", ToggleMusicMute)
net.Receive("CustomCoopProgressionPlayMusic", function()
local itemID = net.ReadString()
local sourcePlayer = net.ReadEntity()
local buyerName = net.ReadString()
local item = Progression.GetMusic(itemID)
if not item then return end
local soundPath = "sound/" .. item.sound
if not file.Exists(soundPath, "GAME") then
notification.AddLegacy(
"Music file missing on this client. Reconnect once to download server music.",
NOTIFY_ERROR,
8
)
return
end
if IsValid(activeMusicChannel) then
activeMusicChannel:Stop()
activeMusicChannel = nil
end
activeMusicSource = nil
activeMusicIsSpatial = item.spatial == true
local flags = activeMusicIsSpatial and "3d noplay" or "noplay"
sound.PlayFile(soundPath, flags, function(channel, errorID, errorName)
if not IsValid(channel) then
notification.AddLegacy(
"Could not play " .. item.name .. ": " .. tostring(errorName or errorID),
NOTIFY_ERROR,
6
)
return
end
activeMusicChannel = channel
activeMusicSource = sourcePlayer
local volume = math.Clamp(musicVolume:GetFloat(), 0, 1)
channel:SetVolume(volume)
if activeMusicIsSpatial and IsValid(activeMusicSource) then
local minimumDistance = math.max(tonumber(item.minimumDistance) or 120, 1)
local maximumDistance = math.max(tonumber(item.maximumDistance) or 2200, minimumDistance + 1)
channel:Set3DFadeDistance(minimumDistance, maximumDistance)
channel:SetPos(activeMusicSource:WorldSpaceCenter())
end
channel:Play()
if volume <= 0.001 then
notification.AddLegacy(
buyerName .. " is playing " .. item.name .. ", but your shop music is muted. Type !musicmute to hear it.",
NOTIFY_ERROR,
7
)
else
notification.AddLegacy(buyerName .. " is playing " .. item.name .. ".", NOTIFY_GENERIC, 5)
end
end)
end)
hook.Add("Think", "CustomCoopProgressionMoveMusicSource", function()
if not activeMusicIsSpatial or not IsValid(activeMusicChannel) then return end
if not IsValid(activeMusicSource) then return end
activeMusicChannel:SetPos(activeMusicSource:WorldSpaceCenter())
end)
local previousScore
local gainedScore = 0
local gainExpires = 0
hook.Add("HUDPaint", "CustomCoopProgressionHUD", function()
local ply = LocalPlayer()
if not IsValid(ply) then return end
local score = ply:GetNW2Int("CustomCoopScore", -1)
if score < 0 then return end
if previousScore and score > previousScore then
gainedScore = score - previousScore
gainExpires = CurTime() + 2
surface.PlaySound("buttons/button14.wav")
end
previousScore = score
local credits = ply:GetNW2Int("CustomCoopCredits", 0)
local playtime = ply:GetNW2Int("CustomCoopPlaytime", 0)
local rank = ply:GetNW2String("CustomCoopRank", Progression.GetRank(score))
local hoverpackEnabled = ply:GetNW2Bool("CustomCoopHoverpackEnabled", false)
local hoverpackThrusting = ply:GetNW2Bool("CustomCoopHoverpackThrusting", false)
local godmodeSeconds = math.max(math.ceil(ply:GetNW2Float("CustomCoopGodmodeUntil", 0) - CurTime()), 0)
local width = 288
local height = 88 + (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)
draw.SimpleText(rank, "CustomCoopProgressionHeading", x + 12, y + 9, colors.accent)
draw.SimpleText("Press F3 for Shop", "CustomCoopProgressionSmall", x + width - 12, y + 13,
colors.muted, TEXT_ALIGN_RIGHT)
draw.SimpleText("Score " .. score, "CustomCoopProgressionBody", x + 12, y + 37, colors.text)
draw.SimpleText("Credits " .. credits, "CustomCoopProgressionBody", x + 151, y + 37, colors.text)
draw.SimpleText("Played " .. Progression.FormatPlaytime(playtime), "CustomCoopProgressionSmall",
x + 12, y + 64, colors.muted)
local statusY = y + 86
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, 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
draw.SimpleText("+" .. gainedScore, "CustomCoopProgressionHeading", x - 10, y + height / 2, colors.success, TEXT_ALIGN_RIGHT, TEXT_ALIGN_CENTER)
end
end)
local function AddProgressionToOfficialRow(ply, row)
if row.CustomCoopProgressionVersion == 2 then return end
row.CustomCoopProgressionVersion = 2
row.CustomCoopProgressionAdded = true
if not IsValid(row.CustomCoopScore) then
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")
end
if not IsValid(row.CustomCoopRank) then
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")
end
row.CustomCoopPlaytime = row:Add("DLabel")
row.CustomCoopPlaytime:Dock(RIGHT)
row.CustomCoopPlaytime:SetWide(145)
row.CustomCoopPlaytime:SetFont("ScoreboardDefault")
row.CustomCoopPlaytime:SetTextColor(Color(75, 115, 155))
row.CustomCoopPlaytime:SetContentAlignment(5)
row.CustomCoopPlaytime:SetTooltip("Total Playtime")
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"))
self.CustomCoopPlaytime:SetText("Played " .. Progression.FormatPlaytime(
ply:GetNW2Int("CustomCoopPlaytime", 0)
))
-- 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 g_Scoreboard.CustomCoopProgressionVersion ~= 2 then
g_Scoreboard.CustomCoopProgressionVersion = 2
g_Scoreboard.CustomCoopProgressionAdded = true
g_Scoreboard.PerformLayout = function(self)
local width = math.min(ScrW() - 40, 1120)
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()
scoreboardRequested = true
scoreboardShownAt = RealTime()
timer.Simple(0, ExtendOfficialScoreboard)
end)
hook.Add("ScoreboardHide", "CustomCoopProgressionScoreboardHide", function()
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)