Skip to content

Commit 26ec84c

Browse files
committed
force part preview forces (draw lines)
draws vector lines and their XYZ components reflecting the linear forces applied to targets, it doesn't render for non-owner players because of the calculations +stop sending force actions if no targets are selected
1 parent 026178b commit 26ec84c

File tree

1 file changed

+319
-4
lines changed

1 file changed

+319
-4
lines changed

lua/pac3/core/client/parts/force.lua

Lines changed: 319 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ BUILDER:StartStorableVars()
1919
}})
2020
:GetSet("Length", 50, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,-32768,32767)) end})
2121
:GetSet("Radius", 50, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,-32768,32767)) end})
22-
:GetSet("Preview",false)
22+
:GetSet("Preview",false, {description = "preview target selection boxes"})
23+
:GetSet("PreviewForces",false, {description = "preview the predicted forces"})
2324

2425
:SetPropertyGroup("BaseForces")
2526
:GetSet("BaseForce", 0)
@@ -57,7 +58,7 @@ Radial gets the base directions from the targets to the force part]]})
5758
:GetSet("AccountMass", false, {description = "Apply acceleration according to mass."})
5859
:GetSet("Falloff", false, {description = "Whether the force to apply should fade with distance"})
5960
:GetSet("ReverseFalloff", false, {description = "The reverse of the falloff means the force fades when getting closer."})
60-
:GetSet("Levitation", false, {description = "Tries to stabilize the force to levitate targets at a certain height relative to the part"})
61+
: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."})
6162
:GetSet("LevitationHeight", 0)
6263

6364
:SetPropertyGroup("Damping")
@@ -102,12 +103,325 @@ function PART:OnRemove()
102103
end
103104

104105

106+
local white = Color(255,255,255)
107+
local red = Color(255,0,0)
108+
local green = Color(0,255,0)
109+
local blue = Color(0,0,255)
110+
local red2 = Color(255,100,100)
111+
local red3 = Color(255,200,200)
112+
local function draw_force_line(pos, amount)
113+
local length = amount:Length()
114+
local magnitude = length / 20
115+
amount:Normalize()
116+
local x = amount.x
117+
local y = amount.y
118+
local z = amount.z
119+
local dir = amount:Angle()
120+
render.DrawLine( pos, 9 * magnitude * x * Vector(1,0,0) + pos, red, false)
121+
render.DrawLine( pos, 9 * magnitude * y * Vector(0,1,0) + pos, green, false)
122+
render.DrawLine( pos, 9 * magnitude * z * Vector(0,0,1) + pos, blue, false)
123+
cam.IgnoreZ( true )
124+
for i=0,8,1 do
125+
local scrolling = -i + math.floor((CurTime() % 1) * 8) + 2
126+
if scrolling == 0 then
127+
render.DrawLine( (i) * magnitude * amount + pos, (i+1) * magnitude * amount + pos, red, false)
128+
elseif scrolling == 1 then
129+
render.DrawLine( (i) * magnitude * amount + pos, (i+1) * magnitude * amount + pos, red2, false)
130+
elseif scrolling == 2 then
131+
render.DrawLine( (i) * magnitude * amount + pos, (i+1) * magnitude * amount + pos, red3, false)
132+
else
133+
render.DrawLine( (i) * magnitude * amount + pos, (i+1) * magnitude * amount + pos, white, false)
134+
end
135+
136+
end
137+
cam.IgnoreZ( false )
138+
end
139+
140+
141+
--convenience functions and tables from net_combat
142+
143+
local pre_excluded_ent_classes = {
144+
["info_player_start"] = true,
145+
["aoc_spawnpoint"] = true,
146+
["info_player_teamspawn"] = true,
147+
["env_tonemap_controller"] = true,
148+
["env_fog_controller"] = true,
149+
["env_skypaint"] = true,
150+
["shadow_control"] = true,
151+
["env_sun"] = true,
152+
["predicted_viewmodel"] = true,
153+
["physgun_beam"] = true,
154+
["ambient_generic"] = true,
155+
["trigger_once"] = true,
156+
["trigger_multiple"] = true,
157+
["trigger_hurt"] = true,
158+
["info_ladder_dismount"] = true,
159+
["info_particle_system"] = true,
160+
["env_sprite"] = true,
161+
["env_fire"] = true,
162+
["env_soundscape"] = true,
163+
["env_smokestack"] = true,
164+
["light"] = true,
165+
["move_rope"] = true,
166+
["keyframe_rope"] = true,
167+
["env_soundscape_proxy"] = true,
168+
["gmod_hands"] = true,
169+
["env_lightglow"] = true,
170+
["point_spotlight"] = true,
171+
["spotlight_end"] = true,
172+
["beam"] = true,
173+
["info_target"] = true,
174+
["func_lod"] = true,
175+
["func_brush"] = true,
176+
["phys_bone_follower"] = true,
177+
}
178+
179+
local physics_point_ent_classes = {
180+
["prop_physics"] = true,
181+
["prop_physics_multiplayer"] = true,
182+
["prop_ragdoll"] = true,
183+
["weapon_striderbuster"] = true,
184+
["item_item_crate"] = true,
185+
["func_breakable_surf"] = true,
186+
["func_breakable"] = true,
187+
["physics_cannister"] = true
188+
}
189+
190+
local function MergeTargetsByID(tbl1, tbl2)
191+
for i,v in ipairs(tbl2) do
192+
tbl1[v:EntIndex()] = v
193+
end
194+
end
195+
196+
local function Is_NPC(ent)
197+
return ent:IsNPC() or ent:IsNextBot() or ent.IsDrGEntity or ent.IsVJBaseSNPC
198+
end
199+
200+
local function ProcessForcesList(ents_hits, tbl, pos, ang, ply)
201+
for i,v in pairs(ents_hits) do
202+
if pre_excluded_ent_classes[v:GetClass()] then ents_hits[i] = nil end
203+
end
204+
local ftime = 0.016 --approximate tick duration
205+
local BASEFORCE = 0
206+
local VECFORCE = Vector(0,0,0)
207+
if tbl.Continuous then
208+
BASEFORCE = tbl.BaseForce * ftime * 3.3333 --weird value to equalize how 600 cancels out gravity
209+
VECFORCE = tbl.AddedVectorForce * ftime * 3.3333
210+
else
211+
BASEFORCE = tbl.BaseForce
212+
VECFORCE = tbl.AddedVectorForce
213+
end
214+
for _,ent in pairs(ents_hits) do
215+
if ent:IsWeapon() or ent:GetClass() == "viewmodel" or ent:GetClass() == "func_physbox_multiplayer" then continue end
216+
if ent:GetPos():Distance(ply:GetPos()) < 300 then
217+
print(ent)
218+
end
219+
local phys_ent
220+
local is_player = ent:IsPlayer()
221+
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())))
222+
local is_npc = Is_NPC(ent)
223+
224+
if is_npc and not tbl.NPC then continue end
225+
if is_player and not (tbl.Players or ent == tbl:GetPlayerOwner()) then continue end
226+
if is_player and not tbl.AffectSelf and ent == tbl:GetPlayerOwner() then continue end
227+
if is_physics and not tbl.PhysicsProps then continue end
228+
if not is_npc and not is_player and not is_physics then
229+
if not tbl.PointEntities then continue end
230+
end
231+
232+
local is_phys = true
233+
phys_ent = ent
234+
is_phys = false
235+
236+
local oldvel
237+
238+
if IsValid(phys_ent) then
239+
oldvel = phys_ent:GetVelocity()
240+
else
241+
oldvel = Vector(0,0,0)
242+
end
243+
244+
245+
local addvel = Vector(0,0,0)
246+
local add_angvel = Vector(0,0,0)
247+
248+
local ent_center = ent:WorldSpaceCenter() or ent:GetPos()
249+
250+
local dir = ent_center - pos --part
251+
local locus_pos = pos
252+
if tbl.Locus ~= nil then
253+
if tbl.Locus:IsValid() then
254+
locus_pos = tbl.Locus:GetWorldPosition()
255+
end
256+
end
257+
local dir2 = ent_center - locus_pos
258+
259+
local dist_multiplier = 1
260+
local damping_dist_mult = 1
261+
local up_mult = 1
262+
local distance = (ent_center - pos):Length()
263+
local height_delta = pos.z + tbl.LevitationHeight - ent_center.z
264+
265+
--what it do
266+
--if delta is -100 (ent is lower than the desired height), that means +100 adjustment direction
267+
--height decides how much to knee the force until it equalizes at 0
268+
--clamp the delta to the ratio levitation height
269+
270+
if tbl.Levitation then
271+
up_mult = math.Clamp(height_delta / (5 + math.abs(tbl.LevitationHeight)),-1,1)
272+
end
273+
274+
if tbl.BaseForceAngleMode == "Radial" then --radial on self
275+
addvel = dir:GetNormalized() * tbl.BaseForce
276+
elseif tbl.BaseForceAngleMode == "Locus" then --radial on locus
277+
addvel = dir2:GetNormalized() * tbl.BaseForce
278+
elseif tbl.BaseForceAngleMode == "Local" then --forward on self
279+
addvel = ang:Forward() * tbl.BaseForce
280+
end
281+
282+
if tbl.VectorForceAngleMode == "Global" then --global
283+
addvel = addvel + tbl.AddedVectorForce
284+
elseif tbl.VectorForceAngleMode == "Local" then --local on self
285+
addvel = addvel
286+
+ang:Forward()*tbl.AddedVectorForce.x
287+
+ang:Right()*tbl.AddedVectorForce.y
288+
+ang:Up()*tbl.AddedVectorForce.z
289+
290+
elseif tbl.VectorForceAngleMode == "Radial" then --relative to locus or self
291+
ang2 = dir:Angle()
292+
addvel = addvel
293+
+ang2:Forward()*tbl.AddedVectorForce.x
294+
+ang2:Right()*tbl.AddedVectorForce.y
295+
+ang2:Up()*tbl.AddedVectorForce.z
296+
elseif tbl.VectorForceAngleMode == "RadialNoPitch" then --relative to locus or self
297+
dir.z = 0
298+
ang2 = dir:Angle()
299+
addvel = addvel
300+
+ang2:Forward()*tbl.AddedVectorForce.x
301+
+ang2:Right()*tbl.AddedVectorForce.y
302+
+ang2:Up()*tbl.AddedVectorForce.z
303+
end
304+
305+
--[[if tbl.TorqueMode == "Global" then
306+
add_angvel = tbl.Torque
307+
elseif tbl.TorqueMode == "Local" then
308+
add_angvel = ang:Forward()*tbl.Torque.x + ang:Right()*tbl.Torque.y + ang:Up()*tbl.Torque.z
309+
elseif tbl.TorqueMode == "TargetLocal" then
310+
add_angvel = tbl.Torque
311+
elseif tbl.TorqueMode == "Radial" then
312+
ang2 = dir:Angle()
313+
addvel = ang2:Forward()*tbl.Torque.x + ang2:Right()*tbl.Torque.y + ang2:Up()*tbl.Torque.z
314+
end]]
315+
316+
local mass = 1
317+
if IsValid(phys_ent) then
318+
if phys_ent.GetMass then
319+
phys_ent:GetMass()
320+
end
321+
end
322+
if is_phys and tbl.AccountMass then
323+
if not is_npc then
324+
addvel = addvel * (1 / math.max(mass,0.1))
325+
else
326+
addvel = addvel
327+
end
328+
add_angvel = add_angvel * (1 / math.max(mass,0.1))
329+
end
330+
331+
if tbl.Falloff then
332+
dist_multiplier = math.Clamp(1 - distance / math.max(tbl.Radius, tbl.Length),0,1)
333+
end
334+
if tbl.ReverseFalloff then
335+
dist_multiplier = 1 - math.Clamp(1 - distance / math.max(tbl.Radius, tbl.Length),0,1)
336+
end
337+
338+
if tbl.DampingFalloff then
339+
damping_dist_mult = math.Clamp(1 - distance / math.max(tbl.Radius, tbl.Length),0,1)
340+
end
341+
if tbl.DampingReverseFalloff then
342+
damping_dist_mult = 1 - math.Clamp(1 - distance / math.max(tbl.Radius, tbl.Length),0,1)
343+
end
344+
damping_dist_mult = damping_dist_mult
345+
local final_damping = 1 - (tbl.Damping * damping_dist_mult)
346+
347+
if tbl.Levitation then
348+
addvel.z = addvel.z * up_mult
349+
end
350+
351+
addvel = addvel * dist_multiplier
352+
draw_force_line(ent:WorldSpaceCenter(), addvel)
353+
354+
end
355+
end
356+
357+
local function preview_process_ents(tbl)
358+
ply = tbl:GetPlayerOwner()
359+
local pos = tbl.pos
360+
local ang = tbl.ang
361+
362+
if tbl.HitboxMode == "Sphere" then
363+
local ents_hits = ents.FindInSphere(pos, tbl.Radius)
364+
ProcessForcesList(ents_hits, tbl, pos, ang, ply)
365+
elseif tbl.HitboxMode == "Box" then
366+
local mins
367+
local maxs
368+
if tbl.HitboxMode == "Box" then
369+
mins = pos - Vector(tbl.Radius, tbl.Radius, tbl.Length)
370+
maxs = pos + Vector(tbl.Radius, tbl.Radius, tbl.Length)
371+
end
372+
373+
local ents_hits = ents.FindInBox(mins, maxs)
374+
ProcessForcesList(ents_hits, tbl, pos, ang, ply)
375+
elseif tbl.HitboxMode == "Cylinder" then
376+
local ents_hits = {}
377+
if tbl.Length ~= 0 and tbl.Radius ~= 0 then
378+
local counter = 0
379+
MergeTargetsByID(ents_hits,ents.FindInSphere(pos, tbl.Radius))
380+
for i=0,1,1/(math.abs(tbl.Length/tbl.Radius)) do
381+
MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length*i, tbl.Radius))
382+
if counter == 200 then break end
383+
counter = counter + 1
384+
end
385+
MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length, tbl.Radius))
386+
--render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*(self.Length - 0.5*self.Radius), 0.5*self.Radius, 10, 10, Color( 255, 255, 255 ) )
387+
elseif tbl.Radius == 0 then MergeTargetsByID(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end
388+
ProcessForcesList(ents_hits, tbl, pos, ang, ply)
389+
elseif tbl.HitboxMode == "Cone" then
390+
local ents_hits = {}
391+
local steps
392+
steps = math.Clamp(4*math.ceil(tbl.Length / (tbl.Radius or 1)),1,50)
393+
for i = 1,0,-1/steps do
394+
MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length*i, i * tbl.Radius))
395+
end
396+
397+
steps = math.Clamp(math.ceil(tbl.Length / (tbl.Radius or 1)),1,4)
398+
399+
if tbl.Radius == 0 then MergeTargetsByID(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end
400+
ProcessForcesList(ents_hits, tbl, pos, ang, ply)
401+
elseif tbl.HitboxMode =="Ray" then
402+
local startpos = pos + Vector(0,0,0)
403+
local endpos = pos + ang:Forward()*tbl.Length
404+
ents_hits = ents.FindAlongRay(startpos, endpos)
405+
ProcessForcesList(ents_hits, tbl, pos, ang, ply)
406+
end
407+
end
408+
105409
function PART:OnDraw()
106410
self.pos,self.ang = self:GetDrawPosition()
107-
if not self.Preview then pac.RemoveHook("PostDrawOpaqueRenderables", "pac_force_Draw"..self.UniqueID) end
411+
if not self.Preview and not self.PreviewForces then pac.RemoveHook("PostDrawOpaqueRenderables", "pac_force_Draw"..self.UniqueID) end
108412

109-
if self.Preview then
413+
if self.Preview or self.PreviewForces then
110414
pac.AddHook("PostDrawOpaqueRenderables", "pac_force_Draw"..self.UniqueID, function()
415+
if self.PreviewForces then
416+
--recalculating forces every drawframe is cringe for other players
417+
if self:GetPlayerOwner() == pac.LocalPlayer then
418+
if self.NPC or self.Players or self.AffectSelf or self.PhysicsProps or self.PointEntities then
419+
preview_process_ents(self)
420+
end
421+
end
422+
end
423+
if not self.Preview then return end
424+
111425
if self.HitboxMode == "Box" then
112426
local mins = Vector(-self.Radius, -self.Radius, -self.Length)
113427
local maxs = Vector(self.Radius, self.Radius, self.Length)
@@ -204,6 +518,7 @@ function PART:Impulse(on)
204518
end
205519
end
206520

521+
if not self.NPC and not self.Players and not self.AffectSelf and not self.PhysicsProps and not self.PointEntities then return end
207522
net.Start("pac_request_force", true)
208523
net.WriteVector(self:GetWorldPosition())
209524
net.WriteAngle(self:GetWorldAngles())

0 commit comments

Comments
 (0)