#nullable enable using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; using Barotrauma.Items.Components; using Barotrauma.Networking; using Barotrauma.Particles; using Barotrauma.Sounds; using FarseerPhysics; using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; namespace Barotrauma.MapCreatures.Behavior { partial class BallastFloraBehavior { public Sprite? branchAtlas, decayAtlas; public readonly Dictionary BranchSprites = new Dictionary(); public readonly List FlowerSprites = new List(), DamagedFlowerSprites = new List(); public readonly List HiddenFlowerSprites = new List(); public readonly List LeafSprites = new List(), DamagedLeafSprites = new List(); public readonly List DamageParticles = new List(); public readonly List DeathParticles = new List(); public static bool AlwaysShowBallastFloraSprite = false; partial void LoadPrefab(ContentXElement element) { if (element.GetAttributeContentPath("branchatlas") is { } branchAtlasPath) { branchAtlas = new Sprite(branchAtlasPath.Value, Rectangle.Empty); } if (element.GetAttributeContentPath("decayatlas") is { } decayAtlasPath) { decayAtlas = new Sprite(decayAtlasPath.Value, Rectangle.Empty); } foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "branchsprite": var type = subElement.GetAttributeEnum("type", VineTileType.Stem); BranchSprites.Add(type, new VineSprite(subElement)); break; case "flowersprite": FlowerSprites.Add(new Sprite(subElement)); break; case "damagedflowersprite": DamagedFlowerSprites.Add(new Sprite(subElement)); break; case "hiddenflowersprite": HiddenFlowerSprites.Add(new Sprite(subElement)); break; case "leafsprite": LeafSprites.Add(new Sprite(subElement)); break; case "damagedleafsprite": DamagedLeafSprites.Add(new Sprite(subElement)); break; case "damageparticle": DamageParticles.Add(new ParticleEmitter(subElement)); break; case "deathparticle": DeathParticles.Add(new ParticleEmitter(subElement)); break; case "targets": LoadTargets(subElement); break; } flowerVariants = FlowerSprites.Count; leafVariants = LeafSprites.Count; } } private void CreateShapnel(Vector2 pos) { float particleAmount = Rand.Range(16, 32); for (int i = 0; i < particleAmount; i++) { GameMain.ParticleManager.CreateParticle("shrapnel", pos, Rand.Vector(Rand.Range(0f, 250.0f)), Rand.Range(0f, 360.0f)); } } partial void UpdateDamage(float deltaTime) { foreach (BallastFloraBranch branch in Branches) { if (branch.IsRoot && isDead) { continue; } if (branch.AccumulatedDamage > 0) { CreateDamageParticle(branch, branch.AccumulatedDamage); if (GameMain.DebugDraw) { var pos = (Parent?.Position ?? Vector2.Zero) + Offset + branch.Position; GUI.AddMessage($"{(int)branch.AccumulatedDamage}", GUIStyle.Red, pos, Vector2.UnitY * 10.0f, 3f, playSound: false, subId: Parent?.Submarine?.ID ?? -1); } } if (Character.Controlled != null && Character.Controlled.CurrentHull == branch.CurrentHull && branch.IsRoot && (branch.AccumulatedDamage > 0.0f || branch.AccumulatedDamage < -0.1f)) { Character.Controlled.UpdateHUDProgressBar(this, GetWorldPosition() + branch.Position, branch.Health / branch.MaxHealth, emptyColor: GUIStyle.HealthBarColorLow, fullColor: GUIStyle.HealthBarColorHigh, textTag: Prefab.DisplayName.Value); } branch.AccumulatedDamage = 0f; if (branch.DamageVisualizationTimer > 0.0f) { branch.DamageVisualizationTimer -= deltaTime; float t1 = (float)Timing.TotalTime * 0.2f + branch.Position.X / 100.0f; float t2 = (float)Timing.TotalTime * 0.5f + branch.Position.Y / 100.0f; branch.ShakeAmount = new Vector2( PerlinNoise.GetPerlin(t1, t2) - 0.5f, PerlinNoise.GetPerlin(t2, t1) - 0.5f) * 10.0f * branch.DamageVisualizationTimer; } } } private void CreateDamageParticle(BallastFloraBranch branch, float deltaTime) { Vector2 pos = GetWorldPosition() + branch.Position; foreach (var particleEmitter in DamageParticles) { particleEmitter.Emit(deltaTime, pos, branch.CurrentHull); } } private void CreateDeathParticle(BallastFloraBranch branch, float deltaTime) { Vector2 pos = GetWorldPosition() + branch.Position; foreach (var particleEmitter in DeathParticles) { particleEmitter.Emit(deltaTime, pos, branch.CurrentHull); } } public void Draw(SpriteBatch spriteBatch) { const float zStep = 0.000001f; float leafDepth = zStep; float flowerDepth = zStep; if (GameMain.DebugDraw) { foreach (Body body in bodies) { Vector2 pos = Parent.Submarine.DrawPosition + ConvertUnits.ToDisplayUnits(body.Position); pos.Y = -pos.Y; GUI.DrawRectangle(spriteBatch, pos, 32f, 32f, 0f, body.UserData is BallastFloraBranch { IsRoot: true } ? Color.Magenta : Color.Cyan, 0.1f, thickness: 1); } foreach (var (key, steps) in IgnoredTargets) { string label = $"Ignored \"{key.Name}\" for {steps} steps"; var (sizeX, sizeY) = GUIStyle.SubHeadingFont.MeasureString(label); Vector2 targetPos = key.WorldPosition; targetPos.Y = -targetPos.Y; GUI.DrawString(spriteBatch, targetPos - new Vector2(sizeX / 2f, sizeY), label, GUIStyle.Red, font: GUIStyle.SubHeadingFont); } } foreach (BallastFloraBranch branch in Branches) { Vector2 pos = Parent.DrawPosition + Offset + branch.Position + branch.ShakeAmount; pos.Y = -pos.Y; float depth = branch.IsRootGrowth ? 0.2f : BranchDepth; float layer1 = depth + 0.01f, layer2 = depth + 0.02f, layer3 = depth + 0.03f; VineSprite branchSprite = BranchSprites[branch.Type]; Color branchColor = (branch.IsRoot || branch.IsRootGrowth) ? RootColor : Color.White; if (GameMain.DebugDraw) { if (branch.DisconnectedFromRoot && branch.ParentBranch == null) { branchColor = Color.Yellow; } else if (branch.DisconnectedFromRoot) { branchColor = Color.Cyan; } else if (branch.ParentBranch == null) { branchColor = Color.Magenta; } #if DEBUG Vector2 basePos = Parent.WorldPosition; foreach (var (from, to) in debugSearchLines) { Vector2 pos1 = basePos - from; pos1.Y = -pos1.Y; Vector2 pos2 = basePos - to; pos2.Y = -pos2.Y; GUI.DrawLine(spriteBatch, pos1, pos2, GUIStyle.Yellow * 0.8f, width: 4); } if (branch.ParentBranch != null) { Vector2 pos2 = Parent.DrawPosition + Offset + branch.ParentBranch.Position; pos2.Y = -pos2.Y; GUI.DrawLine(spriteBatch, pos, pos2, GUIStyle.Green * 0.8f, width: 3); } #endif string label = ""; if (branch == Branches[^1]) { label += $"Current State: {StateMachine.State?.GetType().Name ?? "null!"}\n"; } if (StateMachine.State is GrowToTargetState targetState) { if (targetState.TargetBranches.Contains(branch)) { GUI.DrawRectangle(spriteBatch, pos, branch.Rect.Width, branch.Rect.Height, 0f, Color.Red, thickness: 4); } if (targetState.TargetBranches[^1] == branch) { label += $"Target: {targetState.Target.Name}\n"; Vector2 targetPos = targetState.Target.WorldPosition; targetPos.Y = -targetPos.Y; GUI.DrawLine(spriteBatch, pos, targetPos, Color.Red, width: 4); } } var (sizeX, sizeY) = GUIStyle.SubHeadingFont.MeasureString(label); GUI.DrawString(spriteBatch, pos - new Vector2(sizeX / 2f, branch.Rect.Height + sizeY), label, Color.White, font: GUIStyle.SubHeadingFont); } bool isDamaged = branch.Health < branch.MaxHealth; if (HasBrokenThrough) { if (branchAtlas != null && branchAtlas.Loaded) { spriteBatch.Draw(branchAtlas.Texture, pos + branch.offset, branchSprite.SourceRect, branchColor, 0f, branchSprite.AbsoluteOrigin, BaseBranchScale * branch.VineStep, SpriteEffects.None, layer2); } if (decayAtlas != null && isDamaged && decayAtlas.Loaded) { spriteBatch.Draw(decayAtlas.Texture, pos + branch.offset, branchSprite.SourceRect, branch.HealthColor, 0f, branchSprite.AbsoluteOrigin, BaseBranchScale * branch.VineStep, SpriteEffects.None, layer2 - zStep); } } if (branch.FlowerConfig.Variant >= 0) { int variant = branch.FlowerConfig.Variant; Sprite flowerSprite = HasBrokenThrough ? FlowerSprites[variant] : HiddenFlowerSprites[variant]; float flowerScale = BaseFlowerScale * branch.FlowerConfig.Scale * branch.FlowerStep; if (HasBrokenThrough) { flowerScale *= branch.Pulse; } flowerSprite.Draw(spriteBatch, pos, branchColor, flowerSprite.Origin, scale: flowerScale, rotate: branch.FlowerConfig.Rotation, depth: layer1 - flowerDepth); if (isDamaged && HasBrokenThrough && DamagedFlowerSprites.Count > variant) { DamagedFlowerSprites[variant].Draw(spriteBatch, pos, branch.HealthColor, flowerSprite.Origin, scale: flowerScale, rotate: branch.FlowerConfig.Rotation, depth: layer1 - flowerDepth - zStep); } flowerDepth -= zStep; if (flowerDepth > 0.01f) { flowerDepth = zStep; } } if (branch.LeafConfig.Variant >= 0 && HasBrokenThrough) { int variant = branch.LeafConfig.Variant; Sprite leafSprite = LeafSprites[variant]; leafSprite.Draw(spriteBatch, pos, branchColor, leafSprite.Origin, scale: BaseLeafScale * branch.LeafConfig.Scale * branch.FlowerStep, rotate: branch.LeafConfig.Rotation, depth: layer3 + leafDepth); if (isDamaged && DamagedLeafSprites.Count > variant) { DamagedLeafSprites[variant].Draw(spriteBatch, pos, branch.HealthColor, leafSprite.Origin, scale: BaseLeafScale * branch.LeafConfig.Scale * branch.FlowerStep, rotate: branch.LeafConfig.Rotation, depth: layer3 + leafDepth - zStep); } leafDepth += zStep; if (leafDepth > 0.01f) { flowerDepth = zStep; } } } } public void ClientRead(IReadMessage msg, NetworkHeader header) { switch (header) { case NetworkHeader.Infect: int infectBranch = -1; ushort itemId = msg.ReadUInt16(); bool infect = msg.ReadBoolean(); if (infect) { infectBranch = msg.ReadInt32(); } Entity? entity = Entity.FindEntityByID(itemId); if (entity is Item item) { if (infect) { ClaimTarget(item, Branches.FirstOrDefault(b => b.ID == infectBranch)); } else { RemoveClaim(item); } } else { DebugConsole.AddWarning($"Received Infect.{infect} Network Header with invalid item ID: {itemId}, which belongs to {entity?.ToString() ?? "null!"}"); } break; case NetworkHeader.BranchCreate: int parentId = msg.ReadInt32(); BallastFloraBranch branch = ReadBranch(msg); BallastFloraBranch? parent = Branches.FirstOrDefault(b => b.ID == parentId); if (parent == null) { DebugConsole.AddWarning($"Received BranchCreate with an invalid parent ID: {parentId}, Maximum ID is {Branches.Max(b => b.ID)}"); } UpdateConnections(branch, parent); Branches.Add(branch); OnBranchGrowthSuccess(branch); break; case NetworkHeader.BranchRemove: int removedBranchId = msg.ReadInt32(); BallastFloraBranch? removedBranch = Branches.FirstOrDefault(b => b.ID == removedBranchId); if (removedBranch != null) { RemoveBranch(removedBranch); } else { DebugConsole.AddWarning($"Received BranchRemove for a branch that doesn't exist. ID: {removedBranchId}, Maximum ID is {Branches.Max(b => b.ID)}"); } break; case NetworkHeader.BranchDamage: int damageBranchId = msg.ReadInt32(); float health = msg.ReadSingle(); BallastFloraBranch? damagedBranch = Branches.FirstOrDefault(b => b.ID == damageBranchId); if (damagedBranch != null) { damagedBranch.Health = health; } else { DebugConsole.AddWarning($"Received BranchDamage for a branch that doesn't exist. ID: {damageBranchId}, Maximum ID is {Branches.Max(b => b.ID)}"); } break; case NetworkHeader.Kill: Kill(); break; case NetworkHeader.Remove: Remove(); break; } PowerConsumptionTimer = msg.ReadSingle(); } private BallastFloraBranch ReadBranch(IReadMessage msg) { int id = msg.ReadInt32(); bool isRootGrowth = msg.ReadBoolean(); byte type = (byte)msg.ReadRangedInteger(0b0000, 0b1111); byte sides = (byte)msg.ReadRangedInteger(0b0000, 0b1111); int flowerConfig = msg.ReadRangedInteger(0, 0xFFF); int leafConfig = msg.ReadRangedInteger(0, 0xFFF); int maxHealth = msg.ReadUInt16(); int posX = msg.ReadInt32(), posY = msg.ReadInt32(); int parentBranchIndex = msg.ReadInt32(); Vector2 pos = new Vector2(posX * VineTile.Size, posY * VineTile.Size); BallastFloraBranch? parentBranch = parentBranchIndex < 0 || parentBranchIndex >= Branches.Count ? null : Branches[parentBranchIndex]; return new BallastFloraBranch(this, parentBranch, pos, (VineTileType)type, FoliageConfig.Deserialize(flowerConfig), FoliageConfig.Deserialize(leafConfig)) { ID = id, MaxHealth = maxHealth, Sides = (TileSide) sides, IsRootGrowth = isRootGrowth }; } } }