772 lines
27 KiB
Lua
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)
|