Files
wnsrc/lua/arccw/shared/sh_physbullet2.lua

628 lines
22 KiB
Lua
Raw Normal View History

2024-08-04 22:55:00 +03:00
--[[
| 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/
--]]
ArcCW.PhysBullets = {
}
-- intentionally not 10 despite there being 10 default profiles.
-- for some reason profile indices are previously referenced as zero-indexed but stored as one-indexed
ArcCW.BulletProfileNum = 9
ArcCW.BulletProfileBits = nil
ArcCW.BulletProfiles = {
[0] = "default0",
[1] = "default1",
[2] = "default2",
[3] = "default3",
[4] = "default4",
[5] = "default5",
[6] = "default6",
[7] = "default7",
[8] = "default8",
[9] = "default9",
}
ArcCW.BulletProfileDict = {
["default0"] = {id = 0, name = "default0", color = Color(255, 225, 200)},
["default1"] = {id = 1, name = "default1", color = Color(255, 0, 0)},
["default2"] = {id = 2, name = "default2", color = Color(0, 255, 0)},
["default3"] = {id = 3, name = "default3", color = Color(0, 0, 255)},
["default4"] = {id = 4, name = "default4", color = Color(255, 255, 0)},
["default5"] = {id = 5, name = "default5", color = Color(255, 0, 255)},
["default6"] = {id = 6, name = "default6", color = Color(0, 255, 255)},
["default7"] = {id = 7, name = "default7", color = Color(0, 0, 0)},
["default8"] = {id = 8, name = "default8", color = Color(100, 255, 100)},
["default9"] = {id = 9, name = "default9", color = Color(100, 0, 255)},
--[[]
["profile_name"] = {
color = Color(255, 255, 255),
sprite_head = Material("effects/whiteflare"), -- set false to not draw a sprite, set nil to use default
sprite_tail = Material("effects/smoke_trail"), -- ditto
size = 1, -- Size growth factor of the physbullet (from distance)
size_min = 1, -- Base size of the physbullet
tail_length = 0.02, -- as a fraction of the bullet's velocity
model = "models/weapons/w_bullet.mdl", -- clientside model is not created without this path
model_nodraw = false, -- true to not draw model
particle = "myparticle", -- requires a model path; set to nodraw if you don't wish it to be visible
ThinkBullet = function(bulinfo, bullet) end, -- set bullet.Dead = true to stop processing and delete bullet.
DrawBullet = function(bulinfo, bullet) end, -- return true to prevent default drawing behavior
PhysBulletHit = function(bulinfo, bullet, tr) end,
}
]]
}
local vector_down = Vector(0, 0, 1)
function ArcCW:AddBulletProfile(name, bulinfo)
if istable(name) and !bulinfo then
bulinfo = name
name = tostring(ArcCW.BulletProfileNum + 1)
end
local new = !ArcCW.BulletProfileDict[name]
if new then
ArcCW.BulletProfileNum = ArcCW.BulletProfileNum + 1
ArcCW.BulletProfiles[ArcCW.BulletProfileNum] = name
ArcCW.BulletProfileBits = nil
end
ArcCW.BulletProfileDict[name] = bulinfo
if new then
ArcCW.BulletProfileDict[name].name = name
ArcCW.BulletProfileDict[name].id = ArcCW.BulletProfileNum
end
end
function ArcCW:BulletProfileBitNecessity()
if !ArcCW.BulletProfileBits then
ArcCW.BulletProfileBits = math.min(math.ceil(math.log(ArcCW.BulletProfileNum + 1, 2)), 32)
end
return ArcCW.BulletProfileBits
end
function ArcCW:SendBullet(bullet, attacker)
net.Start("arccw_sendbullet", true)
net.WriteVector(bullet.Pos)
net.WriteAngle(bullet.Vel:Angle())
net.WriteFloat(bullet.Vel:Length())
net.WriteFloat(bullet.Drag)
net.WriteFloat(bullet.Gravity)
net.WriteUInt(bullet.Profile or 0, ArcCW:BulletProfileBitNecessity())
net.WriteBool(bullet.PhysBulletImpact)
net.WriteEntity(bullet.Weapon)
if attacker and attacker:IsValid() and attacker:IsPlayer() and !game.SinglePlayer() then
net.SendOmit(attacker)
else
if game.SinglePlayer() then
net.WriteEntity(attacker)
end
net.Broadcast()
end
end
function ArcCW:ShootPhysBullet(wep, pos, vel, prof, ovr)
ovr = ovr or {}
local pbi = ovr.PhysBulletImpact or wep:GetBuff_Override("Override_PhysBulletImpact")
local num = ovr.Num or wep:GetBuff("Num")
if !prof then
prof = wep:GetBuff_Override("Override_PhysTracerProfile", wep.PhysTracerProfile) or 1
end
if isstring(prof) then
prof = ArcCW.BulletProfileDict[prof].id
end
local bullet = {
DamageMax = wep:GetDamage(0) / num,
DamageMin = wep:GetDamage(math.huge) / num,
Range = wep:GetBuff("Range"),
DamageType = wep:GetBuff_Override("Override_DamageType", wep.DamageType),
Penleft = wep:GetBuff("Penetration"),
Penetration = wep:GetBuff("Penetration"),
ImpactEffect = wep:GetBuff_Override("Override_ImpactEffect", wep.ImpactEffect),
ImpactDecal = wep:GetBuff_Override("Override_ImpactDecal", wep.ImpactDecal),
PhysBulletImpact = pbi == nil and true or pbi,
Gravity = wep:GetBuff("PhysBulletGravity"),
HullSize = wep:GetBuff("HullSize"),
Num = num,
Pos = pos,
Vel = vel,
Drag = wep:GetBuff("PhysBulletDrag"),
Travelled = 0,
StartTime = CurTime(),
Imaginary = false,
Underwater = false,
WeaponClass = wep:GetClass(),
Weapon = wep,
Attacker = wep:GetOwner(),
Filter = {wep:GetOwner()},
Damaged = {},
Burrowing = false,
Dead = false,
Profile = prof
}
table.Merge(bullet, ovr)
table.Add(bullet.Filter, wep.Shields or {})
local owner = wep:GetOwner()
--[[]
if owner and owner:IsNPC() then
bullet.DamageMax = bullet.DamageMax * ArcCW.ConVars["mult_npcdamage"]:GetFloat()
bullet.DamageMin = bullet.DamageMin * ArcCW.ConVars["mult_npcdamage"]:GetFloat()
end
]]
if SERVER and owner and owner:IsPlayer() then
table.Add(bullet.Filter, ArcCW:GetVehicleFilter(owner) or {})
end
if bit.band( util.PointContents( pos ), CONTENTS_WATER ) == CONTENTS_WATER then
bullet.Underwater = true
end
table.insert(ArcCW.PhysBullets, bullet)
-- TODO: This is still bad but unless we can access FLOW_OUTGOING from inside INetChannelInfo I can't think of any better way to do this.
if owner:IsPlayer() and SERVER then
--local ping = owner:Ping() / 1000
--ping = math.Clamp(ping, 0, 0.5)
-- local latency = util.TimeToTicks((owner:Ping() / 1000) * 0.5)
local latency = math.floor(engine.TickCount() - owner:GetCurrentCommand():TickCount() - 1) -- FIXME: this math.floor does nothing
local timestep = engine.TickInterval()
while latency > 0 do
ArcCW:ProgressPhysBullet(bullet, timestep)
latency = latency - 1
end
-- while ping > 0 do
-- ArcCW:ProgressPhysBullet(bullet, timestep)
-- ping = ping - timestep
-- end
end
if SERVER then
-- ArcCW:ProgressPhysBullet(bullet, engine.TickInterval())
ArcCW:SendBullet(bullet, wep:GetOwner())
end
end
if CLIENT then
net.Receive("arccw_sendbullet", function(len, ply)
local pos = net.ReadVector()
local ang = net.ReadAngle()
local vel = net.ReadFloat()
local drag = net.ReadFloat()
local grav = net.ReadFloat()
local profile = net.ReadUInt(ArcCW:BulletProfileBitNecessity())
local impact = net.ReadBool()
local weapon = net.ReadEntity()
local ent = nil
if game.SinglePlayer() then
ent = net.ReadEntity()
end
local bullet = {
Pos = pos,
Vel = ang:Forward() * vel,
Travelled = 0,
StartTime = CurTime(),
Imaginary = false,
Underwater = false,
Dead = false,
Damaged = {},
Drag = drag,
Attacker = ent or weapon:GetOwner(),
Gravity = grav,
Profile = profile,
PhysBulletImpact = impact,
Weapon = weapon,
Filter = {weapon:GetOwner()},
}
if bit.band( util.PointContents( pos ), CONTENTS_WATER ) == CONTENTS_WATER then
bullet.Underwater = true
end
table.insert(ArcCW.PhysBullets, bullet)
end)
end
function ArcCW:DoPhysBullets()
local new = {}
local deltatime = engine.TickInterval()
for _, i in pairs(ArcCW.PhysBullets) do
ArcCW:ProgressPhysBullet(i, deltatime)
-- On the client, bullets must live for at least one tick so we get to render it
-- This prevents invisible tracers up close
if !i.Dead or (CLIENT and CurTime() - i.StartTime <= engine.TickInterval()) then
table.insert(new, i)
elseif CLIENT and IsValid(i.CSModel) then
i.CSModel:Remove()
if i.CSParticle then
i.CSParticle:StopEmission()
i.CSParticle = nil
end
end
end
ArcCW.PhysBullets = new
end
hook.Add("Tick", "ArcCW_DoPhysBullets", ArcCW.DoPhysBullets)
local function indim(vec, maxdim)
if math.abs(vec.x) > maxdim or math.abs(vec.y) > maxdim or math.abs(vec.z) > maxdim then
return false
else
return true
end
end
local ArcCW_BulletGravity = ArcCW.ConVars["bullet_gravity"]
local ArcCW_BulletDrag = ArcCW.ConVars["bullet_drag"]
function ArcCW:ProgressPhysBullet(bullet, timestep)
if bullet.Dead then return end
local oldpos = bullet.Pos
local oldvel = bullet.Vel
local dir = bullet.Vel:GetNormalized()
local spd = bullet.Vel:Length() * timestep
local drag = bullet.Drag * spd * spd * (1 / 150000)
local gravity = timestep * ArcCW_BulletGravity:GetFloat() * (bullet.Gravity or 1)
local attacker = bullet.Attacker
if !IsValid(attacker) then
bullet.Dead = true
return
end
if bullet.Underwater then
drag = drag * 3
end
drag = drag * ArcCW_BulletDrag:GetFloat()
if spd <= 0.001 then bullet.Dead = true return end
local bulinfo = ArcCW.BulletProfileDict[ArcCW.BulletProfiles[bullet.Profile or 1] or ""]
if bulinfo == nil then
return
end
if bulinfo.ThinkBullet then
bulinfo:ThinkBullet(bullet)
end
local newpos = oldpos + (oldvel * timestep)
local newvel = oldvel - (dir * drag)
newvel = newvel - (vector_down * gravity)
if bullet.Imaginary then
-- the bullet has exited the map, but will continue being visible.
bullet.Pos = newpos
bullet.Vel = newvel
bullet.Travelled = bullet.Travelled + spd
if CLIENT and !ArcCW.ConVars["bullet_imaginary"]:GetBool() then
bullet.Dead = true
end
else
if attacker:IsPlayer() then
attacker:LagCompensation(true)
end
local tr
if bullet.HullSize then
local bb = Vector(bullet.HullSize / 2, bullet.HullSize / 2, bullet.HullSize / 2)
tr = util.TraceHull({
start = oldpos,
endpos = newpos,
filter = bullet.Filter,
mask = MASK_SHOT,
mins = -bb,
maxs = bb,
})
if ArcCW.ConVars["dev_shootinfo"]:GetInt() > 0 then
debugoverlay.Line(oldpos, tr.HitPos, 5, SERVER and Color(100,100,255) or Color(255,200,100), true)
debugoverlay.Box(tr.HitPos, -bb, bb, 5, SERVER and Color(100,100,255,0) or Color(255,200,100,0))
end
else
tr = util.TraceLine({
start = oldpos,
endpos = newpos,
filter = bullet.Filter,
mask = MASK_SHOT
})
if ArcCW.ConVars["dev_shootinfo"]:GetInt() > 0 then
debugoverlay.Line(oldpos, tr.HitPos, 5, SERVER and Color(100,100,255) or Color(255,200,100), true)
debugoverlay.Cross(tr.HitPos, 16, 0.05, SERVER and Color(100,100,255) or Color(255,200,100), true)
end
end
if attacker:IsPlayer() then
attacker:LagCompensation(false)
end
if tr.HitSky then
if CLIENT and ArcCW.ConVars["bullet_imaginary"]:GetBool() then
bullet.Imaginary = true
else
bullet.Dead = true
end
bullet.Pos = newpos
bullet.Vel = newvel
bullet.Travelled = bullet.Travelled + spd
if SERVER then
bullet.Dead = true
end
elseif tr.Hit then
bullet.Travelled = bullet.Travelled + (oldpos - tr.HitPos):Length()
bullet.Pos = tr.HitPos
-- if we're the client, we'll get the bullet back when it exits.
if attacker:IsPlayer() then
attacker:LagCompensation(true)
end
if SERVER then
debugoverlay.Cross(tr.HitPos, 5, 5, Color(100,100,255), true)
else
debugoverlay.Cross(tr.HitPos, 5, 5, Color(255,200,100), true)
end
local eid = tr.Entity:EntIndex()
if CLIENT then
-- do an impact effect and forget about it
if !game.SinglePlayer() and bullet.PhysBulletImpact then
attacker:FireBullets({
Src = oldpos,
Dir = dir,
Distance = spd + 16,
Tracer = 0,
Damage = 0,
IgnoreEntity = bullet.Attacker
})
end
bullet.Dead = true
if IsValid(bullet.Weapon) then
bullet.Weapon:GetBuff_Hook("Hook_PhysBulletHit", {bullet = bullet, tr = tr})
end
if bullet.PhysBulletHit then
bullet:PhysBulletHit(bullet, tr)
end
if bulinfo.PhysBulletHit then
bulinfo:PhysBulletHit(bullet, tr)
end
return
elseif SERVER then
local dmgtable
if IsValid(bullet.Weapon) then
bullet.Weapon:GetBuff_Hook("Hook_PhysBulletHit", {bullet = bullet, tr = tr})
dmgtable = bullet.Weapon.BodyDamageMults
dmgtable = bullet.Weapon:GetBuff_Override("Override_BodyDamageMults") or dmgtable
end
if bullet.PhysBulletHit then
bullet:PhysBulletHit(bullet, tr)
end
if bullet.PhysBulletImpact then
local delta = bullet.Travelled / (bullet.Range / ArcCW.HUToM)
delta = math.Clamp(delta, 0, 1)
-- deal some damage
attacker:FireBullets({
Src = oldpos,
Dir = dir,
Distance = spd + 16,
Tracer = 0,
Damage = 0,
IgnoreEntity = bullet.Attacker,
Callback = function(catt, ctr, cdmg)
ArcCW:BulletCallback(catt, ctr, cdmg, bullet, true)
end
}, true)
end
bullet.Damaged[eid] = true
bullet.Dead = true
end
if attacker:IsPlayer() then
attacker:LagCompensation(false)
end
else
-- bullet did not impact anything
bullet.Pos = tr.HitPos
bullet.Vel = newvel
bullet.Travelled = bullet.Travelled + spd
if bullet.Underwater then
if bit.band( util.PointContents( tr.HitPos ), CONTENTS_WATER ) != CONTENTS_WATER then
local utr = util.TraceLine({
start = tr.HitPos,
endpos = oldpos,
filter = bullet.Attacker,
mask = MASK_WATER
})
if utr.Hit then
local fx = EffectData()
fx:SetOrigin(utr.HitPos)
fx:SetScale(10)
util.Effect("gunshotsplash", fx)
end
bullet.Underwater = false
end
else
if bit.band( util.PointContents( tr.HitPos ), CONTENTS_WATER ) == CONTENTS_WATER then
local utr = util.TraceLine({
start = oldpos,
endpos = tr.HitPos,
filter = bullet.Attacker,
mask = MASK_WATER
})
if utr.Hit then
local fx = EffectData()
fx:SetOrigin(utr.HitPos)
fx:SetScale(10)
util.Effect("gunshotsplash", fx)
end
bullet.Underwater = true
end
end
end
end
bullet.OldPos = oldpos
local MaxDimensions = 16384 * 4
local WorldDimensions = 16384
if bullet.StartTime <= (CurTime() - ArcCW.ConVars["bullet_lifetime"]:GetFloat()) then
bullet.Dead = true
elseif !indim(bullet.Pos, MaxDimensions) then
bullet.Dead = true
elseif !indim(bullet.Pos, WorldDimensions) then
bullet.Imaginary = true
end
end
local head = Material("particle/fire")
local tracer = Material("effects/smoke_trail")
function ArcCW:DrawPhysBullets()
cam.Start3D()
for _, i in pairs(ArcCW.PhysBullets) do
local pro = i.Profile or 1
if pro == 7 then continue end -- legacy behavior: 7 is the "invisible" tracer
local bulinfo = ArcCW.BulletProfileDict[ArcCW.BulletProfiles[pro] or ""]
if bulinfo == nil then
print("Failed to find bullet info for profile " .. tostring(i) .. "!")
continue
end
-- Draw function override
if bulinfo.DrawBullet and bulinfo:DrawBullet(i) then
continue
end
i.VelStart = i.VelStart or Vector(i.Vel)
i.PosStart = i.PosStart or Vector(i.Pos)
local rpos = i.Pos
local vel = i.Vel - LocalPlayer():GetVelocity()
local veldir = vel:GetNormalized()
local dampfraction = 1
-- Solve two problems presented by physbullets
-- 1: they come out of the player's eyes and it looks jarring
-- 2: they fly too fast and so tracers aren't that noticeable
if !i.DampenVelocity then i.DampenVelocity = math.Clamp(math.floor(i.VelStart:Length() ^ (bulinfo.dampen_factor or 0.65)), 128, 4096) end
if !i.Imaginary and i.Travelled <= i.DampenVelocity then
if IsValid(i.Weapon) and i.Weapon:GetOwner() == LocalPlayer() then
-- Lerp towards the muzzle position, effectively slowing and dragging the bullet back.
-- Bullet will appear to accelerate suddenly near the threshold, but it should be too fast to notice.
if !i.TracerOrigin then
i.TracerOrigin = i.Weapon:GetTracerOrigin() or i.StartPos
end
dampfraction = (i.Travelled / i.DampenVelocity) ^ 0.5
rpos = LerpVector(dampfraction, i.TracerOrigin or i.PosStart, i.Pos)
if GetConVar("developer"):GetInt() >= 2 then
debugoverlay.Cross(i.TracerOrigin, 2, 5, Color(255, 0, 0), true)
debugoverlay.Cross(rpos, 8, 5, Color(0, 255, 255), true)
debugoverlay.Line(rpos, i.Pos, 5, Color(250, 150, 255), true)
debugoverlay.Cross(i.Pos, 4, 5, Color(255, 0, 255), true)
debugoverlay.Text(rpos, math.Round(dampfraction, 2), 5)
end
else
-- don't draw too close to the firing position if we can't lerp, or it will look ugly
continue
end
end
local col = bulinfo.color
-- TODO: Tracer sizes are still kinda wacky
local sqrdist = EyePos():DistToSqr(rpos)
local distgrow = math.log(sqrdist) ^ 0.5
local size = math.max(0, (bulinfo.size_min or 1) * 0.25 + (bulinfo.size or 1) * distgrow)
local headsize = size * math.Clamp(sqrdist / 4000000, 1, 16)
-- Head is less visible on the sides; it's mostly useful for the shooter and target as tracers aren't very visible from front and back
local dot = EyeAngles():Forward():Dot(veldir)
dot = math.Clamp(((dot * dot) - 0.5) * 2, 0, 1)
headsize = headsize * dot
--size = size * math.Clamp(1 - dot, 0.5, 1)
if bulinfo.sprite_head != false then
render.SetMaterial(bulinfo.sprite_head or head)
render.DrawSprite(rpos, headsize, headsize, col)
end
if bulinfo.sprite_tracer != false and !ArcCW.ConVars["fasttracers"]:GetBool() then
render.SetMaterial(bulinfo.sprite_tracer or tracer)
local len = math.min(vel:Length() * (bulinfo.tail_length or 0.015), 512, (rpos - (i.TracerOrigin or i.PosStart)):Length())
local pos2 = rpos - veldir * len
if i.TracerOrigin and CurTime() - i.StartTime <= engine.TickInterval() then
pos2 = rpos - (rpos - i.TracerOrigin):GetNormalized() * len
end
render.DrawBeam(rpos, pos2, size * 0.25, 0, 0.5, col)
debugoverlay.Line(rpos, pos2, 7, Color(0, 255, 0), true)
end
if bulinfo.model then
if !IsValid(i.CSModel) then
i.CSModel = ClientsideModel(bulinfo.model)
i.CSModel:SetNoDraw(bulinfo.model_nodraw)
if bulinfo.particle then
i.CSParticle = CreateParticleSystem(i.CSModel, bulinfo.particle, PATTACH_ABSORIGIN_FOLLOW, 1)
end
end
i.CSModel:SetPos(rpos)
i.CSModel:SetAngles(i.Vel:Angle())
i.CSModel:SetVelocity(i.Vel)
if i.CSParticle then
i.CSParticle:StartEmission()
i.CSParticle:SetSortOrigin(IsValid(i.Weapon) and i.Weapon:GetShootSrc() or vector_origin)
end
end
end
cam.End3D()
end
hook.Add("PreDrawEffects", "ArcCW_DrawPhysBullets", ArcCW.DrawPhysBullets)
hook.Add("PostCleanupMap", "ArcCW_CleanPhysBullets", function()
ArcCW.PhysBullets = {}
end)
-- Can't run now or files after this in load order cannot add them properly
hook.Add("InitPostEntity", "ArcCW_AddPhysBullets", function()
hook.Run("ArcCW_InitBulletProfiles")
end)