Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs
1 #nullable enable
2 using System;
3 using System.Collections.Generic;
4 using System.Diagnostics;
5 using System.Globalization;
6 using System.Linq;
7 using System.Xml.Linq;
11 using FarseerPhysics;
12 using FarseerPhysics.Dynamics;
13 using Microsoft.Xna.Framework;
14 
16 {
17  class BallastFloraBranch : VineTile
18  {
19  public readonly BallastFloraBehavior? ParentBallastFlora;
20  public int ID = -1;
21 
22  public Item? ClaimedItem;
23  public int ClaimedItemId = -1;
24 
25  public float MaxHealth = 100f;
26 
27  private float health = 100;
28  public float Health
29  {
30  get { return health; }
31  set { health = MathHelper.Clamp(value, 0.0f, MaxHealth); }
32  }
33 
34  public float RemoveTimer = 60.0f;
35 
36  public bool SpawningItem;
37  public Item? AttackItem;
38 
39  public bool IsRoot;
43  public bool IsRootGrowth;
44  public bool Removed;
45 
46  public bool DisconnectedFromRoot;
47 
48  public Hull? CurrentHull;
49 
50  public float Pulse = 1.0f;
51  private bool inflate;
52  private float pulseDelay = Rand.Range(0f, 3f);
53 
54  private BallastFloraBranch? parentBranch;
56  {
57  get { return parentBranch; }
58  set
59  {
60  if (value != parentBranch)
61  {
62  parentBranch = value;
63  if (parentBranch != null)
64  {
65  BranchDepth = parentBranch.BranchDepth + 1;
66  }
67  }
68  }
69  }
73  public int BranchDepth { get; private set; }
74 
75  public float AccumulatedDamage;
77 #if CLIENT
78  public Vector2 ShakeAmount;
79 #endif
80 
81  // Adjacent tiles, used to free up sides when this branch gets removed
82  public readonly Dictionary<TileSide, BallastFloraBranch> Connections = new Dictionary<TileSide, BallastFloraBranch>();
83 
84  public BallastFloraBranch(BallastFloraBehavior? parent, BallastFloraBranch? parentBranch, Vector2 position, VineTileType type, FoliageConfig? flowerConfig = null, FoliageConfig? leafConfig = null, Rectangle? rect = null)
85  : base(null, position, type, flowerConfig, leafConfig, rect)
86  {
87  ParentBranch = parentBranch;
88  ParentBallastFlora = parent;
89  }
90 
91  public void UpdateHealth()
92  {
93  if (MaxHealth <= Health) { return; }
94  Color healthColor = Color.White * (1.0f - Health / MaxHealth);
95  HealthColor = Color.Lerp(HealthColor, healthColor, 0.05f);
96  }
97 
98  public void UpdatePulse(float deltaTime, float inflateSpeed, float deflateSpeed, float delay)
99  {
100  if (ParentBallastFlora == null || DisconnectedFromRoot) { return; }
101 
102  if (pulseDelay > 0)
103  {
104  pulseDelay -= deltaTime;
105  return;
106  }
107 
108  if (inflate)
109  {
110  Pulse += inflateSpeed * deltaTime;
111 
112  if (Pulse > 1.25f)
113  {
114  inflate = false;
115  }
116  }
117  else
118  {
119  Pulse -= deflateSpeed * deltaTime;
120  if (Pulse < 1f)
121  {
122  inflate = true;
123  pulseDelay = delay;
124  }
125  }
126  }
127  }
128 
129  internal partial class BallastFloraBehavior : ISerializableEntity
130  {
131 #if DEBUG
132  public List<Tuple<Vector2, Vector2>> debugSearchLines = new List<Tuple<Vector2, Vector2>>();
133 #endif
134 
135  private readonly static List<BallastFloraBehavior> _entityList = new List<BallastFloraBehavior>();
136  public static IEnumerable<BallastFloraBehavior> EntityList => _entityList;
137 
138  public enum NetworkHeader
139  {
140  Spawn,
141  Kill,
142  BranchCreate,
143  BranchRemove,
144  BranchDamage,
145  Infect,
146  Remove
147  }
148 
149  public enum AttackType
150  {
151  Fire,
152  Explosives,
153  Other,
154  CutFromRoot
155  }
156 
157  public struct AITarget
158  {
159  public Identifier[] Tags;
160  public int Priority;
161 
162  public AITarget(ContentXElement element)
163  {
164  Tags = element.GetAttributeIdentifierArray("tags", Array.Empty<Identifier>())!;
165  Priority = element.GetAttributeInt("priority", 0);
166  }
167 
168  public bool Matches(Item item)
169  {
170  foreach (Identifier targetTag in Tags)
171  {
172  if (item.HasTag(targetTag)) { return true; }
173  }
174  return false;
175  }
176  }
177 
178  [Serialize(0.25f, IsPropertySaveable.Yes, "Scale of the branches.")]
179  public float BaseBranchScale { get; set; }
180 
181  [Serialize(0.25f, IsPropertySaveable.Yes, "Scale of the flowers.")]
182  public float BaseFlowerScale { get; set; }
183 
184  [Serialize(0.5f, IsPropertySaveable.Yes, "Scale of the leaves.")]
185  public float BaseLeafScale { get; set; }
186 
187  [Serialize(0.33f, IsPropertySaveable.Yes, "Chance for a flower to appear on a branch.")]
188  public float FlowerProbability { get; set; }
189 
190  [Serialize(0.7f, IsPropertySaveable.Yes, "Chance for leaves to appear on a branch.")]
191  public float LeafProbability { get; set; }
192 
193  [Serialize(3f, IsPropertySaveable.Yes, "Delay between pulses.")]
194  public float PulseDelay { get; set; }
195 
196  [Serialize(3f, IsPropertySaveable.Yes, "How fast the flower inflates during a pulse.")]
197  public float PulseInflateSpeed { get; set; }
198 
199  [Serialize(1f, IsPropertySaveable.Yes, "How fast the flower deflates.")]
200  public float PulseDeflateSpeed { get; set; }
201 
202  [Serialize(32, IsPropertySaveable.Yes, "How many vines must grow before the plant breaks through the wall.")]
203  public int BreakthroughPoint { get; set; }
204 
205  [Serialize(false, IsPropertySaveable.Yes, "Has the plant grown large enough to expose itself.")]
206  public bool HasBrokenThrough { get; set; }
207 
208  [Serialize(300, IsPropertySaveable.Yes, "How far the ballast flora can detect items from.")]
209  public int Sight { get; set; }
210 
211  [Serialize(100, IsPropertySaveable.Yes, "How much health the branches have.")]
212  public int BranchHealth { get; set; }
213 
214  [Serialize(400, IsPropertySaveable.Yes, "How much health the root has.")]
215  public int RootHealth { get; set; }
216 
217  [Serialize(0.00025f, IsPropertySaveable.Yes, "How fast the root's health regenerates per each grown branch.")]
218  public float HealthRegenPerBranch { get; set; }
219 
220  [Serialize(30, IsPropertySaveable.Yes, "How far away from the root branches can regenerate health (in number of branches). The amount of regen decreases lineary further from the root.")]
221  public int MaxBranchHealthRegenDistance { get; set; }
222 
223  [Serialize("255,255,255,255", IsPropertySaveable.Yes)]
224  public Color RootColor { get; set; }
225 
226  [Serialize(300f, IsPropertySaveable.Yes, "How much power the ballast flora takes from junction boxes.")]
227  public float PowerConsumptionMin { get; set; }
228 
229  [Serialize(3000f, IsPropertySaveable.Yes, "How much the power drain spikes.")]
230  public float PowerConsumptionMax { get; set; }
231 
232  [Serialize(10f, IsPropertySaveable.Yes, "How long it takes for power drain to wind down.")]
233  public float PowerConsumptionDuration { get; set; }
234 
235  [Serialize(250f, IsPropertySaveable.Yes, "How much power does it take to accelerate growth.")]
236  public float PowerRequirement { get; set; }
237 
238  [Serialize(5f, IsPropertySaveable.Yes, "Maximum anger, anger increases when the plant gets damaged and increases growth speed.")]
239  public float MaxAnger { get; set; }
240 
241  [Serialize(10000f, IsPropertySaveable.Yes, "Maximum power buffer.")]
242  public float MaxPowerCapacity { get; set; }
243 
244  [Serialize("", IsPropertySaveable.Yes, "Item prefab that is spawned when threatened.")]
245  public Identifier AttackItemPrefab { get; set; } = Identifier.Empty;
246 
247  [Serialize(0.8f, IsPropertySaveable.Yes, "How resistant the ballast flora is to explosives before it blooms.")]
248  public float ExplosionResistance { get; set; }
249 
250  [Serialize(5f, IsPropertySaveable.Yes, "How much damage is taken from open fires.")]
251  public float FireVulnerability { get; set; }
252 
253  [Serialize(0.5f, IsPropertySaveable.Yes, "How much resistance against fire is gained while submerged.")]
254  public float SubmergedWaterResistance { get; set; }
255 
256  [Serialize(0.8f, IsPropertySaveable.Yes, "What depth the branches will be drawn on.")]
257  public float BranchDepth { get; set; }
258 
259  [Serialize("", IsPropertySaveable.Yes, "What sound to play when the ballast flora bursts through walls.")]
260  public string BurstSound { get; set; } = "";
261 
262  private float availablePower;
263 
264  [Serialize(0f, IsPropertySaveable.Yes, "How much power the ballast flora has stored.")]
265  public float AvailablePower
266  {
267  get => availablePower;
268  set => availablePower = Math.Max(value, MaxPowerCapacity);
269  }
270 
271  private float anger;
272 
273  [Serialize(1f, IsPropertySaveable.Yes, "How enraged the flora is, affects how fast it grows.")]
274  public float Anger
275  {
276  get => anger;
277  set => anger = Math.Clamp(value, 1f, MaxAnger);
278  }
279 
280  public string Name { get; } = "";
281 
282  public Hull Parent { get; private set; }
283 
284  public BallastFloraPrefab Prefab { get; private set; }
285 
286  public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; private set; }
287 
288  public Vector2 Offset;
289 
290  public readonly HashSet<Item> ClaimedTargets = new HashSet<Item>();
291  public readonly HashSet<PowerTransfer> ClaimedJunctionBoxes = new HashSet<PowerTransfer>();
292  public readonly HashSet<PowerContainer> ClaimedBatteries = new HashSet<PowerContainer>();
293  public readonly Dictionary<Item, int> IgnoredTargets = new Dictionary<Item, int>();
294 
295  private readonly List<Tuple<UInt16, int>> tempClaimedTargets = new List<Tuple<ushort, int>>();
296 
297  private int flowerVariants, leafVariants;
298  public readonly List<AITarget> Targets = new List<AITarget>();
299 
300  public float PowerConsumptionTimer;
301 
302  private float defenseCooldown, toxinsCooldown, fireCheckCooldown;
303  private float selfDamageTimer, toxinsTimer, toxinsSpawnTimer;
304 
305  private readonly List<BallastFloraBranch> branchesVulnerableToFire = new List<BallastFloraBranch>();
306 
307  public readonly List<BallastFloraBranch> Branches = new List<BallastFloraBranch>();
308  private BallastFloraBranch? root;
309  private readonly List<Body> bodies = new List<Body>();
310 
311  private bool isDead;
312 
313  public readonly BallastFloraStateMachine StateMachine;
314 
315  public int GrowthWarps;
316 
317  public void OnMapLoaded()
318  {
319  foreach ((ushort itemId, int branchid) in tempClaimedTargets)
320  {
321  if (Entity.FindEntityByID(itemId) is Item item)
322  {
323  ClaimTarget(item, Branches.FirstOrDefault(b => b.ID == branchid), true);
324  }
325  else
326  {
327  string errorMsg = $"Error in BallastFloraBehavior.OnMapLoaded: could not find the item claimed by the ballast flora.";
328  DebugConsole.ThrowError(errorMsg);
329  GameAnalyticsManager.AddErrorEventOnce("BallastFloraBehavior.OnMapLoaded:ClaimedItemNotFound", GameAnalyticsManager.ErrorSeverity.Warning, errorMsg);
330  }
331  }
332 
333  foreach (BallastFloraBranch branch in Branches)
334  {
335  SetHull(branch);
336  if (branch.ClaimedItemId > -1)
337  {
338  if (Entity.FindEntityByID((ushort)branch.ClaimedItemId) is Item item)
339  {
340  branch.ClaimedItem = item;
341  }
342  else
343  {
344  string errorMsg = $"Error in BallastFloraBehavior.OnMapLoaded: could not find the item claimed by a branch.";
345  DebugConsole.ThrowError(errorMsg);
346  GameAnalyticsManager.AddErrorEventOnce("BallastFloraBehavior.OnMapLoaded:BranchClaimedItemNotFound", GameAnalyticsManager.ErrorSeverity.Warning, errorMsg);
347  }
348  }
349  UpdateConnections(branch);
350  CreateBody(branch);
351  }
352  }
353 
354 
355  private int CreateID()
356  {
357  int maxId = Branches.Any() ? Branches.Max(b => b.ID) : 0;
358  return ++maxId;
359  }
360 
361  public Vector2 GetWorldPosition()
362  {
363  return Parent.WorldPosition + Offset;
364  }
365 
366  public BallastFloraBehavior(Hull parent, BallastFloraPrefab prefab, Vector2 offset, bool firstGrowth = false)
367  {
368  Prefab = prefab;
369  Offset = offset;
370  Parent = parent;
371  SerializableProperties = SerializableProperty.DeserializeProperties(this, prefab.Element);
372  LoadPrefab(prefab.Element);
373  StateMachine = new BallastFloraStateMachine(this);
374  if (firstGrowth) { GenerateRoot(); }
375  _entityList.Add(this);
376  }
377 
378  partial void LoadPrefab(ContentXElement element);
379 
380  public void LoadTargets(ContentXElement element)
381  {
382  foreach (var subElement in element.Elements())
383  {
384  Targets.Add(new AITarget(subElement));
385  }
386  }
387 
388  public void Save(XElement element)
389  {
390  XElement saveElement = new XElement(nameof(BallastFloraBehavior),
391  new XAttribute("identifier", Prefab.Identifier),
392  new XAttribute("offset", XMLExtensions.Vector2ToString(Offset)));
393 
394  SerializableProperty.SerializeProperties(this, saveElement);
395 
396  foreach (BallastFloraBranch branch in Branches)
397  {
398  XElement be = new XElement("Branch",
399  new XAttribute("flowerconfig", branch.FlowerConfig.Serialize()),
400  new XAttribute("leafconfig", branch.LeafConfig.Serialize()),
401  new XAttribute("pos", XMLExtensions.Vector2ToString(branch.Position)),
402  new XAttribute("ID", branch.ID),
403  new XAttribute("isroot", branch.IsRoot),
404  new XAttribute("isrootgrowth", branch.IsRootGrowth),
405  new XAttribute("health", branch.Health.ToString("G", CultureInfo.InvariantCulture)),
406  new XAttribute("maxhealth", branch.MaxHealth.ToString("G", CultureInfo.InvariantCulture)),
407  new XAttribute("sides", (int)branch.Sides),
408  new XAttribute("blockedsides", (int)branch.BlockedSides),
409  new XAttribute("tile", (int)branch.Type));
410 
411  if (branch.ClaimedItem != null)
412  {
413  be.Add(new XAttribute("claimed", (int)(branch.ClaimedItem?.ID ?? -1)));
414  }
415  if (branch.ParentBranch != null && !branch.ParentBranch.Removed)
416  {
417  be.Add(new XAttribute("parentbranch", (int)(branch.ParentBranch?.ID ?? -1)));
418  }
419 
420  saveElement.Add(be);
421  }
422 
423  foreach (Item target in ClaimedTargets)
424  {
425  if (target.Infector == null)
426  {
427  string errorMsg = $"Error in BallastFloraBehavior.Save: claimed target \"{target.Prefab.Identifier}\" had no infector set.";
428  DebugConsole.ThrowError(errorMsg);
429  GameAnalyticsManager.AddErrorEventOnce("BallastFloraBehavior.Save:InfectorNull", GameAnalyticsManager.ErrorSeverity.Warning, errorMsg);
430  continue;
431  }
432  XElement te = new XElement("ClaimedTarget", new XAttribute("id", target.ID), new XAttribute("branchId", target.Infector.ID));
433  saveElement.Add(te);
434  }
435 
436  element.Add(saveElement);
437  }
438 
439  public void LoadSave(XElement element, IdRemap idRemap)
440  {
441  List<(BallastFloraBranch branch, int parentBranchId)> branches = new List<(BallastFloraBranch branch, int parentBranchId)>();
442  SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
443  Offset = element.GetAttributeVector2("offset", Vector2.Zero);
444  foreach (var subElement in element.Elements())
445  {
446  switch (subElement.Name.ToString().ToLowerInvariant())
447  {
448  case "branch":
449  LoadBranch(subElement, idRemap);
450  break;
451  case "claimedtarget":
452  int id = subElement.GetAttributeInt("id", -1);
453  int branchId = subElement.GetAttributeInt("branchId", -1);
454  if (id > 0)
455  {
456  tempClaimedTargets.Add(Tuple.Create(idRemap.GetOffsetId(id), branchId));
457  }
458  break;
459  }
460  }
461 
462  foreach ((BallastFloraBranch branch, int parentBranchId) in branches)
463  {
464  if (parentBranchId > -1)
465  {
466  var parentBranch = Branches.Find(b => b.ID == parentBranchId);
467  if (parentBranch == null)
468  {
469  DebugConsole.AddWarning($"Error while loading ballast flora: couldn't find a parent branch with the ID {parentBranchId}");
470  }
471  else
472  {
473  branch.ParentBranch = parentBranch;
474  }
475  }
476  }
477 
478  if (root == null)
479  {
480  Branches.ForEach(b => b.DisconnectedFromRoot = true);
481  }
482  else
483  {
484  CheckDisconnectedFromRoot();
485  }
486 
487  void LoadBranch(XElement branchElement, IdRemap idRemap)
488  {
489  Vector2 pos = branchElement.GetAttributeVector2("pos", Vector2.Zero);
490  bool isRoot = branchElement.GetAttributeBool("isroot", false);
491  bool isRootGrowth = branchElement.GetAttributeBool("isrootgrowth", false);
492  int flowerConfig = getInt("flowerconfig");
493  int leafconfig = getInt("leafconfig");
494  int id = getInt("ID");
495  float health = getFloat("health");
496  float maxhealth = getFloat("maxhealth");
497  int sides = getInt("sides");
498  int blockedSides = getInt("blockedsides");
499  int claimedId = branchElement.GetAttributeInt("claimed", -1);
500  int parentBranchId = branchElement.GetAttributeInt("parentbranch", -1);
501  VineTileType type = (VineTileType)branchElement.GetAttributeInt("tile", 0);
502 
503  BallastFloraBranch newBranch = new BallastFloraBranch(this, null, pos, type, FoliageConfig.Deserialize(flowerConfig), FoliageConfig.Deserialize(leafconfig))
504  {
505  ID = id,
506  Health = health,
507  MaxHealth = maxhealth,
508  Sides = (TileSide)sides,
509  BlockedSides = (TileSide)blockedSides,
510  IsRoot = isRoot,
511  IsRootGrowth = isRootGrowth
512  };
513  branches.Add((newBranch, parentBranchId));
514 
515  if (newBranch.IsRoot) { root = newBranch; }
516 
517  if (claimedId > -1)
518  {
519  newBranch.ClaimedItemId = idRemap.GetOffsetId((ushort)claimedId);
520  }
521 
522  Branches.Add(newBranch);
523 
524  int getInt(string name) => branchElement.GetAttributeInt(name, 0);
525  float getFloat(string name) => branchElement.GetAttributeFloat(name, 0f);
526  }
527  }
528 
529  public void Update(float deltaTime)
530  {
531  if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
532  {
533  if (Branches.Count == 0)
534  {
535  Remove();
536  return;
537  }
538  }
539 
540  foreach (BallastFloraBranch branch in Branches)
541  {
542  branch.UpdateScale(deltaTime);
543  branch.UpdatePulse(deltaTime, PulseInflateSpeed, PulseDeflateSpeed, PulseDelay);
544 #if CLIENT
545  branch.UpdateHealth();
546 #endif
547  }
548 
549  UpdateDamage(deltaTime);
550 
551  UpdatePowerDrain(deltaTime);
552 
553  if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
554 
555  if (root != null && HealthRegenPerBranch > 0.0f)
556  {
557  float healAmount = Branches.Count(b => !b.IsRoot && !b.IsRootGrowth && !b.DisconnectedFromRoot) * HealthRegenPerBranch;
558 
559  foreach (BallastFloraBranch branch in Branches)
560  {
561  if (branch.Health > branch.MaxHealth * 0.9f || branch.DisconnectedFromRoot) { continue; }
562  float branchHealAmount = (float)(MaxBranchHealthRegenDistance - branch.BranchDepth) / MaxBranchHealthRegenDistance * healAmount;
563  if (branchHealAmount <= 0.0f) { continue; }
564  float prevHealth = branch.Health;
565  branch.Health += branchHealAmount;
566  branch.AccumulatedDamage += (prevHealth - branch.Health);
567  }
568  }
569  StateMachine.Update(deltaTime);
570 
571  if (HasBrokenThrough)
572  {
573  // I wasn't 100% sure what the performance impact on this so I decide to limit it to only check every 5 seconds
574  if (fireCheckCooldown <= 0)
575  {
576  UpdateFireSources();
577  fireCheckCooldown = 5f;
578  }
579  else
580  {
581  fireCheckCooldown -= deltaTime;
582  }
583 
584  foreach (BallastFloraBranch branch in branchesVulnerableToFire)
585  {
586  if (!branch.Removed)
587  {
588  DamageBranch(branch, FireVulnerability * deltaTime, AttackType.Fire, null);
589  }
590  }
591  }
592 
593  UpdateSelfDamage(deltaTime);
594 
595  if (Anger > 1f)
596  {
597  Anger -= deltaTime;
598  }
599 
600  if (toxinsTimer > 0.1f)
601  {
602  toxinsSpawnTimer -= deltaTime;
603  if (!AttackItemPrefab.IsEmpty && toxinsSpawnTimer <= 0.0f)
604  {
605  toxinsSpawnTimer = 1.0f;
606  Dictionary<Hull, List<BallastFloraBranch>> branches = new Dictionary<Hull, List<BallastFloraBranch>>();
607  foreach (BallastFloraBranch branch in Branches)
608  {
609  if (branch.CurrentHull == null || branch.FlowerConfig.Variant < 0 || branch.DisconnectedFromRoot) { continue; }
610 
611  if (branches.TryGetValue(branch.CurrentHull, out List<BallastFloraBranch>? list))
612  {
613  list.Add(branch);
614  }
615  else
616  {
617  branches.Add(branch.CurrentHull, new List<BallastFloraBranch> { branch });
618  }
619  }
620 
621  foreach (Hull hull in branches.Keys)
622  {
623  List<BallastFloraBranch> list = branches[hull];
624  if (!list.Any(HasAcidEmitter))
625  {
626  BallastFloraBranch randomBranch = branches[hull].GetRandomUnsynced();
627  randomBranch.SpawningItem = true;
628 
629  ItemPrefab prefab = ItemPrefab.Find(null, AttackItemPrefab);
630 #warning TODO: Parent needs a nullability sanity check
631  Entity.Spawner?.AddItemToSpawnQueue(prefab, Parent!.Position + Offset + randomBranch.Position, Parent.Submarine, onSpawned: item =>
632  {
633  randomBranch.AttackItem = item;
634  randomBranch.SpawningItem = false;
635  });
636  }
637 
638  static bool HasAcidEmitter(BallastFloraBranch b) => b.SpawningItem || (b.AttackItem != null && !b.AttackItem.Removed);
639  }
640  }
641 
642  toxinsTimer -= deltaTime;
643  }
644 
645  if (defenseCooldown >= 0)
646  {
647  defenseCooldown -= deltaTime;
648  }
649 
650  if (toxinsCooldown >= 0)
651  {
652  toxinsCooldown -= deltaTime;
653  }
654  }
655 
656  partial void UpdateDamage(float deltaTime);
657 
658  private readonly List<BallastFloraBranch> toBeRemoved = new List<BallastFloraBranch>();
659  private void UpdateSelfDamage(float deltaTime)
660  {
661  if (selfDamageTimer <= 0)
662  {
663  if (!HasBrokenThrough && !CanGrowMore())
664  {
665  Branches.ForEachMod(branch =>
666  {
667  float maxHealth = branch.IsRoot ? RootHealth : BranchHealth;
668  DamageBranch(branch, Rand.Range(1f, maxHealth), AttackType.Other);
669  });
670  }
671  selfDamageTimer = 1f;
672  }
673  toBeRemoved.Clear();
674  foreach (BallastFloraBranch branch in Branches)
675  {
676  if (!branch.IsRoot)
677  {
678  if (branch.ParentBranch == null || branch.ParentBranch.DisconnectedFromRoot || branch.ParentBranch.Health <= 0.0f)
679  {
680  float parentHealth = branch.ParentBranch == null ? 0.0f : branch.ParentBranch.Health / branch.ParentBranch.MaxHealth;
681  float speed = MathHelper.Lerp(5.0f, 0.1f, parentHealth);
682  DamageBranch(branch, speed * speed * deltaTime, AttackType.CutFromRoot);
683  }
684  }
685  if (branch.Health <= 0.0f)
686  {
687  if (branch.ClaimedItem != null)
688  {
689  RemoveClaim(branch.ClaimedItem);
690  }
691 
692  branch.RemoveTimer -= deltaTime;
693  if (branch.RemoveTimer <= 0.0f)
694  {
695  toBeRemoved.Add(branch);
696  }
697  }
698  }
699  foreach (BallastFloraBranch branch in toBeRemoved)
700  {
701  RemoveBranch(branch);
702  }
703  selfDamageTimer -= deltaTime;
704  }
705 
706  private void UpdatePowerDrain(float deltaTime)
707  {
708  PowerConsumptionTimer += deltaTime;
709  if (PowerConsumptionTimer > PowerConsumptionDuration)
710  {
711  PowerConsumptionTimer = 0f;
712  }
713 
714  float powerConsumption = MathHelper.Lerp(PowerConsumptionMax, PowerConsumptionMin, PowerConsumptionTimer / PowerConsumptionDuration);
715  float powerDelta = powerConsumption * deltaTime;
716 
717  foreach (PowerTransfer jb in ClaimedJunctionBoxes)
718  {
719  if (jb.ExtraLoad > Math.Max(PowerConsumptionMin, PowerConsumptionMax)) { continue; }
720 
721  jb.ExtraLoad = powerConsumption;
722 
723  float currPowerConsumption = -jb.CurrPowerConsumption;
724 
725  if (currPowerConsumption > powerDelta)
726  {
727  AvailablePower += powerDelta;
728  }
729  else
730  {
731  AvailablePower += currPowerConsumption * deltaTime;
732  }
733  }
734 
735  float batteryDrain = powerDelta * 0.1f;
736  foreach (PowerContainer battery in ClaimedBatteries)
737  {
738  float amount = Math.Min(battery.MaxOutPut, batteryDrain);
739 
740  if (battery.Charge > amount)
741  {
742  battery.Charge -= amount;
743  AvailablePower += amount;
744  }
745  }
746  }
747 
751  private void UpdateFireSources()
752  {
753  branchesVulnerableToFire.Clear();
754  foreach (BallastFloraBranch branch in Branches)
755  {
756  if (branch.CurrentHull == null) { continue; }
757 
758  foreach (FireSource source in branch.CurrentHull.FireSources)
759  {
760  if (source.IsInDamageRange(GetWorldPosition() + branch.Position, source.DamageRange))
761  {
762  branchesVulnerableToFire.Add(branch);
763  }
764  }
765  }
766  }
767 
768  private bool IsInWater(BallastFloraBranch branch)
769  {
770  if (branch.CurrentHull == null) { return false; }
771 
772  float surfaceY = branch.CurrentHull.Surface;
773  Vector2 pos = Parent.Position + Offset + branch.Position;
774  return Parent.WaterVolume > 0.0f && pos.Y < surfaceY;
775  }
776 
777  // could probably be moved to the branch constructor
778  public void SetHull(BallastFloraBranch branch)
779  {
780  branch.CurrentHull = Hull.FindHull(GetWorldPosition() + branch.Position, Parent, true);
781  }
782 
783  private void GenerateRoot()
784  {
785  if (root != null)
786  {
787  DebugConsole.ThrowError("Error in ballast flora: tried to grow a root even though root has already been created.\n" + Environment.StackTrace);
788  }
789 
790  root = new BallastFloraBranch(this, null, Vector2.Zero, VineTileType.Stem, FoliageConfig.EmptyConfig, FoliageConfig.EmptyConfig)
791  {
792  BlockedSides = TileSide.Bottom | TileSide.Left | TileSide.Right,
793  GrowthStep = 1f,
794  MaxHealth = RootHealth,
795  Health = RootHealth,
796  IsRoot = true,
797  CurrentHull = Parent,
798  ID = CreateID()
799  };
800 
801  Branches.Add(root);
802  CreateBody(root);
803  }
804 
805  public float GetGrowthSpeed(float deltaTime)
806  {
807  float load = PowerRequirement * Anger * deltaTime;
808 
809  if (AvailablePower > load)
810  {
811  AvailablePower -= load;
812  return Anger * 2f * deltaTime;
813  }
814 
815  return deltaTime;
816  }
817 
818  public bool TryGrowBranch(BallastFloraBranch parent, TileSide side, out List<BallastFloraBranch> result, bool isRootGrowth = false, Vector2? forcePosition = null)
819  {
820  result = new List<BallastFloraBranch>();
821  if (!isRootGrowth && parent.IsSideBlocked(side)) { return false; }
822 
823  Vector2 pos = forcePosition ?? parent.AdjacentPositions[side];
824  Rectangle rect = VineTile.CreatePlantRect(pos);
825 
826  if (CollidesWithWorld(rect, checkOtherBranches: !isRootGrowth))
827  {
828  parent.BlockedSides |= side;
829  parent.FailedGrowthAttempts++;
830  return false;
831  }
832 
833  FoliageConfig flowerConfig = FoliageConfig.EmptyConfig;
834  FoliageConfig leafConfig = FoliageConfig.EmptyConfig;
835 
836  if (FlowerProbability > Rand.Range(0d, 1.0d))
837  {
838  flowerConfig = FoliageConfig.CreateRandomConfig(flowerVariants, 0.5f, 1.0f);
839  }
840 
841  if (LeafProbability > Rand.Range(0d, 1.0d))
842  {
843  leafConfig = FoliageConfig.CreateRandomConfig(leafVariants, 0.5f, 1.0f);
844  }
845 
846  BallastFloraBranch newBranch = new BallastFloraBranch(this, parent, pos, VineTileType.CrossJunction, flowerConfig, leafConfig, rect)
847  {
848  ID = CreateID(),
849  MaxHealth = BranchHealth,
850  Health = BranchHealth,
851  IsRootGrowth = isRootGrowth
852  };
853 
854  SetHull(newBranch);
855 
856  if (newBranch.CurrentHull == null || newBranch.CurrentHull.Submarine != Parent.Submarine)
857  {
858  if (!isRootGrowth) { parent.BlockedSides |= side; }
859  parent.FailedGrowthAttempts++;
860  return false;
861  }
862 
863  UpdateConnections(newBranch, parent);
864 
865  Branches.Add(newBranch);
866  result.Add(newBranch);
867 
868  OnBranchGrowthSuccess(newBranch);
869 
870  if (GrowthWarps > 0)
871  {
872  GrowthWarps--;
873  }
874 
875  int rootGrowthCount = Branches.Count(b => b.IsRootGrowth);
876  if (rootGrowthCount < GetDesiredRootGrowthAmount())
877  {
878  if (root != null)
879  {
880  Vector2 rootGrowthPos = Rand.Vector(Math.Max(rootGrowthCount, 1) * Rand.Range(3.0f, 5.0f));
881  TryGrowBranch(root, TileSide.None, out List<BallastFloraBranch> newRootGrowth, isRootGrowth: true, forcePosition: rootGrowthPos);
882  }
883  }
884 
885 #if SERVER
886  CreateNetworkMessage(new BranchCreateEventData(newBranch, parent));
887 #endif
888  return true;
889  }
890 
891  private int GetDesiredRootGrowthAmount()
892  {
893  if (root == null) { return 0; }
894  return MathHelper.Clamp(Branches.Count(b => !b.IsRootGrowth && b.Health > 0) / 20, 3, 30);
895  }
896 
897  public bool BranchContainsTarget(BallastFloraBranch branch, Item target)
898  {
899  Rectangle worldRect = branch.Rect;
900  worldRect.Location = GetWorldPosition().ToPoint() + worldRect.Location;
901  return worldRect.IntersectsWorld(target.WorldRect);
902  }
903 
904  public void ClaimTarget(Item target, BallastFloraBranch? branch, bool load = false)
905  {
906  target.Infector = branch;
907 
908  if (target.GetComponent<PowerTransfer>() is { } powerTransfer)
909  {
910  ClaimedJunctionBoxes.Add(powerTransfer);
911  }
912 
913  if (target.GetComponent<PowerContainer>() is { } powerContainer)
914  {
915  ClaimedBatteries.Add(powerContainer);
916  }
917 
918  ClaimedTargets.Add(target);
919 
920  if (branch != null)
921  {
922  branch.ClaimedItem = target;
923  }
924 
925 #if SERVER
926  if (!load)
927  {
928  CreateNetworkMessage(new InfectEventData(target, InfectEventData.InfectState.Yes, branch));
929  }
930 #endif
931  }
932 
933  private void UpdateConnections(BallastFloraBranch branch, BallastFloraBranch? parent = null)
934  {
935  foreach (BallastFloraBranch otherBranch in Branches)
936  {
937  var (distX, distY) = branch.Position - otherBranch.Position;
938  int absDistX = (int) Math.Abs(distX), absDistY = (int) Math.Abs(distY);
939 
940  if (absDistX > branch.Rect.Width || absDistY > branch.Rect.Height || absDistX > 0 && absDistY > 0) { continue; }
941 
942  TileSide connectingSide = absDistX > absDistY ? distX > 0 ? TileSide.Right : TileSide.Left : distY > 0 ? TileSide.Top : TileSide.Bottom;
943 
944  TileSide oppositeSide = connectingSide.GetOppositeSide();
945 
946  if (parent != null)
947  {
948  if (otherBranch.BlockedSides.HasFlag(connectingSide))
949  {
950  branch.BlockedSides |= oppositeSide;
951  continue;
952  }
953 
954  if (otherBranch != parent)
955  {
956  otherBranch.BlockedSides |= connectingSide;
957  branch.BlockedSides |= oppositeSide;
958  }
959  else
960  {
961  otherBranch.Sides |= connectingSide;
962  branch.Sides |= oppositeSide;
963  }
964  }
965 
966  branch.Connections.TryAdd(oppositeSide, otherBranch);
967  otherBranch.Connections.TryAdd(connectingSide, branch);
968  }
969  }
970 
971  private void OnBranchGrowthSuccess(BallastFloraBranch newBranch)
972  {
973  if (!HasBrokenThrough)
974  {
975  if (Branches.Count > BreakthroughPoint)
976  {
977  BreakThrough();
978  }
979 
980 #if CLIENT
981  if (newBranch.FlowerConfig.Variant > -1)
982  {
983  Vector2 flowerPos = GetWorldPosition() + newBranch.Position;
984  CreateShapnel(flowerPos);
985  newBranch.GrowthStep = 2.0f;
986  SoundPlayer.PlayDamageSound(BurstSound, 1.0f, flowerPos, range: 800);
987  }
988 #endif
989  }
990 
991  CreateBody(newBranch);
992 
993  foreach (BallastFloraBranch vine in Branches)
994  {
995  vine.UpdateType();
996  }
997  }
998 
1003  private void CreateBody(BallastFloraBranch branch)
1004  {
1005  Rectangle rect = branch.Rect;
1006  Vector2 pos = Parent.Position + Offset + branch.Position;
1007 
1008  float scale = branch.IsRoot ? 3.0f : 1f;
1009  Body branchBody = GameMain.World.CreateRectangle(ConvertUnits.ToSimUnits(rect.Width * scale), ConvertUnits.ToSimUnits(rect.Height * scale), 1.5f);
1010  branchBody.BodyType = BodyType.Static;
1011  branchBody.UserData = branch;
1012  branchBody.SetCollidesWith(Physics.CollisionRepairableWall);
1013  branchBody.SetCollisionCategories(Physics.CollisionRepairableWall);
1014  branchBody.Position = ConvertUnits.ToSimUnits(pos);
1015  branchBody.Enabled = HasBrokenThrough;
1016 
1017  bodies.Add(branchBody);
1018  }
1019 
1020  public void DamageBranch(BallastFloraBranch branch, float amount, AttackType type, Character? attacker = null)
1021  {
1022  float damage = amount;
1023 
1024  if (type != AttackType.Other && type != AttackType.CutFromRoot)
1025  {
1026  branch.DamageVisualizationTimer = 1.0f;
1027  }
1028 
1029  if (branch.IsRootGrowth && root is { Health: > 0.0f }) { return; }
1030 
1031  if (type != AttackType.Other && type != AttackType.CutFromRoot)
1032  {
1033  branch.AccumulatedDamage += damage;
1034  Anger += damage * 0.001f;
1035  }
1036 
1037  if (GameMain.NetworkMember != null)
1038  {
1039  // damage is handled server side
1040  if (GameMain.NetworkMember.IsClient)
1041  {
1042  return;
1043  }
1044  else
1045  {
1046  //accumulate damage on the server's side to ensure clients get notified
1047  if (type == AttackType.Other || type == AttackType.CutFromRoot)
1048  {
1049  branch.AccumulatedDamage += damage;
1050  }
1051  }
1052  }
1053 
1054  if (attacker != null && toxinsCooldown <= 0)
1055  {
1056  toxinsTimer = 25f;
1057  toxinsCooldown = 60f;
1058  }
1059 
1060  if (type == AttackType.Fire)
1061  {
1062  if (attacker is not null)
1063  {
1064  damage *= 1f + attacker.GetStatValue(StatTypes.BallastFloraDamageMultiplier);
1065  }
1066 
1067  if (IsInWater(branch))
1068  {
1069  damage *= 1f - SubmergedWaterResistance;
1070  }
1071 
1072  if (defenseCooldown <= 0)
1073  {
1074  if (StateMachine.State is not DefendWithPumpState)
1075  {
1076  StateMachine.EnterState(new DefendWithPumpState(branch, ClaimedTargets, attacker));
1077  defenseCooldown = 180f;
1078  }
1079  else
1080  {
1081  defenseCooldown = 10f;
1082  }
1083  }
1084  }
1085 
1086  if (damage > 0)
1087  {
1088  damage = Math.Min(damage, branch.Health);
1089  }
1090  else
1091  {
1092  damage = Math.Max(damage, branch.Health - branch.MaxHealth);
1093  }
1094  branch.Health -= damage;
1095 
1096 #if SERVER
1097  GameMain.Server?.KarmaManager?.OnBallastFloraDamaged(attacker, damage);
1098 #endif
1099 
1100  if (branch.Health <= 0 && type != AttackType.CutFromRoot)
1101  {
1102  RemoveBranch(branch);
1103  if (branch.IsRoot) { Kill(); }
1104  }
1105  }
1106 
1107  private void CheckDisconnectedFromRoot()
1108  {
1109  bool foundDisconnected;
1110  do
1111  {
1112  foundDisconnected = false;
1113  foreach (BallastFloraBranch branch in Branches)
1114  {
1115  if (branch.ParentBranch == null || branch.DisconnectedFromRoot) { continue; }
1116  if (branch.ParentBranch.Removed || branch.ParentBranch.DisconnectedFromRoot)
1117  {
1118  branch.DisconnectedFromRoot = true;
1119  foundDisconnected = true;
1120  }
1121  }
1122  } while (foundDisconnected);
1123 
1124  }
1125 
1126  public void RemoveBranch(BallastFloraBranch branch)
1127  {
1128  bool isClient = GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient;
1129 
1130  Anger += 0.01f;
1131 
1132  bool wasRemoved = branch.Removed;
1133  Branches.Remove(branch);
1134  branch.Removed = true;
1135 
1136  CheckDisconnectedFromRoot();
1137 
1138  bodies.ForEachMod(body =>
1139  {
1140  if (body.UserData == branch)
1141  {
1142  GameMain.World.Remove(body);
1143  bodies.Remove(body);
1144  foreach (var (tileSide, otherBranch) in branch.Connections)
1145  {
1146  TileSide opposite = tileSide.GetOppositeSide();
1147  otherBranch.BlockedSides &= ~opposite;
1148  otherBranch.Sides &= ~opposite;
1149 
1150  otherBranch.UpdateType();
1151 
1152  if (isClient) { continue; }
1153 
1154  // Remove branches that are not connected to anything anymore
1155  if ((otherBranch.Type == VineTileType.Stem || otherBranch.Sides == TileSide.None) && !otherBranch.IsRoot)
1156  {
1157  RemoveBranch(otherBranch);
1158  }
1159  }
1160  }
1161  });
1162 
1163 #if CLIENT
1164  CreateDeathParticle(branch, 1.0f);
1165 #endif
1166 
1167  if (isClient) { return; }
1168 
1169  int rootGrowthCount = Branches.Count(b => b.IsRootGrowth);
1170  if (rootGrowthCount > GetDesiredRootGrowthAmount())
1171  {
1172  var rootGrowth = Branches.LastOrDefault(b => b.IsRootGrowth);
1173  if (rootGrowth != null)
1174  {
1175  RemoveBranch(rootGrowth);
1176  }
1177  }
1178 
1179  if (branch.ClaimedItem != null)
1180  {
1181  RemoveClaim(branch.ClaimedItem);
1182  }
1183 
1184  if (branch.IsRoot)
1185  {
1186  Kill();
1187  return;
1188  }
1189 #if SERVER
1190  if (!wasRemoved && Parent != null && !Parent.Removed)
1191  {
1192  CreateNetworkMessage(new BranchRemoveEventData(branch));
1193  }
1194 #endif
1195  }
1196 
1197  public void RemoveClaim(Item item)
1198  {
1199  if (!IgnoredTargets.ContainsKey(item))
1200  {
1201  IgnoredTargets.Add(item, 10);
1202  }
1203 
1204  ClaimedTargets.Remove(item);
1205  item.Infector = null;
1206 
1207  foreach (var branch in Branches)
1208  {
1209  if (branch.ClaimedItem == item)
1210  {
1211  branch.ClaimedItem = null;
1212  }
1213  }
1214 
1215  ClaimedJunctionBoxes.ForEachMod(jb =>
1216  {
1217  if (jb.Item == item)
1218  {
1219  ClaimedJunctionBoxes.Remove(jb);
1220  }
1221  });
1222 
1223  ClaimedBatteries.ForEachMod(bat =>
1224  {
1225  if (bat.Item == item)
1226  {
1227  ClaimedBatteries.Remove(bat);
1228  }
1229  });
1230 #if SERVER
1231  if (!item.Removed && Parent != null && !Parent.Removed)
1232  {
1233  CreateNetworkMessage(new InfectEventData(item, InfectEventData.InfectState.No, null));
1234  }
1235 #endif
1236  }
1237 
1238  public void Kill()
1239  {
1240  isDead = true;
1241 
1242  foreach (var branch in Branches)
1243  {
1244  branch.DisconnectedFromRoot = true;
1245  }
1246 
1247  foreach (Item target in ClaimedTargets.ToList())
1248  {
1249  RemoveClaim(target);
1250  target.Infector = null;
1251  }
1252  Debug.Assert(ClaimedTargets.Count == 0);
1253  Debug.Assert(ClaimedJunctionBoxes.Count == 0);
1254  Debug.Assert(ClaimedBatteries.Count == 0);
1255 
1256  StateMachine?.State?.Exit();
1257 #if SERVER
1258  if (Parent != null && !Parent.Removed)
1259  {
1260  CreateNetworkMessage(new KillEventData());
1261  }
1262 #endif
1263  }
1264 
1265  public void Remove()
1266  {
1267  Kill();
1268 
1269  Branches.ForEachMod(RemoveBranch);
1270  Branches.Clear();
1271  toBeRemoved.Clear();
1272  Parent.BallastFlora = null;
1273 
1274  // clean up leftover (can probably be removed)
1275  foreach (Body body in bodies)
1276  {
1277  Debug.Assert(false, "Leftover bodies found after the ballast flora has died.");
1278  GameMain.World.Remove(body);
1279  }
1280 
1281  _entityList.Remove(this);
1282 #if SERVER
1283  if (Parent != null && !Parent.Removed)
1284  {
1285  CreateNetworkMessage(new RemoveEventData());
1286  }
1287 #endif
1288  }
1289 
1290  private void BreakThrough()
1291  {
1292  HasBrokenThrough = true;
1293 
1294  foreach (Body body in bodies)
1295  {
1296  body.Enabled = true;
1297  }
1298 
1299 #if CLIENT
1300  foreach (BallastFloraBranch branch in Branches)
1301  {
1302  CreateShapnel(GetWorldPosition() + branch.Position);
1303  }
1304 
1305  SoundPlayer.PlayDamageSound(BurstSound, BreakthroughPoint, GetWorldPosition(), range: 800);
1306 #endif
1307  }
1308 
1309  private bool CanGrowMore() => Branches.Any(b => b.CanGrowMore());
1310 
1311  private bool CollidesWithWorld(Rectangle rect, bool checkOtherBranches = true)
1312  {
1313  if (checkOtherBranches && Branches.Any(g => g.Rect.Contains(rect))) { return true; }
1314 
1315  Rectangle worldRect = rect;
1316  worldRect.Location = (Parent.Position + Offset).ToPoint() + worldRect.Location;
1317  worldRect.Y -= worldRect.Height;
1318 
1319  Vector2 topLeft = ConvertUnits.ToSimUnits(new Vector2(worldRect.Left, worldRect.Top)),
1320  topRight = ConvertUnits.ToSimUnits(new Vector2(worldRect.Right, worldRect.Top)),
1321  bottomLeft = ConvertUnits.ToSimUnits(new Vector2(worldRect.Left, worldRect.Bottom)),
1322  bottomRight = ConvertUnits.ToSimUnits(new Vector2(worldRect.Right, worldRect.Bottom));
1323 
1324  bool hasCollision = LineCollides(topLeft, topRight) || LineCollides(topRight, bottomRight) || LineCollides(bottomRight, bottomLeft) || LineCollides(bottomLeft, topLeft);
1325 
1326  return hasCollision;
1327  }
1328 
1329  private static bool LineCollides(Vector2 point1, Vector2 point2)
1330  {
1331  const Category category = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel;
1332  return Submarine.PickBody(point1, point2, collisionCategory: category, customPredicate: CustomPredicate) != null;
1333 
1334  static bool CustomPredicate(Fixture f)
1335  {
1336  bool hasCollision = f.CollidesWith.HasFlag(Physics.CollisionItem);
1337  Body body = f.Body;
1338 
1339  if (body.UserData == null) { return false; }
1340 
1341  switch (body.UserData)
1342  {
1343  case Submarine _:
1344  case Structure _:
1345  return hasCollision;
1346  default:
1347  return false;
1348  }
1349  }
1350  }
1351  }
1352 }
Identifier[] GetAttributeIdentifierArray(Identifier[] def, params string[] keys)
int GetAttributeInt(string key, int def)
float ExtraLoad
Additional load coming from somewhere else than the devices connected to the junction box (e....
void UpdatePulse(float deltaTime, float inflateSpeed, float deflateSpeed, float delay)
BallastFloraBranch(BallastFloraBehavior? parent, BallastFloraBranch? parentBranch, Vector2 position, VineTileType type, FoliageConfig? flowerConfig=null, FoliageConfig? leafConfig=null, Rectangle? rect=null)
StatTypes
StatTypes are used to alter several traits of a character. They are mostly used by talents.
Definition: Enums.cs:180