Files
wnsrc/lua/entities/prop_vehicle_zapc/init.lua
lifestorm 94063e4369 Upload
2024-08-04 22:55:00 +03:00

1354 lines
39 KiB
Lua
Raw Blame History

--[[
| This file was obtained through the combined efforts
| of Madbluntz & Plymouth Antiquarian Society.
|
| Credits: lifestorm, Gregory Wayne Rossel JR.,
| Maloy, DrPepper10 @ RIP, Atle!
|
| Visit for more: https://plymouth.thetwilightzone.ru/
--]]
-- ZAPC
-- Copyright (c) 2012 Zaubermuffin
--
-- Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
--
-- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-- Resources first.
AddCSLuaFile('cl_init.lua')
AddCSLuaFile('shared.lua')
include('shared.lua')
-- Spawn Icon
resource.AddFile('materials/vgui/entities/prop_vehicle_zapc.vmt')
-- HUD
resource.AddFile('materials/zapc_hud/seat.png')
resource.AddFile('materials/zapc_hud/seat_driver.png')
resource.AddFile('materials/zapc_hud/seat_gunner.png')
resource.AddFile('materials/zapc_hud/crosshair.png')
resource.AddFile('materials/zapc_hud/rocket.png')
-- Feel free to change those.
local function CreateCVar(name, default, text)
local cvar = CreateConVar(name, default, FCVAR_DEMO, text)
return function() return cvar:GetFloat() end
end
local function CreateCVarInt(name, default, text)
local cvar = CreateConVar(name, default, FCVAR_DEMO, text)
return function() return cvar:GetInt() end
end
local ZAPC_PRIMARY_DAMAGE = CreateCVar('zapc_primary_damage', 40, 'The damage inflicted by the turret')
local ZAPC_PRIMARY_DELAY = CreateCVar('zapc_primary_delay', 0.1, 'The delay between two turret shots')
local ZAPC_PRIMARY_FORCE = CreateCVar('zapc_primary_force', 2, 'The force applies to objects hit by the turret')
local ZAPC_PRIMARY_SPREAD = CreateCVar('zapc_primary_spread', 0.01, 'The spread of the turret.')
local ZAPC_EXPLOSION_MAGNITUDE = CreateCVarInt('zapc_explosion_magnitude', 600, 'Damage done by the explosion upon destroying an APC. Must be an integer.')
local ZAPC_EXPLOSION_RADIUS = CreateCVarInt('zapc_explosion_radius', 230, 'Explosion radius upon destroying an APC. Must be an integer.')
local ZAPC_REPAIR_FORCE = CreateCVarInt('zapc_repair_force', 15, 'How much health can be repaired each tick using the easteregg. Must be an integer.')
local ZAPC_REMOTE_LOCK_DISTANCE = CreateCVarInt('zapc_remote_lock_distance', 300, 'The maximum distance a player can be away to lock the hatch.')
local ZAPC_REPAIR_SOUND = 'suitrecharge.chargingloop' -- why this sound? WHY NOT?
local ZAPC_ALARM_INTERVAL = SoundDuration('ambient/alarms/apc_alarm_loop1.wav') * 0.5 -- time between two alarm waves - tightly tied to the sound, don't change it!
-- Do not feel free to modify anything below.
-- Make sure it doesn't conflict.
local ZAPC_VIEW_UNRELATED, ZAPC_VIEW_DRIVER, ZAPC_VIEW_GUNNER, ZAPC_VIEW_PASSENGER = ZAPC_VIEW_UNRELATED, ZAPC_VIEW_DRIVER, ZAPC_VIEW_GUNNER, ZAPC_VIEW_PASSENGER
local ZAPC_MAX_HEALTH = ZAPC_MAX_HEALTH
local ZAPC_PRIMARY_RELOAD_TIME, ZAPC_SECONDARY_RELOAD_TIME = ZAPC_PRIMARY_RELOAD_TIME, ZAPC_SECONDARY_RELOAD_TIME
local ZAPC_MAX_PASSENGERS = ZAPC_MAX_PASSENGERS
-- Get rid of them.
for k, v in pairs(_G) do
if type(k) == 'string' and k:find('^ZAPC_') then
_G[k] = nil
end
end
-- Resources.
local MODEL = Model('models/combine_apc.mdl')
local INVISIBLE_MODEL = Model('models/Items/combine_rifle_ammo01.mdl')
-- Checks if a player has access to a certain thing.
-- Possible actions:
-- "personal": Is able to enter and use gunner/driver functions. This will be checked upon any action and if it isn't fulfilled anymore, the player will be thrown out from the APC.
-- "destruct": Is able to use zapc_sd self-destruct any APC by looking at it from any distance (while not being inside any APC). Passed as parameter is the APC in question.
-- "alarm": Is able to use zapc_sa alarming "easter egg" any APC by looking at it. Passed as parameter is the APC in question.
-- "repair": Is able to use +zapc_repair repairing "easter egg" (that can transform APCs into so called 'golden APCs'. Passed as parameter is the APC in question. This might be called although the repair itself won't start (due to other limitations).
-- "tool": Is able to use any tool (except duplicator, which will stay explicitly forbidden) on an APC. Needs to have proper called SENT:CanTool on the SENT in order to work (i.e. by the sandbox gamemode). Passed as parameters are the trace and the toolmode.
local function CheckAccess(ply, action, apc, ...)
return hook.Call('ZAPC_CheckAccess', nil, ply, action, apc, ...) ~= false
end
-- Replies to a player.
local function ReplyTo(ply, text, ...)
if IsValid(ply) then
ply:PrintMessage(HUD_PRINTCONSOLE, '[APC] ' .. string.format(text, ...))
else
print('[APC] ' .. string.format(text, ...))
end
end
-- Beep.
local function Beep(apc, snd)
if not IsValid(apc) then
return
end
apc:EmitSound(snd)
end
-- Pouff
local function BlowUp(apc)
if IsValid(apc) then
apc:Explode()
end
end
local function Unsatisfiable()
return false
end
function ENT:Initialize()
-- Move us upwards.
self:SetPos(self:GetPos() + Vector(0, 0, 60))
-- ghastly ghost! boooooo!
self:SetModel(INVISIBLE_MODEL)
self:SetRenderMode(RENDERMODE_NONE)
self:SetNoDraw(true)
-- Cheat
self:SetMoveType(MOVETYPE_NONE)
self:SetSolid(SOLID_NONE)
self:SetNotSolid(true)
-- Create the driver seat.
self.DriverSeat = ents.Create('prop_vehicle_jeep')
self.DriverSeat:SetSolid(SOLID_VPHYSICS)
self.DriverSeat:SetPos(self:GetPos() - self:GetUp()*50)
self.DriverSeat:SetAngles(self:GetAngles())
self.DriverSeat:SetModel(MODEL)
self.DriverSeat:SetKeyValue('vehiclescript', 'scripts/vehicles/zapc_script.txt', 0)
self.DriverSeat:SetKeyValue('limitview', 0, 0)
self.DriverSeat.CanTool = function(_, ...) return self:CanTool(...) end
self.DriverSeat.PhysgunPickup = function(_, ...) return self:PhysgunPickup(...) end
self.DriverSeat.PhysgunDisabled = true -- redundant, but eh.
self.DriverSeat:Spawn()
-- The gunner "seat".
self.GunnerSeat = ents.Create('prop_vehicle_prisoner_pod')
self.GunnerSeat:SetModel(INVISIBLE_MODEL)
self.GunnerSeat:SetSolid(SOLID_NONE)
self.GunnerSeat:SetNotSolid(true)
self.GunnerSeat:SetPos(self:GetPos() - self:GetUp()*10 + self:GetRight()*10)
local ang = self:GetAngles()
ang:RotateAroundAxis(self:GetUp(), 180)
self.GunnerSeat:SetAngles(ang)
self.GunnerSeat:SetKeyValue('limitview', 0, 0)
self.GunnerSeat:SetRenderMode(RENDERMODE_NONE)
self.GunnerSeat.CanTool = Unsatisfiable
self.GunnerSeat.PhysgunPickup = Unsatisfiable
self.GunnerSeat.PhysgunDisabled = true -- redundant, but eh.
self.GunnerSeat:Spawn()
self.GunnerSeat:GetPhysicsObject():EnableCollisions(false)
self.GunnerSeat:SetMoveType(MOVETYPE_NONE)
-- Turret.
-- Could be a custom SENT, but we're just abusing the gmod turret here.
self.Turret = ents.Create('base_gmodentity')
self.Turret:SetModel(INVISIBLE_MODEL)
self.Turret:SetPos(self.DriverSeat:GetAttachment(self.DriverSeat:LookupAttachment('muzzle')).Pos)
self.Turret:Spawn()
self.Turret:SetParent(self.DriverSeat)
self.Turret.NextShot = 0
self.Turret:SetNotSolid(true)
self.Turret:SetMoveType(MOVETYPE_NONE)
self.Turret:SetRenderMode(RENDERMODE_NONE)
self.Turret.CanTool = Unsatisfiable
self.Turret.PhysgunPickup = Unsatisfiable
self.Turret.PhysgunDisabled = true -- redundant, but eh.
self.Turret.APC = self
-- The driver and the gunner are nil per default.
-- Not necessary, but to declare the variables.
self.Driver, self.Gunner = nil, nil
-- Weld everything to the real driving thing.
-- But set us slightly differently first.
self:SetParent(self.DriverSeat)
self.GunnerSeat:SetParent(self.DriverSeat)
-- pew pew stuff.
self.Bullets = 50
self.NextPrimary, self.NextSecondary = 0, 0
-- Health!
self:SetMaxHealth(ZAPC_MAX_HEALTH())
self:SetHealth(ZAPC_MAX_HEALTH())
-- informational wiring. Necessary for certain things.
self.DriverSeat.APC = self
self.GunnerSeat.APC = self
-- We are APC. A callback would be unhandier.
self.IsAPC = true
-- Passengers!
self.Passengers = {}
self.HatchOpened = false
-- Lock us!
self.DriverSeat:Fire('TurnOff', '')
self.TurnedOn = false
self.SlowDrive = false
end
-- Introduced in the March '15 update, I don't think we need to know
-- what class we are.
function ENT:SetVehicleClass(name)
assert(name == 'prop_vehicle_zapc', 'invalid class name')
end
-- Informs the user about his gunner view status.
local function SendViewMode(ply, status)
if not IsValid(ply) then
return
end
umsg.Start('ZAPC_VU', ply)
umsg.Char(status)
umsg.End()
end
local function LimitPitch(ang)
return math.Clamp(ang, -45, 45)
end
local function ProcessAngle(ang)
ang.pitch = LimitPitch(math.NormalizeAngle(ang.pitch))
ang.yaw = math.NormalizeAngle(ang.yaw)
ang.roll = math.NormalizeAngle(ang.roll)
return ang
end
function ENT:Think()
self:UpdateGun()
self:NextThink(CurTime() + 1/200)
if self.NextAlarm and self.NextAlarm <= CurTime() then
self.DriverSeat:GetPhysicsObject():AddVelocity(Vector(0, 0, 150))
self.NextAlarm = self.NextAlarm + ZAPC_ALARM_INTERVAL * (self.Special and 0.5 or 1)
end
if IsValid(self.Gunner) then
if self.NextPrimary < CurTime() and self.Gunner:KeyDown(IN_ATTACK) then
self:FireTurret()
end
if self.NextSecondary < CurTime() and self.Gunner:KeyDown(IN_ATTACK2) then
self:FireRocket()
end
end
return true
end
-- Updates the gun according to the gunner's view.
function ENT:UpdateGun()
if not IsValid(self.Gunner) then
return
end
local gunnerAngles, apcAngles = ProcessAngle(self.Gunner:EyeAngles()), self.DriverSeat:GetAngles()
self.DriverSeat:SetPoseParameter('vehicle_weapon_yaw', math.NormalizeAngle(gunnerAngles.yaw - apcAngles.yaw - 90))
self.DriverSeat:SetPoseParameter('vehicle_weapon_pitch', LimitPitch(math.NormalizeAngle(gunnerAngles.pitch - apcAngles.pitch)))
if self.Special and self.SirenSound then
self.SirenSound:ChangePitch(self.DriverSeat:GetPoseParameter('vehicle_weapon_pitch') + 95, 0)
end
end
-- Returns everybody that should know about updates.
-- Because, if we put nil or [NULL] into a umsg, it's sent to everybody.
function ENT:GetReceipees()
local t = RecipientFilter()
if IsValid(self.Driver) then
t:AddPlayer(self.Driver)
end
if IsValid(self.Gunner) then
t:AddPlayer(self.Gunner)
end
return t
end
-- Send the new passengers to whoever cares
function ENT:PassengersUpdate(ply)
if IsValid(self.Driver) or IsValid(self.Gunner) then
umsg.Start('ZAPC_PU', ply or self:GetReceipees())
umsg.Entity(self.Driver or NULL)
umsg.Entity(self.Gunner or NULL)
for i = 1, ZAPC_MAX_PASSENGERS() do
local pass = self.Passengers[i]
umsg.Entity(IsValid(pass) and pass:GetPassenger(0) or NULL)
end
umsg.End()
end
end
-- Sends a bullet update.
function ENT:BulletUpdate(ply)
umsg.Start('ZAPC_BU', ply or self:GetReceipees())
umsg.Char(math.max(self.Bullets, 0))
umsg.End()
end
-- Health update
function ENT:HealthUpdate(ply)
umsg.Start('ZAPC_HU', ply or self:GetReceipees())
umsg.Short(self:Health())
umsg.End()
end
function ENT:HatchUpdate(ply)
local recv = ply
if not recv then
recv = self:GetReceipees()
for k, v in pairs(self.Passengers) do
recv:AddPlayer(v:GetPassenger(0))
end
end
SendUserMessage('ZAPC_HOU', recv, self.HatchOpened)
end
function ENT:ToggleHatch()
if self.Destructing then
return
end
self.HatchOpened = not self.HatchOpened
self:HatchUpdate()
end
local function KeyPress(ply, key)
local vehicle = ply:GetVehicle()
local apc = vehicle.APC
if IsValid(apc) then
if (not CheckAccess(ply, 'personal', apc)) and (vehicle == apc.DriverSeat or vehicle == apc.GunnerSeat) then
ply:ExitVehicle()
ReplyTo(ply, '[ZAPC] You are no longer authorized to use this specialized vehicle. If you think this is a mistake, please contact the nearest police station.')
return
end
-- Driver?
if ply == apc.Driver then
if key == IN_ATTACK then
apc:ToggleEngine()
elseif key == IN_ATTACK2 then
apc:ToggleSpeed()
elseif key == IN_SPEED then
if not IsValid(apc.Gunner) then
apc:DriverToGunner(ply)
end
elseif key == IN_RELOAD then
apc:ToggleSiren()
elseif key == IN_WALK then
apc:ToggleHatch()
end
-- Gunner!
elseif ply == apc.Gunner then
-- Reloading?
if key == IN_RELOAD then
apc:ReloadGun()
return true
elseif key == IN_SPEED then
if not IsValid(apc.Driver) then
apc:GunnerToDriver(ply)
return true
end
end
end
end
end
hook.Add('KeyPress', '_ZAPC.KeyPress', KeyPress)
local function ColdBoot(apc)
if IsValid(apc) and apc.TurnedOn and not apc.Destructing then
apc.DriverSeat:Fire('TurnOn', '')
apc.DriverSeat:Fire('HandbrakeOff', '')
end
end
function ENT:StartEngine()
timer.Simple(1, function() ColdBoot(self) end)
self:EmitSound('apc_engine_start')
end
function ENT:StopEngine()
self.DriverSeat:Fire('TurnOff', '')
self:StopSound('apc_engine_start')
self:EmitSound('apc_engine_stop')
if self.MovingSound then
self:StopSound(self.MovingSound)
self.MovingSound = nil
end
self.Direction = nil
end
function ENT:AddDriver(driver)
self.Driver = driver
-- Health before APC, so the APC overwrites the thing.
self:HealthUpdate(driver)
self:EnterPlayer(driver)
-- And the bullets.
self:BulletUpdate(driver)
SendViewMode(driver, ZAPC_VIEW_DRIVER)
self:PassengersUpdate()
end
function ENT:AddGunner(gunner)
self.Gunner = gunner
-- Health before APC, so the APC overwrites the thing.
self:HealthUpdate(gunner)
self:EnterPlayer(gunner)
-- And the bullets.
self:BulletUpdate(gunner)
SendViewMode(gunner, ZAPC_VIEW_GUNNER)
self:PassengersUpdate()
end
-- Adds a new passenger in the back-door-hatch
-- unlucky name is unlucky
function ENT:AddPassenger(ply)
local seat = ents.Create('prop_vehicle_prisoner_pod')
seat:SetModel(INVISIBLE_MODEL)
seat:SetSolid(SOLID_NONE)
seat:SetNotSolid(true)
seat:SetNoDraw(true)
seat:SetColor(Color(255, 255, 255, 0))
seat:SetPos(self:GetPos())
seat:SetAngles(self.DriverSeat:GetAngles())
seat:SetParent(self.DriverSeat)
seat:SetKeyValue('limitview', 0, 0)
seat.CanTool = Unsatisfiable
seat.PhysgunPickup = Unsatisfiable
seat.PhysgunDisabled = true -- redundant, but eh.
seat.APC = self
seat:Spawn()
seat:SetMoveType(MOVETYPE_NONE)
local phys = seat:GetPhysicsObject()
phys:EnableCollisions(false)
table.insert(self.Passengers, seat)
self:EnterPlayer(ply)
ply:EnterVehicle(seat)
SendViewMode(ply, ZAPC_VIEW_PASSENGER)
self:PassengersUpdate()
end
-- Hook callback when a new passenger (gunner, driver, passenger) is detected.
function ENT:NewPassenger(ply, seat)
if self.DriverSeat == seat then
self:AddDriver(ply)
elseif self.GunnerSeat == seat then
self:AddGunner(ply)
else
-- I suppose it's a passenger. Those were added manually anyway - but let's check
for k, v in pairs(self.Passengers) do
if v == seat then
return
end
end
error('Attempt to add ' .. tostring(ply) .. ' to ' .. tostring(self) .. ' - but player is not inside the APC.')
end
end
local function ResetLastPassenger(apc, ply)
if not IsValid(apc) or not IsValid(ply) or apc.LastPassenger ~= ply then
return
end
apc.LastPassenger = nil
end
-- This function may be called multiple times, depending on how GMod feels right now.
-- Therefore, the if-checks SHOULD NOT BE COMBINED (no if seat == and ply ==). No.
-- Note: Due to historical reasons, this is here to remove ANY KIND OF PASSENGER - not just "passengers".
function ENT:RemovePassenger(ply, seat)
if seat == self.DriverSeat then
if ply == self.Driver then
self.Driver = nil
self:ExitPlayer(ply, true)
ply:SetPos(self:GetPos() + self:GetForward()*60)
end
elseif seat == self.GunnerSeat then
if ply == self.Gunner then
self.Gunner = nil
self.LastPassenger = ply
timer.Simple(0.5, function() ResetLastPassenger(self, ply) end)
self:ExitPlayer(ply, true)
self.DriverSeat:SetPoseParameter('vehicle_weapon_yaw', 0)
self.DriverSeat:SetPoseParameter('vehicle_weapon_pitch', 0)
ply:SetPos(self:GetPos() - self:GetForward()*60)
end
else
-- Passenger?
for k, v in pairs(self.Passengers) do
if v == seat then
-- Make sure he doesn't get in IMMEDIATELY AGAIN.
self.LastPassenger = ply
timer.Simple(0.5, function() ResetLastPassenger(self, ply) end)
-- ok bye
self:ExitPlayer(ply, true)
ply:SetPos(self:GetPos() + self:GetRight() * 150)
seat:Remove()
table.remove(self.Passengers, k)
self:PassengersUpdate()
return
end
end
error('Unable to find ' .. tostring(ply) .. ' as passenger in ' .. tostring(seat))
end
self:PassengersUpdate()
end
function ENT:DriverToGunner(driver)
if self.Destructing then
return
end
driver:ExitVehicle()
driver:EnterVehicle(self.GunnerSeat)
end
function ENT:GunnerToDriver(gunner)
if self.Destructing then
return
end
gunner:ExitVehicle()
gunner:EnterVehicle(self.DriverSeat)
end
local function ReloadComplete(apc)
if not IsValid(apc) or apc.Bullets ~= -1 or apc.Destructing then
return
end
apc:EmitSound('Weapon_AR2.Reload_Push')
apc.Bullets = 50
apc:BulletUpdate()
end
function ENT:ReloadGun()
-- eeh.
if self.Bullets == -1 or self.Bullets == 50 or self.Destructing then
return
end
self.Bullets = 0
self:BulletUpdate()
self.NextPrimary = CurTime() + ZAPC_PRIMARY_RELOAD_TIME()
self:PrimaryUpdate()
self.Bullets = -1
timer.Simple(ZAPC_PRIMARY_RELOAD_TIME(), function() ReloadComplete(self) end)
end
local function CanPlayerEnterVehicle(ply, vehicle, role)
local apc = vehicle.APC
if IsValid(apc) then
local access = CheckAccess(ply, 'personal', apc)
-- +walk moves us into the passenger bay.
if access and vehicle == apc.DriverSeat and ply:KeyDown(IN_WALK) then
return false
end
-- If we are exploding or trying to access and invalid seat, nope
if apc.Destructing or (not access and (vehicle == apc.DriverSeat or vehicle == apc.GunnerSeat)) then
apc:EmitSound('buttons.snd47')
return false
else
return true
end
end
end
hook.Add('CanPlayerEnterVehicle', '_ZAPC.CanPlayerEnterVehicle', CanPlayerEnterVehicle)
local function PlayerEnteredVehicle(ply, veh, role)
if IsValid(veh.APC) then
veh.APC:NewPassenger(ply, veh)
end
end
hook.Add('PlayerEnteredVehicle', '_ZAPC.PlayerEnteredVehicle', PlayerEnteredVehicle)
local function PlayerUse(ply, ent)
if IsValid(ent.APC) and not IsValid(ply:GetVehicle()) then
local apc = ent.APC
-- The APC is NOT blowing up and this isn't the last passenger?
if not apc.Destructing and apc.LastPassenger ~= ply then
-- If we don't have a driver OR he isn't authorized
local driverE = IsValid(apc.Driver)
if (driverE or not CheckAccess(ply, 'personal', apc) or ply:KeyDown(IN_WALK)) and apc.HatchOpened and #apc.Passengers < ZAPC_MAX_PASSENGERS() then
apc:AddPassenger(ply)
return true
-- Driver?
elseif not driverE and CheckAccess(ply, 'personal', apc) then
-- It's okay. Just return.
return
-- Authorized to be gunner?
elseif CheckAccess(ply, 'personal', apc) and not IsValid(apc.Gunner) then
ply:EnterVehicle(apc.GunnerSeat)
return true
end
end
end
end
hook.Add('PlayerUse', '_ZAPC.PlayerUse', PlayerUse)
local function CanExitVehicle(vehicle, ply)
local apc = vehicle.APC
if IsValid(apc) then
if apc.Destructing or (ply ~= apc.Driver and ply ~= apc.Gunner and not apc.HatchOpened) then
return false
end
end
end
hook.Add('CanExitVehicle', '_ZAPC.CanExitVehicle', CanExitVehicle)
function ENT:FireTurret()
-- more gun? No gun.
if not IsValid(self.Gunner) or self.Destructing then
return
end
self.NextPrimary = CurTime() + ZAPC_PRIMARY_DELAY()
-- click click click
if self.Bullets <= 0 then
self:EmitSound('Weapon_Shotgun.Empty', 200, 100)
return
end
local turret = self.Turret
-- Prepare.
self.Bullets = self.Bullets - 1
self:BulletUpdate()
-- If we call this RIGHT NOW (i.e. inside KeyPress), weird stuff happens.
-- Don't do that.
-- In Think, it's okay. So, just delay it.
local muzzleTach = self.DriverSeat:LookupAttachment('muzzle')
local attach = self.DriverSeat:GetAttachment(muzzleTach)
attach.Pos = attach.Pos - 5 * attach.Ang:Forward() - attach.Ang:Up()*6
turret:SetPos(attach.Pos)
turret:SetAngles(attach.Ang)
-- And now, pewpew.
turret:EmitSound('Weapon_AR2.Single')
local bullet = {}
bullet.Num = 1
bullet.Src = attach.Pos
bullet.Dir = attach.Ang:Forward()
bullet.Spread = Vector(ZAPC_PRIMARY_SPREAD(), ZAPC_PRIMARY_SPREAD(), 0)
bullet.Tracer = 1
bullet.TracerName = 'AR2Tracer'
bullet.Force = ZAPC_PRIMARY_FORCE()
bullet.Damage = ZAPC_PRIMARY_DAMAGE() * (self.Special and 100 or 1)
bullet.Attacker = self.Gunner
turret:FireBullets(bullet)
local effd = EffectData()
effd:SetEntity(self.DriverSeat)
effd:SetAngles(attach.Ang)
effd:SetOrigin(attach.Pos)
effd:SetScale(1.8)
effd:SetAttachment(muzzleTach)
util.Effect('AirboatMuzzleFlash', effd)
if self.Bullets == 0 then
self:ReloadGun()
end
end
-- Sends updates for primary
function ENT:PrimaryUpdate(ply)
SendUserMessage('ZAPC_LPU', ply or self:GetReceipees())
end
function ENT:SecondaryUpdate(ply)
SendUserMessage('ZAPC_LSU', ply or self:GetReceipees())
end
-- cleans the dishes, duh.
function ENT:FireRocket()
if self.NextSecondary > CurTime() or self.Destructing then
return
end
self.NextSecondary = CurTime() + ZAPC_SECONDARY_RELOAD_TIME()
self:SecondaryUpdate()
self:EmitSound('PropAPC.FireRocket')
local attachLookup = self.DriverSeat:LookupAttachment('cannon_muzzle')
local attach = self.DriverSeat:GetAttachment(attachLookup)
local ent = ents.Create('prop_vehicle_zapc_rocket')
ent:SetOwner(self.Gunner)
ent:SetPos(attach.Pos + self:GetForward()*2)
ent:SetAngles(attach.Ang)
ent.APC = self
ent:Spawn()
if self.Special then
ent:SetModel('models/props_junk/watermelon01.mdl')
ent:SetMaterial('models/shiny')
ent:SetColor(Color(0, 242, 255, 255))
end
-- MUZZLEDUZZLE
local ed = EffectData()
ed:SetEntity(self.DriverSeat)
ed:SetScale(2)
ed:SetAttachment(attachLookup)
util.Effect('AirboatMuzzleFlash', ed)
timer.Simple(ZAPC_SECONDARY_RELOAD_TIME(), function() Beep(self, 'buttons.snd6') end)
end
-- Helper function called to un-solid a player. Calls itself recursively.
local ZAPC_GHOST_LIST = {}
local function UnsolidifyPlayer(ply)
if not IsValid(ply) or not ply:Alive() or IsValid(ply:GetVehicle()) then
ZAPC_GHOST_LIST[ply] = nil
return
end
-- Check for collisions
local tr = util.TraceEntity({ start = ply:GetPos(), endpos = ply:GetPos(), filter = ply}, ply)
if tr.Hit then
timer.Simple(0.5, function() UnsolidifyPlayer(ply) end)
return
end
ply:SetCustomCollisionCheck(ZAPC_GHOST_LIST[ply])
ZAPC_GHOST_LIST[ply] = nil
end
-- Assure that ghosted players don't collide.
local function ShouldCollide(ent1, ent2)
if not IsValid(ent1) or not IsValid(ent2) then
return
end
if (ent2:IsPlayer() and ZAPC_GHOST_LIST[ent1] ~= nil) or (ent1:IsPlayer() and ZAPC_GHOST_LIST[ent2] ~= nil) then
return false
end
end
hook.Add('ShouldCollide', '_ZAPC.ShouldCollide', ShouldCollide)
-- Gets rid of a player... in a nice way.
function ENT:ExitPlayer(ply, nopos)
SendViewMode(ply, ZAPC_VIEW_UNRELATED)
ply:GodDisable()
ply:SetColor(Color(255, 255, 255, 255))
ply:SetNoDraw(false)
ZAPC_GHOST_LIST[ply] = ply:GetCustomCollisionCheck()
timer.Simple(0.5, function() UnsolidifyPlayer(ply) end)
if not nopos then
ply:SetPos(self:GetPos() + self:GetForward()*60)
end
end
-- Hello Fr<46>ulein!
function ENT:EnterPlayer(ply)
ply:GodEnable()
ply:SetNoDraw(true)
ply:SetColor(Color(255, 255, 255, 0))
-- Send the DriverSeat as APC.
SendUserMessage('ZAPC_AU', ply, self.DriverSeat)
-- And hatches!
self:HatchUpdate(ply)
end
-- Ouch.
local function EntityTakeDamage(ent, dmg)
if IsValid(ent) and ent:GetClass() == 'prop_vehicle_jeep' and IsValid(ent.APC) and dmg:GetDamage() > 1 then
ent.APC:Pewpew(dmg:GetDamage())
end
end
hook.Add('EntityTakeDamage', '_ZAPC.EntityTakeDamage', EntityTakeDamage)
local function PlayerSpawn(ply)
-- Under NO circumstances can you re-spawn inside the APC. This prevents KillSilent/Spawn combos to switch characters while inside.
if IsValid(ply) and IsValid(ply:GetVehicle()) and IsValid(ply:GetVehicle().APC) then
ply:ExitVehicle()
end
end
hook.Add('PlayerSpawn', '_ZAPC.PlayerSpawn', PlayerSpawn)
function ENT:Pewpew(amount)
if self.Special then
amount = amount * 0.001
end
self:SetHealth(math.max(self:Health() - amount, 0))
if self:Health() <= 0 then
-- We are not allowed to do this in this frame.
timer.Simple(0, function() BlowUp(self) end)
return
end
self:HealthUpdate()
end
local function CleanupGibs(ent, gibs)
for k, v in pairs(gibs) do
if IsValid(v) then
v:Remove()
end
end
end
-- POUFF.
function ENT:Explode()
-- Do not explode multiple times.
if self.Died then
return
end
local apc = self.DriverSeat
local driver, gunner = self.Driver, self.Gunner
if IsValid(driver) then
self:ExitPlayer(driver, true)
driver:ExitVehicle()
driver:SetPos(apc:GetPos() + apc:GetForward()*150 - apc:GetRight()*50 + apc:GetUp()*150)
driver:SetVelocity(apc:GetForward()*500 + apc:GetUp()*600 - apc:GetRight() * math.random(50, 200))
self.Driver = nil
driver:Kill()
end
if IsValid(gunner) then
self:ExitPlayer(gunner, true)
gunner:ExitVehicle()
gunner:SetPos(apc:GetPos() + apc:GetForward()*150 + apc:GetRight()*50 + apc:GetUp()*150)
gunner:SetVelocity(apc:GetForward()*500 + apc:GetUp()*600 + apc:GetRight() * math.random(50, 200))
self.Gunner = nil
gunner:Kill()
end
for k, v in pairs(self.Passengers) do
local pass = v:GetPassenger(0)
self:ExitPlayer(pass)
pass:ExitVehicle()
pass:Kill()
end
-- Now, when we explode, we explode. Of course.
local expl = ents.Create('env_explosion')
expl:SetPos(apc:GetPos() + apc:GetForward()*80 + apc:GetUp()*10)
expl:SetKeyValue('imagnitude', tostring(ZAPC_EXPLOSION_MAGNITUDE()))
expl:SetKeyValue('iradiusoverride', tostring(ZAPC_EXPLOSION_RADIUS()))
expl:Spawn()
expl:Fire('explode', '', 0)
-- Create some gibs.
local gibs = {}
for i = 1,5 do
local ent = ents.Create('prop_physics')
ent:SetModel('models/combine_apc_destroyed_gib0' .. i .. '.mdl')
ent:SetPos(apc:GetPos())
ent:SetAngles(apc:GetAngles())
ent:Spawn()
table.insert(gibs, ent)
end
-- Fix the undo/cleanup history.
undo.ReplaceEntity(self, gibs[1])
cleanup.ReplaceEntity(self, gibs[1])
gibs[1]:CallOnRemove('ZAPCCleanUp', CleanupGibs, gibs)
-- Special for the wheel!
local ent = ents.Create('prop_physics')
ent:SetModel('models/combine_apc_destroyed_gib06.mdl')
ent:SetPos(self:GetPos() - self:GetRight()*110 - self:GetUp()*30)
ent:SetAngles(self:GetForward():Angle())
ent:Spawn()
table.insert(gibs, ent)
-- The softy
expl = ents.Create('env_physexplosion')
expl:SetPos(apc:GetPos() + apc:GetForward()*80)
expl:SetKeyValue('magnitude', tostring(ZAPC_EXPLOSION_MAGNITUDE()))
expl:SetKeyValue('spawnflags', '1') -- 19 would be push players + disorient players
expl:Spawn()
expl:Fire('explode', '', 0)
expl:Fire('kill', '', 1)
-- And dead.
self:Remove()
self.Died = true
end
-- dam dam daaaam
function ENT:OnRemove()
-- Eject people
local driver = self.Driver
if IsValid(driver) then
self:ExitPlayer(driver, true)
end
local gunner = self.Gunner
if IsValid(gunner) then
self:ExitPlayer(gunner)
end
-- Passengers are a bit special; because the table is modified (i.e. the passenger is removed)
for k, v in pairs(self.Passengers) do
local pass = v:GetPassenger(0)
self:ExitPlayer(pass)
end
-- Get rid of the APC
if IsValid(self.DriverSeat) then
self.DriverSeat:Remove()
end
if IsValid(self.GunnerSeat) then
self.GunnerSeat:Remove()
end
for k, v in pairs(self.Passengers) do
v:Remove()
end
if self.SirenSound then
self.SirenSound:Stop()
end
self:StopSound(ZAPC_REPAIR_SOUND)
self:StopSound('apc_engine_start')
end
local function PlayerLeaveVehicle(ply, vehicle)
if IsValid(vehicle.APC) and not vehicle.APC.Destructing then
-- Is it the player?
if vehicle.APC.Driver == ply then
timer.Simple(0, function() ColdBoot(vehicle.APC) end)
end
vehicle.APC:RemovePassenger(ply, vehicle)
end
end
hook.Add('PlayerLeaveVehicle', '_ZAPC.PlayerLeaveVehicle', PlayerLeaveVehicle)
-- If passengers disconnect while being inside, everything fucks up majorly.
local function PlayerDisconnected(ply)
local vehicle = ply:GetVehicle()
if IsValid(vehicle) and IsValid(vehicle.APC) then
vehicle.APC:RemovePassenger(ply, vehicle)
end
end
hook.Add('PlayerDisconnected', '_ZAPC.PlayerDisconnected', PlayerDisconnected)
concommand.Add('zapc_sd', function(ply, cmd, args)
if not IsValid(ply) then
return
end
local ent = ply:GetEyeTrace().Entity
if not IsValid(ent.APC) or ent.APC.Destructing or not CheckAccess(ply, 'destruct', ent.APC) or ply:GetVehicle() == ent.APC.DriverSeat or ply:GetVehicle() == ent.APC.GunnerSeat then
return
end
ent.APC:SelfDestruct()
end, nil, "Blows up the APC you are looking at - if you have the required authorization.")
concommand.Add('zapc_sa', function(ply, cmd, args)
if not IsValid(ply) then
return
end
local ent = ply:GetEyeTrace().Entity
if not IsValid(ent.APC) or ent.APC.Destructing or not CheckAccess(ply, 'alarm', ent.APC) or ply:GetVehicle() == ent.APC.DriverSeat or ply:GetVehicle() == ent.APC.GunnerSeat then
return
end
ent.APC:StartAlarm()
end, nil, "Starts an alarm on the APC you are looking at - if you have the required authorization.")
function ENT:SelfDestruct()
self.Destructing = true
-- Make the beeps!
-- Normal beeps
-- [1, 3] @ 1
for i = 0, 3 do
timer.Simple(i, function() Beep(self, 'buttons.snd16') end)
end
-- Faster beeps
-- [3, 5] @ 5
for i = 1, 4 do
timer.Simple(3 + i/2, function() Beep(self, 'buttons.snd16') end)
end
-- BEEEEP
-- [5.25, 6.5] @ 0.25
for i = 1, 9 do
timer.Simple(5 + i/8, function() Beep(self, 'buttons.snd16') end)
end
timer.Simple(6.25, function() BlowUp(self) end)
self.DriverSeat:Fire('TurnOff', '')
self.DriverSeat:Fire('Lock', '')
if IsValid(self.Driver) or IsValid(self.Gunner) then
SendUserMessage('ZAPC_SD', self:GetReceipees())
end
end
function ENT:StartRepairing(ply)
self.RepairingPlayer = ply
local effectdata = EffectData()
effectdata:SetScale(500)
effectdata:SetRadius(100000)
self.RepairEffectData = effectdata
-- And start the effect! Yaaay!
timer.Create('ZAPCRepair' .. self:EntIndex(), 0.1, 0, function() if IsValid(self) then self:RepairEffect() end end)
self:EmitSound(ZAPC_REPAIR_SOUND)
end
function ENT:RepairEffect()
-- Is the player still valid?
local tr = self.RepairingPlayer:GetEyeTrace()
-- Too close?
if not IsValid(self.RepairingPlayer) or tr.Entity ~= self.DriverSeat or tr.HitPos:Distance(self.RepairingPlayer:GetPos()) < 270 then
self:EndRepairing()
return
end
-- Do the effect!
local ed = self.RepairEffectData
ed:SetStart(self.RepairingPlayer:EyePos())
ed:SetOrigin(tr.HitPos)
ed:SetNormal(tr.HitNormal)
util.Effect("GaussTracer", ed)
-- Now repair us!
self:SetHealth(self:Health() + ZAPC_REPAIR_FORCE())
self:HealthUpdate()
-- Are we full again?
if self:Health() > ZAPC_MAX_HEALTH()*1.5 and not self.Special then
self:MakeSpecial()
-- End the healing. :(
self:EndRepairing()
end
end
function ENT:MakeSpecial()
-- BUT MAKE US SHINY
self.Special = true
self:EmitSound('bounce.concrete')
local effectdata = EffectData()
effectdata:SetOrigin(self.DriverSeat:GetPos())
effectdata:SetScale(1000)
effectdata:SetRadius(100)
effectdata:SetColor(255, 0, 255, 255)
for i = 1, 10 do
timer.Simple(i/20, function()
util.Effect('ThumperDust', effectdata)
end)
end
timer.Simple(0.6, function()
self.DriverSeat:SetMaterial('models/shiny')
self.DriverSeat:SetColor(Color(255, 215, 0, 255))
end)
if self.SirenSound then
self.SirenSound:Stop()
self.SirenSound = nil
self.Siren = false
end
end
function ENT:EndRepairing()
timer.Destroy('ZAPCRepair' .. self:EntIndex())
self.RepairingPlayer = nil
self:StopSound(ZAPC_REPAIR_SOUND)
end
concommand.Add('+zapc_repair', function(ply, cmd, args)
-- Check the trace
local ent = ply:GetEyeTrace().Entity
if not IsValid(ent) or not IsValid(ent.APC) then
return
end
-- Is it even allowed?
if ZAPC_REPAIR_FORCE() == 0 then
return
end
-- It's an APC! Amazing!
local zapc = ent.APC
-- If we aren't allowed to do anything, the APC is already being healed OR it is at full health, do nothing
if not CheckAccess(ply, 'repair', zapc) or IsValid(zapc.HealingPlayer) or zapc:Health() >= ZAPC_MAX_HEALTH()*1.5 or zapc.Destructing then
return
end
zapc:StartRepairing(ply)
end, nil, "What could this function possibly do? Better don't touch it.")
concommand.Add('-zapc_repair', function(ply, cmd, args)
local ent = ply:GetEyeTrace().Entity
if not IsValid(ent) or not IsValid(ent.APC) then
return
end
if ent.APC.RepairingPlayer == ply then
ent.APC:EndRepairing()
end
end, nil, "What could this function possibly do? Better don't touch it.")
function ENT:ToggleEngine()
if self.Destructing then
return
end
self.TurnedOn = not self.TurnedOn
self[(self.TurnedOn and 'Start' or 'Stop') .. 'Engine'](self)
end
local function StopSirenSound(apc)
if not IsValid(apc) then
return
end
apc.SirenSound = nil
end
function ENT:ToggleSiren()
if self.Destructing then
return
end
-- Don't enable before we died.
if not self.Siren and self.SirenSound then
return
end
self.Siren = not self.Siren
if self.SirenSound then
self.SirenSound:FadeOut(1.5)
timer.Simple(1.6, function() StopSirenSound(self) end)
self.NextAlarm = nil
else
self.SirenSound = CreateSound(self.DriverSeat, self.Special and 'd1_trainstation.ChaseMusic' or 'd1_canals_08.siren01')
self.SirenSound:SetSoundLevel(self.Special and 110 or 150)
self.SirenSound:PlayEx(1.0, 100)
end
end
function ENT:StartAlarm()
if self.Destructing then
return
end
-- If the siren isn't running, enable it
if not self.Siren then
self:ToggleSiren()
end
self.SirenSound:ChangePitch(200, 0)
self.NextAlarm = CurTime() + ZAPC_ALARM_INTERVAL
end
function ENT:ToggleSpeed()
--~ self.SlowDrive = not self.SlowDrive
--~ self.DriverSeat:SetKeyValue('vehiclescript', 'scripts/vehicles/zapc_script' .. (self.SlowDrive and '_slow' or '') .. '.txt')
--~ RunConsoleCommand('vehicle_flushscript')
end
-- Fixes the view
local function SetupPlayerVisibility(ply, ent)
if IsValid(ply:GetVehicle().APC) then
-- Add above him too
AddOriginToPVS(ply:GetVehicle():GetPos() + Vector(0, 0, 10))
end
end
hook.Add('SetupPlayerVisibility', '_ZAPC.SetupPlayerVisibility', SetupPlayerVisibility)
-- Non-admins and non-authorized personal cannot do ANYTHING to us.
-- ANYTHING!
function ENT:CanTool(ply, tr, mode)
return CheckAccess(ply, 'tool', self, tr, mode) and mode ~= 'duplicator'
end
function ENT:PhysgunPickup(ply)
return false
end
concommand.Add('zapc_togglehatch', function(ply, cmd, args)
local apc = ply:GetEyeTrace().Entity.APC
-- Check if it's an APC
if not IsValid(apc) then
ReplyTo(ply, 'You are not looking at an APC.')
return
end
-- Check what way we want it locked
local opened = not apc.HatchOpened
-- Do we have a parameter-sided override?
if #args > 0 and #args[1] > 0 then
opened = tobool(args[1])
end
-- For HUI reasons.
local openedString
-- `locked' describes the state we want to put it in.
if opened then
openedString = 'unlock'
else
openedString = 'lock'
end
-- openedString describes what the player wishes to do
-- Check distance
if apc.DriverSeat:GetPos():Distance(ply:GetPos()) > ZAPC_REMOTE_LOCK_DISTANCE() then
ReplyTo(ply, "You are too far away to %s this APC's hatch.", openedString)
return
end
-- Check authorization
if (not CheckAccess(ply, 'remotelock', apc, locked)) then
ReplyTo(ply, "You are not authorized to %s this APC's hatch.", openedString)
return
end
-- Check if we are even changing something
if opened == apc.HatchOpened then
ReplyTo(ply, "The APC's hatch is already %sed", openedString) -- I love English
return
end
-- Do it then, I guess
apc:ToggleHatch()
ReplyTo(ply, 'Success. Hatch is now %sed.', openedString)
end)
concommand.Add('zapc_enterhatch', function(ply, cmd, args)
local apc = ply:GetEyeTrace().Entity.APC
-- Check if it's an APC
if not IsValid(apc) then
ReplyTo(ply, 'You are not looking at an APC.')
return
end
-- Check distance. In tests, 100 seemed OK. No need to have a configuration for this.
if ply:GetEyeTrace().HitPos:Distance(ply:EyePos()) > 100 then
ReplyTo(ply, "You are too far away to enter this APC.")
return
end
if not apc.HatchOpened then
ReplyTo(ply, "This APC's hatch is closed.")
return
end
if #apc.Passengers >= ZAPC_MAX_PASSENGERS() then
ReplyTo(ply, "This APC is full.")
return
end
-- Add him.
apc:AddPassenger(ply)
end)
--[[
Because some people fail to realize that hooks should only return a value if they wish this action to be final,
there are some addons that break the APC. Currently, there are none that I know of. However, there have
been in the past and this is the reason this function exists.
What this function does is basically "wrapping" the old functions. If such a "rogue" function has been found,
it will be wrapped and re-placed. Currently, the wrapper does nothing, and I believe simply re-inserting it into
the hook table is enough to execute said function *after* ours.
There would be an extremely easy way to fix the issue - but we needed lua 5.2 for the __pairs metamethod.
]]
local function WrapFunction(event, name)
local func = hook.GetTable()[event][name]
if not func then
return
end
hook.Remove(event, name)
hook.Add(event, name, function(...) func(...) end)
end
local function Initialize()
--[[
The official HALL OF SHAME.
(NOW HIRING)
]]--
end
hook.Add('Initialize', '_ZAPC.Initialize', Initialize)
-- In case this happens again, here's a few debuggers.
local function DumpHooks(ply, cmd, args)
if IsValid(ply) and (game.IsDedicated() or not ply:IsListenServerHost()) then
ply:PrintMessage(HUD_PRINTCONSOLE, "[ZAPC] This is a server command. Execute it on the server, not the client.")
return
end
local hooks = { 'PlayerEnteredVehicle', 'PlayerUse', 'CanPlayerEnterVehicle', 'KeyPress', 'CanExitVehicle', 'ShouldCollide', 'EntityTakeDamage', 'PlayerLeaveVehicle' }
print("\n[ZAPC] Dumping possible hook collisions...")
for _, h in pairs(hooks) do
print("[ZAPC] " .. h)
for k, v in pairs(hook.GetTable()[h]) do
print('[ZAPC] "' .. tostring(k) .. '"')
end
print()
end
print("[ZAPC] Hooks dumped.")
end
concommand.Add('zapc_dumphooks', DumpHooks, nil, "Dumps all hooks that could possibly cause havok.")