539 lines
17 KiB
Lua
539 lines
17 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 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 buyerName = net.ReadString()
|
|
local item = Progression.GetMusic(itemID)
|
|
if not item then return end
|
|
|
|
if IsValid(activeMusicChannel) then
|
|
activeMusicChannel:Stop()
|
|
activeMusicChannel = nil
|
|
end
|
|
|
|
sound.PlayFile("sound/" .. item.sound, "noplay", 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
|
|
channel:SetVolume(math.Clamp(musicVolume:GetFloat(), 0, 1))
|
|
channel:Play()
|
|
notification.AddLegacy(buyerName .. " is playing " .. item.name .. ".", NOTIFY_GENERIC, 5)
|
|
end)
|
|
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)
|