diff --git a/lua/pac3/core/client/parts/force.lua b/lua/pac3/core/client/parts/force.lua index 7c0c20c26..ccd038c91 100644 --- a/lua/pac3/core/client/parts/force.lua +++ b/lua/pac3/core/client/parts/force.lua @@ -19,7 +19,8 @@ BUILDER:StartStorableVars() }}) :GetSet("Length", 50, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,-32768,32767)) end}) :GetSet("Radius", 50, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,-32768,32767)) end}) - :GetSet("Preview",false) + :GetSet("Preview",false, {description = "preview target selection boxes"}) + :GetSet("PreviewForces",false, {description = "preview the predicted forces"}) :SetPropertyGroup("BaseForces") :GetSet("BaseForce", 0) @@ -57,7 +58,7 @@ Radial gets the base directions from the targets to the force part]]}) :GetSet("AccountMass", false, {description = "Apply acceleration according to mass."}) :GetSet("Falloff", false, {description = "Whether the force to apply should fade with distance"}) :GetSet("ReverseFalloff", false, {description = "The reverse of the falloff means the force fades when getting closer."}) - :GetSet("Levitation", false, {description = "Tries to stabilize the force to levitate targets at a certain height relative to the part"}) + :GetSet("Levitation", false, {description = "Tries to stabilize the force to levitate targets at a certain height relative to the part.\nRequires vertical forces. Easiest way is to enter 0 0 500 in 'added vector force' with the Global vector mode which is already there by default."}) :GetSet("LevitationHeight", 0) :SetPropertyGroup("Damping") @@ -102,12 +103,325 @@ function PART:OnRemove() end +local white = Color(255,255,255) +local red = Color(255,0,0) +local green = Color(0,255,0) +local blue = Color(0,0,255) +local red2 = Color(255,100,100) +local red3 = Color(255,200,200) +local function draw_force_line(pos, amount) + local length = amount:Length() + local magnitude = length / 20 + amount:Normalize() + local x = amount.x + local y = amount.y + local z = amount.z + local dir = amount:Angle() + render.DrawLine( pos, 9 * magnitude * x * Vector(1,0,0) + pos, red, false) + render.DrawLine( pos, 9 * magnitude * y * Vector(0,1,0) + pos, green, false) + render.DrawLine( pos, 9 * magnitude * z * Vector(0,0,1) + pos, blue, false) + cam.IgnoreZ( true ) + for i=0,8,1 do + local scrolling = -i + math.floor((CurTime() % 1) * 8) + 2 + if scrolling == 0 then + render.DrawLine( (i) * magnitude * amount + pos, (i+1) * magnitude * amount + pos, red, false) + elseif scrolling == 1 then + render.DrawLine( (i) * magnitude * amount + pos, (i+1) * magnitude * amount + pos, red2, false) + elseif scrolling == 2 then + render.DrawLine( (i) * magnitude * amount + pos, (i+1) * magnitude * amount + pos, red3, false) + else + render.DrawLine( (i) * magnitude * amount + pos, (i+1) * magnitude * amount + pos, white, false) + end + + end + cam.IgnoreZ( false ) +end + + +--convenience functions and tables from net_combat + +local pre_excluded_ent_classes = { + ["info_player_start"] = true, + ["aoc_spawnpoint"] = true, + ["info_player_teamspawn"] = true, + ["env_tonemap_controller"] = true, + ["env_fog_controller"] = true, + ["env_skypaint"] = true, + ["shadow_control"] = true, + ["env_sun"] = true, + ["predicted_viewmodel"] = true, + ["physgun_beam"] = true, + ["ambient_generic"] = true, + ["trigger_once"] = true, + ["trigger_multiple"] = true, + ["trigger_hurt"] = true, + ["info_ladder_dismount"] = true, + ["info_particle_system"] = true, + ["env_sprite"] = true, + ["env_fire"] = true, + ["env_soundscape"] = true, + ["env_smokestack"] = true, + ["light"] = true, + ["move_rope"] = true, + ["keyframe_rope"] = true, + ["env_soundscape_proxy"] = true, + ["gmod_hands"] = true, + ["env_lightglow"] = true, + ["point_spotlight"] = true, + ["spotlight_end"] = true, + ["beam"] = true, + ["info_target"] = true, + ["func_lod"] = true, + ["func_brush"] = true, + ["phys_bone_follower"] = true, +} + +local physics_point_ent_classes = { + ["prop_physics"] = true, + ["prop_physics_multiplayer"] = true, + ["prop_ragdoll"] = true, + ["weapon_striderbuster"] = true, + ["item_item_crate"] = true, + ["func_breakable_surf"] = true, + ["func_breakable"] = true, + ["physics_cannister"] = true +} + +local function MergeTargetsByID(tbl1, tbl2) + for i,v in ipairs(tbl2) do + tbl1[v:EntIndex()] = v + end +end + +local function Is_NPC(ent) + return ent:IsNPC() or ent:IsNextBot() or ent.IsDrGEntity or ent.IsVJBaseSNPC +end + +local function ProcessForcesList(ents_hits, tbl, pos, ang, ply) + for i,v in pairs(ents_hits) do + if pre_excluded_ent_classes[v:GetClass()] then ents_hits[i] = nil end + end + local ftime = 0.016 --approximate tick duration + local BASEFORCE = 0 + local VECFORCE = Vector(0,0,0) + if tbl.Continuous then + BASEFORCE = tbl.BaseForce * ftime * 3.3333 --weird value to equalize how 600 cancels out gravity + VECFORCE = tbl.AddedVectorForce * ftime * 3.3333 + else + BASEFORCE = tbl.BaseForce + VECFORCE = tbl.AddedVectorForce + end + for _,ent in pairs(ents_hits) do + if ent:IsWeapon() or ent:GetClass() == "viewmodel" or ent:GetClass() == "func_physbox_multiplayer" then continue end + if ent:GetPos():Distance(ply:GetPos()) < 300 then + print(ent) + end + local phys_ent + local is_player = ent:IsPlayer() + local is_physics = (physics_point_ent_classes[ent:GetClass()] or string.find(ent:GetClass(),"item_") or string.find(ent:GetClass(),"ammo_") or (ent:IsWeapon() and not IsValid(ent:GetOwner()))) + local is_npc = Is_NPC(ent) + + if is_npc and not tbl.NPC then continue end + if is_player and not (tbl.Players or ent == tbl:GetPlayerOwner()) then continue end + if is_player and not tbl.AffectSelf and ent == tbl:GetPlayerOwner() then continue end + if is_physics and not tbl.PhysicsProps then continue end + if not is_npc and not is_player and not is_physics then + if not tbl.PointEntities then continue end + end + + local is_phys = true + phys_ent = ent + is_phys = false + + local oldvel + + if IsValid(phys_ent) then + oldvel = phys_ent:GetVelocity() + else + oldvel = Vector(0,0,0) + end + + + local addvel = Vector(0,0,0) + local add_angvel = Vector(0,0,0) + + local ent_center = ent:WorldSpaceCenter() or ent:GetPos() + + local dir = ent_center - pos --part + local locus_pos = pos + if tbl.Locus ~= nil then + if tbl.Locus:IsValid() then + locus_pos = tbl.Locus:GetWorldPosition() + end + end + local dir2 = ent_center - locus_pos + + local dist_multiplier = 1 + local damping_dist_mult = 1 + local up_mult = 1 + local distance = (ent_center - pos):Length() + local height_delta = pos.z + tbl.LevitationHeight - ent_center.z + + --what it do + --if delta is -100 (ent is lower than the desired height), that means +100 adjustment direction + --height decides how much to knee the force until it equalizes at 0 + --clamp the delta to the ratio levitation height + + if tbl.Levitation then + up_mult = math.Clamp(height_delta / (5 + math.abs(tbl.LevitationHeight)),-1,1) + end + + if tbl.BaseForceAngleMode == "Radial" then --radial on self + addvel = dir:GetNormalized() * tbl.BaseForce + elseif tbl.BaseForceAngleMode == "Locus" then --radial on locus + addvel = dir2:GetNormalized() * tbl.BaseForce + elseif tbl.BaseForceAngleMode == "Local" then --forward on self + addvel = ang:Forward() * tbl.BaseForce + end + + if tbl.VectorForceAngleMode == "Global" then --global + addvel = addvel + tbl.AddedVectorForce + elseif tbl.VectorForceAngleMode == "Local" then --local on self + addvel = addvel + +ang:Forward()*tbl.AddedVectorForce.x + +ang:Right()*tbl.AddedVectorForce.y + +ang:Up()*tbl.AddedVectorForce.z + + elseif tbl.VectorForceAngleMode == "Radial" then --relative to locus or self + ang2 = dir:Angle() + addvel = addvel + +ang2:Forward()*tbl.AddedVectorForce.x + +ang2:Right()*tbl.AddedVectorForce.y + +ang2:Up()*tbl.AddedVectorForce.z + elseif tbl.VectorForceAngleMode == "RadialNoPitch" then --relative to locus or self + dir.z = 0 + ang2 = dir:Angle() + addvel = addvel + +ang2:Forward()*tbl.AddedVectorForce.x + +ang2:Right()*tbl.AddedVectorForce.y + +ang2:Up()*tbl.AddedVectorForce.z + end + + --[[if tbl.TorqueMode == "Global" then + add_angvel = tbl.Torque + elseif tbl.TorqueMode == "Local" then + add_angvel = ang:Forward()*tbl.Torque.x + ang:Right()*tbl.Torque.y + ang:Up()*tbl.Torque.z + elseif tbl.TorqueMode == "TargetLocal" then + add_angvel = tbl.Torque + elseif tbl.TorqueMode == "Radial" then + ang2 = dir:Angle() + addvel = ang2:Forward()*tbl.Torque.x + ang2:Right()*tbl.Torque.y + ang2:Up()*tbl.Torque.z + end]] + + local mass = 1 + if IsValid(phys_ent) then + if phys_ent.GetMass then + phys_ent:GetMass() + end + end + if is_phys and tbl.AccountMass then + if not is_npc then + addvel = addvel * (1 / math.max(mass,0.1)) + else + addvel = addvel + end + add_angvel = add_angvel * (1 / math.max(mass,0.1)) + end + + if tbl.Falloff then + dist_multiplier = math.Clamp(1 - distance / math.max(tbl.Radius, tbl.Length),0,1) + end + if tbl.ReverseFalloff then + dist_multiplier = 1 - math.Clamp(1 - distance / math.max(tbl.Radius, tbl.Length),0,1) + end + + if tbl.DampingFalloff then + damping_dist_mult = math.Clamp(1 - distance / math.max(tbl.Radius, tbl.Length),0,1) + end + if tbl.DampingReverseFalloff then + damping_dist_mult = 1 - math.Clamp(1 - distance / math.max(tbl.Radius, tbl.Length),0,1) + end + damping_dist_mult = damping_dist_mult + local final_damping = 1 - (tbl.Damping * damping_dist_mult) + + if tbl.Levitation then + addvel.z = addvel.z * up_mult + end + + addvel = addvel * dist_multiplier + draw_force_line(ent:WorldSpaceCenter(), addvel) + + end +end + +local function preview_process_ents(tbl) + ply = tbl:GetPlayerOwner() + local pos = tbl.pos + local ang = tbl.ang + + if tbl.HitboxMode == "Sphere" then + local ents_hits = ents.FindInSphere(pos, tbl.Radius) + ProcessForcesList(ents_hits, tbl, pos, ang, ply) + elseif tbl.HitboxMode == "Box" then + local mins + local maxs + if tbl.HitboxMode == "Box" then + mins = pos - Vector(tbl.Radius, tbl.Radius, tbl.Length) + maxs = pos + Vector(tbl.Radius, tbl.Radius, tbl.Length) + end + + local ents_hits = ents.FindInBox(mins, maxs) + ProcessForcesList(ents_hits, tbl, pos, ang, ply) + elseif tbl.HitboxMode == "Cylinder" then + local ents_hits = {} + if tbl.Length ~= 0 and tbl.Radius ~= 0 then + local counter = 0 + MergeTargetsByID(ents_hits,ents.FindInSphere(pos, tbl.Radius)) + for i=0,1,1/(math.abs(tbl.Length/tbl.Radius)) do + MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length*i, tbl.Radius)) + if counter == 200 then break end + counter = counter + 1 + end + MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length, tbl.Radius)) + --render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*(self.Length - 0.5*self.Radius), 0.5*self.Radius, 10, 10, Color( 255, 255, 255 ) ) + elseif tbl.Radius == 0 then MergeTargetsByID(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end + ProcessForcesList(ents_hits, tbl, pos, ang, ply) + elseif tbl.HitboxMode == "Cone" then + local ents_hits = {} + local steps + steps = math.Clamp(4*math.ceil(tbl.Length / (tbl.Radius or 1)),1,50) + for i = 1,0,-1/steps do + MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length*i, i * tbl.Radius)) + end + + steps = math.Clamp(math.ceil(tbl.Length / (tbl.Radius or 1)),1,4) + + if tbl.Radius == 0 then MergeTargetsByID(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end + ProcessForcesList(ents_hits, tbl, pos, ang, ply) + elseif tbl.HitboxMode =="Ray" then + local startpos = pos + Vector(0,0,0) + local endpos = pos + ang:Forward()*tbl.Length + ents_hits = ents.FindAlongRay(startpos, endpos) + ProcessForcesList(ents_hits, tbl, pos, ang, ply) + end +end + function PART:OnDraw() self.pos,self.ang = self:GetDrawPosition() - if not self.Preview then pac.RemoveHook("PostDrawOpaqueRenderables", "pac_force_Draw"..self.UniqueID) end + if not self.Preview and not self.PreviewForces then pac.RemoveHook("PostDrawOpaqueRenderables", "pac_force_Draw"..self.UniqueID) end - if self.Preview then + if self.Preview or self.PreviewForces then pac.AddHook("PostDrawOpaqueRenderables", "pac_force_Draw"..self.UniqueID, function() + if self.PreviewForces then + --recalculating forces every drawframe is cringe for other players + if self:GetPlayerOwner() == pac.LocalPlayer then + if self.NPC or self.Players or self.AffectSelf or self.PhysicsProps or self.PointEntities then + preview_process_ents(self) + end + end + end + if not self.Preview then return end + if self.HitboxMode == "Box" then local mins = Vector(-self.Radius, -self.Radius, -self.Length) local maxs = Vector(self.Radius, self.Radius, self.Length) @@ -204,6 +518,7 @@ function PART:Impulse(on) end end + if not self.NPC and not self.Players and not self.AffectSelf and not self.PhysicsProps and not self.PointEntities then return end net.Start("pac_request_force", true) net.WriteVector(self:GetWorldPosition()) net.WriteAngle(self:GetWorldAngles())