Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs
1 using FarseerPhysics;
2 using FarseerPhysics.Dynamics;
3 using FarseerPhysics.Dynamics.Contacts;
4 using Microsoft.Xna.Framework;
5 using System;
6 using System.Collections.Generic;
7 using System.Globalization;
8 using System.Linq;
9 
11 {
13  {
14  [Editable, Serialize(0.0f, IsPropertySaveable.Yes, description: "The maximum amount of force applied to the triggering entitites.", alwaysUseInstanceValues: true)]
15  public float Force { get; set; }
16  [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Determines if the force gets higher the closer the triggerer is to the center of the trigger.", alwaysUseInstanceValues: true)]
17  public bool DistanceBasedForce { get; set; }
18  [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Determines if the force fluctuates over time or if it stays constant.", alwaysUseInstanceValues: true)]
19  public bool ForceFluctuation { get; set; }
20  [Serialize(1.0f, IsPropertySaveable.Yes, description: "How much the fluctuation affects the force. 1 is the maximum fluctuation, 0 is no fluctuation.", alwaysUseInstanceValues: true)]
21  private float ForceFluctuationStrength
22  {
23  get
24  {
25  return forceFluctuationStrength;
26  }
27  set
28  {
29  forceFluctuationStrength = Math.Clamp(value, 0.0f, 1.0f);
30  }
31  }
32  [Serialize(1.0f, IsPropertySaveable.Yes, description: "How fast (cycles per second) the force fluctuates.", alwaysUseInstanceValues: true)]
33  private float ForceFluctuationFrequency
34  {
35  get
36  {
37  return forceFluctuationFrequency;
38  }
39  set
40  {
41  forceFluctuationFrequency = Math.Max(value, 0.01f);
42  }
43  }
44  [Serialize(0.01f, IsPropertySaveable.Yes, description: "How often (in seconds) the force fluctuation is calculated.", alwaysUseInstanceValues: true)]
45  private float ForceFluctuationInterval
46  {
47  get
48  {
49  return forceFluctuationInterval;
50  }
51  set
52  {
53  forceFluctuationInterval = Math.Max(value, 0.01f);
54  }
55  }
56 
57  public PhysicsBody PhysicsBody { get; private set; }
58  private float Radius { get; set; }
59  private float RadiusInDisplayUnits { get; set; }
60  private bool TriggeredOnce { get; set; }
61  private float CurrentForceFluctuation { get; set; } = 1.0f;
62  public bool TriggerActive { get; private set; }
63  private float ForceFluctuationTimer { get; set; }
64  private static float TimeInLevel
65  {
66  get
67  {
68  return GameMain.GameSession?.RoundDuration ?? 0.0f;
69  }
70  }
71 
72  [Serialize(false, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
73  public bool ApplyEffectsToCharactersInsideSub { get; set; }
74 
75  [Serialize(false, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
76  public bool MoveOutsideSub { get; set; }
77 
78  private readonly LevelTrigger.TriggererType triggeredBy;
79  private readonly HashSet<Entity> triggerers = new HashSet<Entity>();
80  private readonly bool triggerOnce;
81  private readonly List<ISerializableEntity> statusEffectTargets = new List<ISerializableEntity>();
85  private readonly List<StatusEffect> statusEffects = new List<StatusEffect>();
89  private readonly List<Attack> attacks = new List<Attack>();
90 
91  private float forceFluctuationStrength;
92  private float forceFluctuationFrequency;
93  private float forceFluctuationInterval;
94 
95  public TriggerComponent(Item item, ContentXElement element) : base(item, element)
96  {
97  string triggeredByAttribute = element.GetAttributeString("triggeredby", "Character");
98  if (!Enum.TryParse(triggeredByAttribute, out triggeredBy))
99  {
100  DebugConsole.ThrowError($"Error in ForceComponent config: \"{triggeredByAttribute}\" is not a valid triggerer type.",
101  contentPackage: element.ContentPackage);
102  }
103  triggerOnce = element.GetAttributeBool("triggeronce", false);
104  string parentDebugName = $"TriggerComponent in {item.Name}";
105  foreach (var subElement in element.Elements())
106  {
107  switch (subElement.Name.ToString().ToLowerInvariant())
108  {
109  case "statuseffect":
110  LevelTrigger.LoadStatusEffect(statusEffects, subElement, parentDebugName);
111  break;
112  case "attack":
113  case "damage":
114  LevelTrigger.LoadAttack(subElement, parentDebugName, triggerOnce, attacks);
115  break;
116  }
117  }
118  IsActive = true;
119  }
120 
121  public override void OnItemLoaded()
122  {
123  base.OnItemLoaded();
124  float radiusAttribute = originalElement.GetAttributeFloat("radius", 10.0f);
125  Radius = ConvertUnits.ToSimUnits(radiusAttribute * item.Scale);
126  PhysicsBody = new PhysicsBody(0.0f, 0.0f, Radius, 1.5f, BodyType.Static, Physics.CollisionWall, LevelTrigger.GetCollisionCategories(triggeredBy))
127  {
128  UserData = item
129  };
131  PhysicsBody.FarseerBody.SetIsSensor(true);
132  PhysicsBody.FarseerBody.OnCollision += OnCollision;
133  PhysicsBody.FarseerBody.OnSeparation += OnSeparation;
134  RadiusInDisplayUnits = ConvertUnits.ToDisplayUnits(PhysicsBody.Radius);
135  }
136 
137  public override void OnMapLoaded()
138  {
139  base.OnMapLoaded();
142  }
143 
144  private bool OnCollision(Fixture sender, Fixture other, Contact contact)
145  {
146  if (LevelTrigger.GetEntity(other) is not Entity entity) { return false; }
147  if (!LevelTrigger.IsTriggeredByEntity(entity, triggeredBy, mustBeOnSpecificSub: (!MoveOutsideSub, item.Submarine))) { return false; }
148  triggerers.Add(entity);
149  return true;
150  }
151 
152  private void OnSeparation(Fixture sender, Fixture other, Contact contact)
153  {
154  if (LevelTrigger.GetEntity(other) is not Entity entity)
155  {
156  return;
157  }
158  if (entity is Character character && (!character.Enabled || character.Removed) && triggerers.Contains(entity))
159  {
160  triggerers.Remove(entity);
161  return;
162  }
163  if (LevelTrigger.CheckContactsForOtherFixtures(PhysicsBody, other, entity))
164  {
165  return;
166  }
167  triggerers.Remove(entity);
168  }
169 
170  public override void Update(float deltaTime, Camera cam)
171  {
172  if (item.Submarine != null && MoveOutsideSub)
173  {
174  item.SetTransform(ConvertUnits.ToSimUnits(item.WorldPosition), item.Rotation);
175  item.CurrentHull = null;
176  item.Submarine = null;
179  }
180 
182 
183  if (triggerOnce)
184  {
185  if (TriggeredOnce) { return; }
186  if (triggerers.Count > 0)
187  {
188  TriggeredOnce = true;
189  IsActive = false;
190  triggerers.Clear();
191  }
192  }
193 
194  TriggerActive = triggerers.Any();
195 
197  {
198  ForceFluctuationTimer += deltaTime;
199  if (ForceFluctuationTimer >= ForceFluctuationInterval)
200  {
201  float v = MathF.Sin(2 * MathF.PI * ForceFluctuationFrequency * TimeInLevel);
202  float amount = MathUtils.InverseLerp(-1.0f, 1.0f, v);
203  CurrentForceFluctuation = MathHelper.Lerp(1.0f - ForceFluctuationStrength, 1.0f, amount);
204  ForceFluctuationTimer = 0.0f;
205  GameMain.NetworkMember?.CreateEntityEvent(this);
206  }
207  }
208 
209  foreach (Entity triggerer in triggerers)
210  {
211  LevelTrigger.ApplyStatusEffects(statusEffects, item.WorldPosition, triggerer, deltaTime, statusEffectTargets);
212 
213  if (triggerer is IDamageable damageable)
214  {
215  LevelTrigger.ApplyAttacks(attacks, damageable, item.WorldPosition, deltaTime);
216  }
217  else if (triggerer is Submarine submarine)
218  {
219  LevelTrigger.ApplyAttacks(attacks, item.WorldPosition, deltaTime);
220  foreach (Character c2 in Character.CharacterList)
221  {
222  if (c2.Submarine == submarine)
223  {
224  LevelTrigger.ApplyAttacks(attacks, c2, item.WorldPosition, deltaTime);
225  }
226  }
227  }
228 
229  if (Math.Abs(Force) < 0.01f)
230  {
231  // Just ignore very minimal forces
232  continue;
233  }
234  else if (triggerer is Character c)
235  {
236  if (c.AnimController.Collider.BodyType == BodyType.Dynamic)
237  {
238  if (c.AnimController.Collider.Enabled)
239  {
240  ApplyForce(c.AnimController.Collider);
241  }
242  foreach (var limb in c.AnimController.Limbs)
243  {
244  ApplyForce(limb.body, multiplier: limb.Mass * c.AnimController.Collider.Mass / c.AnimController.Mass);
245  }
246  }
247  }
248  else if (triggerer is Submarine s)
249  {
250  ApplyForce(s.SubBody.Body);
251  }
252  else if (triggerer is Item i && i.body != null)
253  {
254  ApplyForce(i.body);
255  }
256  }
257 
258  item.SendSignal(IsActive ? "1" : "0", "state_out");
259  }
260 
261  private void ApplyForce(PhysicsBody body, float multiplier = 1.0f)
262  {
263  Vector2 diff = ConvertUnits.ToDisplayUnits(PhysicsBody.SimPosition - body.SimPosition);
264  if (diff.LengthSquared() < 0.0001f) { return; }
265  float distanceFactor = DistanceBasedForce ? LevelTrigger.GetDistanceFactor(body, PhysicsBody, RadiusInDisplayUnits) : 1.0f;
266  if (distanceFactor <= 0.0f) { return; }
267  Vector2 force = distanceFactor * (CurrentForceFluctuation * Force) * Vector2.Normalize(diff) * multiplier;
268  if (force.LengthSquared() < 0.01f) { return; }
269  body.ApplyForce(force);
270  }
271 
272  public override void Move(Vector2 amount, bool ignoreContacts = false)
273  {
274  if (PhysicsBody != null)
275  {
276  if (ignoreContacts)
277  {
278  PhysicsBody.SetTransformIgnoreContacts(PhysicsBody.SimPosition + ConvertUnits.ToSimUnits(amount), 0.0f);
279  }
280  else
281  {
282  PhysicsBody.SetTransform(PhysicsBody.SimPosition + ConvertUnits.ToSimUnits(amount), 0.0f);
283  }
285  }
286  }
287 
288  protected override void RemoveComponentSpecific()
289  {
290  if (PhysicsBody != null)
291  {
293  PhysicsBody = null;
294  }
295  }
296 
297  public override void ReceiveSignal(Signal signal, Connection connection)
298  {
299  base.ReceiveSignal(signal, connection);
300  switch (connection.Name)
301  {
302  case "set_force":
303  if (!FloatTryParse(signal, out float force)) { break; }
304  Force = force;
305  break;
306  case "set_distancebasedforce":
307  if (!bool.TryParse(signal.value, out bool distanceBasedForce)) { break; }
308  DistanceBasedForce = distanceBasedForce;
309  break;
310  case "set_forcefluctuation":
311  if (!bool.TryParse(signal.value, out bool forceFluctuation)) { break; }
312  ForceFluctuation = forceFluctuation;
313  break;
314  case "set_forcefluctuationstrength":
315  if (!FloatTryParse(signal, out float forceFluctuationStrength)) { break; }
316  ForceFluctuationStrength = forceFluctuationStrength;
317  break;
318  case "set_forcefluctuationfrequency":
319  if (!FloatTryParse(signal, out float forceFluctuationFrequency)) { break; }
320  ForceFluctuationFrequency = forceFluctuationFrequency;
321  break;
322  case "set_forcefluctuationinterval":
323  if (!FloatTryParse(signal, out float forceFluctuationInterval)) { break; }
324  ForceFluctuationInterval = forceFluctuationInterval;
325  break;
326  }
327 
328  static bool FloatTryParse(Signal signal, out float value)
329  {
330  return float.TryParse(signal.value, NumberStyles.Any, CultureInfo.InvariantCulture, out value);
331  }
332  }
333  }
334 }
335 
string? GetAttributeString(string key, string? def)
float GetAttributeFloat(string key, float def)
ContentPackage? ContentPackage
bool GetAttributeBool(string key, bool def)
Submarine Submarine
Definition: Entity.cs:53
static GameSession?? GameSession
Definition: GameMain.cs:88
static NetworkMember NetworkMember
Definition: GameMain.cs:190
void SetTransform(Vector2 simPosition, float rotation, bool findNewHull=true, bool setPrevTransform=true)
void SendSignal(string signal, string connectionName)
The base class for components holding the different functionalities of the item
override void OnItemLoaded()
Called when all the components of the item have been loaded. Use to initialize connections between co...
override void OnMapLoaded()
Called when all items have been loaded. Use to initialize connections between items.
static void LoadStatusEffect(List< StatusEffect > statusEffects, ContentXElement element, string parentDebugName)
static void ApplyStatusEffects(List< StatusEffect > statusEffects, Vector2 worldPosition, Entity triggerer, float deltaTime, List< ISerializableEntity > targets)
static void LoadAttack(ContentXElement element, string parentDebugName, bool triggerOnce, List< Attack > attacks)
static bool IsTriggeredByEntity(Entity entity, TriggererType triggeredBy, bool mustBeOutside=false,(bool mustBe, Submarine sub) mustBeOnSpecificSub=default)
static void RemoveInActiveTriggerers(PhysicsBody physicsBody, HashSet< Entity > triggerers)
static float GetDistanceFactor(PhysicsBody triggererBody, PhysicsBody triggerBody, float colliderRadius)
static void ApplyAttacks(List< Attack > attacks, IDamageable damageable, Vector2 worldPosition, float deltaTime)
Applies attacks to a damageable.
void ApplyForce(Vector2 force, float maxVelocity=NetConfig.MaxPhysicsBodyVelocity)
bool SetTransform(Vector2 simPosition, float rotation, bool setPrevTransform=true)
bool SetTransformIgnoreContacts(Vector2 simPosition, float rotation, bool setPrevTransform=true)