From 4f1b6758ba1baf036fe4a79419db94bd74e4b3ee Mon Sep 17 00:00:00 2001 From: 0xFEEDC0DE64 Date: Sun, 6 Sep 2026 17:29:03 +0200 Subject: [PATCH] Shop prices configurable and new music system --- README.md | 34 ++++- gamemode/cl_progression.lua | 207 ++++++++++++++++++++++++++-- gamemode/init.lua | 3 +- gamemode/progression_shared.lua | 17 +++ gamemode/sv_progression.lua | 231 ++++++++++++++++++++++++++++++-- 5 files changed, 469 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index d640882..f62bf52 100644 --- a/README.md +++ b/README.md @@ -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 ` + +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. diff --git a/gamemode/cl_progression.lua b/gamemode/cl_progression.lua index b76a0cb..b3c048e 100644 --- a/gamemode/cl_progression.lua +++ b/gamemode/cl_progression.lua @@ -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 diff --git a/gamemode/init.lua b/gamemode/init.lua index f2ad3e2..b376048 100644 --- a/gamemode/init.lua +++ b/gamemode/init.lua @@ -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" ) diff --git a/gamemode/progression_shared.lua b/gamemode/progression_shared.lua index f9cf6d1..9fb35f3 100644 --- a/gamemode/progression_shared.lua +++ b/gamemode/progression_shared.lua @@ -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) diff --git a/gamemode/sv_progression.lua b/gamemode/sv_progression.lua index 5cb8933..325981d 100644 --- a/gamemode/sv_progression.lua +++ b/gamemode/sv_progression.lua @@ -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 " + if IsValid(ply) then ply:ChatPrint("[CO-OP] " .. usage) else print("[CO-OP] " .. usage) end + return + end + + SetShopPrice(ply, itemType, itemID, price) +end)