Files
garrysmod-coop/gamemode/cl_progression.lua
T

577 lines
19 KiB
Lua

if not CLIENT then return end
local Progression = CustomCoopProgression
local shopFrame
local scoreboardFrame
local shopState = {
score = 0,
credits = 0,
equippedModel = Progression.DefaultModelID,
equippedWeapon = Progression.DefaultWeaponID,
ownedModels = {},
ownedWeapons = {},
ownedMusic = {},
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
})
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
concommand.Add("coop_shop", RequestShop)
hook.Add("ShowSpare1", "CustomCoopProgressionF3", function()
RequestShop()
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()
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 owned = isModel and shopState.ownedModels[item.id]
or isWeapon and shopState.ownedWeapons[item.id]
or isMusic and shopState.ownedMusic[item.id]
local equipped = isModel and shopState.equippedModel == item.id
or isWeapon and shopState.equippedWeapon == 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 equipped 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)
preview:SetFOV(isModel and 30 or 38)
if isModel 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 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"
else
actionText = equipped and "Equipped" or (owned or currentPrice == 0) and "Equip" or "Buy"
end
button:SetText(actionText)
button:SetEnabled(not equipped)
button.DoClick = function()
button:SetEnabled(false)
button:SetText("Please wait...")
SelectItem(itemType, item.id)
timer.Simple(1, function()
if IsValid(button) then
button:SetEnabled(not equipped)
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 }
}) 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.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",
shopState.score,
shopState.credits,
Progression.GetRank(shopState.score)
),
"CustomCoopProgressionBody",
colors.accent
)
summary:SetPos(19, 52)
summary:SetSize(shopFrame:GetWide() - 38, 25)
local help = AddText(
shopFrame,
"Kills add to both totals. Purchases and deaths spend Credits only; Lifetime Score and rank never decrease.",
"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)
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.equippedModel = net.ReadString()
shopState.equippedWeapon = net.ReadString()
shopState.ownedModels = { [Progression.DefaultModelID] = true }
shopState.ownedWeapons = { [Progression.DefaultWeaponID] = true }
shopState.ownedMusic = {}
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
end
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 rank = ply:GetNW2String("CustomCoopRank", Progression.GetRank(score))
local width, height = 288, 66
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("Score " .. score, "CustomCoopProgressionBody", x + 12, y + 37, colors.text)
draw.SimpleText("Credits " .. credits, "CustomCoopProgressionBody", x + 151, y + 37, colors.text)
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 RefreshScoreboard(list)
if not IsValid(list) then return end
list:Clear()
local players = player.GetAll()
table.sort(players, function(left, right)
return left:GetNW2Int("CustomCoopScore", 0) > right:GetNW2Int("CustomCoopScore", 0)
end)
for _, ply in ipairs(players) do
list:AddLine(
ply:Nick(),
ply:GetNW2String("CustomCoopRank", "Newbie"),
ply:GetNW2Int("CustomCoopScore", 0),
ply:Ping()
)
end
end
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
end)
hook.Add("ScoreboardHide", "CustomCoopProgressionScoreboardHide", function()
if IsValid(scoreboardFrame) then scoreboardFrame:Remove() end
scoreboardFrame = nil
return true
end)