Shop prices configurable and new music system

This commit is contained in:
2026-09-06 17:29:03 +02:00
parent 1a94238bcb
commit 4f1b6758ba
5 changed files with 469 additions and 23 deletions
+31 -3
View File
@@ -14,18 +14,46 @@ Player controls:
- `F3`, chat `!shop`, or client console `coop_shop`: open the rewards shop
- Chat `!points`: print personal Score, Credits, and rank
- Chat `!musicmute` or client console `coop_music_mute`: toggle purchased server music locally
- Client console `coop_music_volume 0.0` through `coop_music_volume 1.0`: set music volume
- Hold `TAB`: compare all connected players' Lifetime Scores and ranks
The shop contains persistent Half-Life 2 player models and permanent bonus
weapon unlocks. Only one bonus weapon is equipped at a time. The server checks
all kills, balances, prices, ownership, and equipment; clients cannot award
points or choose a price.
weapon unlocks. Only one bonus weapon is equipped at a time. It also contains a
Music category. Buying a song unlocks it permanently; selecting Play broadcasts
it to all connected players. One song can play at a time, with a short cooldown
afterward. Each player can mute or adjust the music locally.
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.
Administrators get an `Admin Prices` tab in the F3 shop. Select an item, enter a
non-negative Credit price, and save it. The server permission-checks and persists
the change in SQLite, then broadcasts the new catalogue to all connected clients.
The equivalent server/admin console command is:
`coop_setprice <model|weapon|music> <item_id> <credits>`
For example: `coop_setprice music song_id 500`. The maximum accepted price is
100,000,000 Credits. Existing owners retain their unlocks when a price changes.
Configuration is in `gamemode/progression_shared.lua`:
- `Progression.Ranks`: rank names and Lifetime Score thresholds
- `Progression.Models`: player-model catalogue and prices
- `Progression.Weapons`: bonus-weapon catalogue and prices
- `Progression.Music`: music catalogue, default prices, paths, and durations
- `Progression.MonsterClasses`: NPC classes that award points
Catalogue prices are initial defaults. Once seeded, live prices come from the
`custom_coop_shop_prices` SQLite table so in-game edits survive crashes and
restarts.
Music files are not bundled with this repository. Every server owner must supply
the audio files referenced by their `Progression.Music` entries, place them
under `garrysmod/sound/`, and register their virtual paths with
`resource.AddFile` so connecting clients receive them. Keep locally supplied
audio outside the gamemode source tree and distribute or back it up separately.
Player-spawned NPCs and unknown/friendly NPC classes do not award points.
+199 -8
View File
@@ -9,8 +9,17 @@ local shopState = {
equippedModel = Progression.DefaultModelID,
equippedWeapon = Progression.DefaultWeaponID,
ownedModels = {},
ownedWeapons = {}
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",
@@ -76,6 +85,12 @@ local function SelectItem(itemType, 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)
@@ -87,8 +102,14 @@ end
local function AddItemCard(layout, itemType, item)
local isModel = itemType == "model"
local owned = isModel and shopState.ownedModels[item.id] or shopState.ownedWeapons[item.id]
local equipped = isModel and shopState.equippedModel == item.id or shopState.equippedWeapon == item.id
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)
@@ -102,7 +123,7 @@ local function AddItemCard(layout, itemType, item)
end
local previewModel = item.model
if not isModel and item.weaponClass ~= "" then
if isWeapon and item.weaponClass ~= "" then
local storedWeapon = weapons.GetStored(item.weaponClass)
previewModel = storedWeapon and storedWeapon.WorldModel
end
@@ -126,7 +147,8 @@ local function AddItemCard(layout, itemType, item)
if IsValid(entity) then entity:SetAngles(Angle(0, RealTime() * 18 % 360, 0)) end
end
else
local noPreview = AddText(card, itemType == "weapon" and "MAP LOADOUT" or "NO PREVIEW", "CustomCoopProgressionHeading", colors.muted)
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)
@@ -140,8 +162,8 @@ local function AddItemCard(layout, itemType, item)
description:SetPos(12, 202)
description:SetSize(228, 38)
local priceText = item.price == 0 and "FREE" or (item.price .. " credits")
local price = AddText(card, priceText, "CustomCoopProgressionBody", item.price <= shopState.credits and colors.success or colors.warning)
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)
@@ -149,12 +171,24 @@ local function AddItemCard(layout, itemType, item)
button:SetPos(112, 244)
button:SetSize(128, 36)
button:SetFont("CustomCoopProgressionBody")
button:SetText(equipped and "Equipped" or (owned or item.price == 0) and "Equip" or "Buy")
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
@@ -181,6 +215,88 @@ local function CreateCataloguePage(sheet, title, itemType, items)
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
@@ -224,6 +340,18 @@ local function OpenShop()
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()
@@ -233,6 +361,7 @@ net.Receive("CustomCoopProgressionOpenShop", function()
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()
@@ -241,12 +370,24 @@ net.Receive("CustomCoopProgressionOpenShop", function()
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()
@@ -254,6 +395,56 @@ net.Receive("CustomCoopProgressionNotice", function()
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
+2 -1
View File
@@ -20,4 +20,5 @@ include( "shared.lua" )
include( "maprotation.lua" )
include( "sv_progression.lua" )
resource.AddFile( "sound/weapons/universal/pap.wav" )
resource.AddFile( "sound/weapons/universal/pap.wav" )
resource.AddFile( "sound/custom_coop_music/trololo.mp3" )
+17
View File
@@ -9,6 +9,8 @@ Progression.KillReward = 1
Progression.DeathCreditCost = 5
Progression.DefaultModelID = "kleiner"
Progression.DefaultWeaponID = "none"
Progression.MaximumShopPrice = 100000000
Progression.MusicCooldownPadding = 10
Progression.Ranks = {
{ score = 0, name = "Newbie" },
@@ -145,6 +147,17 @@ Progression.Weapons = {
}
}
Progression.Music = {
{
id = "trololo",
name = "Trolololo",
sound = "custom_coop_music/trololo.mp3",
price = 250,
duration = 162,
description = "Play the full song for everybody currently on the server. Unlocks permanently."
}
}
-- Only these combat NPC classes award points. Keeping an allow-list avoids
-- rewards for friendly citizens, scripted targets and manually invented entity
-- classes. Add a class here if a future coop map uses another real monster.
@@ -217,6 +230,10 @@ function Progression.GetWeapon(id)
return FindByID(Progression.Weapons, id)
end
function Progression.GetMusic(id)
return FindByID(Progression.Music, id)
end
function Progression.GetRank(score)
local result = Progression.Ranks[1].name
score = math.max(tonumber(score) or 0, 0)
+220 -11
View File
@@ -3,11 +3,17 @@ if not SERVER then return end
local Progression = CustomCoopProgression
local playerStates = setmetatable({}, { __mode = "k" })
local requestCooldowns = setmetatable({}, { __mode = "k" })
local shopPrices = {}
local musicBusyUntil = 0
util.AddNetworkString("CustomCoopProgressionOpenShop")
util.AddNetworkString("CustomCoopProgressionRequestShop")
util.AddNetworkString("CustomCoopProgressionSelectItem")
util.AddNetworkString("CustomCoopProgressionNotice")
util.AddNetworkString("CustomCoopProgressionSetPrice")
util.AddNetworkString("CustomCoopProgressionPricesChanged")
util.AddNetworkString("CustomCoopProgressionPlayMusic")
util.AddNetworkString("CustomCoopProgressionToggleMusicMute")
local function Query(query, context)
local result = sql.Query(query)
@@ -43,10 +49,84 @@ local function CreateTables()
PRIMARY KEY (steamid64, item_type, item_id)
)
]], "could not create ownership table")
Query([[
CREATE TABLE IF NOT EXISTS custom_coop_shop_prices (
item_type TEXT NOT NULL,
item_id TEXT NOT NULL,
price INTEGER NOT NULL,
PRIMARY KEY (item_type, item_id)
)
]], "could not create shop price table")
end
CreateTables()
local catalogues = {
{ itemType = "model", items = Progression.Models },
{ itemType = "weapon", items = Progression.Weapons },
{ itemType = "music", items = Progression.Music }
}
local function PriceKey(itemType, itemID)
return itemType .. ":" .. itemID
end
local function GetCatalogueItem(itemType, itemID)
if itemType == "model" then return Progression.GetModel(itemID) end
if itemType == "weapon" then return Progression.GetWeapon(itemID) end
if itemType == "music" then return Progression.GetMusic(itemID) end
end
local function GetShopPrice(itemType, item)
return shopPrices[PriceKey(itemType, item.id)] or math.max(math.floor(item.price or 0), 0)
end
local function LoadShopPrices()
for _, catalogue in ipairs(catalogues) do
for _, item in ipairs(catalogue.items) do
Query(string.format(
"INSERT OR IGNORE INTO custom_coop_shop_prices (item_type, item_id, price) VALUES (%s, %s, %d)",
SQLString(catalogue.itemType), SQLString(item.id), math.max(math.floor(item.price or 0), 0)
), "could not seed shop price")
end
end
local rows = Query("SELECT item_type, item_id, price FROM custom_coop_shop_prices", "could not load shop prices")
if not istable(rows) then return end
for _, row in ipairs(rows) do
local item = GetCatalogueItem(row.item_type, row.item_id)
local price = math.floor(tonumber(row.price) or -1)
if item and price >= 0 and price <= Progression.MaximumShopPrice then
shopPrices[PriceKey(row.item_type, row.item_id)] = price
end
end
end
LoadShopPrices()
local function WriteShopPrices()
local entries = {}
for _, catalogue in ipairs(catalogues) do
for _, item in ipairs(catalogue.items) do
entries[#entries + 1] = {
itemType = catalogue.itemType,
id = item.id,
price = GetShopPrice(catalogue.itemType, item)
}
end
end
net.WriteUInt(math.min(#entries, 255), 8)
for index = 1, math.min(#entries, 255) do
local entry = entries[index]
net.WriteString(entry.itemType)
net.WriteString(entry.id)
net.WriteUInt(math.min(entry.price, 4294967295), 32)
end
end
local function EmptyState()
return {
score = 0,
@@ -54,7 +134,8 @@ local function EmptyState()
equippedModel = Progression.DefaultModelID,
equippedWeapon = Progression.DefaultWeaponID,
ownedModels = { [Progression.DefaultModelID] = true },
ownedWeapons = { [Progression.DefaultWeaponID] = true }
ownedWeapons = { [Progression.DefaultWeaponID] = true },
ownedMusic = {}
}
end
@@ -109,6 +190,8 @@ local function LoadPlayer(ply)
state.ownedModels[owned.item_id] = true
elseif owned.item_type == "weapon" and Progression.GetWeapon(owned.item_id) then
state.ownedWeapons[owned.item_id] = true
elseif owned.item_type == "music" and Progression.GetMusic(owned.item_id) then
state.ownedMusic[owned.item_id] = true
end
end
end
@@ -186,6 +269,9 @@ local function SendShop(ply)
for id in pairs(state.ownedWeapons) do
owned[#owned + 1] = { itemType = "weapon", id = id }
end
for id in pairs(state.ownedMusic) do
owned[#owned + 1] = { itemType = "music", id = id }
end
net.Start("CustomCoopProgressionOpenShop")
net.WriteUInt(math.min(state.score, 4294967295), 32)
@@ -197,6 +283,8 @@ local function SendShop(ply)
net.WriteString(owned[index].itemType)
net.WriteString(owned[index].id)
end
net.WriteBool(ply:IsAdmin())
WriteShopPrices()
net.Send(ply)
end
@@ -214,10 +302,70 @@ local function RequestAllowed(ply)
return true
end
local function PurchaseOrEquip(ply, itemType, itemID)
if itemType ~= "model" and itemType ~= "weapon" then return end
local function BroadcastShopPrices()
net.Start("CustomCoopProgressionPricesChanged")
WriteShopPrices()
net.Broadcast()
end
local item = itemType == "model" and Progression.GetModel(itemID) or Progression.GetWeapon(itemID)
local function SetShopPrice(actor, itemType, itemID, requestedPrice)
local item = GetCatalogueItem(itemType, itemID)
local price = math.floor(tonumber(requestedPrice) or -1)
if not item or price < 0 or price > Progression.MaximumShopPrice then
if IsValid(actor) then SendNotice(actor, false, "Invalid shop item or price.") end
return false
end
local result = Query(string.format(
"INSERT OR REPLACE INTO custom_coop_shop_prices (item_type, item_id, price) VALUES (%s, %s, %d)",
SQLString(itemType), SQLString(itemID), price
), "could not save shop price")
if result == false then
if IsValid(actor) then SendNotice(actor, false, "The new price could not be saved.") end
return false
end
shopPrices[PriceKey(itemType, itemID)] = price
BroadcastShopPrices()
local message = string.format("Set %s to %d credits.", item.name, price)
if IsValid(actor) then
SendNotice(actor, true, message)
else
print("[custom_coop progression] " .. message)
end
return true
end
local function PlayMusic(ply, item)
local now = CurTime()
if musicBusyUntil > now then
SendNotice(ply, false, string.format(
"Music is busy. Try again in %d seconds.",
math.ceil(musicBusyUntil - now)
))
return
end
if not file.Exists("sound/" .. item.sound, "GAME") then
SendNotice(ply, false, "That music file is unavailable on the server.")
return
end
local duration = math.max(tonumber(item.duration) or 0, 1)
musicBusyUntil = now + duration + math.max(Progression.MusicCooldownPadding or 0, 0)
net.Start("CustomCoopProgressionPlayMusic")
net.WriteString(item.id)
net.WriteString(ply:Nick())
net.Broadcast()
PrintMessage(HUD_PRINTTALK, string.format("[CO-OP] %s is playing %s.", ply:Nick(), item.name))
SendNotice(ply, true, "Playing " .. item.name .. " for everyone.")
end
local function PurchaseOrEquip(ply, itemType, itemID)
local item = GetCatalogueItem(itemType, itemID)
if not item then return end
if itemType == "weapon" and item.weaponClass ~= "" and not weapons.GetStored(item.weaponClass) then
@@ -225,16 +373,30 @@ local function PurchaseOrEquip(ply, itemType, itemID)
return
end
if itemType == "music" and not file.Exists("sound/" .. item.sound, "GAME") then
SendNotice(ply, false, "That music file is unavailable. No credits were spent.")
return
end
local state = GetState(ply)
if not state then return end
local ownedItems = itemType == "model" and state.ownedModels or state.ownedWeapons
local alreadyOwned = ownedItems[itemID] == true or item.price == 0
local ownedItems = itemType == "model" and state.ownedModels
or itemType == "weapon" and state.ownedWeapons
or state.ownedMusic
local price = GetShopPrice(itemType, item)
local alreadyOwned = ownedItems[itemID] == true or price == 0
local previousWeaponID = state.equippedWeapon
if itemType == "music" and alreadyOwned then
PlayMusic(ply, item)
SendShop(ply)
return
end
if not alreadyOwned then
if state.credits < item.price then
SendNotice(ply, false, "You need " .. (item.price - state.credits) .. " more credits.")
if state.credits < price then
SendNotice(ply, false, "You need " .. (price - state.credits) .. " more credits.")
return
end
@@ -247,7 +409,7 @@ local function PurchaseOrEquip(ply, itemType, itemID)
local updateResult = insertResult ~= false and Query(string.format(
"UPDATE custom_coop_players SET credits = credits - %d WHERE steamid64 = %s AND credits >= %d",
item.price, SQLString(ply:SteamID64()), item.price
price, SQLString(ply:SteamID64()), price
), "could not debit purchase")
if insertResult == false or updateResult == false then
@@ -257,10 +419,17 @@ local function PurchaseOrEquip(ply, itemType, itemID)
end
sql.Commit()
state.credits = state.credits - item.price
state.credits = state.credits - price
ownedItems[itemID] = true
end
if itemType == "music" then
SyncPlayer(ply, state)
SendNotice(ply, true, "Purchased " .. item.name .. ". Select it again to play it for everyone.")
SendShop(ply)
return
end
local column = itemType == "model" and "equipped_model" or "equipped_weapon"
local equipResult = Query(string.format(
"UPDATE custom_coop_players SET %s = %s WHERE steamid64 = %s",
@@ -357,7 +526,8 @@ hook.Add("PlayerInitialSpawn", "CustomCoopProgressionLoad", function(ply)
timer.Simple(4, function()
if not IsValid(ply) then return end
ply:ChatPrint("[CO-OP] Kill monsters to earn Score and Credits. Your lifetime Score determines your rank.")
ply:ChatPrint("[CO-OP] Press F3 or type !shop to spend Credits on permanent models and bonus weapons.")
ply:ChatPrint("[CO-OP] Press F3 or type !shop to buy permanent models, bonus weapons, and server music.")
ply:ChatPrint("[CO-OP] Bought music plays for everyone. Type !musicmute if you do not want to hear it.")
ply:ChatPrint("[CO-OP] Dying costs up to " .. Progression.DeathCreditCost .. " Credits, but never reduces Score or rank.")
ply:ChatPrint("[CO-OP] Type !points for your totals. Hold TAB to compare lifetime scores.")
end)
@@ -429,6 +599,12 @@ hook.Add("PlayerSay", "CustomCoopProgressionChatCommands", function(ply, text)
end
return ""
end
if command == "!musicmute" or command == "/musicmute" then
net.Start("CustomCoopProgressionToggleMusicMute")
net.Send(ply)
return ""
end
end)
net.Receive("CustomCoopProgressionRequestShop", function(_, ply)
@@ -445,3 +621,36 @@ net.Receive("CustomCoopProgressionSelectItem", function(_, ply)
PurchaseOrEquip(ply, itemType, itemID)
end)
net.Receive("CustomCoopProgressionSetPrice", function(_, ply)
if not RequestAllowed(ply) then return end
if not ply:IsAdmin() then
SendNotice(ply, false, "Only server administrators can change shop prices.")
return
end
local itemType = net.ReadString()
local itemID = net.ReadString()
local price = net.ReadUInt(32)
if #itemType > 16 or #itemID > 64 then return end
SetShopPrice(ply, itemType, itemID, price)
end)
concommand.Add("coop_setprice", function(ply, _, args)
if IsValid(ply) and not ply:IsAdmin() then
SendNotice(ply, false, "Only server administrators can change shop prices.")
return
end
local itemType = string.lower(args[1] or "")
local itemID = string.lower(args[2] or "")
local price = tonumber(args[3])
if not GetCatalogueItem(itemType, itemID) or not price then
local usage = "Usage: coop_setprice <model|weapon|music> <item_id> <credits>"
if IsValid(ply) then ply:ChatPrint("[CO-OP] " .. usage) else print("[CO-OP] " .. usage) end
return
end
SetShopPrice(ply, itemType, itemID, price)
end)