diff --git a/README.md b/README.md index 450308a..b772fa9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,10 @@ -# feedc0de Coop Campaign +# feedc0de Coop Addon + +This repository is a complete Garry's Mod addon. It contains the +`feedc0de_coop` gamemode under `gamemodes/` and reusable Lua entities under +`lua/entities/`. Installing the repository as +`garrysmod/addons/feedc0de_coop` enables both the campaign gamemode and the +standalone friendly scanner. ## Persistent progression and rewards shop @@ -114,7 +120,8 @@ The equivalent server/admin console command is: 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`: +Configuration is in +`gamemodes/feedc0de_coop/gamemode/progression_shared.lua`: - `Progression.Ranks`: rank names and Lifetime Score thresholds - `Progression.Models`: player-model catalogue and prices @@ -144,6 +151,27 @@ back it up separately. Player-spawned NPCs and unknown/friendly NPC classes do not award points. +Each map also gets one yellow friendly scanner near the preferred player spawn. +It is an independently reusable scripted entity listed in the Q-menu under +`feedc0de Coop`, so admins can spawn it in Sandbox and other gamemodes too. It +uses normal VPhysics, patrols around its anchor, notices injured players farther +away, flies toward them, and heals them in range with a visible yellow +electrical beam. It cannot award Score or Credits. When destroyed it addresses +its killer with a random farewell and falls as a gravity-enabled physical wreck +for 25 seconds. The gamemode—not the reusable entity—creates a new living +scanner at the same spawn anchor after five seconds. The defaults can be +adjusted with +`gc_friendlyscanner_enabled`, `gc_friendlyscanner_healradius`, +`gc_friendlyscanner_searchradius`, `gc_friendlyscanner_healrate`, and +`gc_friendlyscanner_respawntime`. + +The gamemode establishes its map anchor only after the first player's position +has remained free of teleport-sized jumps for several samples. A downward +brush trace acts as the scanner's floor-distance sensor: every patrol target, +initial Q-menu spawn, and physgun release resolves to the local floor plus the +hover offset and is clamped below the ceiling. This lets the scanner glide up +and down stairs and return to flying height after being placed low or high. + Purchase, equip, behaviour, and price responses update the existing F3 controls in place. They do not recreate, close, move, or scroll the window, so there is no visible frame flash and the selected tab and scroll position stay untouched. diff --git a/gamemode/cl_init.lua b/gamemode/cl_init.lua deleted file mode 100644 index c4378d5..0000000 --- a/gamemode/cl_init.lua +++ /dev/null @@ -1,2 +0,0 @@ -include( "shared.lua" ) -include( "cl_progression.lua" ) diff --git a/backgrounds/abducted_1.jpg b/gamemodes/feedc0de_coop/backgrounds/abducted_1.jpg similarity index 100% rename from backgrounds/abducted_1.jpg rename to gamemodes/feedc0de_coop/backgrounds/abducted_1.jpg diff --git a/backgrounds/abducted_2.jpg b/gamemodes/feedc0de_coop/backgrounds/abducted_2.jpg similarity index 100% rename from backgrounds/abducted_2.jpg rename to gamemodes/feedc0de_coop/backgrounds/abducted_2.jpg diff --git a/backgrounds/abducted_3.jpg b/gamemodes/feedc0de_coop/backgrounds/abducted_3.jpg similarity index 100% rename from backgrounds/abducted_3.jpg rename to gamemodes/feedc0de_coop/backgrounds/abducted_3.jpg diff --git a/backgrounds/evac_1.jpg b/gamemodes/feedc0de_coop/backgrounds/evac_1.jpg similarity index 100% rename from backgrounds/evac_1.jpg rename to gamemodes/feedc0de_coop/backgrounds/evac_1.jpg diff --git a/backgrounds/evac_2.jpg b/gamemodes/feedc0de_coop/backgrounds/evac_2.jpg similarity index 100% rename from backgrounds/evac_2.jpg rename to gamemodes/feedc0de_coop/backgrounds/evac_2.jpg diff --git a/backgrounds/evac_3.jpg b/gamemodes/feedc0de_coop/backgrounds/evac_3.jpg similarity index 100% rename from backgrounds/evac_3.jpg rename to gamemodes/feedc0de_coop/backgrounds/evac_3.jpg diff --git a/backgrounds/evac_4.jpg b/gamemodes/feedc0de_coop/backgrounds/evac_4.jpg similarity index 100% rename from backgrounds/evac_4.jpg rename to gamemodes/feedc0de_coop/backgrounds/evac_4.jpg diff --git a/backgrounds/horror_raid.jpg b/gamemodes/feedc0de_coop/backgrounds/horror_raid.jpg similarity index 100% rename from backgrounds/horror_raid.jpg rename to gamemodes/feedc0de_coop/backgrounds/horror_raid.jpg diff --git a/backgrounds/scream_1.jpg b/gamemodes/feedc0de_coop/backgrounds/scream_1.jpg similarity index 100% rename from backgrounds/scream_1.jpg rename to gamemodes/feedc0de_coop/backgrounds/scream_1.jpg diff --git a/backgrounds/scream_2.jpg b/gamemodes/feedc0de_coop/backgrounds/scream_2.jpg similarity index 100% rename from backgrounds/scream_2.jpg rename to gamemodes/feedc0de_coop/backgrounds/scream_2.jpg diff --git a/backgrounds/scream_3.jpg b/gamemodes/feedc0de_coop/backgrounds/scream_3.jpg similarity index 100% rename from backgrounds/scream_3.jpg rename to gamemodes/feedc0de_coop/backgrounds/scream_3.jpg diff --git a/backgrounds/southtown.jpg b/gamemodes/feedc0de_coop/backgrounds/southtown.jpg similarity index 100% rename from backgrounds/southtown.jpg rename to gamemodes/feedc0de_coop/backgrounds/southtown.jpg diff --git a/entities/weapons/coop_ak47/shared.lua b/gamemodes/feedc0de_coop/entities/weapons/coop_ak47/shared.lua similarity index 100% rename from entities/weapons/coop_ak47/shared.lua rename to gamemodes/feedc0de_coop/entities/weapons/coop_ak47/shared.lua diff --git a/entities/weapons/coop_autoshotgun/shared.lua b/gamemodes/feedc0de_coop/entities/weapons/coop_autoshotgun/shared.lua similarity index 100% rename from entities/weapons/coop_autoshotgun/shared.lua rename to gamemodes/feedc0de_coop/entities/weapons/coop_autoshotgun/shared.lua diff --git a/entities/weapons/coop_deagle/shared.lua b/gamemodes/feedc0de_coop/entities/weapons/coop_deagle/shared.lua similarity index 100% rename from entities/weapons/coop_deagle/shared.lua rename to gamemodes/feedc0de_coop/entities/weapons/coop_deagle/shared.lua diff --git a/entities/weapons/coop_famas/shared.lua b/gamemodes/feedc0de_coop/entities/weapons/coop_famas/shared.lua similarity index 100% rename from entities/weapons/coop_famas/shared.lua rename to gamemodes/feedc0de_coop/entities/weapons/coop_famas/shared.lua diff --git a/entities/weapons/coop_fiveseven/shared.lua b/gamemodes/feedc0de_coop/entities/weapons/coop_fiveseven/shared.lua similarity index 100% rename from entities/weapons/coop_fiveseven/shared.lua rename to gamemodes/feedc0de_coop/entities/weapons/coop_fiveseven/shared.lua diff --git a/entities/weapons/coop_glock/shared.lua b/gamemodes/feedc0de_coop/entities/weapons/coop_glock/shared.lua similarity index 100% rename from entities/weapons/coop_glock/shared.lua rename to gamemodes/feedc0de_coop/entities/weapons/coop_glock/shared.lua diff --git a/entities/weapons/coop_m4/shared.lua b/gamemodes/feedc0de_coop/entities/weapons/coop_m4/shared.lua similarity index 100% rename from entities/weapons/coop_m4/shared.lua rename to gamemodes/feedc0de_coop/entities/weapons/coop_m4/shared.lua diff --git a/entities/weapons/coop_mac10/shared.lua b/gamemodes/feedc0de_coop/entities/weapons/coop_mac10/shared.lua similarity index 100% rename from entities/weapons/coop_mac10/shared.lua rename to gamemodes/feedc0de_coop/entities/weapons/coop_mac10/shared.lua diff --git a/entities/weapons/coop_mp5/shared.lua b/gamemodes/feedc0de_coop/entities/weapons/coop_mp5/shared.lua similarity index 100% rename from entities/weapons/coop_mp5/shared.lua rename to gamemodes/feedc0de_coop/entities/weapons/coop_mp5/shared.lua diff --git a/entities/weapons/coop_p228/shared.lua b/gamemodes/feedc0de_coop/entities/weapons/coop_p228/shared.lua similarity index 100% rename from entities/weapons/coop_p228/shared.lua rename to gamemodes/feedc0de_coop/entities/weapons/coop_p228/shared.lua diff --git a/entities/weapons/coop_para/shared.lua b/gamemodes/feedc0de_coop/entities/weapons/coop_para/shared.lua similarity index 100% rename from entities/weapons/coop_para/shared.lua rename to gamemodes/feedc0de_coop/entities/weapons/coop_para/shared.lua diff --git a/entities/weapons/coop_pumpshotgun/shared.lua b/gamemodes/feedc0de_coop/entities/weapons/coop_pumpshotgun/shared.lua similarity index 100% rename from entities/weapons/coop_pumpshotgun/shared.lua rename to gamemodes/feedc0de_coop/entities/weapons/coop_pumpshotgun/shared.lua diff --git a/entities/weapons/coop_shotgun_base/shared.lua b/gamemodes/feedc0de_coop/entities/weapons/coop_shotgun_base/shared.lua similarity index 100% rename from entities/weapons/coop_shotgun_base/shared.lua rename to gamemodes/feedc0de_coop/entities/weapons/coop_shotgun_base/shared.lua diff --git a/entities/weapons/coop_usp/shared.lua b/gamemodes/feedc0de_coop/entities/weapons/coop_usp/shared.lua similarity index 100% rename from entities/weapons/coop_usp/shared.lua rename to gamemodes/feedc0de_coop/entities/weapons/coop_usp/shared.lua diff --git a/entities/weapons/coop_weapon_base/shared.lua b/gamemodes/feedc0de_coop/entities/weapons/coop_weapon_base/shared.lua similarity index 100% rename from entities/weapons/coop_weapon_base/shared.lua rename to gamemodes/feedc0de_coop/entities/weapons/coop_weapon_base/shared.lua diff --git a/entities/weapons/coop_zoom_base/shared.lua b/gamemodes/feedc0de_coop/entities/weapons/coop_zoom_base/shared.lua similarity index 100% rename from entities/weapons/coop_zoom_base/shared.lua rename to gamemodes/feedc0de_coop/entities/weapons/coop_zoom_base/shared.lua diff --git a/entities/weapons/npc_knife/ai_translations.lua b/gamemodes/feedc0de_coop/entities/weapons/npc_knife/ai_translations.lua similarity index 100% rename from entities/weapons/npc_knife/ai_translations.lua rename to gamemodes/feedc0de_coop/entities/weapons/npc_knife/ai_translations.lua diff --git a/entities/weapons/npc_knife/cl_init.lua b/gamemodes/feedc0de_coop/entities/weapons/npc_knife/cl_init.lua similarity index 100% rename from entities/weapons/npc_knife/cl_init.lua rename to gamemodes/feedc0de_coop/entities/weapons/npc_knife/cl_init.lua diff --git a/entities/weapons/npc_knife/init.lua b/gamemodes/feedc0de_coop/entities/weapons/npc_knife/init.lua similarity index 100% rename from entities/weapons/npc_knife/init.lua rename to gamemodes/feedc0de_coop/entities/weapons/npc_knife/init.lua diff --git a/entities/weapons/npc_knife/shared.lua b/gamemodes/feedc0de_coop/entities/weapons/npc_knife/shared.lua similarity index 100% rename from entities/weapons/npc_knife/shared.lua rename to gamemodes/feedc0de_coop/entities/weapons/npc_knife/shared.lua diff --git a/feedc0de_coop.txt b/gamemodes/feedc0de_coop/feedc0de_coop.txt similarity index 100% rename from feedc0de_coop.txt rename to gamemodes/feedc0de_coop/feedc0de_coop.txt diff --git a/gamemode/allplayersdied.lua b/gamemodes/feedc0de_coop/gamemode/allplayersdied.lua similarity index 100% rename from gamemode/allplayersdied.lua rename to gamemodes/feedc0de_coop/gamemode/allplayersdied.lua diff --git a/gamemode/anticheat.lua b/gamemodes/feedc0de_coop/gamemode/anticheat.lua similarity index 100% rename from gamemode/anticheat.lua rename to gamemodes/feedc0de_coop/gamemode/anticheat.lua diff --git a/gamemode/bringplayers.lua b/gamemodes/feedc0de_coop/gamemode/bringplayers.lua similarity index 100% rename from gamemode/bringplayers.lua rename to gamemodes/feedc0de_coop/gamemode/bringplayers.lua diff --git a/gamemode/chargersystem.lua b/gamemodes/feedc0de_coop/gamemode/chargersystem.lua similarity index 100% rename from gamemode/chargersystem.lua rename to gamemodes/feedc0de_coop/gamemode/chargersystem.lua diff --git a/gamemodes/feedc0de_coop/gamemode/cl_init.lua b/gamemodes/feedc0de_coop/gamemode/cl_init.lua new file mode 100644 index 0000000..97291e0 --- /dev/null +++ b/gamemodes/feedc0de_coop/gamemode/cl_init.lua @@ -0,0 +1,6 @@ +include( "shared.lua" ) +include( "cl_progression.lua" ) + +-- Clear render hooks left by the scanner's former gamemode-local implementation. +hook.Remove("Think", "CustomCoopFriendlyScannerRenderInterpolation") +hook.Remove("PostDrawTranslucentRenderables", "CustomCoopFriendlyScannerEffects") diff --git a/gamemode/cl_progression.lua b/gamemodes/feedc0de_coop/gamemode/cl_progression.lua similarity index 100% rename from gamemode/cl_progression.lua rename to gamemodes/feedc0de_coop/gamemode/cl_progression.lua diff --git a/gamemode/deathspectate.lua b/gamemodes/feedc0de_coop/gamemode/deathspectate.lua similarity index 100% rename from gamemode/deathspectate.lua rename to gamemodes/feedc0de_coop/gamemode/deathspectate.lua diff --git a/gamemode/dynamicmovement.lua b/gamemodes/feedc0de_coop/gamemode/dynamicmovement.lua similarity index 100% rename from gamemode/dynamicmovement.lua rename to gamemodes/feedc0de_coop/gamemode/dynamicmovement.lua diff --git a/gamemodes/feedc0de_coop/gamemode/friendly_scanner_spawner.lua b/gamemodes/feedc0de_coop/gamemode/friendly_scanner_spawner.lua new file mode 100644 index 0000000..ffbb8a3 --- /dev/null +++ b/gamemodes/feedc0de_coop/gamemode/friendly_scanner_spawner.lua @@ -0,0 +1,282 @@ +if not SERVER then return end + +local scannerClass = "feedc0de_friendly_scanner" +local managedTag = "CustomCoopGamemodeManaged" +local reanchorTimer = "Feedc0deCoopFriendlyScannerInitialAnchor" +local respawnTimer = "Feedc0deCoopFriendlyScannerRespawn" +local managedScanner +local anchorFinalized = false +local hoverHeight = 97 + +local preferredSpawnClasses = { + info_player_deathmatch = 1, + info_player_coop = 1, + info_coop_spawn = 1, + gmod_player_start = 1, + info_player_rebel = 2, + info_player_start = 3, + info_player_combine = 4 +} + +for _, className in ipairs(scripted_ents.GetMember("gmod_player_start", "SpawnPointClasses") or {}) do + preferredSpawnClasses[className] = preferredSpawnClasses[className] or 2 +end + +local function FindManagedScanner() + if IsValid(managedScanner) then return managedScanner end + for _, candidate in ipairs(ents.FindByClass(scannerClass)) do + if candidate:GetNW2Bool(managedTag, false) then + managedScanner = candidate + return candidate + end + end +end + +local function FindClearPosition(base, ignoredEntity) + local ignored = {} + if IsValid(managedScanner) then ignored[#ignored + 1] = managedScanner end + if IsValid(ignoredEntity) then ignored[#ignored + 1] = ignoredEntity end + local offsets = { + Vector(36, 0, 0), + Vector(-36, 0, 0), + Vector(0, 36, 0), + Vector(0, -36, 0), + Vector(56, 0, 0), + Vector(-56, 0, 0), + Vector(0, 56, 0), + Vector(0, -56, 0), + Vector(0, 0, 0), + Vector(88, 88, 0), + Vector(-88, -88, 0), + Vector(88, -88, 0), + Vector(-88, 88, 0) + } + for _, offset in ipairs(offsets) do + local horizontal = base + offset + local floorTrace = util.TraceLine({ + start = Vector(horizontal.x, horizontal.y, base.z + 32), + endpos = Vector(horizontal.x, horizontal.y, base.z - 80), + mask = MASK_SOLID_BRUSHONLY + }) + if floorTrace.Hit and not floorTrace.StartSolid and floorTrace.HitNormal.z > 0.45 then + local floorZ = floorTrace.HitPos.z + local ceilingTrace = util.TraceLine({ + start = Vector(horizontal.x, horizontal.y, floorZ + 20), + endpos = Vector(horizontal.x, horizontal.y, floorZ + hoverHeight + 64), + mask = MASK_SOLID_BRUSHONLY + }) + local height = hoverHeight + if ceilingTrace.Hit then height = math.min(height, ceilingTrace.HitPos.z - floorZ - 24) end + if height >= 30 then + local position = Vector(horizontal.x, horizontal.y, floorZ + height) + local clearance = util.TraceHull({ + start = position, + endpos = position, + mins = Vector(-34, -34, -22), + maxs = Vector(34, 34, 22), + filter = ignored, + mask = MASK_SOLID + }) + if not clearance.StartSolid and util.IsInWorld(position) then return position end + end + end + end + return nil +end + +local function FindMapSpawnPosition() + local bestPriority + local candidates = {} + for _, candidate in ipairs(ents.GetAll()) do + local priority = preferredSpawnClasses[candidate:GetClass()] + if priority and candidate:IsInWorld() then + if candidate:HasSpawnFlags(1) then priority = 0 end + if not bestPriority or priority < bestPriority then + bestPriority = priority + candidates = { candidate } + elseif priority == bestPriority then + candidates[#candidates + 1] = candidate + end + end + end + local base = #candidates > 0 and candidates[math.random(#candidates)]:GetPos() or vector_origin + return FindClearPosition(base) +end + +local function SpawnManagedScanner(position) + local enabled = GetConVar("gc_friendlyscanner_enabled") + if enabled and not enabled:GetBool() then return end + local existing = FindManagedScanner() + if IsValid(existing) then return existing end + + local scanner = ents.Create(scannerClass) + if not IsValid(scanner) then + ErrorNoHalt("[feedc0de_coop] The friendly scanner addon entity is unavailable\n") + return + end + position = position or FindMapSpawnPosition() + if not isvector(position) then + ErrorNoHalt("[feedc0de_coop] No in-world clearance for the friendly scanner near a player spawn\n") + scanner:Remove() + return + end + scanner:SetPos(position) + scanner:SetAngles(Angle(0, math.random(0, 359), 0)) + scanner:Spawn() + scanner:Activate() + scanner:SetPatrolAnchor(position, true) + scanner:SetNW2Bool(managedTag, true) + managedScanner = scanner + return scanner +end + +local function PlayerSpawnPosition(ply) + if IsValid(ply) then + local spawnpoint = ply:GetVar("LastSpawnpoint") + if IsValid(spawnpoint) then + local position = FindClearPosition(spawnpoint:GetPos()) + if isvector(position) then return position end + end + + -- Teleport-based coop maps can keep their nominal Source spawn in + -- empty void. Retry against the player's actual post-teleport origin. + local position = FindClearPosition(ply:GetPos(), ply) + if isvector(position) then return position end + end + return FindMapSpawnPosition() +end + +local function BeginPlayerSpawnTracking(ply) + if anchorFinalized or timer.Exists(reanchorTimer) or not IsValid(ply) then return end + + local previousPosition + local stableSamples = 0 + timer.Create(reanchorTimer, 0.15, 30, function() + if anchorFinalized then + timer.Remove(reanchorTimer) + return + end + if not IsValid(ply) or not ply:Alive() then + previousPosition = nil + stableSamples = 0 + return + end + + local currentPosition = ply:GetPos() + local candidate = FindClearPosition(currentPosition, ply) + local ordinaryMovement = isvector(previousPosition) + and currentPosition:DistToSqr(previousPosition) <= 80 * 80 + if ordinaryMovement and isvector(candidate) then + stableSamples = stableSamples + 1 + else + -- Teleports and invalid/void positions restart stabilization. + stableSamples = 0 + end + previousPosition = currentPosition + + if stableSamples < 4 then return end + local scanner = FindManagedScanner() or SpawnManagedScanner(candidate) + if not IsValid(scanner) then return end + scanner:SetPatrolAnchor(candidate, true) + anchorFinalized = true + timer.Remove(reanchorTimer) + end) +end + +local function ResetManagedScanner() + timer.Remove(reanchorTimer) + timer.Remove(respawnTimer) + managedScanner = nil + anchorFinalized = false +end + +hook.Add("InitPostEntity", "Feedc0deCoopFriendlyScannerSpawn", ResetManagedScanner) +hook.Add("PostCleanupMap", "Feedc0deCoopFriendlyScannerCleanup", ResetManagedScanner) +hook.Add("PlayerSpawn", "Feedc0deCoopFriendlyScannerInitialPlayerAnchor", function(ply) + BeginPlayerSpawnTracking(ply) +end) + +hook.Add("Feedc0deFriendlyScannerDestroyed", "Feedc0deCoopFriendlyScannerRespawn", function(scanner, anchor, managed) + if not managed then return end + if managedScanner == scanner then managedScanner = nil end + timer.Remove(respawnTimer) + local delay = GetConVar("gc_friendlyscanner_respawntime") + timer.Create(respawnTimer, math.max(delay and delay:GetFloat() or 5, 1), 1, function() + SpawnManagedScanner(anchor) + end) +end) + +concommand.Remove("coop_friendlyscanner_here") +concommand.Add("coop_friendlyscanner_here", function(ply) + if IsValid(ply) and not ply:IsAdmin() then return end + local anchorPlayer = IsValid(ply) and ply or player.GetHumans()[1] + if not IsValid(anchorPlayer) then return end + local scanner = FindManagedScanner() or SpawnManagedScanner() + if not IsValid(scanner) then return end + local position = FindClearPosition(anchorPlayer:GetPos(), anchorPlayer) + if not isvector(position) then return end + scanner:SetPatrolAnchor(position, true) + anchorFinalized = true +end) + +concommand.Remove("coop_friendlyscanner_reset_spawn") +concommand.Add("coop_friendlyscanner_reset_spawn", function(ply) + if IsValid(ply) and not ply:IsAdmin() then return end + local referencePlayer = IsValid(ply) and ply or player.GetHumans()[1] + local scanner = FindManagedScanner() or SpawnManagedScanner() + if not IsValid(scanner) then return end + local position = PlayerSpawnPosition(referencePlayer) + if not isvector(position) then return end + scanner:SetPatrolAnchor(position, true) + anchorFinalized = true +end) + +concommand.Remove("coop_friendlyscanner_arm_next_spawn") +concommand.Add("coop_friendlyscanner_arm_next_spawn", function(ply) + if IsValid(ply) and not ply:IsAdmin() then return end + timer.Remove(reanchorTimer) + timer.Remove(respawnTimer) + for _, scanner in ipairs(ents.FindByClass(scannerClass)) do + if scanner:GetNW2Bool(managedTag, false) then scanner:Remove() end + end + managedScanner = nil + anchorFinalized = false +end) + +concommand.Remove("coop_friendlyscanner_reload") +concommand.Add("coop_friendlyscanner_reload", function(ply) + if IsValid(ply) and not ply:IsAdmin() then return end + local scanner = FindManagedScanner() + if not IsValid(scanner) then return end + local anchor = scanner:GetSpawnAnchor() + scanner:SetNW2Bool(managedTag, false) + scanner:Remove() + managedScanner = nil + timer.Simple(0.1, function() + SpawnManagedScanner(anchor) + end) +end) + +-- Remove hooks from the pre-addon implementation during live migration. +hook.Remove("Think", "CustomCoopFriendlyScannerThink") +hook.Remove("PlayerInitialSpawn", "CustomCoopFriendlyScannerPlayerRelationship") +hook.Remove("PlayerSpawn", "CustomCoopFriendlyScannerFollowActualSpawn") +hook.Remove("PhysgunPickup", "CustomCoopFriendlyScannerPhysgunPickup") +hook.Remove("PhysgunDrop", "CustomCoopFriendlyScannerPhysgunDrop") +hook.Remove("EntityTakeDamage", "CustomCoopFriendlyScannerPhysgunProtection") +hook.Remove("OnNPCKilled", "CustomCoopFriendlyScannerKilled") +timer.Remove("CustomCoopFriendlyScannerRespawn") + +timer.Simple(0, function() + for _, candidate in ipairs(ents.GetAll()) do + if candidate:GetNW2Bool("CustomCoopFriendlyScanner", false) and candidate:GetClass() ~= scannerClass then + candidate:Remove() + end + end + local existing = FindManagedScanner() + if IsValid(existing) and CurTime() > 10 then + -- Hot-refreshing this file must not make the next ordinary player + -- respawn reposition an already-established scanner. + anchorFinalized = true + end +end) diff --git a/gamemode/headcrabs.lua b/gamemodes/feedc0de_coop/gamemode/headcrabs.lua similarity index 100% rename from gamemode/headcrabs.lua rename to gamemodes/feedc0de_coop/gamemode/headcrabs.lua diff --git a/gamemode/init.lua b/gamemodes/feedc0de_coop/gamemode/init.lua similarity index 92% rename from gamemode/init.lua rename to gamemodes/feedc0de_coop/gamemode/init.lua index 2fbe1f5..25454a1 100644 --- a/gamemode/init.lua +++ b/gamemodes/feedc0de_coop/gamemode/init.lua @@ -19,5 +19,6 @@ include( "noheadcrabs.lua" ) include( "shared.lua" ) include( "maprotation.lua" ) include( "sv_progression.lua" ) +include( "friendly_scanner_spawner.lua" ) resource.AddFile( "sound/weapons/universal/pap.wav" ) diff --git a/gamemode/levels.lua b/gamemodes/feedc0de_coop/gamemode/levels.lua similarity index 100% rename from gamemode/levels.lua rename to gamemodes/feedc0de_coop/gamemode/levels.lua diff --git a/gamemode/mapperstuff.lua b/gamemodes/feedc0de_coop/gamemode/mapperstuff.lua similarity index 100% rename from gamemode/mapperstuff.lua rename to gamemodes/feedc0de_coop/gamemode/mapperstuff.lua diff --git a/gamemode/maprotation.lua b/gamemodes/feedc0de_coop/gamemode/maprotation.lua similarity index 100% rename from gamemode/maprotation.lua rename to gamemodes/feedc0de_coop/gamemode/maprotation.lua diff --git a/gamemode/noheadcrabs.lua b/gamemodes/feedc0de_coop/gamemode/noheadcrabs.lua similarity index 100% rename from gamemode/noheadcrabs.lua rename to gamemodes/feedc0de_coop/gamemode/noheadcrabs.lua diff --git a/gamemode/progression_shared.lua b/gamemodes/feedc0de_coop/gamemode/progression_shared.lua similarity index 100% rename from gamemode/progression_shared.lua rename to gamemodes/feedc0de_coop/gamemode/progression_shared.lua diff --git a/gamemode/realisticdamage.lua b/gamemodes/feedc0de_coop/gamemode/realisticdamage.lua similarity index 100% rename from gamemode/realisticdamage.lua rename to gamemodes/feedc0de_coop/gamemode/realisticdamage.lua diff --git a/gamemode/shared.lua b/gamemodes/feedc0de_coop/gamemode/shared.lua similarity index 94% rename from gamemode/shared.lua rename to gamemodes/feedc0de_coop/gamemode/shared.lua index 57899dd..d9749ac 100644 --- a/gamemode/shared.lua +++ b/gamemodes/feedc0de_coop/gamemode/shared.lua @@ -1,13 +1,13 @@ GM.Name = "feedc0de Coop Campaign" -GM.Author = "0xFEEDC0DE64" -GM.Email = "daniel@brunner.ninja" -GM.Website = "brunner.ninja" - +GM.Author = "0xFEEDC0DE64" +GM.Email = "daniel@brunner.ninja" +GM.Website = "brunner.ninja" + DeriveGamemode("sandbox") include( "levels.lua" ) include( "progression_shared.lua" ) - + if (SERVER) then function ChangeRandomLevel() local randomLevel = GetRandomLevel() @@ -40,49 +40,49 @@ if (SERVER) then timer.Simple(0, NextRandomLevel) end) end - --- timer.Simple( --- 1, --- function() --- function HideThings(name) --- if (name == "CHudDamageIndicator" or name == "CHudHintDisplay") then --- return false --- end --- end --- hook.Add("HUDShouldDraw", "HideThings", HideThings) --- end --- ) - + +-- timer.Simple( +-- 1, +-- function() +-- function HideThings(name) +-- if (name == "CHudDamageIndicator" or name == "CHudHintDisplay") then +-- return false +-- end +-- end +-- hook.Add("HUDShouldDraw", "HideThings", HideThings) +-- end +-- ) + function GM:IsSpawnpointSuitable(ply, spawnpointent, bMakeSuitable) - local Pos = spawnpointent:GetPos() - - -- Note that we're searching the default hull size here for a player in the way of our spawning. - -- This seems pretty rough, seeing as our player's hull could be different.. but it should do the job - -- ( HL2DM kills everything within a 128 unit radius ) - local Ents = ents.FindInBox(Pos + Vector(-16, -16, 0), Pos + Vector(16, 16, 72)) - - if (ply:Team() == TEAM_SPECTATOR or ply:Team() == TEAM_UNASSIGNED) then - return true - end - - local Blockers = 0 - - for k, v in pairs(Ents) do - if (IsValid(v) and v:GetClass() == "player" and v:Alive()) then - Blockers = Blockers + 1 - - if (bMakeSuitable) then - v:SetVelocity(v:GetPos() + Vector(100, 100, 0)) - end - end - end - - if (bMakeSuitable) then - return true - end - if (Blockers > 0) then - return false - end + local Pos = spawnpointent:GetPos() + + -- Note that we're searching the default hull size here for a player in the way of our spawning. + -- This seems pretty rough, seeing as our player's hull could be different.. but it should do the job + -- ( HL2DM kills everything within a 128 unit radius ) + local Ents = ents.FindInBox(Pos + Vector(-16, -16, 0), Pos + Vector(16, 16, 72)) + + if (ply:Team() == TEAM_SPECTATOR or ply:Team() == TEAM_UNASSIGNED) then + return true + end + + local Blockers = 0 + + for k, v in pairs(Ents) do + if (IsValid(v) and v:GetClass() == "player" and v:Alive()) then + Blockers = Blockers + 1 + + if (bMakeSuitable) then + v:SetVelocity(v:GetPos() + Vector(100, 100, 0)) + end + end + end + + if (bMakeSuitable) then + return true + end + if (Blockers > 0) then + return false + end return true end @@ -152,7 +152,20 @@ if SERVER then end if #candidates == 0 then - return DefaultPlayerSelectSpawn(self, ply, transition) + if isfunction(DefaultPlayerSelectSpawn) then + return DefaultPlayerSelectSpawn(self, ply, transition) + end + + local inherited = self.BaseClass and self.BaseClass.PlayerSelectSpawn + if isfunction(inherited) then + return inherited(self, ply, transition) + end + + local remembered = ply:GetNWEntity("CustomCoopLastSpawn") + if IsValid(remembered) then return remembered end + + ErrorNoHalt("[feedc0de_coop] No valid player spawn or inherited spawn selector is available\n") + return end local lastSpawnpoint = ply:GetVar("LastSpawnpoint") @@ -252,67 +265,67 @@ function GM:PlayerLoadout(ply) ply:GiveAmmo(6, "Buckshot", true) ply:GiveAmmo(6, "357", true) end - -hook.Add( "SpawnMenuOpen", "DisallowSpawnMenu", function () - return LocalPlayer():IsAdmin() -end) - -hook.Add( "ContextMenuOpen", "DisallowSpawnMenu", function () - return LocalPlayer():IsAdmin() -end) - -function GM:PlayerNoClip(ply, desiredState) - if desiredState then - return ply:IsAdmin() - else - return true - end -end - -function GM:PlayerDeathSound() - return true -end - -function GM:DrawDeathNotice(x, y) - return false -end - -function GM:PlayerSpawnVehicle(ply, model, name, table) - return ply:IsAdmin() -end - -function GM:PlayerSpawnSWEP(ply, weapon, info) - return ply:IsAdmin() -end - -function GM:PlayerSpawnSENT(ply, class) - return ply:IsAdmin() -end - -function GM:PlayerSpawnRagdoll(ply, model) - return ply:IsAdmin() -end - -function GM:PlayerSpawnProp(ply, model) - return ply:IsAdmin() -end - -function GM:PlayerSpawnObject(ply, model, skin) - return ply:IsAdmin() -end - -function GM:PlayerSpawnNPC(ply, npc_type, weapon) - return ply:IsAdmin() -end - -function GM:PlayerSpawnEffect(ply, model) - return ply:IsAdmin() -end - -function GM:PlayerGiveSWEP(ply, weapon, swep) - return ply:IsAdmin() -end - ---function GM:HUDAmmoPickedUp( item, amount ) --- return false; ---end + +hook.Add( "SpawnMenuOpen", "DisallowSpawnMenu", function () + return LocalPlayer():IsAdmin() +end) + +hook.Add( "ContextMenuOpen", "DisallowSpawnMenu", function () + return LocalPlayer():IsAdmin() +end) + +function GM:PlayerNoClip(ply, desiredState) + if desiredState then + return ply:IsAdmin() + else + return true + end +end + +function GM:PlayerDeathSound() + return true +end + +function GM:DrawDeathNotice(x, y) + return false +end + +function GM:PlayerSpawnVehicle(ply, model, name, table) + return ply:IsAdmin() +end + +function GM:PlayerSpawnSWEP(ply, weapon, info) + return ply:IsAdmin() +end + +function GM:PlayerSpawnSENT(ply, class) + return ply:IsAdmin() +end + +function GM:PlayerSpawnRagdoll(ply, model) + return ply:IsAdmin() +end + +function GM:PlayerSpawnProp(ply, model) + return ply:IsAdmin() +end + +function GM:PlayerSpawnObject(ply, model, skin) + return ply:IsAdmin() +end + +function GM:PlayerSpawnNPC(ply, npc_type, weapon) + return ply:IsAdmin() +end + +function GM:PlayerSpawnEffect(ply, model) + return ply:IsAdmin() +end + +function GM:PlayerGiveSWEP(ply, weapon, swep) + return ply:IsAdmin() +end + +--function GM:HUDAmmoPickedUp( item, amount ) +-- return false; +--end diff --git a/gamemode/spawninit.lua b/gamemodes/feedc0de_coop/gamemode/spawninit.lua similarity index 100% rename from gamemode/spawninit.lua rename to gamemodes/feedc0de_coop/gamemode/spawninit.lua diff --git a/gamemode/sv_progression.lua b/gamemodes/feedc0de_coop/gamemode/sv_progression.lua similarity index 100% rename from gamemode/sv_progression.lua rename to gamemodes/feedc0de_coop/gamemode/sv_progression.lua diff --git a/gamemode/weaponsystem.lua b/gamemodes/feedc0de_coop/gamemode/weaponsystem.lua similarity index 100% rename from gamemode/weaponsystem.lua rename to gamemodes/feedc0de_coop/gamemode/weaponsystem.lua diff --git a/gamemode/zombies.lua b/gamemodes/feedc0de_coop/gamemode/zombies.lua similarity index 100% rename from gamemode/zombies.lua rename to gamemodes/feedc0de_coop/gamemode/zombies.lua diff --git a/icon24.png b/gamemodes/feedc0de_coop/icon24.png similarity index 100% rename from icon24.png rename to gamemodes/feedc0de_coop/icon24.png diff --git a/loading-screen/README.md b/gamemodes/feedc0de_coop/loading-screen/README.md similarity index 100% rename from loading-screen/README.md rename to gamemodes/feedc0de_coop/loading-screen/README.md diff --git a/loading-screen/app.js b/gamemodes/feedc0de_coop/loading-screen/app.js similarity index 100% rename from loading-screen/app.js rename to gamemodes/feedc0de_coop/loading-screen/app.js diff --git a/loading-screen/index.html b/gamemodes/feedc0de_coop/loading-screen/index.html similarity index 100% rename from loading-screen/index.html rename to gamemodes/feedc0de_coop/loading-screen/index.html diff --git a/loading-screen/server.py b/gamemodes/feedc0de_coop/loading-screen/server.py similarity index 100% rename from loading-screen/server.py rename to gamemodes/feedc0de_coop/loading-screen/server.py diff --git a/loading-screen/style.css b/gamemodes/feedc0de_coop/loading-screen/style.css similarity index 100% rename from loading-screen/style.css rename to gamemodes/feedc0de_coop/loading-screen/style.css diff --git a/logo.png b/gamemodes/feedc0de_coop/logo.png similarity index 100% rename from logo.png rename to gamemodes/feedc0de_coop/logo.png diff --git a/lua/autorun/feedc0de_friendly_scanner.lua b/lua/autorun/feedc0de_friendly_scanner.lua new file mode 100644 index 0000000..553483b --- /dev/null +++ b/lua/autorun/feedc0de_friendly_scanner.lua @@ -0,0 +1,6 @@ +list.Set("NPC", "feedc0de_friendly_scanner", { + Name = "Friendly Healing Scanner", + Class = "feedc0de_friendly_scanner", + Category = "feedc0de Coop", + AdminOnly = true +}) diff --git a/lua/entities/feedc0de_friendly_scanner/cl_init.lua b/lua/entities/feedc0de_friendly_scanner/cl_init.lua new file mode 100644 index 0000000..b5babb4 --- /dev/null +++ b/lua/entities/feedc0de_friendly_scanner/cl_init.lua @@ -0,0 +1,69 @@ +include("shared.lua") + +local healingBeam = Material("effects/laser1") +local healingGlow = Material("sprites/light_glow02_add") +local beamColor = Color(255, 218, 65, 235) +local beamCoreColor = Color(255, 252, 205, 255) + +local function LightPosition(scanner) + local attachmentID = scanner:LookupAttachment("light") + local attachment = attachmentID and attachmentID > 0 and scanner:GetAttachment(attachmentID) + return attachment and attachment.Pos or (scanner:WorldSpaceCenter() + scanner:GetForward() * 16) +end + +local function DrawHealingArc(startPosition, endPosition, scannerIndex) + local direction = endPosition - startPosition + local normalized = direction:GetNormalized() + local perpendicular = normalized:Cross(Vector(0, 0, 1)) + if perpendicular:LengthSqr() < 0.01 then perpendicular = Vector(1, 0, 0) end + perpendicular:Normalize() + local vertical = normalized:Cross(perpendicular):GetNormalized() + local time = CurTime() + local segments = 12 + + render.SetMaterial(healingBeam) + render.StartBeam(segments + 1) + for segment = 0, segments do + local fraction = segment / segments + local position = LerpVector(fraction, startPosition, endPosition) + if segment > 0 and segment < segments then + local taper = math.sin(fraction * math.pi) + position = position + + perpendicular * math.sin(time * 13 + segment * 2.31 + scannerIndex) * 6 * taper + + vertical * math.cos(time * 17 + segment * 1.73 + scannerIndex) * 4 * taper + end + render.AddBeam(position, 4.5, fraction, beamColor) + end + render.EndBeam() + render.DrawBeam(startPosition, endPosition, 1.25, 0, 1, beamCoreColor) +end + +function ENT:Draw() + self:DrawModel() +end + +function ENT:DrawTranslucent() + if self:GetNW2Bool("CustomCoopScannerWreck", false) then return end + + local lightPosition = LightPosition(self) + render.SetMaterial(healingGlow) + render.DrawSprite(lightPosition, 24, 24, beamColor) + + local light = DynamicLight(self:EntIndex()) + if light then + light.pos = lightPosition + light.r = 255 + light.g = 205 + light.b = 45 + light.brightness = 2 + light.Decay = 500 + light.Size = 105 + light.DieTime = CurTime() + 0.2 + end + + local patient = self:GetHealingTarget() + if IsValid(patient) and patient:IsPlayer() then + DrawHealingArc(lightPosition, patient:WorldSpaceCenter(), self:EntIndex()) + render.DrawSprite(patient:WorldSpaceCenter(), 15, 15, beamCoreColor) + end +end diff --git a/lua/entities/feedc0de_friendly_scanner/init.lua b/lua/entities/feedc0de_friendly_scanner/init.lua new file mode 100644 index 0000000..09dcd0f --- /dev/null +++ b/lua/entities/feedc0de_friendly_scanner/init.lua @@ -0,0 +1,508 @@ +AddCSLuaFile("cl_init.lua") +AddCSLuaFile("shared.lua") +include("shared.lua") + +local enabled = CreateConVar("gc_friendlyscanner_enabled", "1", FCVAR_ARCHIVE, "Enable friendly healing scanners") +local healRadius = CreateConVar("gc_friendlyscanner_healradius", "360", FCVAR_ARCHIVE, "Friendly scanner healing radius") +local patientSearchRadius = CreateConVar("gc_friendlyscanner_searchradius", "700", FCVAR_ARCHIVE, "Radius in which the friendly scanner looks for hurt players") +local healRate = CreateConVar("gc_friendlyscanner_healrate", "15", FCVAR_ARCHIVE, "Friendly scanner health restored per second") +local respawnTime = CreateConVar("gc_friendlyscanner_respawntime", "5", FCVAR_ARCHIVE, "Friendly scanner respawn delay") + +local hoverHeight = 97 +local patrolRadius = 250 +local flightHullMins = Vector(-34, -34, -22) +local flightHullMaxs = Vector(34, 34, 22) +local worldUp = Vector(0, 0, 1) +local farewellMessages = { + "I will get you for that, %s!", + "That was by accident, right, %s?", + "I was the yellow scanner, %s. The yellow one!", + "Remember: medics have feelings too, %s.", + "My repair bill is going straight to %s.", + "Five seconds, %s. Then we are having a talk.", + "I hope your next medkit rolls away from you, %s.", + "Diagnostic result: %s is hazardous to helpful machinery." +} + +local function ClampVectorLength(value, maximum) + local length = value:Length() + if length <= maximum or length <= 0 then return value end + return value * (maximum / length) +end + +local function IsScanner(ent) + return IsValid(ent) and ent:GetClass() == "feedc0de_friendly_scanner" +end + +function ENT:SpawnFunction(ply, trace, className) + if not trace.Hit then return end + local scanner = ents.Create(className) + if not IsValid(scanner) then return end + local position = trace.HitPos + trace.HitNormal * hoverHeight + scanner:SetPos(position) + scanner:SetAngles(Angle(0, IsValid(ply) and ply:EyeAngles().y or 0, 0)) + scanner:Spawn() + scanner:Activate() + scanner:SetSpawnAnchor(position) + if IsValid(ply) then scanner:SetCreator(ply) end + return scanner +end + +function ENT:Initialize() + self:SetModel("models/combine_scanner.mdl") + self:PhysicsInit(SOLID_VPHYSICS) + self:SetMoveType(MOVETYPE_VPHYSICS) + self:SetSolid(SOLID_VPHYSICS) + self:SetCollisionGroup(COLLISION_GROUP_NONE) + self:SetUseType(SIMPLE_USE) + self:SetRenderMode(RENDERMODE_TRANSCOLOR) + self:SetColor(Color(255, 210, 55)) + self:SetMaxHealth(100) + self:SetHealth(100) + self:SetHealingTarget(NULL) + if self:GetSpawnAnchor() == vector_origin then self:SetSpawnAnchor(self:GetPos()) end + + local physics = self:GetPhysicsObject() + if not IsValid(physics) then + ErrorNoHalt("[feedc0de_scanner] Scanner model has no VPhysics object\n") + self:Remove() + return + end + physics:EnableGravity(false) + physics:EnableDrag(true) + physics:SetMass(12) + physics:SetDamping(0.5, 1.8) + physics:Wake() + + self.NextControllerThink = CurTime() + self.LastControllerThink = CurTime() + self.NextDestinationAt = 0 + self.HealRemainder = 0 + self.NextIdleSound = CurTime() + math.Rand(3, 7) + self:SetNW2Bool("CustomCoopFriendlyScanner", true) + + self.FlightSound = CreateSound(self, "NPC_CScanner.FlyLoop") + if self.FlightSound then self.FlightSound:PlayEx(0.55, 100) end + + -- NPC-list spawning places scripted entities directly on the floor and + -- does not necessarily call SpawnFunction. Establish the desired anchor + -- from a downward floor sensor after the physics object exists. + timer.Simple(0, function() + if not IsValid(self) or self.Destroyed then return end + local hoverPoint = self:FindFloorHoverPoint(self:GetPos()) + if isvector(hoverPoint) then self:SetPatrolAnchor(hoverPoint, false) end + end) +end + +function ENT:SetPatrolAnchor(position, moveThere) + if not isvector(position) then return end + self:SetSpawnAnchor(position) + self.PatrolDestination = nil + self.FocusPlayer = nil + self.NextDestinationAt = 0 + local physics = self:GetPhysicsObject() + if IsValid(physics) then + physics:SetVelocity(vector_origin) + physics:AddAngleVelocity(-physics:GetAngleVelocity()) + end + if moveThere then self:SetPos(position) end +end + +function ENT:FindFloorHoverPoint(position) + local anchor = self:GetSpawnAnchor() + local probeZ = math.max(position.z, self:GetPos().z) + local floorTrace = util.TraceLine({ + start = Vector(position.x, position.y, probeZ), + endpos = Vector(position.x, position.y, probeZ - 512), + filter = self, + mask = MASK_SOLID_BRUSHONLY + }) + if not floorTrace.Hit or floorTrace.StartSolid or floorTrace.HitNormal.z <= 0.45 then + return Vector(position.x, position.y, anchor.z) + end + + local floorZ = floorTrace.HitPos.z + local ceilingTrace = util.TraceLine({ + start = Vector(position.x, position.y, floorZ + 20), + endpos = Vector(position.x, position.y, floorZ + hoverHeight + 64), + filter = self, + mask = MASK_SOLID_BRUSHONLY + }) + local targetZ = floorZ + hoverHeight + if ceilingTrace.Hit then targetZ = math.min(targetZ, ceilingTrace.HitPos.z - 24) end + targetZ = math.max(targetZ, floorZ + 30) + return Vector(position.x, position.y, targetZ) +end + +function ENT:PlayersNearAnchor(maximumDistance) + local result = {} + local maximumDistanceSquared = maximumDistance * maximumDistance + for _, ply in ipairs(player.GetHumans()) do + if ply:Alive() and ply:GetPos():DistToSqr(self:GetSpawnAnchor()) <= maximumDistanceSquared then + result[#result + 1] = ply + end + end + return result +end + +function ENT:FindHealingPatient() + local maximumDistance = math.max(patientSearchRadius:GetFloat(), healRadius:GetFloat(), 0) + local maximumDistanceSquared = maximumDistance * maximumDistance + local closest + local closestDistance + for _, ply in ipairs(player.GetHumans()) do + local distance = self:GetPos():DistToSqr(ply:GetPos()) + local maximumHealth = math.max(ply:GetMaxHealth(), 100) + if ply:Alive() + and ply:Health() > 0 + and ply:Health() < maximumHealth + and distance <= maximumDistanceSquared + and (not closestDistance or distance < closestDistance) + then + -- Acquisition deliberately ignores line of sight. The scanner + -- must first approach patients around spawn-room corners; the + -- actual healing beam still requires a clear trace. + closest = ply + closestDistance = distance + end + end + return closest +end + +function ENT:CanHealPatient(patient) + if not IsValid(patient) or not patient:IsPlayer() or not patient:Alive() then return false end + local maximumDistance = math.max(healRadius:GetFloat(), 0) + if self:GetPos():DistToSqr(patient:GetPos()) > maximumDistance * maximumDistance then return false end + local trace = util.TraceLine({ + start = self:WorldSpaceCenter(), + endpos = patient:WorldSpaceCenter(), + filter = { self, patient }, + mask = MASK_SOLID_BRUSHONLY + }) + return not trace.Hit +end + +function ENT:ChooseDestination(preferredPlayer) + local nearbyPlayers = self:PlayersNearAnchor(380) + local focus = IsValid(preferredPlayer) and preferredPlayer + or (#nearbyPlayers > 0 and nearbyPlayers[math.random(#nearbyPlayers)] or nil) + self.FocusPlayer = focus + + for attempt = 1, 24 do + local destination + if IsValid(focus) and (IsValid(preferredPlayer) or math.Rand(0, 1) < 0.65) then + local angle = math.Rand(0, math.pi * 2) + local offset = Vector(math.cos(angle), math.sin(angle), 0) * math.Rand(55, 90) + destination = self:FindFloorHoverPoint(focus:GetPos() + offset) + else + local angle = math.Rand(0, math.pi * 2) + local offset = Vector(math.cos(angle), math.sin(angle), 0) * math.Rand(35, patrolRadius) + destination = self:FindFloorHoverPoint(self:GetSpawnAnchor() + offset) + end + + local fromAnchor = destination - self:GetSpawnAnchor() + local horizontal = Vector(fromAnchor.x, fromAnchor.y, 0) + if horizontal:Length() > patrolRadius then + horizontal = horizontal:GetNormalized() * patrolRadius + destination.x = self:GetSpawnAnchor().x + horizontal.x + destination.y = self:GetSpawnAnchor().y + horizontal.y + end + + local clear = util.TraceHull({ + start = destination, + endpos = destination, + mins = flightHullMins, + maxs = flightHullMaxs, + filter = self, + mask = MASK_SOLID + }) + local route = util.TraceHull({ + start = self:GetPos(), + endpos = destination, + mins = flightHullMins, + maxs = flightHullMaxs, + filter = self, + mask = MASK_SOLID_BRUSHONLY + }) + local sufficientlyFar = self:GetPos():DistToSqr(destination) >= 70 * 70 + if not clear.StartSolid + and not route.Hit + and util.IsInWorld(destination) + and (IsValid(preferredPlayer) or sufficientlyFar or attempt > 16) + then + self.PatrolDestination = destination + local path = destination - self:GetPos() + local horizontalPath = Vector(path.x, path.y, 0) + if horizontalPath:LengthSqr() > 1 then + horizontalPath:Normalize() + self.SwayAxis = Vector(-horizontalPath.y, horizontalPath.x, 0) + end + self.HoldPosition = nil + self.NextDestinationAt = CurTime() + math.Rand(5, 9) + return + end + end + + self.PatrolDestination = nil + self.HoldPosition = self:GetPos() + self.NextDestinationAt = CurTime() + 1 +end + +function ENT:ApplyFlightController(targetPosition, lookDirection, elapsed, feedForwardVelocity, lookingAtPlayer) + local physics = self:GetPhysicsObject() + if not IsValid(physics) or self.PhysgunHeld then return end + + feedForwardVelocity = feedForwardVelocity or vector_origin + local positionError = targetPosition - self:GetPos() + local horizontalError = Vector(positionError.x, positionError.y, 0) + local horizontalFeedForward = Vector(feedForwardVelocity.x, feedForwardVelocity.y, 0) + local desiredHorizontal = ClampVectorLength(horizontalError * 0.55 + horizontalFeedForward, 36) + local desiredVelocity = Vector( + desiredHorizontal.x, + desiredHorizontal.y, + math.Clamp(positionError.z * 0.5 + feedForwardVelocity.z, -12, 12) + ) + local velocityError = desiredVelocity - physics:GetVelocity() + -- Strong enough to recover promptly after a barrel or explosion launches + -- it, while remaining an ordinary force-driven VPhysics body that can be + -- displaced and spun by the collision itself. + -- ApplyForceCenter changes VPhysics velocity during the current physics + -- step. Treating these as continuous-force gains above 1 makes velocity + -- overshoot and reverse every update. Correct only a small, elapsed-time + -- scaled fraction of the error per step for critically damped motion. + local horizontalGain = math.min(elapsed * 3.5, 0.2) + local verticalGain = math.min(elapsed * 4, 0.24) + local force = Vector( + velocityError.x * physics:GetMass() * horizontalGain, + velocityError.y * physics:GetMass() * horizontalGain, + velocityError.z * physics:GetMass() * verticalGain + ) + force = ClampVectorLength(force, physics:GetMass() * 40) + physics:ApplyForceCenter(force) + + if lookDirection:LengthSqr() > 1 then + local lookAngles = lookDirection:Angle() + local wavePitch = lookingAtPlayer and 0 or math.Clamp(-feedForwardVelocity.z * 0.8, -8, 8) + local desiredPitch = math.Clamp(math.NormalizeAngle(lookAngles.p) + wavePitch, -15, 40) + local desiredAngles = Angle(desiredPitch, lookAngles.y, 0) + local yawError = math.AngleDifference(desiredAngles.y, self:GetAngles().y) + local angularVelocity = physics:GetAngleVelocity() + local desiredYawVelocity = math.Clamp(yawError * 1.6, -75, 75) + + local currentUp = self:GetUp() + local desiredUp = desiredAngles:Up() + local alignment = math.Clamp(currentUp:Dot(desiredUp), -1, 1) + local tiltAngle = math.deg(math.acos(alignment)) + local rightingAxis = currentUp:Cross(desiredUp) + if rightingAxis:LengthSqr() < 0.001 and alignment < 0 then + rightingAxis = self:GetRight() + elseif rightingAxis:LengthSqr() > 0 then + rightingAxis:Normalize() + end + + -- Normal steering stays soft, but a collision that rolls the scanner + -- far over gets a decisive recovery. Suppress most yaw authority + -- while badly tilted so turning cannot compete with self-righting. + local severeTilt = tiltAngle > 25 + local yawAuthority = math.Clamp((alignment - 0.15) / 0.7, 0, 1) + local rightingSpeed = math.min(tiltAngle * (severeTilt and 5 or 3), 220) + local desiredWorldAngularVelocity = rightingAxis * rightingSpeed + + worldUp * desiredYawVelocity * yawAuthority + local desiredAngularVelocity = physics:WorldToLocalVector(desiredWorldAngularVelocity) + local correctionGain = math.min(elapsed * (severeTilt and 9 or 4.5), severeTilt and 0.55 or 0.25) + local correctionLimit = severeTilt and 30 or 12 + local correction = (desiredAngularVelocity - angularVelocity) * correctionGain + physics:AddAngleVelocity(ClampVectorLength(correction, correctionLimit)) + end +end + +function ENT:Think() + if self.Destroyed then return false end + local now = CurTime() + if (self.NextControllerThink or 0) > now then + self:NextThink(self.NextControllerThink) + return true + end + local elapsed = math.Clamp(now - (self.LastControllerThink or now), 0.01, 0.1) + self.LastControllerThink = now + self.NextControllerThink = now + 0.05 + self:NextThink(self.NextControllerThink) + + if not enabled:GetBool() then + self:Remove() + return true + end + + local patient = self:FindHealingPatient() + local canHeal = self:CanHealPatient(patient) + self:SetHealingTarget(canHeal and patient or NULL) + if IsValid(patient) and self.FocusPlayer ~= patient then + self:ChooseDestination(patient) + elseif not self.PatrolDestination and (self.NextDestinationAt or 0) <= now then + self:ChooseDestination(IsValid(patient) and patient or nil) + end + + local wavePhase = now + self:EntIndex() + local wave = math.sin(wavePhase) * 10 + local waveVelocity = math.cos(wavePhase) * 10 + local sway = math.cos(wavePhase) * 6 + local swayVelocity = -math.sin(wavePhase) * 6 + local swayAxis = self.SwayAxis or Vector(self:GetRight().x, self:GetRight().y, 0):GetNormalized() + local targetPosition = self.PatrolDestination or self.HoldPosition or self:GetSpawnAnchor() + targetPosition = Vector(targetPosition.x, targetPosition.y, targetPosition.z + wave) + swayAxis * sway + local feedForwardVelocity = swayAxis * swayVelocity + Vector(0, 0, waveVelocity) + + local travel = targetPosition - self:GetPos() + if travel:LengthSqr() < 12 * 12 and self.PatrolDestination then + self.HoldPosition = self.PatrolDestination + self.PatrolDestination = nil + self.NextDestinationAt = now + math.Rand(1.5, 3) + elseif travel:LengthSqr() > 1 then + local obstacle = util.TraceHull({ + start = self:GetPos(), + endpos = self:GetPos() + travel:GetNormalized() * math.min(travel:Length(), 42), + mins = flightHullMins, + maxs = flightHullMaxs, + filter = self, + mask = MASK_SOLID + }) + if obstacle.Hit then + self.PatrolDestination = nil + self.HoldPosition = self:GetPos() + self.NextDestinationAt = now + 0.8 + targetPosition = self.HoldPosition + feedForwardVelocity = vector_origin + travel = targetPosition - self:GetPos() + end + end + + local horizontalTravel = Vector(travel.x, travel.y, 0) + local lookDirection + local lookingAtPlayer = false + if IsValid(patient) then + lookDirection = patient:EyePos() - self:WorldSpaceCenter() + lookingAtPlayer = true + elseif IsValid(self.FocusPlayer) then + lookDirection = self.FocusPlayer:EyePos() - self:WorldSpaceCenter() + lookingAtPlayer = true + elseif horizontalTravel:LengthSqr() > 12 * 12 then + lookDirection = travel + else + lookDirection = Vector(self:GetForward().x, self:GetForward().y, 0) + end + self:ApplyFlightController(targetPosition, lookDirection, elapsed, feedForwardVelocity, lookingAtPlayer) + + self:SetPoseParameter("dynamo_wheel", math.NormalizeAngle(now * 180)) + self:SetPoseParameter("tail_control", 0) + self:SetPoseParameter("alert_control", IsValid(patient) and 1 or 0.25) + self:SetPoseParameter("flex_vert", 0) + self:SetPoseParameter("flex_horz", 0) + + if (self.NextIdleSound or 0) <= now then + self.NextIdleSound = now + math.Rand(6, 12) + self:EmitSound("NPC_CScanner.Idle", 65, math.random(96, 106), 0.42) + end + + if canHeal then + self.HealRemainder = (self.HealRemainder or 0) + math.max(healRate:GetFloat(), 0) * elapsed + local health = math.floor(self.HealRemainder) + if health > 0 then + self.HealRemainder = self.HealRemainder - health + patient:SetHealth(math.min(patient:Health() + health, math.max(patient:GetMaxHealth(), 100))) + end + if (self.NextHealSound or 0) <= now then + self.NextHealSound = now + 1.2 + self:EmitSound("items/smallmedkit1.wav", 58, math.random(105, 118), 0.28) + end + else + self.HealRemainder = 0 + end + return true +end + +function ENT:OnTakeDamage(damageInfo) + local physics = self:GetPhysicsObject() + if self.Destroyed then + if IsValid(physics) and damageInfo:GetDamageForce():LengthSqr() > 0 then + physics:ApplyForceOffset(damageInfo:GetDamageForce(), damageInfo:GetDamagePosition()) + physics:Wake() + end + return + end + + if self.PhysgunHeld + or (self.PhysgunProtectedUntil or 0) > CurTime() + or damageInfo:IsDamageType(DMG_CRUSH) + then + return + end + + if IsValid(physics) and damageInfo:GetDamageForce():LengthSqr() > 0 then + physics:ApplyForceOffset(damageInfo:GetDamageForce(), damageInfo:GetDamagePosition()) + end + self:SetHealth(self:Health() - math.max(damageInfo:GetDamage(), 0)) + if self:Health() > 0 or self.Destroyed then return end + self.Destroyed = true + + local killer = IsValid(damageInfo:GetAttacker()) and damageInfo:GetAttacker() or damageInfo:GetInflictor() + if IsValid(killer) and not killer:IsPlayer() and IsValid(killer:GetOwner()) then killer = killer:GetOwner() end + if IsValid(killer) and killer:IsPlayer() then + PrintMessage(HUD_PRINTTALK, "[Friendly Scanner] " .. string.format( + farewellMessages[math.random(#farewellMessages)], killer:Nick() + )) + else + PrintMessage(HUD_PRINTTALK, "[Friendly Scanner] Tell my charging station I loved it.") + end + + local anchor = self:GetSpawnAnchor() + local managed = self:GetNW2Bool("CustomCoopGamemodeManaged", false) + local effect = EffectData() + effect:SetOrigin(self:WorldSpaceCenter()) + util.Effect("cball_explode", effect, true, true) + self:EmitSound("npc/scanner/scanner_explode_crash2.wav", 72, 105, 0.8) + + -- Leave the destroyed scanner behind as an ordinary physical wreck. The + -- controller no longer fights gravity or impact forces, so explosions, + -- props and players can throw it around naturally. + self:SetHealingTarget(NULL) + self:SetNW2Bool("CustomCoopFriendlyScanner", false) + self:SetNW2Bool("CustomCoopGamemodeManaged", false) + self:SetNW2Bool("CustomCoopScannerWreck", true) + self:SetColor(Color(90, 82, 68)) + if IsValid(physics) then + physics:EnableGravity(true) + physics:EnableDrag(true) + physics:SetDamping(0.05, 0.08) + physics:SetMass(18) + physics:Wake() + local damageForce = damageInfo:GetDamageForce() + if damageForce:LengthSqr() > 0 then + physics:ApplyForceOffset(damageForce, damageInfo:GetDamagePosition()) + end + end + SafeRemoveEntityDelayed(self, 25) + hook.Run("Feedc0deFriendlyScannerDestroyed", self, anchor, managed) +end + +function ENT:OnRemove() + if self.FlightSound then + self.FlightSound:Stop() + self.FlightSound = nil + end +end + +hook.Add("PhysgunPickup", "Feedc0deFriendlyScannerPhysgunPickup", function(_, ent) + if not IsScanner(ent) then return end + ent.PhysgunHeld = true + ent.PhysgunProtectedUntil = CurTime() + 1 + ent.PatrolDestination = nil + return true +end) + +hook.Add("PhysgunDrop", "Feedc0deFriendlyScannerPhysgunDrop", function(_, ent) + if not IsScanner(ent) then return end + ent.PhysgunHeld = false + ent.PhysgunProtectedUntil = CurTime() + 2 + local hoverPoint = ent:FindFloorHoverPoint(ent:GetPos()) + ent:SetPatrolAnchor(isvector(hoverPoint) and hoverPoint or ent:GetPos(), false) +end) diff --git a/lua/entities/feedc0de_friendly_scanner/shared.lua b/lua/entities/feedc0de_friendly_scanner/shared.lua new file mode 100644 index 0000000..76afdbc --- /dev/null +++ b/lua/entities/feedc0de_friendly_scanner/shared.lua @@ -0,0 +1,13 @@ +ENT.Type = "anim" +ENT.Base = "base_anim" +ENT.PrintName = "Friendly Healing Scanner" +ENT.Author = "0xFEEDC0DE64" +ENT.Category = "feedc0de Coop" +ENT.Spawnable = true +ENT.AdminOnly = true +ENT.RenderGroup = RENDERGROUP_BOTH + +function ENT:SetupDataTables() + self:NetworkVar("Vector", 0, "SpawnAnchor") + self:NetworkVar("Entity", 0, "HealingTarget") +end diff --git a/lua/feedc0de_scanner_live_loader.lua b/lua/feedc0de_scanner_live_loader.lua new file mode 100644 index 0000000..9942f15 --- /dev/null +++ b/lua/feedc0de_scanner_live_loader.lua @@ -0,0 +1,14 @@ +-- Hot-load bridge for an SRCDS process that started before this addon existed. +-- A normal server start discovers and registers the addon without this file. +if not SERVER then return end + +local previousENT = ENT +ENT = {} +include("entities/feedc0de_friendly_scanner/shared.lua") +include("entities/feedc0de_friendly_scanner/init.lua") +scripted_ents.Register(ENT, "feedc0de_friendly_scanner") +ENT = previousENT + +AddCSLuaFile("autorun/feedc0de_friendly_scanner.lua") +include("autorun/feedc0de_friendly_scanner.lua") +print("[feedc0de_scanner] scripted entity and Q-menu entry registered") diff --git a/content/materials/effects/actionflash1.vtf b/materials/effects/actionflash1.vtf similarity index 100% rename from content/materials/effects/actionflash1.vtf rename to materials/effects/actionflash1.vtf diff --git a/content/materials/effects/actionflash2.vmt b/materials/effects/actionflash2.vmt similarity index 100% rename from content/materials/effects/actionflash2.vmt rename to materials/effects/actionflash2.vmt diff --git a/content/materials/effects/actionflash2.vtf b/materials/effects/actionflash2.vtf similarity index 100% rename from content/materials/effects/actionflash2.vtf rename to materials/effects/actionflash2.vtf diff --git a/content/materials/effects/multi/muzzleflash1.vmt b/materials/effects/multi/muzzleflash1.vmt similarity index 100% rename from content/materials/effects/multi/muzzleflash1.vmt rename to materials/effects/multi/muzzleflash1.vmt diff --git a/content/materials/effects/multi/muzzleflash1.vtf b/materials/effects/multi/muzzleflash1.vtf similarity index 100% rename from content/materials/effects/multi/muzzleflash1.vtf rename to materials/effects/multi/muzzleflash1.vtf diff --git a/content/materials/effects/multi/muzzleflash2.vmt b/materials/effects/multi/muzzleflash2.vmt similarity index 100% rename from content/materials/effects/multi/muzzleflash2.vmt rename to materials/effects/multi/muzzleflash2.vmt diff --git a/content/materials/effects/multi/muzzleflash2.vtf b/materials/effects/multi/muzzleflash2.vtf similarity index 100% rename from content/materials/effects/multi/muzzleflash2.vtf rename to materials/effects/multi/muzzleflash2.vtf diff --git a/content/materials/effects/multi/muzzleflash3.vmt b/materials/effects/multi/muzzleflash3.vmt similarity index 100% rename from content/materials/effects/multi/muzzleflash3.vmt rename to materials/effects/multi/muzzleflash3.vmt diff --git a/content/materials/effects/multi/muzzleflash3.vtf b/materials/effects/multi/muzzleflash3.vtf similarity index 100% rename from content/materials/effects/multi/muzzleflash3.vtf rename to materials/effects/multi/muzzleflash3.vtf diff --git a/content/materials/effects/multi/muzzleflash4.vmt b/materials/effects/multi/muzzleflash4.vmt similarity index 100% rename from content/materials/effects/multi/muzzleflash4.vmt rename to materials/effects/multi/muzzleflash4.vmt diff --git a/content/materials/effects/multi/muzzleflash4.vtf b/materials/effects/multi/muzzleflash4.vtf similarity index 100% rename from content/materials/effects/multi/muzzleflash4.vtf rename to materials/effects/multi/muzzleflash4.vtf diff --git a/content/materials/effects/multi/muzzleflashx.vmt b/materials/effects/multi/muzzleflashx.vmt similarity index 100% rename from content/materials/effects/multi/muzzleflashx.vmt rename to materials/effects/multi/muzzleflashx.vmt diff --git a/content/materials/effects/multi/muzzleflashx.vtf b/materials/effects/multi/muzzleflashx.vtf similarity index 100% rename from content/materials/effects/multi/muzzleflashx.vtf rename to materials/effects/multi/muzzleflashx.vtf diff --git a/content/materials/effects/muzzle_root.vmt b/materials/effects/muzzle_root.vmt similarity index 100% rename from content/materials/effects/muzzle_root.vmt rename to materials/effects/muzzle_root.vmt diff --git a/content/materials/effects/muzzle_root.vtf b/materials/effects/muzzle_root.vtf similarity index 100% rename from content/materials/effects/muzzle_root.vtf rename to materials/effects/muzzle_root.vtf diff --git a/content/materials/effects/muzzleflash4p.vmt b/materials/effects/muzzleflash4p.vmt similarity index 100% rename from content/materials/effects/muzzleflash4p.vmt rename to materials/effects/muzzleflash4p.vmt diff --git a/content/materials/effects/muzzleflash4p.vtf b/materials/effects/muzzleflash4p.vtf similarity index 100% rename from content/materials/effects/muzzleflash4p.vtf rename to materials/effects/muzzleflash4p.vtf diff --git a/content/materials/effects/muzzleflash_ptp.vmt b/materials/effects/muzzleflash_ptp.vmt similarity index 100% rename from content/materials/effects/muzzleflash_ptp.vmt rename to materials/effects/muzzleflash_ptp.vmt diff --git a/content/materials/effects/muzzleflash_ptp.vtf b/materials/effects/muzzleflash_ptp.vtf similarity index 100% rename from content/materials/effects/muzzleflash_ptp.vtf rename to materials/effects/muzzleflash_ptp.vtf diff --git a/content/materials/vgui/entities/ptp_cs_ak47.vmt b/materials/vgui/entities/ptp_cs_ak47.vmt similarity index 100% rename from content/materials/vgui/entities/ptp_cs_ak47.vmt rename to materials/vgui/entities/ptp_cs_ak47.vmt diff --git a/content/materials/vgui/entities/ptp_cs_ak47.vtf b/materials/vgui/entities/ptp_cs_ak47.vtf similarity index 100% rename from content/materials/vgui/entities/ptp_cs_ak47.vtf rename to materials/vgui/entities/ptp_cs_ak47.vtf diff --git a/content/materials/vgui/entities/ptp_cs_aug.vmt b/materials/vgui/entities/ptp_cs_aug.vmt similarity index 100% rename from content/materials/vgui/entities/ptp_cs_aug.vmt rename to materials/vgui/entities/ptp_cs_aug.vmt diff --git a/content/materials/vgui/entities/ptp_cs_aug.vtf b/materials/vgui/entities/ptp_cs_aug.vtf similarity index 100% rename from content/materials/vgui/entities/ptp_cs_aug.vtf rename to materials/vgui/entities/ptp_cs_aug.vtf diff --git a/content/materials/vgui/entities/ptp_cs_autoshotgun.vmt b/materials/vgui/entities/ptp_cs_autoshotgun.vmt similarity index 100% rename from content/materials/vgui/entities/ptp_cs_autoshotgun.vmt rename to materials/vgui/entities/ptp_cs_autoshotgun.vmt diff --git a/content/materials/vgui/entities/ptp_cs_autoshotgun.vtf b/materials/vgui/entities/ptp_cs_autoshotgun.vtf similarity index 100% rename from content/materials/vgui/entities/ptp_cs_autoshotgun.vtf rename to materials/vgui/entities/ptp_cs_autoshotgun.vtf diff --git a/content/materials/vgui/entities/ptp_cs_awp.vmt b/materials/vgui/entities/ptp_cs_awp.vmt similarity index 100% rename from content/materials/vgui/entities/ptp_cs_awp.vmt rename to materials/vgui/entities/ptp_cs_awp.vmt diff --git a/content/materials/vgui/entities/ptp_cs_awp.vtf b/materials/vgui/entities/ptp_cs_awp.vtf similarity index 100% rename from content/materials/vgui/entities/ptp_cs_awp.vtf rename to materials/vgui/entities/ptp_cs_awp.vtf diff --git a/content/materials/vgui/entities/ptp_cs_deagle.vmt b/materials/vgui/entities/ptp_cs_deagle.vmt similarity index 100% rename from content/materials/vgui/entities/ptp_cs_deagle.vmt rename to materials/vgui/entities/ptp_cs_deagle.vmt diff --git a/content/materials/vgui/entities/ptp_cs_deagle.vtf b/materials/vgui/entities/ptp_cs_deagle.vtf similarity index 100% rename from content/materials/vgui/entities/ptp_cs_deagle.vtf rename to materials/vgui/entities/ptp_cs_deagle.vtf diff --git a/content/materials/vgui/entities/ptp_cs_elites.vmt b/materials/vgui/entities/ptp_cs_elites.vmt similarity index 100% rename from content/materials/vgui/entities/ptp_cs_elites.vmt rename to materials/vgui/entities/ptp_cs_elites.vmt diff --git a/content/materials/vgui/entities/ptp_cs_elites.vtf b/materials/vgui/entities/ptp_cs_elites.vtf similarity index 100% rename from content/materials/vgui/entities/ptp_cs_elites.vtf rename to materials/vgui/entities/ptp_cs_elites.vtf diff --git a/content/materials/vgui/entities/ptp_cs_famas.vmt b/materials/vgui/entities/ptp_cs_famas.vmt similarity index 100% rename from content/materials/vgui/entities/ptp_cs_famas.vmt rename to materials/vgui/entities/ptp_cs_famas.vmt diff --git a/content/materials/vgui/entities/ptp_cs_famas.vtf b/materials/vgui/entities/ptp_cs_famas.vtf similarity index 100% rename from content/materials/vgui/entities/ptp_cs_famas.vtf rename to materials/vgui/entities/ptp_cs_famas.vtf diff --git a/content/materials/vgui/entities/ptp_cs_fiveseven.vmt b/materials/vgui/entities/ptp_cs_fiveseven.vmt similarity index 100% rename from content/materials/vgui/entities/ptp_cs_fiveseven.vmt rename to materials/vgui/entities/ptp_cs_fiveseven.vmt diff --git a/content/materials/vgui/entities/ptp_cs_fiveseven.vtf b/materials/vgui/entities/ptp_cs_fiveseven.vtf similarity index 100% rename from content/materials/vgui/entities/ptp_cs_fiveseven.vtf rename to materials/vgui/entities/ptp_cs_fiveseven.vtf diff --git a/content/materials/vgui/entities/ptp_cs_g3sg1.vmt b/materials/vgui/entities/ptp_cs_g3sg1.vmt similarity index 100% rename from content/materials/vgui/entities/ptp_cs_g3sg1.vmt rename to materials/vgui/entities/ptp_cs_g3sg1.vmt diff --git a/content/materials/vgui/entities/ptp_cs_g3sg1.vtf b/materials/vgui/entities/ptp_cs_g3sg1.vtf similarity index 100% rename from content/materials/vgui/entities/ptp_cs_g3sg1.vtf rename to materials/vgui/entities/ptp_cs_g3sg1.vtf diff --git a/content/materials/vgui/entities/ptp_cs_galil.vmt b/materials/vgui/entities/ptp_cs_galil.vmt similarity index 100% rename from content/materials/vgui/entities/ptp_cs_galil.vmt rename to materials/vgui/entities/ptp_cs_galil.vmt diff --git a/content/materials/vgui/entities/ptp_cs_galil.vtf b/materials/vgui/entities/ptp_cs_galil.vtf similarity index 100% rename from content/materials/vgui/entities/ptp_cs_galil.vtf rename to materials/vgui/entities/ptp_cs_galil.vtf diff --git a/content/materials/vgui/entities/ptp_cs_glock.vmt b/materials/vgui/entities/ptp_cs_glock.vmt similarity index 100% rename from content/materials/vgui/entities/ptp_cs_glock.vmt rename to materials/vgui/entities/ptp_cs_glock.vmt diff --git a/content/materials/vgui/entities/ptp_cs_glock.vtf b/materials/vgui/entities/ptp_cs_glock.vtf similarity index 100% rename from content/materials/vgui/entities/ptp_cs_glock.vtf rename to materials/vgui/entities/ptp_cs_glock.vtf diff --git a/content/materials/vgui/entities/ptp_cs_knife.vmt b/materials/vgui/entities/ptp_cs_knife.vmt similarity index 100% rename from content/materials/vgui/entities/ptp_cs_knife.vmt rename to materials/vgui/entities/ptp_cs_knife.vmt diff --git a/content/materials/vgui/entities/ptp_cs_knife.vtf b/materials/vgui/entities/ptp_cs_knife.vtf similarity index 100% rename from content/materials/vgui/entities/ptp_cs_knife.vtf rename to materials/vgui/entities/ptp_cs_knife.vtf diff --git a/content/materials/vgui/entities/ptp_cs_m4.vmt b/materials/vgui/entities/ptp_cs_m4.vmt similarity index 100% rename from content/materials/vgui/entities/ptp_cs_m4.vmt rename to materials/vgui/entities/ptp_cs_m4.vmt diff --git a/content/materials/vgui/entities/ptp_cs_m4.vtf b/materials/vgui/entities/ptp_cs_m4.vtf similarity index 100% rename from content/materials/vgui/entities/ptp_cs_m4.vtf rename to materials/vgui/entities/ptp_cs_m4.vtf diff --git a/content/materials/vgui/entities/ptp_cs_mac10.vmt b/materials/vgui/entities/ptp_cs_mac10.vmt similarity index 100% rename from content/materials/vgui/entities/ptp_cs_mac10.vmt rename to materials/vgui/entities/ptp_cs_mac10.vmt diff --git a/content/materials/vgui/entities/ptp_cs_mac10.vtf b/materials/vgui/entities/ptp_cs_mac10.vtf similarity index 100% rename from content/materials/vgui/entities/ptp_cs_mac10.vtf rename to materials/vgui/entities/ptp_cs_mac10.vtf diff --git a/content/materials/vgui/entities/ptp_cs_mp5.vmt b/materials/vgui/entities/ptp_cs_mp5.vmt similarity index 100% rename from content/materials/vgui/entities/ptp_cs_mp5.vmt rename to materials/vgui/entities/ptp_cs_mp5.vmt diff --git a/content/materials/vgui/entities/ptp_cs_mp5.vtf b/materials/vgui/entities/ptp_cs_mp5.vtf similarity index 100% rename from content/materials/vgui/entities/ptp_cs_mp5.vtf rename to materials/vgui/entities/ptp_cs_mp5.vtf diff --git a/content/materials/vgui/entities/ptp_cs_p228.vmt b/materials/vgui/entities/ptp_cs_p228.vmt similarity index 100% rename from content/materials/vgui/entities/ptp_cs_p228.vmt rename to materials/vgui/entities/ptp_cs_p228.vmt diff --git a/content/materials/vgui/entities/ptp_cs_p228.vtf b/materials/vgui/entities/ptp_cs_p228.vtf similarity index 100% rename from content/materials/vgui/entities/ptp_cs_p228.vtf rename to materials/vgui/entities/ptp_cs_p228.vtf diff --git a/content/materials/vgui/entities/ptp_cs_p90.vmt b/materials/vgui/entities/ptp_cs_p90.vmt similarity index 100% rename from content/materials/vgui/entities/ptp_cs_p90.vmt rename to materials/vgui/entities/ptp_cs_p90.vmt diff --git a/content/materials/vgui/entities/ptp_cs_p90.vtf b/materials/vgui/entities/ptp_cs_p90.vtf similarity index 100% rename from content/materials/vgui/entities/ptp_cs_p90.vtf rename to materials/vgui/entities/ptp_cs_p90.vtf diff --git a/content/materials/vgui/entities/ptp_cs_para.vmt b/materials/vgui/entities/ptp_cs_para.vmt similarity index 100% rename from content/materials/vgui/entities/ptp_cs_para.vmt rename to materials/vgui/entities/ptp_cs_para.vmt diff --git a/content/materials/vgui/entities/ptp_cs_para.vtf b/materials/vgui/entities/ptp_cs_para.vtf similarity index 100% rename from content/materials/vgui/entities/ptp_cs_para.vtf rename to materials/vgui/entities/ptp_cs_para.vtf diff --git a/content/materials/vgui/entities/ptp_cs_pumpshotgun.vmt b/materials/vgui/entities/ptp_cs_pumpshotgun.vmt similarity index 100% rename from content/materials/vgui/entities/ptp_cs_pumpshotgun.vmt rename to materials/vgui/entities/ptp_cs_pumpshotgun.vmt diff --git a/content/materials/vgui/entities/ptp_cs_pumpshotgun.vtf b/materials/vgui/entities/ptp_cs_pumpshotgun.vtf similarity index 100% rename from content/materials/vgui/entities/ptp_cs_pumpshotgun.vtf rename to materials/vgui/entities/ptp_cs_pumpshotgun.vtf diff --git a/content/materials/vgui/entities/ptp_cs_scout.vmt b/materials/vgui/entities/ptp_cs_scout.vmt similarity index 100% rename from content/materials/vgui/entities/ptp_cs_scout.vmt rename to materials/vgui/entities/ptp_cs_scout.vmt diff --git a/content/materials/vgui/entities/ptp_cs_scout.vtf b/materials/vgui/entities/ptp_cs_scout.vtf similarity index 100% rename from content/materials/vgui/entities/ptp_cs_scout.vtf rename to materials/vgui/entities/ptp_cs_scout.vtf diff --git a/content/materials/vgui/entities/ptp_cs_sg550.vmt b/materials/vgui/entities/ptp_cs_sg550.vmt similarity index 100% rename from content/materials/vgui/entities/ptp_cs_sg550.vmt rename to materials/vgui/entities/ptp_cs_sg550.vmt diff --git a/content/materials/vgui/entities/ptp_cs_sg550.vtf b/materials/vgui/entities/ptp_cs_sg550.vtf similarity index 100% rename from content/materials/vgui/entities/ptp_cs_sg550.vtf rename to materials/vgui/entities/ptp_cs_sg550.vtf diff --git a/content/materials/vgui/entities/ptp_cs_sg552.vmt b/materials/vgui/entities/ptp_cs_sg552.vmt similarity index 100% rename from content/materials/vgui/entities/ptp_cs_sg552.vmt rename to materials/vgui/entities/ptp_cs_sg552.vmt diff --git a/content/materials/vgui/entities/ptp_cs_sg552.vtf b/materials/vgui/entities/ptp_cs_sg552.vtf similarity index 100% rename from content/materials/vgui/entities/ptp_cs_sg552.vtf rename to materials/vgui/entities/ptp_cs_sg552.vtf diff --git a/content/materials/vgui/entities/ptp_cs_tmp.vmt b/materials/vgui/entities/ptp_cs_tmp.vmt similarity index 100% rename from content/materials/vgui/entities/ptp_cs_tmp.vmt rename to materials/vgui/entities/ptp_cs_tmp.vmt diff --git a/content/materials/vgui/entities/ptp_cs_tmp.vtf b/materials/vgui/entities/ptp_cs_tmp.vtf similarity index 100% rename from content/materials/vgui/entities/ptp_cs_tmp.vtf rename to materials/vgui/entities/ptp_cs_tmp.vtf diff --git a/content/materials/vgui/entities/ptp_cs_ump45.vmt b/materials/vgui/entities/ptp_cs_ump45.vmt similarity index 100% rename from content/materials/vgui/entities/ptp_cs_ump45.vmt rename to materials/vgui/entities/ptp_cs_ump45.vmt diff --git a/content/materials/vgui/entities/ptp_cs_ump45.vtf b/materials/vgui/entities/ptp_cs_ump45.vtf similarity index 100% rename from content/materials/vgui/entities/ptp_cs_ump45.vtf rename to materials/vgui/entities/ptp_cs_ump45.vtf diff --git a/content/materials/vgui/entities/ptp_cs_usp.vmt b/materials/vgui/entities/ptp_cs_usp.vmt similarity index 100% rename from content/materials/vgui/entities/ptp_cs_usp.vmt rename to materials/vgui/entities/ptp_cs_usp.vmt diff --git a/content/materials/vgui/entities/ptp_cs_usp.vtf b/materials/vgui/entities/ptp_cs_usp.vtf similarity index 100% rename from content/materials/vgui/entities/ptp_cs_usp.vtf rename to materials/vgui/entities/ptp_cs_usp.vtf diff --git a/content/materials/vgui/entities/ptp_weapon_flash.vmt b/materials/vgui/entities/ptp_weapon_flash.vmt similarity index 100% rename from content/materials/vgui/entities/ptp_weapon_flash.vmt rename to materials/vgui/entities/ptp_weapon_flash.vmt diff --git a/content/materials/vgui/entities/ptp_weapon_flash.vtf b/materials/vgui/entities/ptp_weapon_flash.vtf similarity index 100% rename from content/materials/vgui/entities/ptp_weapon_flash.vtf rename to materials/vgui/entities/ptp_weapon_flash.vtf diff --git a/content/materials/vgui/entities/ptp_weapon_grenade.vmt b/materials/vgui/entities/ptp_weapon_grenade.vmt similarity index 100% rename from content/materials/vgui/entities/ptp_weapon_grenade.vmt rename to materials/vgui/entities/ptp_weapon_grenade.vmt diff --git a/content/materials/vgui/entities/ptp_weapon_grenade.vtf b/materials/vgui/entities/ptp_weapon_grenade.vtf similarity index 100% rename from content/materials/vgui/entities/ptp_weapon_grenade.vtf rename to materials/vgui/entities/ptp_weapon_grenade.vtf diff --git a/content/materials/vgui/entities/ptp_weapon_smoke.vmt b/materials/vgui/entities/ptp_weapon_smoke.vmt similarity index 100% rename from content/materials/vgui/entities/ptp_weapon_smoke.vmt rename to materials/vgui/entities/ptp_weapon_smoke.vmt diff --git a/content/materials/vgui/entities/ptp_weapon_smoke.vtf b/materials/vgui/entities/ptp_weapon_smoke.vtf similarity index 100% rename from content/materials/vgui/entities/ptp_weapon_smoke.vtf rename to materials/vgui/entities/ptp_weapon_smoke.vtf diff --git a/content/sound/theend/missionfailed.wav b/sound/theend/missionfailed.wav similarity index 100% rename from content/sound/theend/missionfailed.wav rename to sound/theend/missionfailed.wav diff --git a/content/sound/weapons/universal/firemode.wav b/sound/weapons/universal/firemode.wav similarity index 100% rename from content/sound/weapons/universal/firemode.wav rename to sound/weapons/universal/firemode.wav diff --git a/content/sound/weapons/universal/iron_in.wav b/sound/weapons/universal/iron_in.wav similarity index 100% rename from content/sound/weapons/universal/iron_in.wav rename to sound/weapons/universal/iron_in.wav diff --git a/content/sound/weapons/universal/iron_out.wav b/sound/weapons/universal/iron_out.wav similarity index 100% rename from content/sound/weapons/universal/iron_out.wav rename to sound/weapons/universal/iron_out.wav diff --git a/content/sound/weapons/universal/pap.wav b/sound/weapons/universal/pap.wav similarity index 100% rename from content/sound/weapons/universal/pap.wav rename to sound/weapons/universal/pap.wav diff --git a/content/sound/weapons/universal/zoom.wav b/sound/weapons/universal/zoom.wav similarity index 100% rename from content/sound/weapons/universal/zoom.wav rename to sound/weapons/universal/zoom.wav