using Barotrauma.Extensions; using Barotrauma.Networking; using FarseerPhysics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; namespace Barotrauma.Items.Components { partial class ElectricalDischarger : Powered, IServerSerializable { private static readonly List list = new List(); public static IEnumerable List { get { return list; } } const int MaxNodes = 100; const float MaxNodeDistance = 150.0f; public struct Node { public Vector2 WorldPosition; public int ParentIndex; public float Length; public float Angle; public Node(Vector2 worldPosition, int parentIndex, float length = 0.0f, float angle = 0.0f) { WorldPosition = worldPosition; ParentIndex = parentIndex; Length = length; Angle = angle; } } public override bool IsActive { get { return base.IsActive; } set { base.IsActive = value; if (!value) { nodes.Clear(); charactersInRange.Clear(); } } } [Serialize(500.0f, IsPropertySaveable.Yes, description: "How far the discharge can travel from the item.", alwaysUseInstanceValues: true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 5000.0f)] public float Range { get; set; } [Serialize(25.0f, IsPropertySaveable.Yes, description: "How much further can the discharge be carried when moving across walls.", alwaysUseInstanceValues: true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] public float RangeMultiplierInWalls { get; set; } [Serialize(0.0f, IsPropertySaveable.No)] public float RaycastRange { get; set; } [Serialize(0.25f, IsPropertySaveable.Yes, description: "The duration of an individual discharge (in seconds)."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 60.0f, ValueStep = 0.1f, DecimalCount = 2)] public float Duration { get; set; } [Serialize(0.25f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 60.0f, ValueStep = 0.1f, DecimalCount = 2)] public float Reload { get; set; } [Serialize(false, IsPropertySaveable.Yes, "If set to true, the discharge cannot travel inside the submarine nor shock anyone inside."), Editable] public bool OutdoorsOnly { get; set; } [Serialize(false, IsPropertySaveable.Yes)] public bool IgnoreUser { get; set; } private readonly List nodes = new List(); public IEnumerable Nodes { get { return nodes; } } private readonly List<(Character character, Node node)> charactersInRange = new List<(Character character, Node node)>(); private bool charging; private float timer; private readonly Attack attack; private Character user; private float reloadTimer; public ElectricalDischarger(Item item, ContentXElement element) : base(item, element) { list.Add(this); foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "attack": attack = new Attack(subElement, item.Name); break; } } InitProjSpecific(); } partial void InitProjSpecific(); public override bool Use(float deltaTime, Character character = null) { //already active, do nothing if (IsActive) { return false; } if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return false; } if (character != null && !CharacterUsable) { return false; } charging = true; timer = Duration; IsActive = true; user = character; #if SERVER if (GameMain.Server != null) { item.CreateServerEvent(this); } #endif return false; } public override void Update(float deltaTime, Camera cam) { #if CLIENT frameOffset = Rand.Int(electricitySprite.FrameCount); #endif if (timer <= 0.0f) { if (reloadTimer > 0.0f) { reloadTimer -= deltaTime; return; } IsActive = false; return; } timer -= deltaTime; if (charging) { bool hasPower = false; if (item.Connections == null) { //no connections and can't be wired = must be powered by something like batteries hasPower = HasPower; } else { hasPower = GetAvailableInstantaneousBatteryPower() >= PowerConsumption; } if (hasPower) { var batteries = GetDirectlyConnectedBatteries().Where(static b => !b.OutputDisabled && b.Charge > 0.0001f && b.MaxOutPut > 0.0001f); float neededPower = PowerConsumption; while (neededPower > 0.0001f && batteries.Any()) { float takePower = neededPower / batteries.Count(); takePower = Math.Min(takePower, batteries.Min(b => Math.Min(b.Charge * 3600.0f, b.MaxOutPut))); foreach (PowerContainer battery in batteries) { neededPower -= takePower; battery.Charge -= takePower / 3600.0f; #if SERVER if (GameMain.Server != null) { battery.Item.CreateServerEvent(battery); } #endif } } Discharge(); } } } /// /// Discharge coil doesn't consume grid power, directly takes from the batteries on its grid instead. /// public override float GetCurrentPowerConsumption(Connection conn = null) { return 0; } public override void UpdateBroken(float deltaTime, Camera cam) { base.UpdateBroken(deltaTime, cam); nodes.Clear(); charactersInRange.Clear(); } private void Discharge() { reloadTimer = Reload; ApplyStatusEffects(ActionType.OnUse, 1.0f); FindNodes(item.WorldPosition, Range); if (attack != null) { foreach ((Character character, Node node) in charactersInRange) { if (character == null || character.Removed) { continue; } character.ApplyAttack(user, node.WorldPosition, attack, MathHelper.Clamp(Voltage, 1.0f, MaxOverVoltageFactor), impulseDirection: character.WorldPosition - node.WorldPosition); } } DischargeProjSpecific(); charging = false; } partial void DischargeProjSpecific(); public void FindNodes(Vector2 worldPosition, float range) { if (RaycastRange > 0.0f) { float angle = 0.0f; float dir = 1; if (item.body != null) { angle += item.body.Rotation; dir = item.body.Dir; } worldPosition += new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle)) * RaycastRange * dir; } //see which submarines are within range so we can skip structures that are in far-away subs List submarinesInRange = new List(); foreach (Submarine sub in Submarine.Loaded) { if (item.Submarine == sub) { submarinesInRange.Add(sub); } else if (sub != null) { Rectangle subBorders = new Rectangle( sub.Borders.X - (int)range, sub.Borders.Y + (int)range, sub.Borders.Width + (int)(range * 2), sub.Borders.Height + (int)(range * 2)); subBorders.Location += MathUtils.ToPoint(sub.SubBody.Position); if (Submarine.RectContains(subBorders, worldPosition)) { submarinesInRange.Add(sub); } } } //get all walls within range the arc could potentially hit List entitiesInRange = new List(100); foreach (Structure structure in Structure.WallList) { if (!structure.HasBody || structure.IsPlatform) { continue; } if (structure.Submarine != null&& !submarinesInRange.Contains(structure.Submarine)) { continue; } var structureWorldRect = structure.WorldRect; if (worldPosition.X < structureWorldRect.X - range) { continue; } if (worldPosition.X > structureWorldRect.Right + range) { continue; } if (worldPosition.Y > structureWorldRect.Y + range) { continue; } if (worldPosition.Y < structureWorldRect.Y - structureWorldRect.Height - range) { continue; } if (structure.Submarine != null) { if (!submarinesInRange.Contains(structure.Submarine)) { continue; } if (OutdoorsOnly) { //check if there's a hull at either side of the wall Vector2 normal = new Vector2( (float)-Math.Sin(structure.IsHorizontal ? -structure.BodyRotation : MathHelper.PiOver2 - structure.BodyRotation), (float)Math.Cos(structure.IsHorizontal ? -structure.BodyRotation : MathHelper.PiOver2 - structure.BodyRotation)); Vector2 structurePos = structure.Position; float offsetAmount = Submarine.GridSize.X * 2; if (structure.HasBody) { structurePos = ConvertUnits.ToDisplayUnits(structure.Bodies.First().Position); offsetAmount = Math.Max( offsetAmount, structure.IsHorizontal ? structure.BodyHeight : structure.BodyWidth); } if (Hull.FindHull(structurePos + normal * offsetAmount, useWorldCoordinates: false) != null && Hull.FindHull(structurePos - normal * offsetAmount, useWorldCoordinates: false) != null) { continue; } } } entitiesInRange.Add(structure); } nodes.Clear(); if (RaycastRange > 0.0f) { nodes.Add(new Node(item.WorldPosition, -1)); int parentNodeIndex = 0; AddNodesBetweenPoints(item.WorldPosition, worldPosition, 0.5f, ref parentNodeIndex); } else { nodes.Add(new Node(worldPosition, -1)); } //get all characters within range the arc could potentially hit float totalRange = RaycastRange + range; foreach (Character character in Character.CharacterList) { if (!character.Enabled) { continue; } if (IgnoreUser && character == user) { continue; } if (OutdoorsOnly && character.Submarine != null) { continue; } if (character.Submarine != null && !submarinesInRange.Contains(character.Submarine)) { continue; } if (Vector2.DistanceSquared(character.WorldPosition, worldPosition) < totalRange * totalRange * RangeMultiplierInWalls) { entitiesInRange.Add(character); } //if the weapon does a raycast, check distance to the ray too (not just the end of the ray) if (RaycastRange > 0) { float distSqr = MathUtils.LineSegmentToPointDistanceSquared(worldPosition, item.WorldPosition, character.WorldPosition); //if the distance from the initial raycast to the character is small (e.g. goes through the character), we know it must hit if (distSqr < range * range * RangeMultiplierInWalls) { if (!entitiesInRange.Contains(character)) { entitiesInRange.Add(character); } charactersInRange.Add((character, nodes.First())); } } } FindNodes(entitiesInRange, worldPosition, nodes.Count - 1, range); //construct final nodes (w/ lengths and angles so they don't have to be recalculated when rendering the discharge) for (int i = 0; i < nodes.Count; i++) { if (nodes[i].ParentIndex < 0) { continue; } Node parentNode = nodes[nodes[i].ParentIndex]; float length = Vector2.Distance(nodes[i].WorldPosition, parentNode.WorldPosition) * Rand.Range(1.0f, 1.25f); float angle = MathUtils.VectorToAngle(parentNode.WorldPosition - nodes[i].WorldPosition); nodes[i] = new Node(nodes[i].WorldPosition, nodes[i].ParentIndex, length, angle); } } private void FindNodes(List entitiesInRange, Vector2 currPos, int parentNodeIndex, float currentRange) { if (currentRange <= 0.0f || nodes.Count >= MaxNodes) { return; } //find the closest structure int closestIndex = -1; float closestDist = float.MaxValue; for (int i = 0; i < entitiesInRange.Count; i++) { float dist = float.MaxValue; if (entitiesInRange[i] is Structure structure) { if (structure.IsHorizontal) { dist = Math.Abs(structure.WorldPosition.Y - currPos.Y); if (currPos.X < structure.WorldRect.X) dist += structure.WorldRect.X - currPos.X; else if (currPos.X > structure.WorldRect.Right) dist += currPos.X - structure.WorldRect.Right; } else { dist = Math.Abs(structure.WorldPosition.X - currPos.X); if (currPos.Y < structure.WorldRect.Y - structure.Rect.Height) dist += (structure.WorldRect.Y - structure.Rect.Height) - currPos.Y; else if (currPos.Y > structure.WorldRect.Y) dist += currPos.Y - structure.WorldRect.Y; } } else if (entitiesInRange[i] is Character character) { dist = MathF.Sqrt(MathUtils.LineSegmentToPointDistanceSquared(currPos, nodes[parentNodeIndex].WorldPosition, character.WorldPosition)); } if (dist < closestDist) { closestIndex = i; closestDist = dist; } } if (closestIndex == -1 || closestDist > currentRange) { //nothing in range, create some arcs to random directions for (int i = 0; i < Rand.Int(4); i++) { Vector2 targetPos = currPos + Rand.Vector(MaxNodeDistance * Rand.Range(0.5f, 1.5f)); nodes.Add(new Node(targetPos, parentNodeIndex)); } return; } currentRange -= closestDist; if (entitiesInRange[closestIndex] is Structure targetStructure) { if (targetStructure.IsHorizontal) { //which side of the structure to add the nodes to //if outside the sub, use the sides that's furthers from the sub's center position //otherwise the side that's closer to the previous node int yDir = OutdoorsOnly && targetStructure.Submarine != null ? Math.Sign(targetStructure.WorldPosition.Y - targetStructure.Submarine.WorldPosition.Y) : Math.Sign(currPos.Y - targetStructure.WorldPosition.Y); int sectionIndex = targetStructure.FindSectionIndex(currPos, world: true, clamp: true); if (sectionIndex == -1) { return; } Vector2 sectionPos = targetStructure.SectionPosition(sectionIndex, world: true); Vector2 targetPos = new Vector2( MathHelper.Clamp(sectionPos.X, targetStructure.WorldRect.X, targetStructure.WorldRect.Right), sectionPos.Y + targetStructure.BodyHeight / 2 * yDir); //create nodes from the current position to the closest point on the structure AddNodesBetweenPoints(currPos, targetPos, 0.25f, ref parentNodeIndex); //add a node at the closest point nodes.Add(new Node(targetPos, parentNodeIndex)); int nodeIndex = nodes.Count - 1; entitiesInRange.RemoveAt(closestIndex); float newRange = currentRange - (targetStructure.Rect.Width / 2) * (1.0f / RangeMultiplierInWalls); //continue the discharge to the left edge of the structure and extend from there int leftNodeIndex = nodeIndex; Vector2 leftPos = targetStructure.SectionPosition(0, world: true); leftPos.Y += targetStructure.BodyHeight / 2 * yDir; AddNodesBetweenPoints(targetPos, leftPos, 0.05f, ref leftNodeIndex); nodes.Add(new Node(leftPos, leftNodeIndex)); FindNodes(entitiesInRange, leftPos, nodes.Count - 1, newRange); //continue the discharge to the right edge of the structure and extend from there int rightNodeIndex = nodeIndex; Vector2 rightPos = targetStructure.SectionPosition(targetStructure.SectionCount - 1, world: true); leftPos.Y += targetStructure.BodyHeight / 2 * yDir; AddNodesBetweenPoints(targetPos, rightPos, 0.05f, ref rightNodeIndex); nodes.Add(new Node(rightPos, rightNodeIndex)); FindNodes(entitiesInRange, rightPos, nodes.Count - 1, newRange); } else { int xDir = OutdoorsOnly && targetStructure.Submarine != null ? Math.Sign(targetStructure.WorldPosition.X - targetStructure.Submarine.WorldPosition.X) : Math.Sign(currPos.X - targetStructure.WorldPosition.X); int sectionIndex = targetStructure.FindSectionIndex(currPos, world: true, clamp: true); if (sectionIndex == -1) { return; } Vector2 sectionPos = targetStructure.SectionPosition(sectionIndex, world: true); Vector2 targetPos = new Vector2( sectionPos.X + targetStructure.BodyWidth / 2 * xDir, MathHelper.Clamp(sectionPos.Y, targetStructure.WorldRect.Y - targetStructure.Rect.Height, targetStructure.WorldRect.Y)); //create nodes from the current position to the closest point on the structure AddNodesBetweenPoints(currPos, targetPos, 0.25f, ref parentNodeIndex); //add a node at the closest point nodes.Add(new Node(targetPos, parentNodeIndex)); int nodeIndex = nodes.Count - 1; entitiesInRange.RemoveAt(closestIndex); float newRange = currentRange - (targetStructure.Rect.Height / 2) * (1.0f / RangeMultiplierInWalls); //continue the discharge to the top edge of the structure and extend from there int topNodeIndex = nodeIndex; Vector2 topPos = targetStructure.SectionPosition(0, world: true); topPos.X += targetStructure.BodyWidth / 2 * xDir; AddNodesBetweenPoints(targetPos, topPos, 0.05f, ref topNodeIndex); nodes.Add(new Node(topPos, topNodeIndex)); FindNodes(entitiesInRange, topPos, nodes.Count - 1, newRange); //continue the discharge to the bottom edge of the structure and extend from there int bottomNodeIndex = nodeIndex; Vector2 bottomBos = targetStructure.SectionPosition(targetStructure.SectionCount - 1, world: true); bottomBos.X += targetStructure.BodyWidth / 2 * xDir; AddNodesBetweenPoints(targetPos, bottomBos, 0.05f, ref bottomNodeIndex); nodes.Add(new Node(bottomBos, bottomNodeIndex)); FindNodes(entitiesInRange, bottomBos, nodes.Count - 1, newRange); } //check if any character is close to this structure for (int j = 0; j < entitiesInRange.Count; j++) { var otherEntity = entitiesInRange[j]; if (otherEntity is not Character character) { continue; } if (IgnoreUser && character == user) { continue; } if (OutdoorsOnly && character.Submarine != null) { continue; } Vector2 characterMin = new Vector2(character.AnimController.Limbs.Min(l => l.WorldPosition.X), character.AnimController.Limbs.Min(l => l.WorldPosition.Y)); Vector2 characterMax = new Vector2(character.AnimController.Limbs.Max(l => l.WorldPosition.X), character.AnimController.Limbs.Max(l => l.WorldPosition.Y)); if (targetStructure.IsHorizontal) { if (characterMax.X < targetStructure.WorldRect.X) { continue; } if (characterMin.X > targetStructure.WorldRect.Right) { continue; } if (Math.Abs(characterMin.Y - targetStructure.WorldPosition.Y) > currentRange && Math.Abs(characterMax.Y - targetStructure.WorldPosition.Y) > currentRange) { continue; } } else { if (characterMax.Y < targetStructure.WorldRect.Y - targetStructure.Rect.Height) { continue; } if (characterMin.Y > targetStructure.WorldRect.Y) { continue; } if (Math.Abs(characterMin.X - targetStructure.WorldPosition.X) > currentRange && Math.Abs(characterMax.X - targetStructure.WorldPosition.X) > currentRange) { continue; } } if (!charactersInRange.Any(c => c.character == character)) { charactersInRange.Add((character, nodes[parentNodeIndex])); } float closestNodeDistSqr = float.MaxValue; int closestNodeIndex = -1; for (int i = 0; i < nodes.Count; i++) { float distSqr = Vector2.DistanceSquared(character.WorldPosition, nodes[i].WorldPosition); if (distSqr < closestNodeDistSqr) { closestNodeDistSqr = distSqr; closestNodeIndex = i; } } if (closestNodeIndex > -1) { FindNodes(entitiesInRange, nodes[closestNodeIndex].WorldPosition, closestNodeIndex, currentRange - (float)Math.Sqrt(closestNodeDistSqr)); } } } else if (entitiesInRange[closestIndex] is Character character) { Vector2 targetPos = character.WorldPosition; //create nodes from the current position to the closest point on the character AddNodesBetweenPoints(currPos, targetPos, 0.25f, ref parentNodeIndex); nodes.Add(new Node(targetPos, parentNodeIndex)); entitiesInRange.RemoveAt(closestIndex); if (!charactersInRange.Any(c => c.character == character)) { charactersInRange.Add((character, nodes[parentNodeIndex])); } FindNodes(entitiesInRange, targetPos, nodes.Count - 1, currentRange); } } private void AddNodesBetweenPoints(Vector2 currPos, Vector2 targetPos, float variance, ref int parentNodeIndex) { Vector2 diff = targetPos - currPos; float dist = diff.Length(); Vector2 normal = new Vector2(-diff.Y, diff.X) / dist; for (float x = MaxNodeDistance; x < dist - MaxNodeDistance; x += MaxNodeDistance * Rand.Range(0.5f, 1.0f)) { //0 at the edges, 1 at the center float normalOffset = (0.5f - Math.Abs(x / dist - 0.5f)) * 2.0f; normalOffset *= variance * dist * Rand.Range(-1.0f, 1.0f); nodes.Add(new Node(currPos + (diff / dist) * x + normal * normalOffset, parentNodeIndex)); parentNodeIndex = nodes.Count - 1; } } public override void ReceiveSignal(Signal signal, Connection connection) { switch (connection.Name) { case "activate": case "use": case "trigger_in": if (signal.value != "0") { item.Use(1.0f); } break; } } protected override void RemoveComponentSpecific() { base.RemoveComponentSpecific(); list.Remove(this); } public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.WriteUInt16(user?.ID ?? Entity.NullEntityID); } } }