3 using System.Collections.Generic;
4 using System.Diagnostics;
5 using System.Globalization;
12 using FarseerPhysics.Dynamics;
13 using Microsoft.Xna.Framework;
27 private float health = 100;
30 get {
return health; }
31 set { health = MathHelper.Clamp(value, 0.0f,
MaxHealth); }
52 private float pulseDelay = Rand.Range(0f, 3f);
57 get {
return parentBranch; }
60 if (value != parentBranch)
63 if (parentBranch !=
null)
82 public readonly Dictionary<TileSide, BallastFloraBranch>
Connections =
new Dictionary<TileSide, BallastFloraBranch>();
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)
95 HealthColor = Color.Lerp(HealthColor, healthColor, 0.05f);
98 public void UpdatePulse(
float deltaTime,
float inflateSpeed,
float deflateSpeed,
float delay)
104 pulseDelay -= deltaTime;
110 Pulse += inflateSpeed * deltaTime;
119 Pulse -= deflateSpeed * deltaTime;
132 public List<Tuple<Vector2, Vector2>> debugSearchLines =
new List<Tuple<Vector2, Vector2>>();
135 private readonly
static List<BallastFloraBehavior> _entityList =
new List<BallastFloraBehavior>();
136 public static IEnumerable<BallastFloraBehavior> EntityList => _entityList;
138 public enum NetworkHeader
149 public enum AttackType
170 foreach (Identifier targetTag
in Tags)
172 if (item.HasTag(targetTag)) {
return true; }
179 public float BaseBranchScale {
get;
set; }
182 public float BaseFlowerScale {
get;
set; }
185 public float BaseLeafScale {
get;
set; }
187 [Serialize(0.33f,
IsPropertySaveable.Yes,
"Chance for a flower to appear on a branch.")]
188 public float FlowerProbability {
get;
set; }
191 public float LeafProbability {
get;
set; }
194 public float PulseDelay {
get;
set; }
196 [Serialize(3f,
IsPropertySaveable.Yes,
"How fast the flower inflates during a pulse.")]
197 public float PulseInflateSpeed {
get;
set; }
200 public float PulseDeflateSpeed {
get;
set; }
202 [Serialize(32,
IsPropertySaveable.Yes,
"How many vines must grow before the plant breaks through the wall.")]
203 public int BreakthroughPoint {
get;
set; }
205 [Serialize(
false,
IsPropertySaveable.Yes,
"Has the plant grown large enough to expose itself.")]
206 public bool HasBrokenThrough {
get;
set; }
208 [Serialize(300,
IsPropertySaveable.Yes,
"How far the ballast flora can detect items from.")]
209 public int Sight {
get;
set; }
212 public int BranchHealth {
get;
set; }
215 public int RootHealth {
get;
set; }
217 [Serialize(0.00025f,
IsPropertySaveable.Yes,
"How fast the root's health regenerates per each grown branch.")]
218 public float HealthRegenPerBranch {
get;
set; }
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; }
224 public Color RootColor {
get;
set; }
226 [Serialize(300f,
IsPropertySaveable.Yes,
"How much power the ballast flora takes from junction boxes.")]
227 public float PowerConsumptionMin {
get;
set; }
230 public float PowerConsumptionMax {
get;
set; }
232 [Serialize(10f,
IsPropertySaveable.Yes,
"How long it takes for power drain to wind down.")]
233 public float PowerConsumptionDuration {
get;
set; }
235 [Serialize(250f,
IsPropertySaveable.Yes,
"How much power does it take to accelerate growth.")]
236 public float PowerRequirement {
get;
set; }
238 [Serialize(5f,
IsPropertySaveable.Yes,
"Maximum anger, anger increases when the plant gets damaged and increases growth speed.")]
239 public float MaxAnger {
get;
set; }
242 public float MaxPowerCapacity {
get;
set; }
244 [Serialize(
"",
IsPropertySaveable.Yes,
"Item prefab that is spawned when threatened.")]
245 public Identifier AttackItemPrefab {
get;
set; } = Identifier.Empty;
247 [Serialize(0.8f,
IsPropertySaveable.Yes,
"How resistant the ballast flora is to explosives before it blooms.")]
248 public float ExplosionResistance {
get;
set; }
251 public float FireVulnerability {
get;
set; }
253 [Serialize(0.5f,
IsPropertySaveable.Yes,
"How much resistance against fire is gained while submerged.")]
254 public float SubmergedWaterResistance {
get;
set; }
256 [Serialize(0.8f,
IsPropertySaveable.Yes,
"What depth the branches will be drawn on.")]
257 public float BranchDepth {
get;
set; }
259 [Serialize(
"",
IsPropertySaveable.Yes,
"What sound to play when the ballast flora bursts through walls.")]
260 public string BurstSound {
get;
set; } =
"";
262 private float availablePower;
264 [Serialize(0f,
IsPropertySaveable.Yes,
"How much power the ballast flora has stored.")]
265 public float AvailablePower
267 get => availablePower;
268 set => availablePower = Math.Max(value, MaxPowerCapacity);
273 [Serialize(1f,
IsPropertySaveable.Yes,
"How enraged the flora is, affects how fast it grows.")]
277 set => anger = Math.Clamp(value, 1f, MaxAnger);
280 public string Name {
get; } =
"";
282 public Hull Parent {
get;
private set; }
284 public BallastFloraPrefab Prefab {
get;
private set; }
286 public Dictionary<Identifier, SerializableProperty> SerializableProperties {
get;
private set; }
288 public Vector2 Offset;
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>();
295 private readonly List<Tuple<UInt16, int>> tempClaimedTargets =
new List<Tuple<ushort, int>>();
297 private int flowerVariants, leafVariants;
298 public readonly List<AITarget> Targets =
new List<AITarget>();
300 public float PowerConsumptionTimer;
302 private float defenseCooldown, toxinsCooldown, fireCheckCooldown;
303 private float selfDamageTimer, toxinsTimer, toxinsSpawnTimer;
305 private readonly List<BallastFloraBranch> branchesVulnerableToFire =
new List<BallastFloraBranch>();
307 public readonly List<BallastFloraBranch> Branches =
new List<BallastFloraBranch>();
308 private BallastFloraBranch? root;
309 private readonly List<Body> bodies =
new List<Body>();
313 public readonly BallastFloraStateMachine StateMachine;
315 public int GrowthWarps;
317 public void OnMapLoaded()
319 foreach ((ushort itemId,
int branchid) in tempClaimedTargets)
321 if (Entity.FindEntityByID(itemId) is Item item)
323 ClaimTarget(item, Branches.FirstOrDefault(b => b.ID == branchid),
true);
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);
333 foreach (BallastFloraBranch branch
in Branches)
336 if (branch.ClaimedItemId > -1)
338 if (Entity.FindEntityByID((ushort)branch.ClaimedItemId) is Item item)
340 branch.ClaimedItem = item;
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);
349 UpdateConnections(branch);
355 private int CreateID()
357 int maxId = Branches.Any() ? Branches.Max(b => b.ID) : 0;
361 public Vector2 GetWorldPosition()
363 return Parent.WorldPosition + Offset;
366 public BallastFloraBehavior(Hull parent, BallastFloraPrefab prefab, Vector2 offset,
bool firstGrowth =
false)
371 SerializableProperties = SerializableProperty.DeserializeProperties(
this, prefab.Element);
372 LoadPrefab(prefab.Element);
373 StateMachine =
new BallastFloraStateMachine(
this);
374 if (firstGrowth) { GenerateRoot(); }
375 _entityList.Add(
this);
378 partial
void LoadPrefab(ContentXElement element);
380 public void LoadTargets(ContentXElement element)
382 foreach (var subElement
in element.Elements())
384 Targets.Add(
new AITarget(subElement));
388 public void Save(XElement element)
390 XElement saveElement =
new XElement(nameof(BallastFloraBehavior),
391 new XAttribute(
"identifier", Prefab.Identifier),
392 new XAttribute(
"offset", XMLExtensions.Vector2ToString(Offset)));
394 SerializableProperty.SerializeProperties(
this, saveElement);
396 foreach (BallastFloraBranch branch
in Branches)
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));
411 if (branch.ClaimedItem !=
null)
413 be.Add(
new XAttribute(
"claimed", (
int)(branch.ClaimedItem?.ID ?? -1)));
415 if (branch.ParentBranch !=
null && !branch.ParentBranch.Removed)
417 be.Add(
new XAttribute(
"parentbranch", (
int)(branch.ParentBranch?.ID ?? -1)));
423 foreach (Item target
in ClaimedTargets)
425 if (target.Infector ==
null)
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);
432 XElement te =
new XElement(
"ClaimedTarget",
new XAttribute(
"id", target.ID),
new XAttribute(
"branchId", target.Infector.ID));
436 element.Add(saveElement);
439 public void LoadSave(XElement element, IdRemap idRemap)
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())
446 switch (subElement.Name.ToString().ToLowerInvariant())
449 LoadBranch(subElement, idRemap);
451 case "claimedtarget":
452 int id = subElement.GetAttributeInt(
"id", -1);
453 int branchId = subElement.GetAttributeInt(
"branchId", -1);
456 tempClaimedTargets.Add(Tuple.Create(idRemap.GetOffsetId(
id), branchId));
462 foreach ((BallastFloraBranch branch,
int parentBranchId) in branches)
464 if (parentBranchId > -1)
466 var parentBranch = Branches.Find(b => b.ID == parentBranchId);
467 if (parentBranch ==
null)
469 DebugConsole.AddWarning($
"Error while loading ballast flora: couldn't find a parent branch with the ID {parentBranchId}");
473 branch.ParentBranch = parentBranch;
480 Branches.ForEach(b => b.DisconnectedFromRoot =
true);
484 CheckDisconnectedFromRoot();
487 void LoadBranch(XElement branchElement, IdRemap idRemap)
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);
503 BallastFloraBranch newBranch =
new BallastFloraBranch(
this,
null, pos, type, FoliageConfig.Deserialize(flowerConfig), FoliageConfig.Deserialize(leafconfig))
507 MaxHealth = maxhealth,
508 Sides = (TileSide)sides,
509 BlockedSides = (TileSide)blockedSides,
511 IsRootGrowth = isRootGrowth
513 branches.Add((newBranch, parentBranchId));
515 if (newBranch.IsRoot) { root = newBranch; }
519 newBranch.ClaimedItemId = idRemap.GetOffsetId((ushort)claimedId);
522 Branches.Add(newBranch);
524 int getInt(
string name) => branchElement.GetAttributeInt(name, 0);
525 float getFloat(
string name) => branchElement.GetAttributeFloat(name, 0f);
529 public void Update(
float deltaTime)
531 if (GameMain.NetworkMember ==
null || !GameMain.NetworkMember.IsClient)
533 if (Branches.Count == 0)
540 foreach (BallastFloraBranch branch
in Branches)
542 branch.UpdateScale(deltaTime);
543 branch.UpdatePulse(deltaTime, PulseInflateSpeed, PulseDeflateSpeed, PulseDelay);
545 branch.UpdateHealth();
549 UpdateDamage(deltaTime);
551 UpdatePowerDrain(deltaTime);
553 if (GameMain.NetworkMember !=
null && GameMain.NetworkMember.IsClient) {
return; }
555 if (root !=
null && HealthRegenPerBranch > 0.0f)
557 float healAmount = Branches.Count(b => !b.IsRoot && !b.IsRootGrowth && !b.DisconnectedFromRoot) * HealthRegenPerBranch;
559 foreach (BallastFloraBranch branch
in Branches)
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);
569 StateMachine.Update(deltaTime);
571 if (HasBrokenThrough)
574 if (fireCheckCooldown <= 0)
577 fireCheckCooldown = 5f;
581 fireCheckCooldown -= deltaTime;
584 foreach (BallastFloraBranch branch
in branchesVulnerableToFire)
588 DamageBranch(branch, FireVulnerability * deltaTime, AttackType.Fire,
null);
593 UpdateSelfDamage(deltaTime);
600 if (toxinsTimer > 0.1f)
602 toxinsSpawnTimer -= deltaTime;
603 if (!AttackItemPrefab.IsEmpty && toxinsSpawnTimer <= 0.0f)
605 toxinsSpawnTimer = 1.0f;
606 Dictionary<Hull, List<BallastFloraBranch>> branches =
new Dictionary<Hull, List<BallastFloraBranch>>();
607 foreach (BallastFloraBranch branch
in Branches)
609 if (branch.CurrentHull ==
null || branch.FlowerConfig.Variant < 0 || branch.DisconnectedFromRoot) {
continue; }
611 if (branches.TryGetValue(branch.CurrentHull, out List<BallastFloraBranch>? list))
617 branches.Add(branch.CurrentHull,
new List<BallastFloraBranch> { branch });
621 foreach (Hull hull
in branches.Keys)
623 List<BallastFloraBranch> list = branches[hull];
624 if (!list.Any(HasAcidEmitter))
626 BallastFloraBranch randomBranch = branches[hull].GetRandomUnsynced();
627 randomBranch.SpawningItem =
true;
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 =>
633 randomBranch.AttackItem = item;
634 randomBranch.SpawningItem =
false;
638 static bool HasAcidEmitter(BallastFloraBranch b) => b.SpawningItem || (b.AttackItem !=
null && !b.AttackItem.Removed);
642 toxinsTimer -= deltaTime;
645 if (defenseCooldown >= 0)
647 defenseCooldown -= deltaTime;
650 if (toxinsCooldown >= 0)
652 toxinsCooldown -= deltaTime;
656 partial
void UpdateDamage(
float deltaTime);
658 private readonly List<BallastFloraBranch> toBeRemoved =
new List<BallastFloraBranch>();
659 private void UpdateSelfDamage(
float deltaTime)
661 if (selfDamageTimer <= 0)
663 if (!HasBrokenThrough && !CanGrowMore())
665 Branches.ForEachMod(branch =>
667 float maxHealth = branch.IsRoot ? RootHealth : BranchHealth;
668 DamageBranch(branch, Rand.Range(1f, maxHealth), AttackType.Other);
671 selfDamageTimer = 1f;
674 foreach (BallastFloraBranch branch
in Branches)
678 if (branch.ParentBranch ==
null || branch.ParentBranch.DisconnectedFromRoot || branch.ParentBranch.Health <= 0.0f)
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);
685 if (branch.Health <= 0.0f)
687 if (branch.ClaimedItem !=
null)
689 RemoveClaim(branch.ClaimedItem);
692 branch.RemoveTimer -= deltaTime;
693 if (branch.RemoveTimer <= 0.0f)
695 toBeRemoved.Add(branch);
699 foreach (BallastFloraBranch branch
in toBeRemoved)
701 RemoveBranch(branch);
703 selfDamageTimer -= deltaTime;
706 private void UpdatePowerDrain(
float deltaTime)
708 PowerConsumptionTimer += deltaTime;
709 if (PowerConsumptionTimer > PowerConsumptionDuration)
711 PowerConsumptionTimer = 0f;
714 float powerConsumption = MathHelper.Lerp(PowerConsumptionMax, PowerConsumptionMin, PowerConsumptionTimer / PowerConsumptionDuration);
715 float powerDelta = powerConsumption * deltaTime;
719 if (jb.
ExtraLoad > Math.Max(PowerConsumptionMin, PowerConsumptionMax)) {
continue; }
725 if (currPowerConsumption > powerDelta)
727 AvailablePower += powerDelta;
731 AvailablePower += currPowerConsumption * deltaTime;
735 float batteryDrain = powerDelta * 0.1f;
738 float amount = Math.Min(battery.
MaxOutPut, batteryDrain);
740 if (battery.
Charge > amount)
743 AvailablePower += amount;
751 private void UpdateFireSources()
753 branchesVulnerableToFire.Clear();
754 foreach (BallastFloraBranch branch
in Branches)
756 if (branch.CurrentHull ==
null) {
continue; }
758 foreach (FireSource source
in branch.CurrentHull.FireSources)
760 if (source.IsInDamageRange(GetWorldPosition() + branch.Position, source.DamageRange))
762 branchesVulnerableToFire.Add(branch);
768 private bool IsInWater(BallastFloraBranch branch)
770 if (branch.CurrentHull ==
null) {
return false; }
772 float surfaceY = branch.CurrentHull.Surface;
773 Vector2 pos = Parent.Position + Offset + branch.Position;
774 return Parent.WaterVolume > 0.0f && pos.Y < surfaceY;
778 public void SetHull(BallastFloraBranch branch)
780 branch.CurrentHull = Hull.FindHull(GetWorldPosition() + branch.Position, Parent,
true);
783 private void GenerateRoot()
787 DebugConsole.ThrowError(
"Error in ballast flora: tried to grow a root even though root has already been created.\n" + Environment.StackTrace);
790 root =
new BallastFloraBranch(
this,
null, Vector2.Zero, VineTileType.Stem, FoliageConfig.EmptyConfig, FoliageConfig.EmptyConfig)
792 BlockedSides = TileSide.Bottom | TileSide.Left | TileSide.Right,
794 MaxHealth = RootHealth,
797 CurrentHull = Parent,
805 public float GetGrowthSpeed(
float deltaTime)
807 float load = PowerRequirement * Anger * deltaTime;
809 if (AvailablePower > load)
811 AvailablePower -= load;
812 return Anger * 2f * deltaTime;
818 public bool TryGrowBranch(BallastFloraBranch parent, TileSide side, out List<BallastFloraBranch> result,
bool isRootGrowth =
false, Vector2? forcePosition =
null)
820 result =
new List<BallastFloraBranch>();
821 if (!isRootGrowth && parent.IsSideBlocked(side)) {
return false; }
823 Vector2 pos = forcePosition ?? parent.AdjacentPositions[side];
824 Rectangle rect = VineTile.CreatePlantRect(pos);
826 if (CollidesWithWorld(rect, checkOtherBranches: !isRootGrowth))
828 parent.BlockedSides |= side;
829 parent.FailedGrowthAttempts++;
833 FoliageConfig flowerConfig = FoliageConfig.EmptyConfig;
834 FoliageConfig leafConfig = FoliageConfig.EmptyConfig;
836 if (FlowerProbability > Rand.Range(0d, 1.0d))
838 flowerConfig = FoliageConfig.CreateRandomConfig(flowerVariants, 0.5f, 1.0f);
841 if (LeafProbability > Rand.Range(0d, 1.0d))
843 leafConfig = FoliageConfig.CreateRandomConfig(leafVariants, 0.5f, 1.0f);
846 BallastFloraBranch newBranch =
new BallastFloraBranch(
this, parent, pos, VineTileType.CrossJunction, flowerConfig, leafConfig, rect)
849 MaxHealth = BranchHealth,
851 IsRootGrowth = isRootGrowth
856 if (newBranch.CurrentHull ==
null || newBranch.CurrentHull.Submarine != Parent.Submarine)
858 if (!isRootGrowth) { parent.BlockedSides |= side; }
859 parent.FailedGrowthAttempts++;
863 UpdateConnections(newBranch, parent);
865 Branches.Add(newBranch);
866 result.Add(newBranch);
868 OnBranchGrowthSuccess(newBranch);
875 int rootGrowthCount = Branches.Count(b => b.IsRootGrowth);
876 if (rootGrowthCount < GetDesiredRootGrowthAmount())
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);
886 CreateNetworkMessage(
new BranchCreateEventData(newBranch, parent));
891 private int GetDesiredRootGrowthAmount()
893 if (root ==
null) {
return 0; }
894 return MathHelper.Clamp(Branches.Count(b => !b.IsRootGrowth && b.Health > 0) / 20, 3, 30);
897 public bool BranchContainsTarget(BallastFloraBranch branch, Item target)
900 worldRect.Location = GetWorldPosition().ToPoint() + worldRect.Location;
901 return worldRect.IntersectsWorld(target.WorldRect);
904 public void ClaimTarget(Item target, BallastFloraBranch? branch,
bool load =
false)
906 target.Infector = branch;
908 if (target.GetComponent<
PowerTransfer>() is { } powerTransfer)
910 ClaimedJunctionBoxes.Add(powerTransfer);
915 ClaimedBatteries.Add(powerContainer);
918 ClaimedTargets.Add(target);
922 branch.ClaimedItem = target;
928 CreateNetworkMessage(
new InfectEventData(target, InfectEventData.InfectState.Yes, branch));
933 private void UpdateConnections(BallastFloraBranch branch, BallastFloraBranch? parent =
null)
935 foreach (BallastFloraBranch otherBranch
in Branches)
937 var (distX, distY) = branch.Position - otherBranch.Position;
938 int absDistX = (int) Math.Abs(distX), absDistY = (int) Math.Abs(distY);
940 if (absDistX > branch.Rect.Width || absDistY > branch.Rect.Height || absDistX > 0 && absDistY > 0) {
continue; }
942 TileSide connectingSide = absDistX > absDistY ? distX > 0 ? TileSide.Right : TileSide.Left : distY > 0 ? TileSide.Top : TileSide.Bottom;
944 TileSide oppositeSide = connectingSide.GetOppositeSide();
948 if (otherBranch.BlockedSides.HasFlag(connectingSide))
950 branch.BlockedSides |= oppositeSide;
954 if (otherBranch != parent)
956 otherBranch.BlockedSides |= connectingSide;
957 branch.BlockedSides |= oppositeSide;
961 otherBranch.Sides |= connectingSide;
962 branch.Sides |= oppositeSide;
966 branch.Connections.TryAdd(oppositeSide, otherBranch);
967 otherBranch.Connections.TryAdd(connectingSide, branch);
971 private void OnBranchGrowthSuccess(BallastFloraBranch newBranch)
973 if (!HasBrokenThrough)
975 if (Branches.Count > BreakthroughPoint)
981 if (newBranch.FlowerConfig.Variant > -1)
983 Vector2 flowerPos = GetWorldPosition() + newBranch.Position;
984 CreateShapnel(flowerPos);
985 newBranch.GrowthStep = 2.0f;
986 SoundPlayer.PlayDamageSound(BurstSound, 1.0f, flowerPos, range: 800);
991 CreateBody(newBranch);
993 foreach (BallastFloraBranch vine
in Branches)
1003 private void CreateBody(BallastFloraBranch branch)
1006 Vector2 pos = Parent.Position + Offset + branch.Position;
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;
1017 bodies.Add(branchBody);
1020 public void DamageBranch(BallastFloraBranch branch,
float amount, AttackType type, Character? attacker =
null)
1022 float damage = amount;
1024 if (type != AttackType.Other && type != AttackType.CutFromRoot)
1026 branch.DamageVisualizationTimer = 1.0f;
1029 if (branch.IsRootGrowth && root is { Health: > 0.0f }) {
return; }
1031 if (type != AttackType.Other && type != AttackType.CutFromRoot)
1033 branch.AccumulatedDamage += damage;
1034 Anger += damage * 0.001f;
1037 if (GameMain.NetworkMember !=
null)
1040 if (GameMain.NetworkMember.IsClient)
1047 if (type == AttackType.Other || type == AttackType.CutFromRoot)
1049 branch.AccumulatedDamage += damage;
1054 if (attacker !=
null && toxinsCooldown <= 0)
1057 toxinsCooldown = 60f;
1060 if (type == AttackType.Fire)
1062 if (attacker is not
null)
1064 damage *= 1f + attacker.GetStatValue(
StatTypes.BallastFloraDamageMultiplier);
1067 if (IsInWater(branch))
1069 damage *= 1f - SubmergedWaterResistance;
1072 if (defenseCooldown <= 0)
1074 if (StateMachine.State is not DefendWithPumpState)
1076 StateMachine.EnterState(
new DefendWithPumpState(branch, ClaimedTargets, attacker));
1077 defenseCooldown = 180f;
1081 defenseCooldown = 10f;
1088 damage = Math.Min(damage, branch.Health);
1092 damage = Math.Max(damage, branch.Health - branch.MaxHealth);
1094 branch.Health -= damage;
1097 GameMain.Server?.KarmaManager?.OnBallastFloraDamaged(attacker, damage);
1100 if (branch.Health <= 0 && type != AttackType.CutFromRoot)
1102 RemoveBranch(branch);
1103 if (branch.IsRoot) { Kill(); }
1107 private void CheckDisconnectedFromRoot()
1109 bool foundDisconnected;
1112 foundDisconnected =
false;
1113 foreach (BallastFloraBranch branch
in Branches)
1115 if (branch.ParentBranch ==
null || branch.DisconnectedFromRoot) {
continue; }
1116 if (branch.ParentBranch.Removed || branch.ParentBranch.DisconnectedFromRoot)
1118 branch.DisconnectedFromRoot =
true;
1119 foundDisconnected =
true;
1122 }
while (foundDisconnected);
1126 public void RemoveBranch(BallastFloraBranch branch)
1128 bool isClient = GameMain.NetworkMember !=
null && GameMain.NetworkMember.IsClient;
1132 bool wasRemoved = branch.Removed;
1133 Branches.Remove(branch);
1134 branch.Removed =
true;
1136 CheckDisconnectedFromRoot();
1138 bodies.ForEachMod(body =>
1140 if (body.UserData == branch)
1142 GameMain.World.Remove(body);
1143 bodies.Remove(body);
1144 foreach (var (tileSide, otherBranch) in branch.Connections)
1146 TileSide opposite = tileSide.GetOppositeSide();
1147 otherBranch.BlockedSides &= ~opposite;
1148 otherBranch.Sides &= ~opposite;
1150 otherBranch.UpdateType();
1152 if (isClient) { continue; }
1155 if ((otherBranch.Type == VineTileType.Stem || otherBranch.Sides == TileSide.None) && !otherBranch.IsRoot)
1157 RemoveBranch(otherBranch);
1164 CreateDeathParticle(branch, 1.0f);
1167 if (isClient) {
return; }
1169 int rootGrowthCount = Branches.Count(b => b.IsRootGrowth);
1170 if (rootGrowthCount > GetDesiredRootGrowthAmount())
1172 var rootGrowth = Branches.LastOrDefault(b => b.IsRootGrowth);
1173 if (rootGrowth !=
null)
1175 RemoveBranch(rootGrowth);
1179 if (branch.ClaimedItem !=
null)
1181 RemoveClaim(branch.ClaimedItem);
1190 if (!wasRemoved && Parent !=
null && !Parent.Removed)
1192 CreateNetworkMessage(
new BranchRemoveEventData(branch));
1197 public void RemoveClaim(Item item)
1199 if (!IgnoredTargets.ContainsKey(item))
1201 IgnoredTargets.Add(item, 10);
1204 ClaimedTargets.Remove(item);
1205 item.Infector =
null;
1207 foreach (var branch
in Branches)
1209 if (branch.ClaimedItem == item)
1211 branch.ClaimedItem =
null;
1215 ClaimedJunctionBoxes.ForEachMod(jb =>
1217 if (jb.
Item == item)
1219 ClaimedJunctionBoxes.Remove(jb);
1223 ClaimedBatteries.ForEachMod(bat =>
1225 if (bat.Item == item)
1227 ClaimedBatteries.Remove(bat);
1231 if (!item.Removed && Parent !=
null && !Parent.Removed)
1233 CreateNetworkMessage(
new InfectEventData(item, InfectEventData.InfectState.No,
null));
1242 foreach (var branch
in Branches)
1244 branch.DisconnectedFromRoot =
true;
1247 foreach (Item target
in ClaimedTargets.ToList())
1249 RemoveClaim(target);
1250 target.Infector =
null;
1252 Debug.Assert(ClaimedTargets.Count == 0);
1253 Debug.Assert(ClaimedJunctionBoxes.Count == 0);
1254 Debug.Assert(ClaimedBatteries.Count == 0);
1256 StateMachine?.State?.Exit();
1258 if (Parent !=
null && !Parent.Removed)
1260 CreateNetworkMessage(
new KillEventData());
1265 public void Remove()
1269 Branches.ForEachMod(RemoveBranch);
1271 toBeRemoved.Clear();
1272 Parent.BallastFlora =
null;
1275 foreach (Body body
in bodies)
1277 Debug.Assert(
false,
"Leftover bodies found after the ballast flora has died.");
1278 GameMain.World.Remove(body);
1281 _entityList.Remove(
this);
1283 if (Parent !=
null && !Parent.Removed)
1285 CreateNetworkMessage(
new RemoveEventData());
1290 private void BreakThrough()
1292 HasBrokenThrough =
true;
1294 foreach (Body body
in bodies)
1296 body.Enabled =
true;
1300 foreach (BallastFloraBranch branch
in Branches)
1302 CreateShapnel(GetWorldPosition() + branch.Position);
1305 SoundPlayer.PlayDamageSound(BurstSound, BreakthroughPoint, GetWorldPosition(), range: 800);
1309 private bool CanGrowMore() => Branches.Any(b => b.CanGrowMore());
1311 private bool CollidesWithWorld(Rectangle rect,
bool checkOtherBranches =
true)
1313 if (checkOtherBranches && Branches.Any(g => g.Rect.Contains(rect))) {
return true; }
1316 worldRect.Location = (Parent.Position + Offset).ToPoint() + worldRect.Location;
1317 worldRect.Y -= worldRect.Height;
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));
1324 bool hasCollision = LineCollides(topLeft, topRight) || LineCollides(topRight, bottomRight) || LineCollides(bottomRight, bottomLeft) || LineCollides(bottomLeft, topLeft);
1326 return hasCollision;
1329 private static bool LineCollides(Vector2 point1, Vector2 point2)
1331 const Category category = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel;
1332 return Submarine.PickBody(point1, point2, collisionCategory: category, customPredicate: CustomPredicate) !=
null;
1334 static bool CustomPredicate(Fixture f)
1336 bool hasCollision = f.CollidesWith.HasFlag(Physics.CollisionItem);
1339 if (body.UserData ==
null) {
return false; }
1341 switch (body.UserData)
1345 return hasCollision;
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....
float CurrPowerConsumption
int BranchDepth
How far from the root this branch is
bool IsRootGrowth
Decorative branches that grow around the root
bool DisconnectedFromRoot
void UpdatePulse(float deltaTime, float inflateSpeed, float deflateSpeed, float delay)
readonly? BallastFloraBehavior ParentBallastFlora
BallastFloraBranch? ParentBranch
float DamageVisualizationTimer
readonly Dictionary< TileSide, BallastFloraBranch > Connections
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.
AITarget(ContentXElement element)