diff --git a/README.md b/README.md index decaeb4..f6c0721 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Player controls: - 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 +- With Hoverpack enabled, jump normally and continue holding `Space` to hover - Hold `TAB`: compare all connected players' Lifetime Scores and ranks The shop contains persistent Half-Life 2 player models and permanent bonus @@ -29,6 +30,17 @@ Music entries may set `spatial = true` plus `minimumDistance` and and fades over distance. Supply spatial audio as 44.1 kHz mono MP3 or WAV for reliable in-game positioning. +The Behaviours category contains persistent, toggleable player abilities. The +Hoverpack costs 500 Credits by default and is enabled immediately after buying +it. A short Space press remains a normal jump; holding Space for 0.3 seconds in +the air maintains altitude and rises slowly. Releasing Space stops thrust and +restores normal gravity. Pressing and holding Space again can restart thrust +while still airborne. Thrust consumes the standard AUX power meter and stops at +zero. After a 1.25-second pause with Space released, AUX recharges while waiting +and recharges faster while walking; active sprinting does not receive the extra +hoverpack recharge. Its built-in looping thruster sound follows the player and +is audible to nearby players. + 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. @@ -42,7 +54,7 @@ their current non-negative value. Set `Progression.AdminShopTestingBypass` to `false` to disable this behavior. The equivalent server/admin console command is: -`coop_setprice ` +`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. @@ -53,6 +65,9 @@ Configuration is in `gamemode/progression_shared.lua`: - `Progression.Models`: player-model catalogue and prices - `Progression.Weapons`: bonus-weapon catalogue and prices - `Progression.Music`: music catalogue, default prices, paths, and durations +- `Progression.Behaviours`: toggleable ability catalogue and default prices +- `Progression.Hoverpack*`: hover delay, rise speed, AUX drain/recharge rates, + and thruster sound settings - `Progression.MonsterClasses`: NPC classes that award points Catalogue prices are initial defaults. Once seeded, live prices come from the diff --git a/gamemode/cl_progression.lua b/gamemode/cl_progression.lua index 6e8e66a..5a5cb56 100644 --- a/gamemode/cl_progression.lua +++ b/gamemode/cl_progression.lua @@ -11,6 +11,8 @@ local shopState = { ownedModels = {}, ownedWeapons = {}, ownedMusic = {}, + ownedBehaviours = {}, + enabledBehaviours = {}, prices = {}, isAdmin = false } @@ -106,11 +108,14 @@ local function AddItemCard(layout, itemType, item) local isModel = itemType == "model" local isWeapon = itemType == "weapon" local isMusic = itemType == "music" + local isBehaviour = itemType == "behaviour" 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 isBehaviour and shopState.ownedBehaviours[item.id] + local selected = isModel and shopState.equippedModel == item.id or isWeapon and shopState.equippedWeapon == item.id + or isBehaviour and shopState.enabledBehaviours[item.id] local currentPrice = CurrentPrice(itemType, item) local card = layout:Add("DPanel") @@ -118,7 +123,7 @@ local function AddItemCard(layout, itemType, item) card.Paint = function(self, width, height) local color = self:IsHovered() and colors.panelHover or colors.panel draw.RoundedBox(8, 0, 0, width, height, color) - if equipped then + if selected then surface.SetDrawColor(colors.success) surface.DrawOutlinedRect(0, 0, width, height, 2) end @@ -149,7 +154,10 @@ local function AddItemCard(layout, itemType, item) if IsValid(entity) then entity:SetAngles(Angle(0, RealTime() * 18 % 360, 0)) end end else - local previewText = isMusic and "SERVER MUSIC" or isWeapon and "MAP LOADOUT" or "NO PREVIEW" + local previewText = isMusic and "SERVER MUSIC" + or isBehaviour and "PLAYER ABILITY" + or isWeapon and "MAP LOADOUT" + or "NO PREVIEW" local noPreview = AddText(card, previewText, "CustomCoopProgressionHeading", colors.muted) noPreview:SetContentAlignment(5) noPreview:SetPos(8, 8) @@ -176,18 +184,20 @@ local function AddItemCard(layout, itemType, item) local actionText if isMusic then actionText = (owned or currentPrice == 0) and "Play" or "Buy" + elseif isBehaviour then + actionText = owned and (selected and "Disable" or "Enable") or "Buy & Enable" else - actionText = equipped and "Equipped" or (owned or currentPrice == 0) and "Equip" or "Buy" + actionText = selected and "Equipped" or (owned or currentPrice == 0) and "Equip" or "Buy" end button:SetText(actionText) - button:SetEnabled(not equipped) + 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(not equipped) + button:SetEnabled(isBehaviour or not selected) button:SetText(actionText) end end) @@ -273,7 +283,8 @@ local function CreateAdminPage(sheet) for _, catalogue in ipairs({ { itemType = "model", items = Progression.Models }, { itemType = "weapon", items = Progression.Weapons }, - { itemType = "music", items = Progression.Music } + { itemType = "music", items = Progression.Music }, + { itemType = "behaviour", items = Progression.Behaviours } }) do for _, item in ipairs(catalogue.items) do local row = list:AddLine(catalogue.itemType, item.id, item.name, CurrentPrice(catalogue.itemType, item)) @@ -343,6 +354,7 @@ local function OpenShop() 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) if shopState.isAdmin then CreateAdminPage(sheet) end end @@ -364,6 +376,8 @@ net.Receive("CustomCoopProgressionOpenShop", function() shopState.ownedModels = { [Progression.DefaultModelID] = true } shopState.ownedWeapons = { [Progression.DefaultWeaponID] = true } shopState.ownedMusic = {} + shopState.ownedBehaviours = {} + shopState.enabledBehaviours = {} for _ = 1, net.ReadUInt(8) do local itemType = net.ReadString() @@ -374,9 +388,14 @@ net.Receive("CustomCoopProgressionOpenShop", function() shopState.ownedWeapons[itemID] = true elseif itemType == "music" then shopState.ownedMusic[itemID] = true + elseif itemType == "behaviour" then + shopState.ownedBehaviours[itemID] = true end end + for _ = 1, net.ReadUInt(8) do + shopState.enabledBehaviours[net.ReadString()] = true + end shopState.isAdmin = net.ReadBool() shopState.prices = ReadShopPrices() @@ -503,14 +522,27 @@ hook.Add("HUDPaint", "CustomCoopProgressionHUD", function() local credits = ply:GetNW2Int("CustomCoopCredits", 0) local rank = ply:GetNW2String("CustomCoopRank", Progression.GetRank(score)) - local width, height = 288, 66 + local hoverpackEnabled = ply:GetNW2Bool("CustomCoopHoverpackEnabled", false) + local hoverpackThrusting = ply:GetNW2Bool("CustomCoopHoverpackThrusting", false) + local width, height = 288, hoverpackEnabled and 90 or 66 local x, y = ScrW() - width - 18, 18 draw.RoundedBox(8, x, y, width, height, colors.background) draw.SimpleText(rank, "CustomCoopProgressionHeading", x + 12, y + 9, colors.accent) + draw.SimpleText("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) + 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, y + 64, + hoverpackThrusting and colors.success or colors.muted) + end + if CurTime() < gainExpires then draw.SimpleText("+" .. gainedScore, "CustomCoopProgressionHeading", x - 10, y + height / 2, colors.success, TEXT_ALIGN_RIGHT, TEXT_ALIGN_CENTER) end diff --git a/gamemode/progression_shared.lua b/gamemode/progression_shared.lua index 54ee1bc..cc9ee46 100644 --- a/gamemode/progression_shared.lua +++ b/gamemode/progression_shared.lua @@ -12,6 +12,15 @@ Progression.DefaultWeaponID = "none" Progression.MaximumShopPrice = 100000000 Progression.MusicCooldownPadding = 10 Progression.AdminShopTestingBypass = true +Progression.HoverpackHoldDelay = 0.3 +Progression.HoverpackRiseSpeed = 24 +Progression.HoverpackDrainPerSecond = 16 +Progression.HoverpackRechargeDelay = 1.25 +Progression.HoverpackRechargePerSecond = 14 +Progression.HoverpackWalkingRechargePerSecond = 22 +Progression.HoverpackSound = "PhysicsCannister.ThrusterLoop" +Progression.HoverpackSoundVolume = 0.55 +Progression.HoverpackSoundPitch = 115 Progression.Ranks = { { score = 0, name = "Newbie" }, @@ -162,6 +171,15 @@ Progression.Music = { } } +Progression.Behaviours = { + { + id = "hoverpack", + name = "Hoverpack", + price = 500, + description = "Hold Space after jumping to hover and rise slowly. Toggle this behaviour on or off." + } +} + -- 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. @@ -238,6 +256,10 @@ function Progression.GetMusic(id) return FindByID(Progression.Music, id) end +function Progression.GetBehaviour(id) + return FindByID(Progression.Behaviours, id) +end + function Progression.GetRank(score) local result = Progression.Ranks[1].name score = math.max(tonumber(score) or 0, 0) @@ -249,3 +271,51 @@ function Progression.GetRank(score) return result end + +local function SetHoverpackThrusting(ply, thrusting) + if SERVER and Progression.SetHoverpackThrusting then + Progression.SetHoverpackThrusting(ply, thrusting) + end +end + +hook.Add("SetupMove", "CustomCoopProgressionHoverpack", function(ply, moveData) + local jumpHeld = moveData:KeyDown(IN_JUMP) + local suitPower = ply.GetSuitPower and ply:GetSuitPower() or 100 + local canHover = ply:GetNW2Bool("CustomCoopHoverpackEnabled", false) + and ply:Alive() + and ply:GetMoveType() == MOVETYPE_WALK + and ply:WaterLevel() < 2 + and not ply:InVehicle() + and suitPower > 0.01 + + if not jumpHeld or not canHover then + ply.CustomCoopHoverHoldStarted = nil + ply.CustomCoopHoverJumpWasHeld = false + SetHoverpackThrusting(ply, false) + return + end + + if not ply.CustomCoopHoverJumpWasHeld then + ply.CustomCoopHoverHoldStarted = CurTime() + end + ply.CustomCoopHoverJumpWasHeld = true + + -- Continually reset while grounded. A normal jump therefore happens first, + -- and hovering only starts after Space remains held in the air. + if ply:OnGround() then + ply.CustomCoopHoverHoldStarted = CurTime() + SetHoverpackThrusting(ply, false) + return + end + + ply.CustomCoopHoverHoldStarted = ply.CustomCoopHoverHoldStarted or CurTime() + if CurTime() - ply.CustomCoopHoverHoldStarted < Progression.HoverpackHoldDelay then + SetHoverpackThrusting(ply, false) + return + end + + local velocity = moveData:GetVelocity() + velocity.z = Progression.HoverpackRiseSpeed + moveData:SetVelocity(velocity) + SetHoverpackThrusting(ply, true) +end) diff --git a/gamemode/sv_progression.lua b/gamemode/sv_progression.lua index 08cf7bd..5ff73f7 100644 --- a/gamemode/sv_progression.lua +++ b/gamemode/sv_progression.lua @@ -78,6 +78,14 @@ local function CreateTables() PRIMARY KEY (item_type, item_id) ) ]], "could not create shop price table") + + Query([[ + CREATE TABLE IF NOT EXISTS custom_coop_enabled_behaviours ( + steamid64 TEXT NOT NULL, + item_id TEXT NOT NULL, + PRIMARY KEY (steamid64, item_id) + ) + ]], "could not create enabled behaviour table") end CreateTables() @@ -85,7 +93,8 @@ CreateTables() local catalogues = { { itemType = "model", items = Progression.Models }, { itemType = "weapon", items = Progression.Weapons }, - { itemType = "music", items = Progression.Music } + { itemType = "music", items = Progression.Music }, + { itemType = "behaviour", items = Progression.Behaviours } } for _, item in ipairs(Progression.Music) do @@ -112,6 +121,7 @@ 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 + if itemType == "behaviour" then return Progression.GetBehaviour(itemID) end end local function GetShopPrice(itemType, item) @@ -172,6 +182,9 @@ local function EmptyState() ownedModels = { [Progression.DefaultModelID] = true }, ownedWeapons = { [Progression.DefaultWeaponID] = true }, ownedMusic = {}, + ownedBehaviours = {}, + enabledBehaviours = {}, + behavioursDirty = false, pendingOwnedItems = {}, profileDirty = false } @@ -183,6 +196,11 @@ local function SyncPlayer(ply, state) ply:SetNW2Int("CustomCoopScore", state.score) ply:SetNW2Int("CustomCoopCredits", state.credits) ply:SetNW2String("CustomCoopRank", Progression.GetRank(state.score)) + ply:SetNW2Bool("CustomCoopHoverpackEnabled", state.enabledBehaviours.hoverpack == true) + if state.enabledBehaviours.hoverpack and ply.EquipSuit + and (not ply.IsSuitEquipped or not ply:IsSuitEquipped()) then + ply:EquipSuit() + end end local function LoadPlayer(ply) @@ -232,6 +250,20 @@ local function LoadPlayer(ply) state.ownedWeapons[owned.item_id] = true elseif owned.item_type == "music" and Progression.GetMusic(owned.item_id) then state.ownedMusic[owned.item_id] = true + elseif owned.item_type == "behaviour" and Progression.GetBehaviour(owned.item_id) then + state.ownedBehaviours[owned.item_id] = true + end + end + end + + local enabledRows = Query( + "SELECT item_id FROM custom_coop_enabled_behaviours WHERE steamid64 = " .. SQLString(steamID64), + "could not load enabled behaviours" + ) + if istable(enabledRows) then + for _, enabled in ipairs(enabledRows) do + if state.ownedBehaviours[enabled.item_id] and Progression.GetBehaviour(enabled.item_id) then + state.enabledBehaviours[enabled.item_id] = true end end end @@ -262,7 +294,7 @@ local function SavePlayerState(ply, state) if not state then return true end local hasPendingOwnership = next(state.pendingOwnedItems) ~= nil - if not state.profileDirty and not hasPendingOwnership then return true end + if not state.profileDirty and not hasPendingOwnership and not state.behavioursDirty then return true end local steamID64 = state.steamID64 if not steamID64 or steamID64 == "" then return false end @@ -301,6 +333,23 @@ local function SavePlayerState(ply, state) end end + if result ~= false and state.behavioursDirty then + result = Query( + "DELETE FROM custom_coop_enabled_behaviours WHERE steamid64 = " .. SQLString(steamID64), + "could not clear enabled behaviours" + ) + + if result ~= false then + for itemID in pairs(state.enabledBehaviours) do + result = Query(string.format( + "INSERT INTO custom_coop_enabled_behaviours (steamid64, item_id) VALUES (%s, %s)", + SQLString(steamID64), SQLString(itemID) + ), "could not save enabled behaviour") + if result == false then break end + end + end + end + if result == false then sql.Query("ROLLBACK") return false @@ -317,6 +366,7 @@ local function SavePlayerState(ply, state) end state.profileDirty = false + state.behavioursDirty = false table.Empty(state.pendingOwnedItems) return true end @@ -427,6 +477,70 @@ local function ApplyWeapon(ply, previousWeaponID) ply.CustomCoopGrantedWeapon = item.weaponClass end +function Progression.SetHoverpackThrusting(ply, thrusting) + if not IsValid(ply) then return end + + thrusting = thrusting == true + and ply:GetNW2Bool("CustomCoopHoverpackEnabled", false) + and ply:Alive() + and (not ply.GetSuitPower or ply:GetSuitPower() > 0.01) + + if ply:GetNW2Bool("CustomCoopHoverpackThrusting", false) == thrusting then return end + ply:SetNW2Bool("CustomCoopHoverpackThrusting", thrusting) + + if ply.CustomCoopHoverpackSound then + ply.CustomCoopHoverpackSound:Stop() + ply.CustomCoopHoverpackSound = nil + end + + if thrusting then + ply.CustomCoopHoverpackLastThrust = CurTime() + local soundPatch = CreateSound(ply, Progression.HoverpackSound) + if soundPatch then + ply.CustomCoopHoverpackSound = soundPatch + soundPatch:PlayEx(Progression.HoverpackSoundVolume, Progression.HoverpackSoundPitch) + end + end +end + +hook.Add("Think", "CustomCoopProgressionHoverpackPower", function() + local now = CurTime() + + for _, ply in ipairs(player.GetAll()) do + local previousUpdate = ply.CustomCoopHoverpackPowerUpdated or now + local elapsed = math.Clamp(now - previousUpdate, 0, 0.25) + ply.CustomCoopHoverpackPowerUpdated = now + + if elapsed > 0 and ply:GetNW2Bool("CustomCoopHoverpackEnabled", false) + and ply.GetSuitPower and ply.SetSuitPower then + local power = math.Clamp(ply:GetSuitPower(), 0, 100) + local thrusting = ply:GetNW2Bool("CustomCoopHoverpackThrusting", false) + local newPower = power + + if thrusting then + ply.CustomCoopHoverpackLastThrust = now + newPower = math.max(power - Progression.HoverpackDrainPerSecond * elapsed, 0) + if newPower <= 0 then Progression.SetHoverpackThrusting(ply, false) end + else + local lastThrust = ply.CustomCoopHoverpackLastThrust or 0 + local sprinting = ply:KeyDown(IN_SPEED) and ply:GetVelocity():Length2D() > ply:GetWalkSpeed() + local releasedJump = not ply:KeyDown(IN_JUMP) + + if releasedJump and not sprinting + and now - lastThrust >= Progression.HoverpackRechargeDelay then + local walking = ply:OnGround() and ply:GetVelocity():Length2D() > 20 + local rechargeRate = walking + and Progression.HoverpackWalkingRechargePerSecond + or Progression.HoverpackRechargePerSecond + newPower = math.min(power + rechargeRate * elapsed, 100) + end + end + + if math.abs(newPower - power) >= 0.001 then ply:SetSuitPower(newPower) end + end + end +end) + local function SendShop(ply) local state = GetState(ply) if not state then return end @@ -441,6 +555,9 @@ local function SendShop(ply) for id in pairs(state.ownedMusic) do owned[#owned + 1] = { itemType = "music", id = id } end + for id in pairs(state.ownedBehaviours) do + owned[#owned + 1] = { itemType = "behaviour", id = id } + end net.Start("CustomCoopProgressionOpenShop") net.WriteUInt(math.min(state.score, 4294967295), 32) @@ -452,6 +569,14 @@ local function SendShop(ply) net.WriteString(owned[index].itemType) net.WriteString(owned[index].id) end + local enabledBehaviours = {} + for id in pairs(state.enabledBehaviours) do + enabledBehaviours[#enabledBehaviours + 1] = id + end + net.WriteUInt(math.min(#enabledBehaviours, 255), 8) + for index = 1, math.min(#enabledBehaviours, 255) do + net.WriteString(enabledBehaviours[index]) + end net.WriteBool(ply:IsAdmin()) WriteShopPrices() net.Send(ply) @@ -551,7 +676,8 @@ local function PurchaseOrEquip(ply, itemType, itemID) local ownedItems = itemType == "model" and state.ownedModels or itemType == "weapon" and state.ownedWeapons - or state.ownedMusic + or itemType == "music" and state.ownedMusic + or state.ownedBehaviours local price = GetShopPrice(itemType, item) local alreadyOwned = ownedItems[itemID] == true local previousWeaponID = state.equippedWeapon @@ -563,6 +689,17 @@ local function PurchaseOrEquip(ply, itemType, itemID) return end + if itemType == "behaviour" and alreadyOwned then + local enabled = state.enabledBehaviours[itemID] ~= true + state.enabledBehaviours[itemID] = enabled or nil + state.behavioursDirty = true + SyncPlayer(ply, state) + if not enabled then Progression.SetHoverpackThrusting(ply, false) end + SendNotice(ply, true, item.name .. (enabled and " enabled." or " disabled.")) + SendShop(ply) + return + end + if not alreadyOwned then if state.credits < price then adminCreditBypass = Progression.AdminShopTestingBypass and ply:IsAdmin() @@ -598,6 +735,17 @@ local function PurchaseOrEquip(ply, itemType, itemID) return end + + if itemType == "behaviour" then + state.enabledBehaviours[itemID] = true + state.behavioursDirty = true + SyncPlayer(ply, state) + local action = adminCreditBypass and "Admin test-unlocked and enabled " or "Purchased and enabled " + SendNotice(ply, true, action .. item.name .. ".") + SendShop(ply) + return + end + if itemType == "model" then state.equippedModel = itemID ApplyModel(ply) @@ -674,7 +822,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 buy permanent models, bonus weapons, and server music.") + ply:ChatPrint("[CO-OP] Press F3 or type !shop to buy models, weapons, music, and toggleable behaviours.") + ply:ChatPrint("[CO-OP] Enable the Hoverpack in Behaviours, then hold Space after jumping to hover.") 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.") @@ -682,8 +831,9 @@ hook.Add("PlayerInitialSpawn", "CustomCoopProgressionLoad", function(ply) end) hook.Add("PlayerDisconnected", "CustomCoopProgressionForget", function(ply) + Progression.SetHoverpackThrusting(ply, false) local state = playerStates[ply] - if state and (state.profileDirty or next(state.pendingOwnedItems) ~= nil) then + if state and (state.profileDirty or state.behavioursDirty or next(state.pendingOwnedItems) ~= nil) then disconnectedStates[state.steamID64] = state end @@ -705,6 +855,7 @@ hook.Add("PlayerSetModel", "CustomCoopProgressionApplyModel", function(ply) end) hook.Add("PlayerSpawn", "CustomCoopProgressionApplyLoadout", function(ply) + Progression.SetHoverpackThrusting(ply, false) ply.CustomCoopGrantedWeapon = nil timer.Simple(0.5, function() @@ -738,6 +889,7 @@ end) hook.Add("PlayerDeath", "CustomCoopProgressionDeathPenalty", function(victim) if not IsValid(victim) or victim:IsBot() then return end + Progression.SetHoverpackThrusting(victim, false) ChargeForDeath(victim) end) @@ -806,7 +958,7 @@ concommand.Add("coop_setprice", function(ply, _, args) 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 " + local usage = "Usage: coop_setprice " if IsValid(ply) then ply:ChatPrint("[CO-OP] " .. usage) else print("[CO-OP] " .. usage) end return end