Shop prices configurable and new music system
This commit is contained in:
@@ -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
@@ -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
@@ -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" )
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user