(61d00a474) v0.9.7.1

This commit is contained in:
Regalis
2020-03-04 13:04:10 +01:00
parent 3c50efa5c9
commit 3c09ebe02f
5086 changed files with 786063 additions and 295871 deletions
@@ -0,0 +1,983 @@
using Barotrauma.Networking;
using FarseerPhysics;
using FarseerPhysics.Dynamics;
using FarseerPhysics.Dynamics.Joints;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
partial class DockingPort : ItemComponent, IDrawableComponent, IServerSerializable
{
private static List<DockingPort> list = new List<DockingPort>();
public static IEnumerable<DockingPort> List
{
get { return list; }
}
private Sprite overlaySprite;
private float dockingState;
private Joint joint;
private readonly Hull[] hulls = new Hull[2];
private Gap gap;
private Door door;
private Body[] bodies;
private Body doorBody;
private bool docked;
private bool obstructedWayPointsDisabled;
private float forceLockTimer;
//if the submarine isn't in the correct position to lock within this time after docking has been activated,
//force the sub to the correct position
const float ForceLockDelay = 1.0f;
public int DockingDir { get; private set; }
[Serialize("32.0,32.0", false, description: "How close the docking port has to be to another port to dock.")]
public Vector2 DistanceTolerance { get; set; }
[Serialize(32.0f, false, description: "How close together the docking ports are forced when docked.")]
public float DockedDistance
{
get;
set;
}
[Serialize(true, false, description: "Is the port horizontal.")]
public bool IsHorizontal
{
get;
set;
}
[Serialize(false, false, description: "If set to true, this docking port is used when spawning the submarine docked to an outpost (if possible).")]
public bool MainDockingPort
{
get;
set;
}
public DockingPort DockingTarget { get; private set; }
public bool Docked
{
get
{
return docked;
}
set
{
if (!docked && value)
{
if (DockingTarget == null) AttemptDock();
if (DockingTarget == null) return;
docked = true;
}
else if (docked && !value)
{
Undock();
}
}
}
public DockingPort(Item item, XElement element)
: base(item, element)
{
// isOpen = false;
foreach (XElement subElement in element.Elements())
{
string texturePath = subElement.GetAttributeString("texture", "");
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "sprite":
overlaySprite = new Sprite(subElement, texturePath.Contains("/") ? "" : Path.GetDirectoryName(item.Prefab.FilePath));
break;
}
}
IsActive = true;
list.Add(this);
}
public override void FlipX(bool relativeToSub)
{
if (DockingTarget != null)
{
if (joint != null)
{
CreateJoint(joint is WeldJoint);
LinkHullsToGaps();
}
else if (DockingTarget.joint != null)
{
if (!GameMain.World.BodyList.Contains(DockingTarget.joint.BodyA) ||
!GameMain.World.BodyList.Contains(DockingTarget.joint.BodyB))
{
DockingTarget.CreateJoint(DockingTarget.joint is WeldJoint);
}
DockingTarget.LinkHullsToGaps();
}
}
}
public override void FlipY(bool relativeToSub)
{
FlipX(relativeToSub);
}
private DockingPort FindAdjacentPort()
{
foreach (DockingPort port in list)
{
if (port == this || port.item.Submarine == item.Submarine) continue;
if (Math.Abs(port.item.WorldPosition.X - item.WorldPosition.X) > DistanceTolerance.X) continue;
if (Math.Abs(port.item.WorldPosition.Y - item.WorldPosition.Y) > DistanceTolerance.Y) continue;
return port;
}
return null;
}
private void AttemptDock()
{
var adjacentPort = FindAdjacentPort();
if (adjacentPort != null) Dock(adjacentPort);
}
public void Dock(DockingPort target)
{
if (item.Submarine.DockedTo.Contains(target.item.Submarine)) return;
forceLockTimer = 0.0f;
if (DockingTarget != null)
{
Undock();
}
if (target.item.Submarine == item.Submarine)
{
DebugConsole.ThrowError("Error - tried to dock a submarine to itself");
DockingTarget = null;
return;
}
target.InitializeLinks();
if (!item.linkedTo.Contains(target.item)) item.linkedTo.Add(target.item);
if (!target.item.linkedTo.Contains(item)) target.item.linkedTo.Add(item);
if (!target.item.Submarine.DockedTo.Contains(item.Submarine)) target.item.Submarine.ConnectedDockingPorts.Add(item.Submarine, target);
if (!item.Submarine.DockedTo.Contains(target.item.Submarine)) item.Submarine.ConnectedDockingPorts.Add(target.item.Submarine, this);
DockingTarget = target;
DockingTarget.DockingTarget = this;
docked = true;
DockingTarget.Docked = true;
if (Character.Controlled != null &&
(Character.Controlled.Submarine == DockingTarget.item.Submarine || Character.Controlled.Submarine == item.Submarine))
{
GameMain.GameScreen.Cam.Shake = Vector2.Distance(DockingTarget.item.Submarine.Velocity, item.Submarine.Velocity);
}
DockingDir = GetDir(DockingTarget);
DockingTarget.DockingDir = -DockingDir;
if (door != null && DockingTarget.door != null)
{
WayPoint myWayPoint = WayPoint.WayPointList.Find(wp => door.LinkedGap == wp.ConnectedGap);
WayPoint targetWayPoint = WayPoint.WayPointList.Find(wp => DockingTarget.door.LinkedGap == wp.ConnectedGap);
if (myWayPoint != null && targetWayPoint != null)
{
myWayPoint.linkedTo.Add(targetWayPoint);
targetWayPoint.linkedTo.Add(myWayPoint);
}
}
CreateJoint(false);
#if SERVER
if (GameMain.Server != null)
{
item.CreateServerEvent(this);
}
#endif
}
public void Lock(bool isNetworkMessage, bool forcePosition = false)
{
#if CLIENT
if (GameMain.Client != null && !isNetworkMessage) return;
#endif
if (DockingTarget == null)
{
DebugConsole.ThrowError("Error - attempted to lock a docking port that's not connected to anything");
return;
}
if (!(joint is WeldJoint))
{
DockingDir = GetDir(DockingTarget);
DockingTarget.DockingDir = -DockingDir;
ApplyStatusEffects(ActionType.OnUse, 1.0f);
Vector2 jointDiff = joint.WorldAnchorB - joint.WorldAnchorA;
if (item.Submarine.PhysicsBody.Mass < DockingTarget.item.Submarine.PhysicsBody.Mass ||
DockingTarget.item.Submarine.IsOutpost)
{
item.Submarine.SubBody.SetPosition(item.Submarine.SubBody.Position + ConvertUnits.ToDisplayUnits(jointDiff));
}
else if (DockingTarget.item.Submarine.PhysicsBody.Mass < item.Submarine.PhysicsBody.Mass ||
item.Submarine.IsOutpost)
{
DockingTarget.item.Submarine.SubBody.SetPosition(DockingTarget.item.Submarine.SubBody.Position - ConvertUnits.ToDisplayUnits(jointDiff));
}
ConnectWireBetweenPorts();
CreateJoint(true);
#if SERVER
if (GameMain.Server != null)
{
item.CreateServerEvent(this);
}
#endif
}
List<MapEntity> removedEntities = item.linkedTo.Where(e => e.Removed).ToList();
foreach (MapEntity removed in removedEntities) item.linkedTo.Remove(removed);
if (!item.linkedTo.Any(e => e is Hull) && !DockingTarget.item.linkedTo.Any(e => e is Hull))
{
CreateHulls();
}
}
private void CreateJoint(bool useWeldJoint)
{
if (joint != null)
{
GameMain.World.Remove(joint);
joint = null;
}
Vector2 offset = (IsHorizontal ?
Vector2.UnitX * DockingDir :
Vector2.UnitY * DockingDir);
offset *= DockedDistance * 0.5f;
Vector2 pos1 = item.WorldPosition + offset;
Vector2 pos2 = DockingTarget.item.WorldPosition - offset;
if (useWeldJoint)
{
joint = JointFactory.CreateWeldJoint(GameMain.World,
item.Submarine.PhysicsBody.FarseerBody, DockingTarget.item.Submarine.PhysicsBody.FarseerBody,
ConvertUnits.ToSimUnits(pos1), FarseerPhysics.ConvertUnits.ToSimUnits(pos2), true);
((WeldJoint)joint).FrequencyHz = 1.0f;
}
else
{
var distanceJoint = JointFactory.CreateDistanceJoint(GameMain.World,
item.Submarine.PhysicsBody.FarseerBody, DockingTarget.item.Submarine.PhysicsBody.FarseerBody,
ConvertUnits.ToSimUnits(pos1), FarseerPhysics.ConvertUnits.ToSimUnits(pos2), true);
distanceJoint.Length = 0.01f;
distanceJoint.Frequency = 1.0f;
distanceJoint.DampingRatio = 0.8f;
joint = distanceJoint;
}
joint.CollideConnected = true;
}
public int GetDir(DockingPort dockingTarget = null)
{
if (DockingDir != 0) { return DockingDir; }
if (door != null)
{
if (door.LinkedGap.linkedTo.Count == 1)
{
return IsHorizontal ?
Math.Sign(door.Item.WorldPosition.X - door.LinkedGap.linkedTo[0].WorldPosition.X) :
Math.Sign(door.Item.WorldPosition.Y - door.LinkedGap.linkedTo[0].WorldPosition.Y);
}
else if (dockingTarget?.door?.LinkedGap != null && dockingTarget.door.LinkedGap.linkedTo.Count == 1)
{
return IsHorizontal ?
Math.Sign(dockingTarget.door.LinkedGap.linkedTo[0].WorldPosition.X - dockingTarget.door.Item.WorldPosition.X) :
Math.Sign(dockingTarget.door.LinkedGap.linkedTo[0].WorldPosition.Y - dockingTarget.door.Item.WorldPosition.Y);
}
}
if (dockingTarget != null)
{
return IsHorizontal ?
Math.Sign(dockingTarget.item.WorldPosition.X - item.WorldPosition.X) :
Math.Sign(dockingTarget.item.WorldPosition.Y - item.WorldPosition.Y);
}
if (item.Submarine != null)
{
return IsHorizontal ?
Math.Sign(item.WorldPosition.X - item.Submarine.WorldPosition.X) :
Math.Sign(item.WorldPosition.Y - item.Submarine.WorldPosition.Y);
}
return 0;
}
private void ConnectWireBetweenPorts()
{
Wire wire = item.GetComponent<Wire>();
if (wire == null) return;
wire.Hidden = true;
wire.Locked = true;
if (Item.Connections == null) return;
var powerConnection = Item.Connections.Find(c => c.IsPower);
if (powerConnection == null) return;
if (DockingTarget == null || DockingTarget.item.Connections == null) return;
var recipient = DockingTarget.item.Connections.Find(c => c.IsPower);
if (recipient == null) return;
wire.RemoveConnection(item);
wire.RemoveConnection(DockingTarget.item);
powerConnection.TryAddLink(wire);
wire.Connect(powerConnection, false, false);
recipient.TryAddLink(wire);
wire.Connect(recipient, false, false);
}
private void CreateDoorBody()
{
if (doorBody != null)
{
GameMain.World.Remove(doorBody);
doorBody = null;
}
Vector2 position = ConvertUnits.ToSimUnits(item.Position + (DockingTarget.door.Item.WorldPosition - item.WorldPosition));
if (!MathUtils.IsValid(position))
{
string errorMsg =
"Attempted to create a door body at an invalid position (item pos: " + item.Position
+ ", item world pos: " + item.WorldPosition
+ ", docking target world pos: " + DockingTarget.door.Item.WorldPosition + ")\n" + Environment.StackTrace;
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce(
"DockingPort.CreateDoorBody:InvalidPosition",
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
errorMsg);
position = Vector2.Zero;
}
System.Diagnostics.Debug.Assert(doorBody == null);
doorBody = GameMain.World.CreateRectangle(
DockingTarget.door.Body.width,
DockingTarget.door.Body.height,
1.0f,
position);
doorBody.UserData = DockingTarget.door;
doorBody.CollisionCategories = Physics.CollisionWall;
doorBody.BodyType = BodyType.Static;
}
private void CreateHulls()
{
var hullRects = new Rectangle[] { item.WorldRect, DockingTarget.item.WorldRect };
var subs = new Submarine[] { item.Submarine, DockingTarget.item.Submarine };
bodies = new Body[4];
if (DockingTarget.door != null)
{
CreateDoorBody();
}
if (door != null)
{
DockingTarget.CreateDoorBody();
}
if (IsHorizontal)
{
if (hullRects[0].Center.X > hullRects[1].Center.X)
{
hullRects = new Rectangle[] { DockingTarget.item.WorldRect, item.WorldRect };
subs = new Submarine[] { DockingTarget.item.Submarine, item.Submarine };
}
hullRects[0] = new Rectangle(hullRects[0].Center.X, hullRects[0].Y, ((int)DockedDistance / 2), hullRects[0].Height);
hullRects[1] = new Rectangle(hullRects[1].Center.X - ((int)DockedDistance / 2), hullRects[1].Y, ((int)DockedDistance / 2), hullRects[1].Height);
//expand hulls if needed, so there's no empty space between the sub's hulls and docking port hulls
int leftSubRightSide = int.MinValue, rightSubLeftSide = int.MaxValue;
foreach (Hull hull in Hull.hullList)
{
for (int i = 0; i < 2; i++)
{
if (hull.Submarine != subs[i]) continue;
if (hull.WorldRect.Y < hullRects[i].Y - hullRects[i].Height) continue;
if (hull.WorldRect.Y - hull.WorldRect.Height > hullRects[i].Y) continue;
if (i == 0) //left hull
{
leftSubRightSide = Math.Max(hull.WorldRect.Right, leftSubRightSide);
}
else //upper hull
{
rightSubLeftSide = Math.Min(hull.WorldRect.X, rightSubLeftSide);
}
}
}
//expand left hull to the rightmost hull of the sub at the left side
//(unless the difference is more than 100 units - if the distance is very large
//there's something wrong with the positioning of the docking ports or submarine hulls)
int leftHullDiff = (hullRects[0].X - leftSubRightSide) + 5;
if (leftHullDiff > 0)
{
if (leftHullDiff > 100)
{
DebugConsole.ThrowError("Creating hulls between docking ports failed. The leftmost docking port seems to be very far from any hulls in the left-side submarine.");
}
else
{
hullRects[0].X -= leftHullDiff;
hullRects[0].Width += leftHullDiff;
}
}
int rightHullDiff = (rightSubLeftSide - hullRects[1].Right) + 5;
if (rightHullDiff > 0)
{
if (rightHullDiff > 100)
{
DebugConsole.ThrowError("Creating hulls between docking ports failed. The rightmost docking port seems to be very far from any hulls in the right-side submarine.");
}
else
{
hullRects[1].Width += rightHullDiff;
}
}
for (int i = 0; i < 2; i++)
{
hullRects[i].Location -= MathUtils.ToPoint((subs[i].WorldPosition - subs[i].HiddenSubPosition));
hulls[i] = new Hull(MapEntityPrefab.Find(null, "hull"), hullRects[i], subs[i]);
hulls[i].AddToGrid(subs[i]);
hulls[i].FreeID();
for (int j = 0; j < 2; j++)
{
bodies[i + j * 2] = GameMain.World.CreateEdge(
ConvertUnits.ToSimUnits(new Vector2(hullRects[i].X, hullRects[i].Y - hullRects[i].Height * j)),
ConvertUnits.ToSimUnits(new Vector2(hullRects[i].Right, hullRects[i].Y - hullRects[i].Height * j)));
}
}
gap = new Gap(new Rectangle(hullRects[0].Right - 2, hullRects[0].Y, 4, hullRects[0].Height), true, subs[0]);
}
else
{
if (hullRects[0].Center.Y > hullRects[1].Center.Y)
{
hullRects = new Rectangle[] { DockingTarget.item.WorldRect, item.WorldRect };
subs = new Submarine[] { DockingTarget.item.Submarine, item.Submarine };
}
hullRects[0] = new Rectangle(hullRects[0].X, hullRects[0].Y + (int)(-hullRects[0].Height + DockedDistance) / 2, hullRects[0].Width, ((int)DockedDistance / 2));
hullRects[1] = new Rectangle(hullRects[1].X, hullRects[1].Y - hullRects[1].Height / 2, hullRects[1].Width, ((int)DockedDistance / 2));
//expand hulls if needed, so there's no empty space between the sub's hulls and docking port hulls
int upperSubBottom = int.MaxValue, lowerSubTop = int.MinValue;
foreach (Hull hull in Hull.hullList)
{
for (int i = 0; i < 2; i++)
{
if (hull.Submarine != subs[i]) continue;
if (hull.WorldRect.Right < hullRects[i].X) continue;
if (hull.WorldRect.X > hullRects[i].Right) continue;
if (i == 0) //lower hull
{
lowerSubTop = Math.Max(hull.WorldRect.Y, lowerSubTop);
}
else //upper hull
{
upperSubBottom = Math.Min(hull.WorldRect.Y - hull.WorldRect.Height, upperSubBottom);
}
}
}
//expand lower hull to the topmost hull of the lower sub
//(unless the difference is more than 100 units - if the distance is very large
//there's something wrong with the positioning of the docking ports or submarine hulls)
int lowerHullDiff = ((hullRects[0].Y - hullRects[0].Height) - lowerSubTop) + 5;
if (lowerHullDiff > 0)
{
if (lowerHullDiff > 100)
{
DebugConsole.ThrowError("Creating hulls between docking ports failed. The lower docking port seems to be very far from any hulls in the lower submarine.");
}
else
{
hullRects[0].Height += lowerHullDiff;
}
}
int upperHullDiff = (upperSubBottom - hullRects[1].Y) + 5;
if (upperHullDiff > 0)
{
if (upperHullDiff > 100)
{
DebugConsole.ThrowError("Creating hulls between docking ports failed. The upper docking port seems to be very far from any hulls in the upper submarine.");
}
else
{
hullRects[1].Y += upperHullDiff;
hullRects[1].Height += upperHullDiff;
}
}
//difference between the edges of the hulls (to avoid a gap between the hulls)
//0 is lower
int midHullDiff = ((hullRects[1].Y - hullRects[1].Height) - hullRects[0].Y) + 2;
if (midHullDiff > 100)
{
DebugConsole.ThrowError("Creating hulls between docking ports failed. The upper hull seems to be very far from the lower hull.");
}
else if (midHullDiff > 0)
{
hullRects[0].Height += midHullDiff / 2 + 1;
hullRects[1].Y -= midHullDiff / 2 + 1;
hullRects[1].Height += midHullDiff / 2 + 1;
}
for (int i = 0; i < 2; i++)
{
hullRects[i].Location -= MathUtils.ToPoint((subs[i].WorldPosition - subs[i].HiddenSubPosition));
hulls[i] = new Hull(MapEntityPrefab.Find(null, "hull"), hullRects[i], subs[i]);
hulls[i].AddToGrid(subs[i]);
hulls[i].FreeID();
}
gap = new Gap(new Rectangle(hullRects[0].X, hullRects[0].Y+2, hullRects[0].Width, 4), false, subs[0]);
}
LinkHullsToGaps();
hulls[0].ShouldBeSaved = false;
hulls[1].ShouldBeSaved = false;
item.linkedTo.Add(hulls[0]);
item.linkedTo.Add(hulls[1]);
gap.FreeID();
gap.DisableHullRechecks = true;
gap.ShouldBeSaved = false;
item.linkedTo.Add(gap);
foreach (Body body in bodies)
{
if (body == null) continue;
body.BodyType = BodyType.Static;
body.Friction = 0.5f;
body.CollisionCategories = Physics.CollisionWall;
}
}
private void LinkHullsToGaps()
{
if (gap == null || hulls == null || hulls[0] == null || hulls[1] == null)
{
#if DEBUG
DebugConsole.ThrowError("Failed to link dockingport hulls to gap");
#endif
return;
}
gap.linkedTo.Clear();
if (IsHorizontal)
{
if (hulls[0].WorldRect.X < hulls[1].WorldRect.X)
{
gap.linkedTo.Add(hulls[0]);
gap.linkedTo.Add(hulls[1]);
}
else
{
gap.linkedTo.Add(hulls[1]);
gap.linkedTo.Add(hulls[0]);
}
}
else
{
if (hulls[0].WorldRect.Y > hulls[1].WorldRect.Y)
{
gap.linkedTo.Add(hulls[0]);
gap.linkedTo.Add(hulls[1]);
}
else
{
gap.linkedTo.Add(hulls[1]);
gap.linkedTo.Add(hulls[0]);
}
}
for (int i = 0; i < 2; i++)
{
Gap doorGap = i == 0 ? door?.LinkedGap : DockingTarget?.door?.LinkedGap;
if (doorGap == null) continue;
doorGap.DisableHullRechecks = true;
if (doorGap.linkedTo.Count >= 2) continue;
if (IsHorizontal)
{
if (item.WorldPosition.X < DockingTarget.item.WorldPosition.X)
{
if (!doorGap.linkedTo.Contains(hulls[0])) doorGap.linkedTo.Add(hulls[0]);
}
else
{
if (!doorGap.linkedTo.Contains(hulls[1])) doorGap.linkedTo.Add(hulls[1]);
}
//make sure the left hull is linked to the gap first (gap logic assumes that the first hull is the one to the left)
if (doorGap.linkedTo.Count > 1 && doorGap.linkedTo[0].Rect.X > doorGap.linkedTo[1].Rect.X)
{
var temp = doorGap.linkedTo[0];
doorGap.linkedTo[0] = doorGap.linkedTo[1];
doorGap.linkedTo[1] = temp;
}
}
else
{
if (item.WorldPosition.Y < DockingTarget.item.WorldPosition.Y)
{
if (!doorGap.linkedTo.Contains(hulls[0])) doorGap.linkedTo.Add(hulls[0]);
}
else
{
if (!doorGap.linkedTo.Contains(hulls[1])) doorGap.linkedTo.Add(hulls[1]);
}
//make sure the upper hull is linked to the gap first (gap logic assumes that the first hull is above the second one)
if (doorGap.linkedTo.Count > 1 && doorGap.linkedTo[0].Rect.Y < doorGap.linkedTo[1].Rect.Y)
{
var temp = doorGap.linkedTo[0];
doorGap.linkedTo[0] = doorGap.linkedTo[1];
doorGap.linkedTo[1] = temp;
}
}
}
}
public void Undock()
{
if (DockingTarget == null || !docked) return;
forceLockTimer = 0.0f;
ApplyStatusEffects(ActionType.OnSecondaryUse, 1.0f);
DockingTarget.item.Submarine.ConnectedDockingPorts.Remove(item.Submarine);
item.Submarine.ConnectedDockingPorts.Remove(DockingTarget.item.Submarine);
if (door != null && DockingTarget.door != null)
{
WayPoint myWayPoint = WayPoint.WayPointList.Find(wp => door.LinkedGap == wp.ConnectedGap);
WayPoint targetWayPoint = WayPoint.WayPointList.Find(wp => DockingTarget.door.LinkedGap == wp.ConnectedGap);
if (myWayPoint != null && targetWayPoint != null)
{
myWayPoint.linkedTo.Remove(targetWayPoint);
targetWayPoint.linkedTo.Remove(myWayPoint);
}
}
item.linkedTo.Clear();
docked = false;
DockingTarget.Undock();
DockingTarget = null;
if (doorBody != null)
{
GameMain.World.Remove(doorBody);
doorBody = null;
}
var wire = item.GetComponent<Wire>();
if (wire != null)
{
wire.Drop(null);
}
if (joint != null)
{
GameMain.World.Remove(joint);
joint = null;
}
hulls[0]?.Remove(); hulls[0] = null;
hulls[1]?.Remove(); hulls[1] = null;
if (gap != null)
{
gap.Remove();
gap = null;
}
if (bodies != null)
{
foreach (Body body in bodies)
{
if (body == null) continue;
GameMain.World.Remove(body);
}
bodies = null;
}
Item.Submarine.EnableObstructedWaypoints();
obstructedWayPointsDisabled = false;
#if SERVER
if (GameMain.Server != null)
{
item.CreateServerEvent(this);
}
#endif
}
public override void Update(float deltaTime, Camera cam)
{
if (DockingTarget == null)
{
dockingState = MathHelper.Lerp(dockingState, 0.0f, deltaTime * 10.0f);
if (dockingState < 0.01f) docked = false;
item.SendSignal(0, "0", "state_out", null);
item.SendSignal(0, (FindAdjacentPort() != null) ? "1" : "0", "proximity_sensor", null);
}
else
{
if (!docked)
{
Dock(DockingTarget);
if (DockingTarget == null) { return; }
}
if (joint is DistanceJoint)
{
item.SendSignal(0, "0", "state_out", null);
dockingState = MathHelper.Lerp(dockingState, 0.5f, deltaTime * 10.0f);
forceLockTimer += deltaTime;
Vector2 jointDiff = joint.WorldAnchorB - joint.WorldAnchorA;
if (jointDiff.LengthSquared() > 0.04f * 0.04f && forceLockTimer < ForceLockDelay)
{
float totalMass = item.Submarine.PhysicsBody.Mass + DockingTarget.item.Submarine.PhysicsBody.Mass;
float massRatio1 = 1.0f;
float massRatio2 = 1.0f;
if (item.Submarine.PhysicsBody.BodyType != BodyType.Dynamic)
{
massRatio1 = 0.0f;
massRatio2 = 1.0f;
}
else if (DockingTarget.item.Submarine.PhysicsBody.BodyType != BodyType.Dynamic)
{
massRatio1 = 1.0f;
massRatio2 = 0.0f;
}
else
{
massRatio1 = DockingTarget.item.Submarine.PhysicsBody.Mass / totalMass;
massRatio2 = item.Submarine.PhysicsBody.Mass / totalMass;
}
Vector2 relativeVelocity = DockingTarget.item.Submarine.Velocity - item.Submarine.Velocity;
Vector2 desiredRelativeVelocity = Vector2.Normalize(jointDiff);
item.Submarine.Velocity += (relativeVelocity + desiredRelativeVelocity) * massRatio1;
DockingTarget.item.Submarine.Velocity += (-relativeVelocity - desiredRelativeVelocity) * massRatio2;
}
else
{
Lock(isNetworkMessage: false, forcePosition: true);
}
}
else
{
if (DockingTarget.door != null && doorBody != null)
{
doorBody.Enabled = DockingTarget.door.Body.Enabled;
}
item.SendSignal(0, "1", "state_out", null);
dockingState = MathHelper.Lerp(dockingState, 1.0f, deltaTime * 10.0f);
}
}
if (!obstructedWayPointsDisabled && dockingState >= 0.99f)
{
Item.Submarine.DisableObstructedWayPoints(DockingTarget?.Item.Submarine);
obstructedWayPointsDisabled = true;
}
}
protected override void RemoveComponentSpecific()
{
list.Remove(this);
hulls[0]?.Remove(); hulls[0] = null;
hulls[1]?.Remove(); hulls[1] = null;
gap?.Remove(); gap = null;
overlaySprite?.Remove();
overlaySprite = null;
}
private bool initialized = false;
private void InitializeLinks()
{
if (initialized) { return; }
initialized = true;
float closestDist = 30.0f * 30.0f;
foreach (Item it in Item.ItemList)
{
if (it.Submarine != item.Submarine) continue;
var doorComponent = it.GetComponent<Door>();
if (doorComponent == null) continue;
float distSqr = Vector2.Distance(item.Position, it.Position);
if (distSqr < closestDist)
{
door = doorComponent;
closestDist = distSqr;
}
}
if (!item.linkedTo.Any()) return;
List<MapEntity> linked = new List<MapEntity>(item.linkedTo);
foreach (MapEntity entity in linked)
{
if (entity is Hull hull)
{
hull.Remove();
item.linkedTo.Remove(hull);
continue;
}
if (entity is Gap gap)
{
gap.Remove();
continue;
}
}
}
public override void OnMapLoaded()
{
InitializeLinks();
if (!item.linkedTo.Any()) return;
List<MapEntity> linked = new List<MapEntity>(item.linkedTo);
foreach (MapEntity entity in linked)
{
if (!(entity is Item linkedItem)) { continue; }
var dockingPort = linkedItem.GetComponent<DockingPort>();
if (dockingPort != null)
{
Dock(dockingPort);
}
}
}
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f)
{
#if CLIENT
if (GameMain.Client != null) return;
#endif
bool wasDocked = docked;
DockingPort prevDockingTarget = DockingTarget;
switch (connection.Name)
{
case "toggle":
Docked = !docked;
break;
case "set_active":
case "set_state":
Docked = signal != "0";
break;
}
#if SERVER
if (sender != null && docked != wasDocked)
{
if (docked)
{
if (item.Submarine != null && DockingTarget?.item?.Submarine != null)
GameServer.Log(sender.LogName + " docked " + item.Submarine.Name + " to " + DockingTarget.item.Submarine.Name, ServerLog.MessageType.ItemInteraction);
}
else
{
if (item.Submarine != null && prevDockingTarget?.item?.Submarine != null)
GameServer.Log(sender.LogName + " undocked " + item.Submarine.Name + " from " + prevDockingTarget.item.Submarine.Name, ServerLog.MessageType.ItemInteraction);
}
}
#endif
}
public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
{
msg.Write(docked);
if (docked)
{
msg.Write(DockingTarget.item.ID);
msg.Write(hulls != null && hulls[0] != null && hulls[1] != null && gap != null);
}
}
}
}
@@ -0,0 +1,609 @@
using Barotrauma.Networking;
using FarseerPhysics;
using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Linq;
#if CLIENT
using Barotrauma.Lights;
#endif
using Barotrauma.Extensions;
namespace Barotrauma.Items.Components
{
partial class Door : Pickable, IDrawableComponent, IServerSerializable
{
private Gap linkedGap;
private bool isOpen;
private float openState;
private readonly Sprite doorSprite, weldedSprite, brokenSprite;
private readonly bool scaleBrokenSprite, fadeBrokenSprite;
private readonly bool autoOrientGap;
private bool isStuck;
public bool IsStuck
{
get { return isStuck; }
private set
{
if (isStuck == value) { return; }
isStuck = value;
#if SERVER
item.CreateServerEvent(this);
#endif
}
}
private float resetPredictionTimer;
private float toggleCooldownTimer;
private Character lastUser;
private float damageSoundCooldown;
private Rectangle doorRect;
private bool isBroken;
public bool IsBroken
{
get { return isBroken; }
set
{
if (isBroken == value) return;
isBroken = value;
if (isBroken)
{
DisableBody();
}
else
{
EnableBody();
}
}
}
public PhysicsBody Body { get; private set; }
private float RepairThreshold
{
get { return item.GetComponent<Repairable>() == null ? 0.0f : item.Prefab.Health; }
}
public bool CanBeWelded = true;
private float stuck;
[Serialize(0.0f, false, description: "How badly stuck the door is (in percentages). If the percentage reaches 100, the door needs to be cut open to make it usable again.")]
public float Stuck
{
get { return stuck; }
set
{
if (isOpen || isBroken || !CanBeWelded) return;
stuck = MathHelper.Clamp(value, 0.0f, 100.0f);
if (stuck <= 0.0f) { IsStuck = false; }
if (stuck >= 100.0f) { IsStuck = true; }
}
}
[Serialize(3.0f, true, description: "How quickly the door opens."), Editable]
public float OpeningSpeed { get; private set; }
[Serialize(3.0f, true, description: "How quickly the door closes."), Editable]
public float ClosingSpeed { get; private set; }
[Serialize(1.0f, true, description: "The door cannot be opened/closed during this time after it has been opened/closed by another character."), Editable]
public float ToggleCoolDown { get; private set; }
public bool? PredictedState { get; private set; }
public Gap LinkedGap
{
get
{
if (linkedGap == null)
{
GetLinkedGap();
}
return linkedGap;
}
}
private void GetLinkedGap()
{
linkedGap = item.linkedTo.FirstOrDefault(e => e is Gap) as Gap;
if (linkedGap == null)
{
Rectangle rect = item.Rect;
if (IsHorizontal)
{
rect.Y += 5;
rect.Height += 10;
}
else
{
rect.X -= 5;
rect.Width += 10;
}
linkedGap = new Gap(rect, !IsHorizontal, Item.Submarine)
{
Submarine = item.Submarine
};
item.linkedTo.Add(linkedGap);
}
RefreshLinkedGap();
}
public bool IsHorizontal { get; private set; }
[Serialize("0.0,0.0,0.0,0.0", false, description: "Position and size of the window on the door. The upper left corner is 0,0. Set the width and height to 0 if you don't want the door to have a window.")]
public Rectangle Window { get; set; }
[Editable, Serialize(false, true, description: "Is the door currently open.")]
public bool IsOpen
{
get { return isOpen; }
set
{
isOpen = value;
OpenState = (isOpen) ? 1.0f : 0.0f;
}
}
[Serialize(false, false, description: "If the door has integrated buttons, it can be opened by interacting with it directly (instead of using buttons wired to it).")]
public bool HasIntegratedButtons { get; private set; }
public float OpenState
{
get { return openState; }
set
{
openState = MathHelper.Clamp(value, 0.0f, 1.0f);
#if CLIENT
float size = IsHorizontal ? item.Rect.Width : item.Rect.Height;
if (Math.Abs(lastConvexHullState - openState) * size < 5.0f) { return; }
UpdateConvexHulls();
lastConvexHullState = openState;
#endif
}
}
[Serialize(false, false, description: "Characters and items cannot pass through impassable doors. Useful for things such as ducts that should only let water and air through.")]
public bool Impassable
{
get;
set;
}
public Door(Item item, XElement element)
: base(item, element)
{
IsHorizontal = element.GetAttributeBool("horizontal", false);
canBePicked = element.GetAttributeBool("canbepicked", false);
autoOrientGap = element.GetAttributeBool("autoorientgap", false);
foreach (XElement subElement in element.Elements())
{
string texturePath = subElement.GetAttributeString("texture", "");
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "sprite":
doorSprite = new Sprite(subElement, texturePath.Contains("/") ? "" : Path.GetDirectoryName(item.Prefab.FilePath));
break;
case "weldedsprite":
weldedSprite = new Sprite(subElement, texturePath.Contains("/") ? "" : Path.GetDirectoryName(item.Prefab.FilePath));
break;
case "brokensprite":
brokenSprite = new Sprite(subElement, texturePath.Contains("/") ? "" : Path.GetDirectoryName(item.Prefab.FilePath));
scaleBrokenSprite = subElement.GetAttributeBool("scale", false);
fadeBrokenSprite = subElement.GetAttributeBool("fade", false);
break;
}
}
doorRect = new Rectangle(
item.Rect.Center.X - (int)(doorSprite.size.X / 2 * item.Scale),
item.Rect.Y - item.Rect.Height/2 + (int)(doorSprite.size.Y / 2.0f * item.Scale),
(int)(doorSprite.size.X * item.Scale),
(int)(doorSprite.size.Y * item.Scale));
Body = new PhysicsBody(
ConvertUnits.ToSimUnits(Math.Max(doorRect.Width, 1)),
ConvertUnits.ToSimUnits(Math.Max(doorRect.Height, 1)),
0.0f,
1.5f)
{
UserData = item,
CollisionCategories = Physics.CollisionWall,
BodyType = BodyType.Static,
Friction = 0.5f
};
Body.SetTransformIgnoreContacts(
ConvertUnits.ToSimUnits(new Vector2(doorRect.Center.X, doorRect.Y - doorRect.Height / 2)),
0.0f);
IsActive = true;
}
public override void Move(Vector2 amount)
{
base.Move(amount);
Body?.SetTransform(Body.SimPosition + ConvertUnits.ToSimUnits(amount), 0.0f);
#if CLIENT
UpdateConvexHulls();
#endif
}
private readonly string accessDeniedTxt = TextManager.Get("AccessDenied");
private readonly string cannotOpenText = TextManager.Get("DoorMsgCannotOpen");
public override bool HasRequiredItems(Character character, bool addMessage, string msg = null)
{
Msg = HasAccess(character) ? "ItemMsgOpen" : "ItemMsgForceOpenCrowbar";
ParseMsg();
if (addMessage)
{
msg = msg ?? (HasIntegratedButtons ? accessDeniedTxt : cannotOpenText);
}
return isBroken || base.HasRequiredItems(character, addMessage, msg);
}
public bool CanBeOpenedWithoutTools(Character character)
{
if (isBroken) { return true; }
return HasAccess(character);
}
public override bool Pick(Character picker)
{
if (item.Condition < RepairThreshold) { return true; }
if (requiredItems.None()) { return false; }
if (HasAccess(picker) && HasRequiredItems(picker, false)) { return false; }
return base.Pick(picker);
}
public override bool OnPicked(Character picker)
{
if (item.Condition < RepairThreshold) { return true; }
if (!HasAccess(picker))
{
ToggleState(ActionType.OnPicked, picker);
}
return false;
}
private void ToggleState(ActionType actionType, Character user)
{
if (toggleCooldownTimer > 0.0f && user != lastUser) { OnFailedToOpen(); return; }
toggleCooldownTimer = ToggleCoolDown;
if (IsStuck) { toggleCooldownTimer = 1.0f; OnFailedToOpen(); return; }
lastUser = user;
SetState(PredictedState == null ? !isOpen : !PredictedState.Value, false, true, forcedOpen: actionType == ActionType.OnPicked);
}
public override bool Select(Character character)
{
if (isBroken) { return true; }
bool hasRequiredItems = HasRequiredItems(character, false);
if (HasAccess(character))
{
float originalPickingTime = PickingTime;
PickingTime = 0;
ToggleState(ActionType.OnUse, character);
PickingTime = originalPickingTime;
}
#if CLIENT
else if (hasRequiredItems && character != null && character == Character.Controlled)
{
GUI.AddMessage(accessDeniedTxt, GUI.Style.Red);
}
#endif
return false;
}
public override void Update(float deltaTime, Camera cam)
{
UpdateProjSpecific(deltaTime);
toggleCooldownTimer -= deltaTime;
damageSoundCooldown -= deltaTime;
if (isBroken)
{
//the door has to be restored to 50% health before collision detection on the body is re-enabled
if (item.ConditionPercentage > 50.0f)
{
IsBroken = false;
}
return;
}
bool isClosing = false;
if (!IsStuck)
{
if (PredictedState == null)
{
OpenState += deltaTime * (isOpen ? OpeningSpeed : -ClosingSpeed);
isClosing = openState > 0.0f && openState < 1.0f && !isOpen;
}
else
{
OpenState += deltaTime * ((bool)PredictedState ? OpeningSpeed : -ClosingSpeed);
isClosing = openState > 0.0f && openState < 1.0f && !(bool)PredictedState;
resetPredictionTimer -= deltaTime;
if (resetPredictionTimer <= 0.0f)
{
PredictedState = null;
}
}
LinkedGap.Open = isBroken ? 1.0f : openState;
}
if (isClosing)
{
if (OpenState < 0.9f) { PushCharactersAway(); }
}
else
{
Body.Enabled = Impassable || openState < 1.0f;
}
//don't use the predicted state here, because it might set
//other items to an incorrect state if the prediction is wrong
item.SendSignal(0, (isOpen) ? "1" : "0", "state_out", null);
}
partial void UpdateProjSpecific(float deltaTime);
public override void UpdateBroken(float deltaTime, Camera cam)
{
base.UpdateBroken(deltaTime, cam);
IsBroken = true;
}
private void EnableBody()
{
if (!Impassable)
{
Body.FarseerBody.SetIsSensor(false);
var ce = Body.FarseerBody.ContactList;
while (ce != null && ce.Contact != null)
{
ce.Contact.Enabled = false;
ce = ce.Next;
}
PushCharactersAway();
}
#if CLIENT
UpdateConvexHulls();
#endif
isBroken = false;
}
private void DisableBody()
{
//change the body to a sensor instead of disabling it completely,
//because otherwise repairtool raycasts won't hit it
if (!Impassable)
{
Body.FarseerBody.SetIsSensor(true);
var ce = Body.FarseerBody.ContactList;
while (ce != null && ce.Contact != null)
{
ce.Contact.Enabled = false;
ce = ce.Next;
}
}
linkedGap.Open = 1.0f;
IsOpen = false;
#if CLIENT
if (convexHull != null) convexHull.Enabled = false;
if (convexHull2 != null) convexHull2.Enabled = false;
#endif
}
public void RefreshLinkedGap()
{
LinkedGap.ConnectedDoor = this;
if (autoOrientGap)
{
LinkedGap.AutoOrient();
}
LinkedGap.Open = isBroken ? 1.0f : openState;
LinkedGap.PassAmbientLight = Window != Rectangle.Empty;
}
public override void OnMapLoaded()
{
RefreshLinkedGap();
#if CLIENT
Vector2[] corners = GetConvexHullCorners(Rectangle.Empty);
convexHull = new ConvexHull(corners, Color.Black, item);
if (Window != Rectangle.Empty) convexHull2 = new ConvexHull(corners, Color.Black, item);
UpdateConvexHulls();
#endif
}
public override void OnScaleChanged()
{
#if CLIENT
UpdateConvexHulls();
#endif
if (linkedGap != null)
{
RefreshLinkedGap();
linkedGap.Rect = item.Rect;
}
}
protected override void RemoveComponentSpecific()
{
base.RemoveComponentSpecific();
if (Body != null)
{
Body.Remove();
Body = null;
}
//no need to remove the gap if we're unloading the whole submarine
//otherwise the gap will be removed twice and cause console warnings
if (!Submarine.Unloading)
{
linkedGap?.Remove();
}
doorSprite?.Remove();
weldedSprite?.Remove();
#if CLIENT
convexHull?.Remove();
convexHull2?.Remove();
#endif
}
private void PushCharactersAway()
{
if (!MathUtils.IsValid(item.SimPosition))
{
DebugConsole.ThrowError("Failed to push a character out of a doorway - position of the door is not valid (" + item.SimPosition + ")");
GameAnalyticsManager.AddErrorEventOnce("PushCharactersAway:DoorPosInvalid", GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Failed to push a character out of a doorway - position of the door is not valid (" + item.SimPosition + ").");
return;
}
Vector2 simPos = ConvertUnits.ToSimUnits(new Vector2(item.Rect.X, item.Rect.Y));
Vector2 currSize = IsHorizontal ?
new Vector2(item.Rect.Width * (1.0f - openState), doorSprite.size.Y * item.Scale) :
new Vector2(doorSprite.size.X * item.Scale, item.Rect.Height * (1.0f - openState));
Vector2 simSize = ConvertUnits.ToSimUnits(currSize);
foreach (Character c in Character.CharacterList)
{
if (!c.Enabled) continue;
if (!MathUtils.IsValid(c.SimPosition))
{
if (GameSettings.VerboseLogging) { DebugConsole.ThrowError("Failed to push a character out of a doorway - position of the character \"" + c.Name + "\" is not valid (" + c.SimPosition + ")"); }
GameAnalyticsManager.AddErrorEventOnce("PushCharactersAway:CharacterPosInvalid", GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Failed to push a character out of a doorway - position of the character \"" + c.Name + "\" is not valid (" + c.SimPosition + ")." +
" Removed: " + c.Removed +
" Remoteplayer: " + c.IsRemotePlayer);
continue;
}
int dir = IsHorizontal ? Math.Sign(c.SimPosition.Y - item.SimPosition.Y) : Math.Sign(c.SimPosition.X - item.SimPosition.X);
foreach (Limb limb in c.AnimController.Limbs)
{
if (PushBodyOutOfDoorway(c, limb.body, dir, simPos, simSize) && damageSoundCooldown <= 0.0f)
{
#if CLIENT
SoundPlayer.PlayDamageSound("LimbBlunt", 1.0f, limb.body);
#endif
damageSoundCooldown = 0.5f;
}
}
PushBodyOutOfDoorway(c, c.AnimController.Collider, dir, simPos, simSize);
}
}
private bool PushBodyOutOfDoorway(Character c, PhysicsBody body, int dir, Vector2 doorRectSimPos, Vector2 doorRectSimSize)
{
if (!MathUtils.IsValid(body.SimPosition))
{
DebugConsole.ThrowError("Failed to push a limb out of a doorway - position of the body (character \"" + c.Name + "\") is not valid (" + body.SimPosition + ")");
GameAnalyticsManager.AddErrorEventOnce("PushCharactersAway:LimbPosInvalid", GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Failed to push a character out of a doorway - position of the character \"" + c.Name + "\" is not valid (" + body.SimPosition + ")." +
" Removed: " + c.Removed +
" Remoteplayer: " + c.IsRemotePlayer);
return false;
}
float diff;
if (IsHorizontal)
{
if (body.SimPosition.X < doorRectSimPos.X || body.SimPosition.X > doorRectSimPos.X + doorRectSimSize.X) { return false; }
diff = body.SimPosition.Y - item.SimPosition.Y;
}
else
{
if (body.SimPosition.Y > doorRectSimPos.Y || body.SimPosition.Y < doorRectSimPos.Y - doorRectSimSize.Y) { return false; }
diff = body.SimPosition.X - item.SimPosition.X;
}
//if the limb is at a different side of the door than the character (collider),
//immediately teleport it to the correct side
if (Math.Sign(diff) != dir)
{
if (IsHorizontal)
{
body.SetTransform(new Vector2(body.SimPosition.X, item.SimPosition.Y + dir * doorRectSimSize.Y * 2.0f), body.Rotation);
}
else
{
body.SetTransform(new Vector2(item.SimPosition.X + dir * doorRectSimSize.X * 1.2f, body.SimPosition.Y), body.Rotation);
}
}
//apply an impulse to push the limb further from the door
if (IsHorizontal)
{
if (Math.Abs(body.SimPosition.Y - item.SimPosition.Y) > doorRectSimSize.Y * 0.5f) { return false; }
body.ApplyLinearImpulse(new Vector2(isOpen ? 0.0f : 1.0f, dir * 2.0f), maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
}
else
{
if (Math.Abs(body.SimPosition.X - item.SimPosition.X) > doorRectSimSize.X * 0.5f) { return false; }
body.ApplyLinearImpulse(new Vector2(dir * 2.0f, isOpen ? 0.0f : -1.0f), maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
}
c.SetStun(0.2f);
return true;
}
partial void OnFailedToOpen();
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f)
{
if (IsStuck) return;
bool wasOpen = PredictedState == null ? isOpen : PredictedState.Value;
if (connection.Name == "toggle")
{
if (toggleCooldownTimer > 0.0f && sender != lastUser) { OnFailedToOpen(); return; }
if (IsStuck) { toggleCooldownTimer = 1.0f; OnFailedToOpen(); return; }
toggleCooldownTimer = ToggleCoolDown;
lastUser = sender;
SetState(!wasOpen, false, true, forcedOpen: false);
}
else if (connection.Name == "set_state")
{
bool signalOpen = signal != "0";
if (IsStuck && signalOpen != wasOpen) { toggleCooldownTimer = 1.0f; OnFailedToOpen(); return; }
SetState(signalOpen, false, true, forcedOpen: false);
}
#if SERVER
if (sender != null && wasOpen != isOpen)
{
GameServer.Log(sender.LogName + (isOpen ? " opened " : " closed ") + item.Name, ServerLog.MessageType.ItemInteraction);
}
#endif
}
public void TrySetState(bool open, bool isNetworkMessage, bool sendNetworkMessage = false)
{
SetState(open, isNetworkMessage, sendNetworkMessage, forcedOpen: false);
}
partial void SetState(bool open, bool isNetworkMessage, bool sendNetworkMessage, bool forcedOpen);
}
}
@@ -0,0 +1,482 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
partial class ElectricalDischarger : Powered
{
private static readonly List<ElectricalDischarger> list = new List<ElectricalDischarger>();
public static IEnumerable<ElectricalDischarger> 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, true, description: "How far the discharge can travel from the item."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 5000.0f)]
public float Range
{
get;
set;
}
[Serialize(25.0f, true, description: "How much further can the discharge be carried when moving across walls."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)]
public float RangeMultiplierInWalls
{
get;
set;
}
[Serialize(0.25f, true, description: "The duration of an individual discharge (in seconds)."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)]
public float Duration
{
get;
set;
}
[Serialize(false, true, "If set to true, the discharge cannot travel inside the submarine nor shock anyone inside."), Editable]
public bool OutdoorsOnly
{
get;
set;
}
private readonly List<Node> nodes = new List<Node>();
public IEnumerable<Node> Nodes
{
get { return nodes; }
}
private readonly List<Pair<Character,Node>> charactersInRange = new List<Pair<Character, Node>>();
private bool charging;
private float timer;
private Attack attack;
public ElectricalDischarger(Item item, XElement element) :
base(item, element)
{
list.Add(this);
foreach (XElement 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; }
CurrPowerConsumption = powerConsumption;
charging = true;
timer = Duration;
IsActive = true;
return false;
}
public override void Update(float deltaTime, Camera cam)
{
#if CLIENT
frameOffset = Rand.Int(electricitySprite.FrameCount);
#endif
if (timer <= 0.0f)
{
IsActive = false;
return;
}
timer -= deltaTime;
if (charging)
{
if (GetAvailableBatteryPower() >= powerConsumption)
{
var batteries = item.GetConnectedComponents<PowerContainer>();
float neededPower = powerConsumption;
while (neededPower > 0.0001f && batteries.Count > 0)
{
batteries.RemoveAll(b => b.Charge <= 0.0001f || b.MaxOutPut <= 0.0001f);
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();
}
else if (Voltage > MinVoltage)
{
Discharge();
}
}
}
public override void UpdateBroken(float deltaTime, Camera cam)
{
base.UpdateBroken(deltaTime, cam);
nodes.Clear();
charactersInRange.Clear();
}
private void Discharge()
{
ApplyStatusEffects(ActionType.OnUse, 1.0f);
FindNodes(item.WorldPosition, Range);
if (attack != null)
{
foreach (Pair<Character, Node> characterInRange in charactersInRange)
{
characterInRange.First.ApplyAttack(null, characterInRange.Second.WorldPosition, attack, 1.0f);
}
}
DischargeProjSpecific();
charging = false;
}
partial void DischargeProjSpecific();
private void FindNodes(Vector2 worldPosition, float range)
{
//see which submarines are within range so we can skip structures that are in far-away subs
List<Submarine> submarinesInRange = new List<Submarine>();
foreach (Submarine sub in Submarine.Loaded)
{
if (item.Submarine == sub)
{
submarinesInRange.Add(sub);
}
else
{
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
List<Entity> entitiesInRange = new List<Entity>(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 the structure is within a hull
//add a small offset away from the sub's center so structures right at the edge of a hull are still valid
Vector2 offset = Vector2.Normalize(structure.WorldPosition - structure.Submarine.WorldPosition);
if (Hull.FindHull(structure.Position + offset * Submarine.GridSize, useWorldCoordinates: false) != null) { continue; }
}
}
entitiesInRange.Add(structure);
}
foreach (Character character in Character.CharacterList)
{
if (!character.Enabled) continue;
if (OutdoorsOnly && character.Submarine != null) continue;
if (character.Submarine != null && !submarinesInRange.Contains(character.Submarine)) continue;
if (Vector2.DistanceSquared(character.WorldPosition, worldPosition) < range * range * RangeMultiplierInWalls)
{
entitiesInRange.Add(character);
}
}
nodes.Clear();
nodes.Add(new Node(worldPosition, -1));
FindNodes(entitiesInRange, worldPosition, 0, 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<Entity> 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 = Vector2.Distance(character.WorldPosition, currPos);
}
if (dist < closestDist)
{
closestIndex = i;
closestDist = dist;
}
}
if (closestIndex == -1 || closestDist > currentRange)
{
int originalParentNodeIndex = parentNodeIndex;
//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 Character character)) continue;
if (OutdoorsOnly && character.Submarine != null) continue;
if (targetStructure.IsHorizontal)
{
if (otherEntity.WorldPosition.X < targetStructure.WorldRect.X) continue;
if (otherEntity.WorldPosition.X > targetStructure.WorldRect.Right) continue;
if (Math.Abs(otherEntity.WorldPosition.Y - targetStructure.WorldPosition.Y) > currentRange) continue;
}
else
{
if (otherEntity.WorldPosition.Y < targetStructure.WorldRect.Y - targetStructure.Rect.Height) continue;
if (otherEntity.WorldPosition.Y > targetStructure.WorldRect.Y) continue;
if (Math.Abs(otherEntity.WorldPosition.X - targetStructure.WorldPosition.X) > currentRange) continue;
}
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);
charactersInRange.Add(new Pair<Character, Node>(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.5f))
{
//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;
}
}
protected override void RemoveComponentSpecific()
{
base.RemoveComponentSpecific();
list.Remove(this);
}
}
}
@@ -0,0 +1,669 @@
using Barotrauma.Networking;
using FarseerPhysics;
using FarseerPhysics.Dynamics;
using FarseerPhysics.Dynamics.Contacts;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
partial class Holdable : Pickable, IServerSerializable, IClientSerializable
{
const float MaxAttachDistance = 150.0f;
//the position(s) in the item that the Character grabs
protected Vector2[] handlePos;
private readonly Vector2[] scaledHandlePos;
private InputType prevPickKey;
private string prevMsg;
private Dictionary<RelatedItem.RelationType, List<RelatedItem>> prevRequiredItems;
//the distance from the holding characters elbow to center of the physics body of the item
protected Vector2 holdPos;
protected Vector2 aimPos;
private float swingState;
private bool attachable, attached, attachedByDefault;
private readonly PhysicsBody body;
public PhysicsBody Pusher
{
get;
private set;
}
//the angle in which the Character holds the item
protected float holdAngle;
public PhysicsBody Body
{
get { return item.body ?? body; }
}
[Serialize(false, true, description: "Is the item currently attached to a wall (only valid if Attachable is set to true).")]
public bool Attached
{
get { return attached && item.ParentInventory == null; }
set
{
attached = value;
item.SetActiveSprite();
}
}
[Serialize(true, true, description: "Can the item be pointed to a specific direction or do the characters always hold it in a static pose.")]
public bool Aimable
{
get;
set;
}
[Serialize(false, false, description: "Should the character adjust its pose when aiming with the item. Most noticeable underwater, where the character will rotate its entire body to face the direction the item is aimed at.")]
public bool ControlPose
{
get;
set;
}
[Serialize(false, false, description: "Can the item be attached to walls.")]
public bool Attachable
{
get { return attachable; }
set { attachable = value; }
}
[Serialize(true, false, description: "Can the item be reattached to walls after it has been deattached (only valid if Attachable is set to true).")]
public bool Reattachable
{
get;
set;
}
[Serialize(false, false, description: "Should the item be attached to a wall by default when it's placed in the submarine editor.")]
public bool AttachedByDefault
{
get { return attachedByDefault; }
set { attachedByDefault = value; }
}
[Editable, Serialize("0.0,0.0", false, description: "The position the character holds the item at (in pixels, as an offset from the character's shoulder)."+
" For example, a value of 10,-100 would make the character hold the item 100 pixels below the shoulder and 10 pixels forwards.")]
public Vector2 HoldPos
{
get { return ConvertUnits.ToDisplayUnits(holdPos); }
set { holdPos = ConvertUnits.ToSimUnits(value); }
}
[Serialize("0.0,0.0", false, description: "The position the character holds the item at when aiming (in pixels, as an offset from the character's shoulder)."+
" Works similarly as HoldPos, except that the position is rotated according to the direction the player is aiming at. For example, a value of 10,-100 would make the character hold the item 100 pixels below the shoulder and 10 pixels forwards when aiming directly to the right.")]
public Vector2 AimPos
{
get { return ConvertUnits.ToDisplayUnits(aimPos); }
set { aimPos = ConvertUnits.ToSimUnits(value); }
}
[Editable, Serialize(0.0f, false, description: "The rotation at which the character holds the item (in degrees, relative to the rotation of the character's hand).")]
public float HoldAngle
{
get { return MathHelper.ToDegrees(holdAngle); }
set { holdAngle = MathHelper.ToRadians(value); }
}
private Vector2 swingAmount;
[Editable, Serialize("0.0,0.0", false, description: "How much the item swings around when aiming/holding it (in pixels, as an offset from AimPos/HoldPos).")]
public Vector2 SwingAmount
{
get { return ConvertUnits.ToDisplayUnits(swingAmount); }
set { swingAmount = ConvertUnits.ToSimUnits(value); }
}
[Editable, Serialize(0.0f, false, description: "How fast the item swings around when aiming/holding it (only valid if SwingAmount is set).")]
public float SwingSpeed { get; set; }
[Editable, Serialize(false, false, description: "Should the item swing around when it's being held.")]
public bool SwingWhenHolding { get; set; }
[Editable, Serialize(false, false, description: "Should the item swing around when it's being aimed.")]
public bool SwingWhenAiming { get; set; }
[Editable, Serialize(false, false, description: "Should the item swing around when it's being used (for example, when firing a weapon or a welding tool).")]
public bool SwingWhenUsing { get; set; }
public Holdable(Item item, XElement element)
: base(item, element)
{
body = item.body;
Pusher = null;
if (element.GetAttributeBool("blocksplayers", false))
{
Pusher = new PhysicsBody(item.body.width, item.body.height, item.body.radius, item.body.Density)
{
BodyType = BodyType.Dynamic,
CollidesWith = Physics.CollisionCharacter,
CollisionCategories = Physics.CollisionItemBlocking,
Enabled = false
};
Pusher.FarseerBody.OnCollision += OnPusherCollision;
Pusher.FarseerBody.FixedRotation = false;
Pusher.FarseerBody.IgnoreGravity = true;
}
handlePos = new Vector2[2];
scaledHandlePos = new Vector2[2];
Vector2 previousValue = Vector2.Zero;
for (int i = 1; i < 3; i++)
{
int index = i - 1;
string attributeName = "handle" + i;
var attribute = element.Attribute(attributeName);
// If no value is defind for handle2, use the value of handle1.
var value = attribute != null ? ConvertUnits.ToSimUnits(XMLExtensions.ParseVector2(attribute.Value)) : previousValue;
handlePos[index] = value;
previousValue = value;
}
canBePicked = true;
if (attachable)
{
prevMsg = DisplayMsg;
prevPickKey = PickKey;
prevRequiredItems = new Dictionary<RelatedItem.RelationType, List<RelatedItem>>(requiredItems);
if (item.Submarine != null)
{
if (item.Submarine.Loading)
{
AttachToWall();
Attached = false;
}
else //the submarine is not being loaded, which means we're either in the sub editor or the item has been spawned mid-round
{
if (Screen.Selected == GameMain.SubEditorScreen)
{
//in the sub editor, attach
AttachToWall();
}
else
{
//spawned mid-round, deattach
DeattachFromWall();
}
}
}
}
}
private bool OnPusherCollision(Fixture sender, Fixture other, Contact contact)
{
if (other.Body.UserData is Character character)
{
if (!IsActive) { return false; }
return character != picker;
}
else
{
return true;
}
}
public override void Load(XElement componentElement, bool usePrefabValues)
{
base.Load(componentElement, usePrefabValues);
if (usePrefabValues)
{
//this needs to be loaded regardless
Attached = componentElement.GetAttributeBool("attached", attached);
}
if (attachable)
{
prevMsg = DisplayMsg;
prevPickKey = PickKey;
prevRequiredItems = new Dictionary<RelatedItem.RelationType, List<RelatedItem>>(requiredItems);
}
}
public override void Drop(Character dropper)
{
Drop(true, dropper);
}
private void Drop(bool dropConnectedWires, Character dropper)
{
if (dropConnectedWires)
{
DropConnectedWires(dropper);
}
if (attachable)
{
DeattachFromWall();
if (body != null)
{
item.body = body;
}
}
if (Pusher != null) { Pusher.Enabled = false; }
if (item.body != null) { item.body.Enabled = true; }
IsActive = false;
if (picker == null)
{
if (dropper == null) { return; }
picker = dropper;
}
if (picker.Inventory == null) { return; }
item.Submarine = picker.Submarine;
if (item.body != null)
{
if (item.body.Removed)
{
DebugConsole.ThrowError(
"Failed to drop the Holdable component of the item \"" + item.Name + "\" (body has been removed"
+ (item.Removed ? ", item has been removed)" : ")"));
}
else
{
item.body.ResetDynamics();
Limb heldHand, arm;
if (picker.Inventory.IsInLimbSlot(item, InvSlotType.LeftHand))
{
heldHand = picker.AnimController.GetLimb(LimbType.LeftHand);
arm = picker.AnimController.GetLimb(LimbType.LeftArm);
}
else
{
heldHand = picker.AnimController.GetLimb(LimbType.RightHand);
arm = picker.AnimController.GetLimb(LimbType.RightArm);
}
if (heldHand != null && arm != null)
{
//hand simPosition is actually in the wrist so need to move the item out from it slightly
Vector2 diff = new Vector2(
(heldHand.SimPosition.X - arm.SimPosition.X) / 2f,
(heldHand.SimPosition.Y - arm.SimPosition.Y) / 2.5f);
item.SetTransform(heldHand.SimPosition + diff, 0.0f);
}
else
{
item.SetTransform(picker.SimPosition, 0.0f);
}
}
}
picker.DeselectItem(item);
picker.Inventory.RemoveItem(item);
picker = null;
}
public override void Equip(Character character)
{
picker = character;
if (character != null) item.Submarine = character.Submarine;
if (item.body == null)
{
if (body != null)
{
item.body = body;
}
else
{
return;
}
}
if (!item.body.Enabled)
{
Limb rightHand = picker.AnimController.GetLimb(LimbType.RightHand);
item.SetTransform(rightHand.SimPosition, 0.0f);
}
bool alreadyEquipped = character.HasEquippedItem(item);
bool canSelect = picker.TrySelectItem(item);
if (canSelect || picker.HasEquippedItem(item))
{
if (!canSelect)
{
character.DeselectItem(item);
}
item.body.Enabled = true;
item.body.PhysEnabled = false;
IsActive = true;
#if SERVER
if (!alreadyEquipped) GameServer.Log(character.LogName + " equipped " + item.Name, ServerLog.MessageType.ItemInteraction);
#endif
}
}
public override void Unequip(Character character)
{
if (picker == null) return;
picker.DeselectItem(item);
#if SERVER
GameServer.Log(character.LogName + " unequipped " + item.Name, ServerLog.MessageType.ItemInteraction);
#endif
item.body.PhysEnabled = true;
item.body.Enabled = false;
IsActive = false;
}
public bool CanBeAttached()
{
if (!attachable || !Reattachable) return false;
//can be attached anywhere in sub editor
if (Screen.Selected == GameMain.SubEditorScreen) return true;
//can be attached anywhere inside hulls
if (item.CurrentHull != null) return true;
return Structure.GetAttachTarget(item.WorldPosition) != null;
}
public bool CanBeDeattached()
{
if (!attachable || !attached) return true;
//allow deattaching everywhere in sub editor
if (Screen.Selected == GameMain.SubEditorScreen) return true;
//don't allow deattaching if part of a sub and outside hulls
return item.Submarine == null || item.CurrentHull != null;
}
public override bool Pick(Character picker)
{
if (!attachable)
{
return base.Pick(picker);
}
if (!CanBeDeattached()) return false;
if (Attached)
{
return base.Pick(picker);
}
else
{
//not attached -> pick the item instantly, ignoring picking time
return OnPicked(picker);
}
}
public override bool OnPicked(Character picker)
{
if (base.OnPicked(picker))
{
DeattachFromWall();
#if SERVER
if (GameMain.Server != null && attachable)
{
item.CreateServerEvent(this);
if (picker != null)
{
GameServer.Log(picker.LogName + " detached " + item.Name + " from a wall", ServerLog.MessageType.ItemInteraction);
}
}
#endif
return true;
}
return false;
}
public void AttachToWall()
{
if (!attachable) return;
//outside hulls/subs -> we need to check if the item is being attached on a structure outside the sub
if (item.CurrentHull == null && item.Submarine == null)
{
Structure attachTarget = Structure.GetAttachTarget(item.WorldPosition);
if (attachTarget != null)
{
if (attachTarget.Submarine != null)
{
//set to submarine-relative position
item.SetTransform(ConvertUnits.ToSimUnits(item.WorldPosition - attachTarget.Submarine.Position), 0.0f, false);
}
item.Submarine = attachTarget.Submarine;
}
}
var containedItems = item.ContainedItems;
if (containedItems != null)
{
foreach (Item contained in containedItems)
{
if (contained.body == null) continue;
contained.SetTransform(item.SimPosition, contained.body.Rotation);
}
}
body.Enabled = false;
item.body = null;
DisplayMsg = prevMsg;
PickKey = prevPickKey;
requiredItems = new Dictionary<RelatedItem.RelationType, List<RelatedItem>>(prevRequiredItems);
Attached = true;
}
public void DeattachFromWall()
{
if (!attachable) return;
Attached = false;
//make the item pickable with the default pick key and with no specific tools/items when it's deattached
requiredItems.Clear();
DisplayMsg = "";
PickKey = InputType.Select;
}
public override bool Use(float deltaTime, Character character = null)
{
if (!attachable || item.body == null) { return character == null || character.IsKeyDown(InputType.Aim); }
if (character != null)
{
if (!character.IsKeyDown(InputType.Aim)) { return false; }
if (!CanBeAttached()) { return false; }
if (GameMain.NetworkMember != null)
{
if (character != Character.Controlled)
{
return false;
}
else if (GameMain.NetworkMember.IsServer)
{
return false;
}
else
{
#if CLIENT
Vector2 attachPos = ConvertUnits.ToSimUnits(GetAttachPosition(character));
GameMain.Client.CreateEntityEvent(item, new object[]
{
NetEntityEvent.Type.ComponentState,
item.GetComponentIndex(this),
attachPos
});
#endif
}
return false;
}
else
{
item.Drop(character);
item.SetTransform(ConvertUnits.ToSimUnits(GetAttachPosition(character)), 0.0f);
}
}
AttachToWall();
return true;
}
private Vector2 GetAttachPosition(Character user)
{
if (user == null) { return item.Position; }
Vector2 mouseDiff = user.CursorWorldPosition - user.WorldPosition;
mouseDiff = mouseDiff.ClampLength(MaxAttachDistance);
return new Vector2(
MathUtils.RoundTowardsClosest(user.Position.X + mouseDiff.X, Submarine.GridSize.X),
MathUtils.RoundTowardsClosest(user.Position.Y + mouseDiff.Y, Submarine.GridSize.Y));
}
public override void UpdateBroken(float deltaTime, Camera cam)
{
Update(deltaTime, cam);
}
public override void Update(float deltaTime, Camera cam)
{
if (item.body == null || !item.body.Enabled) return;
if (picker == null || !picker.HasEquippedItem(item))
{
if (Pusher != null) { Pusher.Enabled = false; }
IsActive = false;
return;
}
Vector2 swing = Vector2.Zero;
if (swingAmount != Vector2.Zero)
{
swingState += deltaTime;
swingState %= 1.0f;
if (SwingWhenHolding ||
(SwingWhenAiming && picker.IsKeyDown(InputType.Aim)) ||
(SwingWhenUsing && picker.IsKeyDown(InputType.Aim) && picker.IsKeyDown(InputType.Shoot)))
{
swing = swingAmount * new Vector2(
PerlinNoise.GetPerlin(swingState * SwingSpeed * 0.1f, swingState * SwingSpeed * 0.1f) - 0.5f,
PerlinNoise.GetPerlin(swingState * SwingSpeed * 0.1f + 0.5f, swingState * SwingSpeed * 0.1f + 0.5f) - 0.5f);
}
}
ApplyStatusEffects(ActionType.OnActive, deltaTime, picker);
if (item.body.Dir != picker.AnimController.Dir) Flip();
item.Submarine = picker.Submarine;
if (picker.HasSelectedItem(item))
{
scaledHandlePos[0] = handlePos[0] * item.Scale;
scaledHandlePos[1] = handlePos[1] * item.Scale;
bool aim = picker.IsKeyDown(InputType.Aim) && aimPos != Vector2.Zero && (picker.SelectedConstruction == null || picker.SelectedConstruction.GetComponent<Ladder>() != null);
picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, holdPos + swing, aimPos + swing, aim, holdAngle);
}
else
{
Limb equipLimb = null;
if (picker.Inventory.IsInLimbSlot(item, InvSlotType.Headset) || picker.Inventory.IsInLimbSlot(item, InvSlotType.Head))
{
equipLimb = picker.AnimController.GetLimb(LimbType.Head);
}
else if (picker.Inventory.IsInLimbSlot(item, InvSlotType.InnerClothes) ||
picker.Inventory.IsInLimbSlot(item, InvSlotType.OuterClothes))
{
equipLimb = picker.AnimController.GetLimb(LimbType.Torso);
}
if (equipLimb != null)
{
float itemAngle = (equipLimb.Rotation + holdAngle * picker.AnimController.Dir);
Matrix itemTransfrom = Matrix.CreateRotationZ(equipLimb.Rotation);
Vector2 transformedHandlePos = Vector2.Transform(handlePos[0] * item.Scale, itemTransfrom);
item.body.ResetDynamics();
item.SetTransform(equipLimb.SimPosition - transformedHandlePos, itemAngle);
}
}
}
public void Flip()
{
handlePos[0].X = -handlePos[0].X;
handlePos[1].X = -handlePos[1].X;
item.body.Dir = -item.body.Dir;
}
public override void OnItemLoaded()
{
if (item.Submarine != null && item.Submarine.Loading) return;
OnMapLoaded();
item.SetActiveSprite();
}
public override void OnMapLoaded()
{
if (!attachable) return;
if (Attached)
{
AttachToWall();
}
else
{
if (item.ParentInventory != null)
{
if (body != null)
{
item.body = body;
body.Enabled = false;
}
}
DeattachFromWall();
}
}
public override XElement Save(XElement parentElement)
{
if (!attachable)
{
return base.Save(parentElement);
}
var tempMsg = DisplayMsg;
var tempPickKey = PickKey;
var tempRequiredItems = requiredItems;
DisplayMsg = prevMsg;
PickKey = prevPickKey;
requiredItems = prevRequiredItems;
XElement saveElement = base.Save(parentElement);
DisplayMsg = tempMsg;
PickKey = tempPickKey;
requiredItems = tempRequiredItems;
return saveElement;
}
}
}
@@ -0,0 +1,124 @@
using Barotrauma.Networking;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
partial class LevelResource : ItemComponent, IServerSerializable
{
private PhysicsBody trigger;
private Holdable holdable;
private float deattachTimer;
[Serialize(1.0f, false, description: "How long it takes to deattach the item from the level walls (in seconds).")]
public float DeattachDuration
{
get;
set;
}
[Serialize(0.0f, false, description: "How far along the item is to being deattached. When the timer goes above DeattachDuration, the item is deattached.")]
public float DeattachTimer
{
get { return deattachTimer; }
set
{
//clients don't deattach the item until the server says so (handled in ClientRead)
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
{
return;
}
deattachTimer = Math.Max(0.0f, value);
#if SERVER
if (deattachTimer >= DeattachDuration)
{
if (holdable.Attached) { item.CreateServerEvent(this); }
holdable.DeattachFromWall();
}
else if (Math.Abs(lastSentDeattachTimer - deattachTimer) > 0.1f)
{
item.CreateServerEvent(this);
lastSentDeattachTimer = deattachTimer;
}
#else
if (deattachTimer >= DeattachDuration)
{
holdable.DeattachFromWall();
trigger.Enabled = false;
}
#endif
}
}
public bool Attached
{
get { return holdable == null ? false : holdable.Attached; }
}
public LevelResource(Item item, XElement element) : base(item, element)
{
IsActive = true;
}
public override void Update(float deltaTime, Camera cam)
{
if (!holdable.Attached)
{
trigger.Enabled = false;
IsActive = false;
}
else
{
if (Vector2.DistanceSquared(item.SimPosition, trigger.SimPosition) > 0.01f)
{
trigger.SetTransform(item.SimPosition, 0.0f);
}
IsActive = false;
}
}
public override void OnItemLoaded()
{
holdable = item.GetComponent<Holdable>();
if (holdable == null)
{
DebugConsole.ThrowError("Error while initializing item \"" + item.Name + "\". Level resources require a Holdable component.");
IsActive = false;
return;
}
holdable.Reattachable = false;
if (requiredItems.Any())
{
holdable.PickingTime = float.MaxValue;
}
var body = item.body ?? holdable.Body;
if (body != null)
{
trigger = new PhysicsBody(body.width, body.height, body.radius, body.Density)
{
UserData = item
};
trigger.FarseerBody.SetIsSensor(true);
trigger.FarseerBody.BodyType = BodyType.Static;
trigger.FarseerBody.CollisionCategories = Physics.CollisionWall;
trigger.FarseerBody.CollidesWith = Physics.CollisionNone;
}
}
protected override void RemoveComponentSpecific()
{
if (trigger != null)
{
trigger.Remove();
trigger = null;
}
}
}
}
@@ -0,0 +1,462 @@
using FarseerPhysics;
using FarseerPhysics.Dynamics;
using FarseerPhysics.Dynamics.Contacts;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
class MeleeWeapon : Holdable
{
private float hitPos;
private bool hitting;
private float range;
private float reload;
private float reloadTimer;
private readonly Attack attack;
private readonly HashSet<Entity> hitTargets = new HashSet<Entity>();
private readonly Queue<Fixture> impactQueue = new Queue<Fixture>();
public Character User { get; private set; }
[Serialize(0.0f, false, description: "An estimation of how close the item has to be to the target for it to hit. Used by AI characters to determine when they're close enough to hit a target.")]
public float Range
{
get { return ConvertUnits.ToDisplayUnits(range); }
set { range = ConvertUnits.ToSimUnits(value); }
}
[Serialize(0.5f, false, description: "How long the user has to wait before they can hit with the weapon again (in seconds).")]
public float Reload
{
get { return reload; }
set { reload = Math.Max(0.0f, value); }
}
[Serialize(false, false, description: "Can the weapon hit multiple targets per swing.")]
public bool AllowHitMultiple
{
get;
set;
}
public MeleeWeapon(Item item, XElement element)
: base(item, element)
{
foreach (XElement subElement in element.Elements())
{
if (!subElement.Name.ToString().Equals("attack", StringComparison.OrdinalIgnoreCase)) { continue; }
attack = new Attack(subElement, item.Name + ", MeleeWeapon");
}
item.IsShootable = true;
// TODO: should define this in xml if we have melee weapons that don't require aim to use
item.RequireAimToUse = true;
}
public override bool Use(float deltaTime, Character character = null)
{
if (character == null || reloadTimer > 0.0f) { return false; }
if (Item.RequireAimToUse && !character.IsKeyDown(InputType.Aim) || hitting) { return false; }
//don't allow hitting if the character is already hitting with another weapon
for (int i = 0; i < 2; i++ )
{
if (character.SelectedItems[i] == null || character.SelectedItems[i] == Item) { continue; }
var otherWeapon = character.SelectedItems[i].GetComponent<MeleeWeapon>();
if (otherWeapon == null) { continue; }
if (otherWeapon.hitting) { return false; }
}
SetUser(character);
if (hitPos < MathHelper.PiOver4) { return false; }
ActivateNearbySleepingCharacters();
reloadTimer = reload;
item.body.FarseerBody.CollisionCategories = Physics.CollisionProjectile;
item.body.FarseerBody.CollidesWith = Physics.CollisionCharacter | Physics.CollisionWall;
item.body.FarseerBody.OnCollision += OnCollision;
item.body.FarseerBody.IsBullet = true;
item.body.PhysEnabled = true;
if (!character.AnimController.InWater)
{
foreach (Limb l in character.AnimController.Limbs)
{
if (l.type == LimbType.LeftFoot || l.type == LimbType.LeftThigh || l.type == LimbType.LeftLeg) { continue; }
if (l.type == LimbType.Head || l.type == LimbType.Torso)
{
l.body.ApplyLinearImpulse(new Vector2(character.AnimController.Dir * 7.0f, -4.0f));
}
else
{
l.body.ApplyLinearImpulse(new Vector2(character.AnimController.Dir * 5.0f, -2.0f));
}
}
}
hitting = true;
hitTargets.Clear();
IsActive = true;
if (item.AiTarget != null)
{
item.AiTarget.SoundRange = item.AiTarget.MaxSoundRange;
item.AiTarget.SightRange = item.AiTarget.MaxSightRange;
}
return false;
}
public override void Drop(Character dropper)
{
base.Drop(dropper);
hitting = false;
hitPos = 0.0f;
}
public override void UpdateBroken(float deltaTime, Camera cam)
{
Update(deltaTime, cam);
}
public override void Update(float deltaTime, Camera cam)
{
if (!item.body.Enabled) { impactQueue.Clear(); return; }
if (!picker.HasSelectedItem(item)) { impactQueue.Clear(); IsActive = false; }
while (impactQueue.Count > 0)
{
var impact = impactQueue.Dequeue();
HandleImpact(impact.Body);
}
reloadTimer -= deltaTime;
if (reloadTimer < 0) { reloadTimer = 0; }
if (!picker.IsKeyDown(InputType.Aim) && !hitting) { hitPos = 0.0f; }
ApplyStatusEffects(ActionType.OnActive, deltaTime, picker);
if (item.body.Dir != picker.AnimController.Dir) { Flip(); }
AnimController ac = picker.AnimController;
//TODO: refactor the hitting logic (get rid of the magic numbers, make it possible to use different kinds of animations for different items)
if (!hitting)
{
bool aim = picker.AllowInput && picker.IsKeyDown(InputType.Aim) && reloadTimer <= 0 && (picker.SelectedConstruction == null || picker.SelectedConstruction.GetComponent<Ladder>() != null);
if (aim)
{
hitPos = MathUtils.WrapAnglePi(Math.Min(hitPos + deltaTime * 5f, MathHelper.PiOver4));
ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, false, hitPos, holdAngle + hitPos);
}
else
{
hitPos = 0;
ac.HoldItem(deltaTime, item, handlePos, holdPos, Vector2.Zero, false, holdAngle);
}
}
else
{
hitPos = MathUtils.WrapAnglePi(hitPos - deltaTime * 15f);
ac.HoldItem(deltaTime, item, handlePos, new Vector2(2, 0), Vector2.Zero, false, hitPos, holdAngle + hitPos); // aimPos not used -> zero (new Vector2(-0.3f, 0.2f)), holdPos new Vector2(0.6f, -0.1f)
if (hitPos < -MathHelper.PiOver2)
{
RestoreCollision();
hitting = false;
hitTargets.Clear();
hitPos = 0;
}
}
}
/// <summary>
/// Activate sleeping ragdolls that are close enough to hit with the weapon (otherwise the collision will not be registered)
/// </summary>
private void ActivateNearbySleepingCharacters()
{
foreach (Character c in Character.CharacterList)
{
if (!c.Enabled || !c.AnimController.BodyInRest) { continue; }
//do a broad check first
if (Math.Abs(c.WorldPosition.X - item.WorldPosition.X) > 1000.0f) { continue; }
if (Math.Abs(c.WorldPosition.Y - item.WorldPosition.Y) > 1000.0f) { continue; }
foreach (Limb limb in c.AnimController.Limbs)
{
float hitRange = 2.0f;
if (Vector2.DistanceSquared(limb.SimPosition, item.SimPosition) < hitRange * hitRange)
{
c.AnimController.BodyInRest = false;
break;
}
}
}
}
private void SetUser(Character character)
{
if (User == character) { return; }
if (User != null && User.Removed) { User = null; }
User = character;
}
private void RestoreCollision()
{
impactQueue.Clear();
item.body.FarseerBody.OnCollision -= OnCollision;
item.body.CollisionCategories = Physics.CollisionItem;
item.body.CollidesWith = Physics.CollisionWall;
item.body.FarseerBody.IsBullet = false;
item.body.PhysEnabled = false;
}
private bool OnCollision(Fixture f1, Fixture f2, Contact contact)
{
if (User == null || User.Removed)
{
impactQueue.Enqueue(f2);
return true;
}
//ignore collision if there's a wall between the user and the weapon to prevent hitting through walls
if (Submarine.PickBody(User.AnimController.AimSourceSimPos,
item.SimPosition,
collisionCategory: Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking,
allowInsideFixture: true,
customPredicate: (Fixture fixture) => { return fixture.CollidesWith.HasFlag(Physics.CollisionItem); }) != null)
{
return false;
}
Character targetCharacter = null;
Limb targetLimb = null;
Structure targetStructure = null;
Item targetItem = null;
if (f2.Body.UserData is Limb)
{
targetLimb = (Limb)f2.Body.UserData;
if (targetLimb.IsSevered || targetLimb.character == null || targetLimb.character == User) { return false; }
targetCharacter = targetLimb.character;
if (targetCharacter == picker) { return false; }
if (AllowHitMultiple)
{
if (hitTargets.Contains(targetCharacter)) { return false; }
}
else
{
if (hitTargets.Any(t => t is Character)) { return false; }
}
hitTargets.Add(targetCharacter);
}
else if (f2.Body.UserData is Character)
{
targetCharacter = (Character)f2.Body.UserData;
if (targetCharacter == picker || targetCharacter == User) { return false; }
targetLimb = targetCharacter.AnimController.GetLimb(LimbType.Torso); //Otherwise armor can be bypassed in strange ways
if (AllowHitMultiple)
{
if (hitTargets.Contains(targetCharacter)) { return false; }
}
else
{
if (hitTargets.Any(t => t is Character)) { return false; }
}
hitTargets.Add(targetCharacter);
}
else if (f2.Body.UserData is Structure)
{
targetStructure = (Structure)f2.Body.UserData;
if (AllowHitMultiple)
{
if (hitTargets.Contains(targetStructure)) { return true; }
}
else
{
if (hitTargets.Any(t => t is Structure)) { return true; }
}
hitTargets.Add(targetStructure);
}
else if (f2.Body.UserData is Item)
{
targetItem = (Item)f2.Body.UserData;
if (AllowHitMultiple)
{
if (hitTargets.Contains(targetItem)) { return true; }
}
else
{
if (hitTargets.Any(t => t is Item)) { return true; }
}
hitTargets.Add(targetItem);
}
else
{
return false;
}
if (attack != null)
{
if (targetLimb == null && targetCharacter == null && targetStructure == null && (targetItem == null || ! targetItem.Prefab.DamagedByMeleeWeapons))
{
return false;
}
if (targetLimb != null)
{
targetLimb.character.LastDamageSource = item;
attack.DoDamageToLimb(User, targetLimb, item.WorldPosition, 1.0f);
}
else if (targetCharacter != null)
{
targetCharacter.LastDamageSource = item;
attack.DoDamage(User, targetCharacter, item.WorldPosition, 1.0f);
}
else if (targetStructure != null)
{
attack.DoDamage(User, targetStructure, item.WorldPosition, 1.0f);
}
else if (targetItem != null && targetItem.Prefab.DamagedByMeleeWeapons)
{
attack.DoDamage(User, targetItem, item.WorldPosition, 1.0f);
}
else
{
return false;
}
}
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return true; }
#if SERVER
if (GameMain.Server != null && targetCharacter != null) //TODO: Log structure hits
{
GameMain.Server.CreateEntityEvent(item, new object[]
{
Networking.NetEntityEvent.Type.ApplyStatusEffect,
ActionType.OnUse,
null, //itemcomponent
targetCharacter.ID, targetLimb
});
string logStr = picker?.LogName + " used " + item.Name;
if (item.ContainedItems != null && item.ContainedItems.Any())
{
logStr += " (" + string.Join(", ", item.ContainedItems.Select(i => i?.Name)) + ")";
}
logStr += " on " + targetCharacter.LogName + ".";
Networking.GameServer.Log(logStr, Networking.ServerLog.MessageType.Attack);
}
#endif
if (targetCharacter != null) //TODO: Allow OnUse to happen on structures too maybe??
{
ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb, user: User);
}
if (DeleteOnUse)
{
Entity.Spawner.AddToRemoveQueue(item);
}
return true;
}
private void HandleImpact(Body target)
{
if (User == null || User.Removed || target == null)
{
RestoreCollision();
hitting = false;
User = null;
return;
}
Limb targetLimb = target.UserData as Limb;
Character targetCharacter = targetLimb?.character ?? target.UserData as Character;
Structure targetStructure = target.UserData as Structure;
Item targetItem = target.UserData as Item;
if (attack != null)
{
attack.SetUser(User);
if (targetLimb != null)
{
if (targetLimb.character.Removed) { return; }
targetLimb.character.LastDamageSource = item;
attack.DoDamageToLimb(User, targetLimb, item.WorldPosition, 1.0f);
}
else if (targetCharacter != null)
{
if (targetCharacter.Removed) { return; }
targetCharacter.LastDamageSource = item;
attack.DoDamage(User, targetCharacter, item.WorldPosition, 1.0f);
}
else if (targetStructure != null)
{
if (targetStructure.Removed) { return; }
attack.DoDamage(User, targetStructure, item.WorldPosition, 1.0f);
}
else if (targetItem != null && targetItem.Prefab.DamagedByMeleeWeapons)
{
if (targetItem.Removed) { return; }
attack.DoDamage(User, targetItem, item.WorldPosition, 1.0f);
}
else
{
return;
}
}
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
#if SERVER
if (GameMain.Server != null && targetCharacter != null) //TODO: Log structure hits
{
GameMain.Server.CreateEntityEvent(item, new object[]
{
Networking.NetEntityEvent.Type.ApplyStatusEffect,
ActionType.OnUse,
null, //itemcomponent
targetCharacter.ID, targetLimb
});
string logStr = picker?.LogName + " used " + item.Name;
if (item.ContainedItems != null && item.ContainedItems.Any())
{
logStr += " (" + string.Join(", ", item.ContainedItems.Select(i => i?.Name)) + ")";
}
logStr += " on " + targetCharacter.LogName + ".";
Networking.GameServer.Log(logStr, Networking.ServerLog.MessageType.Attack);
}
#endif
if (targetCharacter != null) //TODO: Allow OnUse to happen on structures too maybe??
{
ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb, user: User);
}
if (DeleteOnUse)
{
Entity.Spawner.AddToRemoveQueue(item);
}
}
}
}
@@ -0,0 +1,260 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
class Pickable : ItemComponent, IServerSerializable
{
protected Character picker;
protected List<InvSlotType> allowedSlots;
private float pickTimer;
private Character activePicker;
private CoroutineHandle pickingCoroutine;
public List<InvSlotType> AllowedSlots
{
get { return allowedSlots; }
}
public Character Picker
{
get { return picker; }
}
public Pickable(Item item, XElement element)
: base(item, element)
{
allowedSlots = new List<InvSlotType>();
string slotString = element.GetAttributeString("slots", "Any");
string[] slotCombinations = slotString.Split(',');
foreach (string slotCombination in slotCombinations)
{
string[] slots = slotCombination.Split('+');
InvSlotType allowedSlot = InvSlotType.None;
foreach (string slot in slots)
{
switch (slot.ToLowerInvariant())
{
case "bothhands":
allowedSlot = InvSlotType.LeftHand | InvSlotType.RightHand;
break;
default:
allowedSlot = allowedSlot | (InvSlotType)Enum.Parse(typeof(InvSlotType), slot.Trim());
break;
}
}
allowedSlots.Add(allowedSlot);
}
canBePicked = true;
}
public override bool Pick(Character picker)
{
//return if someone is already trying to pick the item
if (pickTimer > 0.0f) return false;
if (picker == null || picker.Inventory == null) return false;
if (PickingTime > 0.0f)
{
if (picker.PickingItem == null && PickingTime <= float.MaxValue)
{
#if SERVER
item.CreateServerEvent(this);
#endif
pickingCoroutine = CoroutineManager.StartCoroutine(WaitForPick(picker, PickingTime));
}
return false;
}
else
{
return OnPicked(picker);
}
}
public virtual bool OnPicked(Character picker)
{
if (picker.Inventory.TryPutItemWithAutoEquipCheck(item, picker, allowedSlots))
{
if (!picker.HasSelectedItem(item) && item.body != null) item.body.Enabled = false;
this.picker = picker;
for (int i = item.linkedTo.Count - 1; i >= 0; i--)
{
item.linkedTo[i].RemoveLinked(item);
}
item.linkedTo.Clear();
DropConnectedWires(picker);
ApplyStatusEffects(ActionType.OnPicked, 1.0f, picker);
#if CLIENT
if (!GameMain.Instance.LoadingScreenOpen && picker == Character.Controlled) GUI.PlayUISound(GUISoundType.PickItem);
PlaySound(ActionType.OnPicked, picker);
#endif
return true;
}
#if CLIENT
if (!GameMain.Instance.LoadingScreenOpen && picker == Character.Controlled) GUI.PlayUISound(GUISoundType.PickItemFail);
#endif
return false;
}
private IEnumerable<object> WaitForPick(Character picker, float requiredTime)
{
activePicker = picker;
picker.PickingItem = item;
var leftHand = picker.AnimController.GetLimb(LimbType.LeftHand);
var rightHand = picker.AnimController.GetLimb(LimbType.RightHand);
pickTimer = 0.0f;
while (pickTimer < requiredTime && Screen.Selected != GameMain.SubEditorScreen)
{
//cancel if the item is currently selected
//attempting to pick does not select the item, so if it is selected at this point, another ItemComponent
//must have been selected and we should not keep deattaching (happens when for example interacting with
//an electrical component while holding both a screwdriver and a wrench).
if (picker.SelectedConstruction == item ||
picker.IsKeyDown(InputType.Aim) ||
!picker.CanInteractWith(item) ||
item.Removed || item.ParentInventory != null)
{
StopPicking(picker);
yield return CoroutineStatus.Success;
}
#if CLIENT
Character.Controlled?.UpdateHUDProgressBar(
this,
item.WorldPosition,
pickTimer / requiredTime,
GUI.Style.Red, GUI.Style.Green);
#endif
picker.AnimController.UpdateUseItem(true, item.WorldPosition + new Vector2(0.0f, 100.0f) * ((pickTimer / 10.0f) % 0.1f));
pickTimer += CoroutineManager.DeltaTime;
yield return CoroutineStatus.Running;
}
StopPicking(picker);
bool isNotRemote = true;
#if CLIENT
isNotRemote = !picker.IsRemotePlayer;
#endif
if (isNotRemote) OnPicked(picker);
yield return CoroutineStatus.Success;
}
protected void StopPicking(Character picker)
{
if (picker != null)
{
picker.AnimController.Anim = AnimController.Animation.None;
picker.PickingItem = null;
}
if (pickingCoroutine != null)
{
CoroutineManager.StopCoroutines(pickingCoroutine);
pickingCoroutine = null;
}
activePicker = null;
pickTimer = 0.0f;
}
protected void DropConnectedWires(Character character)
{
Vector2 pos = character == null ? item.SimPosition : character.SimPosition;
foreach (ConnectionPanel connectionPanel in item.GetComponents<ConnectionPanel>())
{
foreach (Connection c in connectionPanel.Connections)
{
foreach (Wire w in c.Wires)
{
if (w == null) continue;
w.Item.Drop(character);
w.Item.SetTransform(pos, 0.0f);
}
}
}
}
public override void Drop(Character dropper)
{
if (picker == null)
{
picker = dropper;
}
Vector2 bodyDropPos = Vector2.Zero;
if (picker == null || picker.Inventory == null)
{
if (item.ParentInventory != null && item.ParentInventory.Owner != null && !item.ParentInventory.Owner.Removed)
{
bodyDropPos = item.ParentInventory.Owner.SimPosition;
if (item.body != null) item.body.ResetDynamics();
}
}
else if (!picker.Removed)
{
DropConnectedWires(picker);
item.Submarine = picker.Submarine;
bodyDropPos = picker.SimPosition;
picker.Inventory.RemoveItem(item);
picker = null;
}
if (item.body != null && !item.body.Enabled)
{
if (item.body.Removed)
{
DebugConsole.ThrowError(
"Failed to drop the Pickable component of the item \"" + item.Name + "\" (body has been removed"
+ (item.Removed ? ", item has been removed)" : ")"));
}
else
{
item.body.ResetDynamics();
item.SetTransform(bodyDropPos, 0.0f);
item.body.Enabled = true;
}
}
}
public virtual void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
{
msg.Write(activePicker == null ? (ushort)0 : activePicker.ID);
}
public virtual void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime)
{
ushort pickerID = msg.ReadUInt16();
if (pickerID == 0)
{
StopPicking(activePicker);
}
else
{
Pick(Entity.FindEntityByID(pickerID) as Character);
}
}
}
}
@@ -0,0 +1,109 @@
using System.Xml.Linq;
using Microsoft.Xna.Framework;
using Barotrauma.Networking;
#if CLIENT
using Microsoft.Xna.Framework.Graphics;
using Barotrauma.Particles;
#endif
namespace Barotrauma.Items.Components
{
class Propulsion : ItemComponent
{
public enum UseEnvironment
{
Air, Water, Both
};
private float useState;
[Serialize(UseEnvironment.Both, false, description: "Can the item be used in air, underwater or both.")]
public UseEnvironment UsableIn { get; set; }
[Serialize(0.0f, false, description: "The force to apply to the user's body."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)]
public float Force { get; set; }
#if CLIENT
private string particles;
[Serialize("", false, description: "The name of the particle prefab the item emits when used.")]
public string Particles
{
get { return particles; }
set { particles = value; }
}
#endif
public Propulsion(Item item, XElement element)
: base(item,element)
{
}
public override bool Use(float deltaTime, Character character = null)
{
if (character == null || character.Removed) return false;
if (!character.IsKeyDown(InputType.Aim) || character.Stun > 0.0f) return false;
IsActive = true;
useState = 0.1f;
if (character.AnimController.InWater)
{
if (UsableIn == UseEnvironment.Air) return true;
}
else
{
if (UsableIn == UseEnvironment.Water) return true;
}
Vector2 dir = Vector2.Normalize(character.CursorPosition - character.Position);
//move upwards if the cursor is at the position of the character
if (!MathUtils.IsValid(dir)) dir = Vector2.UnitY;
Vector2 propulsion = dir * Force;
if (character.AnimController.InWater) character.AnimController.TargetMovement = dir;
foreach (Limb limb in character.AnimController.Limbs)
{
if (limb.WearingItems.Find(w => w.WearableComponent.Item == this.item) == null) continue;
limb.body.ApplyForce(propulsion, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
}
character.AnimController.Collider.ApplyForce(propulsion, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
if (character.SelectedItems[0] == item)
{
character.AnimController.GetLimb(LimbType.RightHand)?.body.ApplyForce(propulsion, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
}
if (character.SelectedItems[1] == item)
{
character.AnimController.GetLimb(LimbType.LeftHand)?.body.ApplyForce(propulsion, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
}
#if CLIENT
if (!string.IsNullOrWhiteSpace(particles))
{
GameMain.ParticleManager.CreateParticle(particles, item.WorldPosition,
item.body.Rotation + ((item.body.Dir > 0.0f) ? 0.0f : MathHelper.Pi), 0.0f, item.CurrentHull);
}
#endif
return true;
}
public override void Update(float deltaTime, Camera cam)
{
useState -= deltaTime;
if (useState <= 0.0f)
{
IsActive = false;
}
if (item.AiTarget != null && IsActive)
{
item.AiTarget.SoundRange = item.AiTarget.MaxSoundRange;
}
}
}
}
@@ -0,0 +1,212 @@
using Barotrauma.Networking;
using FarseerPhysics;
using FarseerPhysics.Collision;
using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
partial class RangedWeapon : ItemComponent
{
private float reload, reloadTimer;
private Vector2 barrelPos;
[Serialize("0.0,0.0", false, description: "The position of the barrel as an offset from the item's center (in pixels). Determines where the projectiles spawn.")]
public string BarrelPos
{
get { return XMLExtensions.Vector2ToString(ConvertUnits.ToDisplayUnits(barrelPos)); }
set { barrelPos = ConvertUnits.ToSimUnits(XMLExtensions.ParseVector2(value)); }
}
[Serialize(1.0f, false, description: "How long the user has to wait before they can fire the weapon again (in seconds).")]
public float Reload
{
get { return reload; }
set { reload = Math.Max(value, 0.0f); }
}
[Serialize(1, false, description: "How projectiles the weapon launches when fired once.")]
public int ProjectileCount
{
get;
set;
}
[Serialize(0.0f, false, description: "Random spread applied to the firing angle of the projectiles when used by a character with sufficient skills to use the weapon (in degrees).")]
public float Spread
{
get;
set;
}
[Serialize(0.0f, false, description: "Random spread applied to the firing angle of the projectiles when used by a character with insufficient skills to use the weapon (in degrees).")]
public float UnskilledSpread
{
get;
set;
}
public Vector2 TransformedBarrelPos
{
get
{
Matrix bodyTransform = Matrix.CreateRotationZ(item.body.Rotation);
Vector2 flippedPos = barrelPos;
if (item.body.Dir < 0.0f) flippedPos.X = -flippedPos.X;
return Vector2.Transform(flippedPos, bodyTransform);
}
}
public RangedWeapon(Item item, XElement element)
: base(item, element)
{
item.IsShootable = true;
// TODO: should define this in xml if we have ranged weapons that don't require aim to use
item.RequireAimToUse = true;
InitProjSpecific(element);
}
partial void InitProjSpecific(XElement element);
public override void Update(float deltaTime, Camera cam)
{
reloadTimer -= deltaTime;
if (reloadTimer < 0.0f)
{
reloadTimer = 0.0f;
IsActive = false;
}
}
private float GetSpread(Character user)
{
float degreeOfFailure = 1.0f - DegreeOfSuccess(user);
degreeOfFailure *= degreeOfFailure;
return MathHelper.ToRadians(MathHelper.Lerp(Spread, UnskilledSpread, degreeOfFailure));
}
public override bool Use(float deltaTime, Character character = null)
{
if (character == null || character.Removed) { return false; }
if ((item.RequireAimToUse && !character.IsKeyDown(InputType.Aim)) || reloadTimer > 0.0f) { return false; }
IsActive = true;
reloadTimer = reload;
if (item.AiTarget != null)
{
item.AiTarget.SoundRange = item.AiTarget.MaxSoundRange;
item.AiTarget.SightRange = item.AiTarget.MaxSightRange;
}
List<Body> limbBodies = new List<Body>();
foreach (Limb l in character.AnimController.Limbs)
{
limbBodies.Add(l.body.FarseerBody);
}
float degreeOfFailure = 1.0f - DegreeOfSuccess(character);
degreeOfFailure *= degreeOfFailure;
if (degreeOfFailure > Rand.Range(0.0f, 1.0f))
{
ApplyStatusEffects(ActionType.OnFailure, 1.0f, character);
}
for (int i = 0; i < ProjectileCount; i++)
{
Projectile projectile = FindProjectile(triggerOnUseOnContainers: true);
if (projectile == null) { return true; }
float spread = GetSpread(character);
float rotation = (item.body.Dir == 1.0f) ? item.body.Rotation : item.body.Rotation - MathHelper.Pi;
rotation += spread * Rand.Range(-0.5f, 0.5f);
projectile.User = character;
//add the limbs of the shooter to the list of bodies to be ignored
//so that the player can't shoot himself
projectile.IgnoredBodies = new List<Body>(limbBodies);
Vector2 projectilePos = item.SimPosition;
Vector2 sourcePos = character?.AnimController == null ? item.SimPosition : character.AnimController.AimSourceSimPos;
Vector2 barrelPos = TransformedBarrelPos + item.body.SimPosition;
//make sure there's no obstacles between the base of the weapon (or the shoulder of the character) and the end of the barrel
if (Submarine.PickBody(sourcePos, barrelPos, projectile.IgnoredBodies, Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking) == null)
{
//no obstacles -> we can spawn the projectile at the barrel
projectilePos = barrelPos;
}
else if ((sourcePos - barrelPos).LengthSquared() > 0.0001f)
{
//spawn the projectile body.GetMaxExtent() away from the position where the raycast hit the obstacle
projectilePos = sourcePos - Vector2.Normalize(barrelPos - projectilePos) * Math.Max(projectile.Item.body.GetMaxExtent(), 0.1f);
}
projectile.Item.body.ResetDynamics();
projectile.Item.SetTransform(projectilePos, rotation);
projectile.Use(deltaTime);
if (projectile.Item.Removed) { continue; }
projectile.User = character;
projectile.Item.body.ApplyTorque(projectile.Item.body.Mass * degreeOfFailure * Rand.Range(-10.0f, 10.0f));
//set the rotation of the projectile again because dropping the projectile resets the rotation
projectile.Item.SetTransform(projectilePos,
rotation + (projectile.Item.body.Dir * projectile.LaunchRotationRadians));
item.RemoveContained(projectile.Item);
if (i == 0)
{
//recoil
item.body.ApplyLinearImpulse(
new Vector2((float)Math.Cos(projectile.Item.body.Rotation), (float)Math.Sin(projectile.Item.body.Rotation)) * item.body.Mass * -50.0f,
maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
}
}
LaunchProjSpecific();
return true;
}
private Projectile FindProjectile(bool triggerOnUseOnContainers = false)
{
var containedItems = item.ContainedItems;
if (containedItems == null) { return null; }
foreach (Item item in containedItems)
{
Projectile projectile = item.GetComponent<Projectile>();
if (projectile != null) { return projectile; }
}
//projectile not found, see if one of the contained items contains projectiles
foreach (Item item in containedItems)
{
var containedSubItems = item.ContainedItems;
if (containedSubItems == null) { continue; }
foreach (Item subItem in containedSubItems)
{
Projectile projectile = subItem.GetComponent<Projectile>();
//apply OnUse statuseffects to the container in case it has to react to it somehow
//(play a sound, spawn more projectiles, reduce condition...)
if (triggerOnUseOnContainers && subItem.Condition > 0.0f)
{
subItem.GetComponent<ItemContainer>()?.Item.ApplyStatusEffects(ActionType.OnUse, 1.0f);
}
if (projectile != null) { return projectile; }
}
}
return null;
}
partial void LaunchProjSpecific();
}
}
@@ -0,0 +1,670 @@
using FarseerPhysics;
using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Extensions;
namespace Barotrauma.Items.Components
{
partial class RepairTool : ItemComponent
{
public enum UseEnvironment
{
Air, Water, Both, None
};
private readonly List<string> fixableEntities;
private Vector2 pickedPosition;
private float activeTimer;
private Vector2 debugRayStartPos, debugRayEndPos;
[Serialize("Both", false, description: "Can the item be used in air, water or both.")]
public UseEnvironment UsableIn
{
get; set;
}
[Serialize(0.0f, false, description: "The distance at which the item can repair targets.")]
public float Range { get; set; }
[Serialize(0.0f, false, description: "Random spread applied to the firing angle when used by a character with sufficient skills to use the tool (in degrees).")]
public float Spread
{
get;
set;
}
[Serialize(0.0f, false, description: "Random spread applied to the firing angle when used by a character with insufficient skills to use the tool (in degrees).")]
public float UnskilledSpread
{
get;
set;
}
[Serialize(0.0f, false, description: "How many units of damage the item removes from structures per second.")]
public float StructureFixAmount
{
get; set;
}
[Serialize(0.0f, false, description: "How much the item decreases the size of fires per second.")]
public float ExtinguishAmount
{
get; set;
}
[Serialize("0.0,0.0", false, description: "The position of the barrel as an offset from the item's center (in pixels).")]
public Vector2 BarrelPos { get; set; }
[Serialize(false, false, description: "Can the item repair things through walls.")]
public bool RepairThroughWalls { get; set; }
[Serialize(false, false, description: "Can the item repair multiple things at once, or will it only affect the first thing the ray from the barrel hits.")]
public bool RepairMultiple { get; set; }
[Serialize(false, false, description: "Can the item repair things through holes in walls.")]
public bool RepairThroughHoles { get; set; }
[Serialize(0.0f, false, description: "The probability of starting a fire somewhere along the ray fired from the barrel (for example, 0.1 = 10% chance to start a fire during a second of use).")]
public float FireProbability { get; set; }
[Serialize(0.0f, false, description: "Force applied to the entity the ray hits.")]
public float TargetForce { get; set; }
public Vector2 TransformedBarrelPos
{
get
{
Matrix bodyTransform = Matrix.CreateRotationZ(item.body.Rotation);
Vector2 flippedPos = BarrelPos;
if (item.body.Dir < 0.0f) flippedPos.X = -flippedPos.X;
return (Vector2.Transform(flippedPos, bodyTransform));
}
}
public RepairTool(Item item, XElement element)
: base(item, element)
{
this.item = item;
if (element.Attribute("limbfixamount") != null)
{
DebugConsole.ThrowError("Error in item \"" + item.Name + "\" - RepairTool damage should be configured using a StatusEffect with Afflictions, not the limbfixamount attribute.");
}
fixableEntities = new List<string>();
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "fixable":
if (subElement.Attribute("name") != null)
{
DebugConsole.ThrowError("Error in RepairTool " + item.Name + " - use identifiers instead of names to configure fixable entities.");
fixableEntities.Add(subElement.Attribute("name").Value);
}
else
{
fixableEntities.Add(subElement.GetAttributeString("identifier", ""));
}
break;
}
}
item.IsShootable = true;
// TODO: should define this in xml if we have repair tools that don't require aim to use
item.RequireAimToUse = true;
InitProjSpecific(element);
}
partial void InitProjSpecific(XElement element);
public override void Update(float deltaTime, Camera cam)
{
activeTimer -= deltaTime;
if (activeTimer <= 0.0f) IsActive = false;
}
private List<Body> ignoredBodies = new List<Body>();
public override bool Use(float deltaTime, Character character = null)
{
if (character == null || character.Removed) return false;
if (item.RequireAimToUse && !character.IsKeyDown(InputType.Aim)) return false;
float degreeOfSuccess = DegreeOfSuccess(character);
if (Rand.Range(0.0f, 0.5f) > degreeOfSuccess)
{
ApplyStatusEffects(ActionType.OnFailure, deltaTime, character);
return false;
}
if (UsableIn == UseEnvironment.None)
{
ApplyStatusEffects(ActionType.OnFailure, deltaTime, character);
return false;
}
if (item.InWater)
{
if (UsableIn == UseEnvironment.Air)
{
ApplyStatusEffects(ActionType.OnFailure, deltaTime, character);
return false;
}
}
else
{
if (UsableIn == UseEnvironment.Water)
{
ApplyStatusEffects(ActionType.OnFailure, deltaTime, character);
return false;
}
}
Vector2 rayStart;
Vector2 sourcePos = character?.AnimController == null ? item.SimPosition : character.AnimController.AimSourceSimPos;
Vector2 barrelPos = item.SimPosition + ConvertUnits.ToSimUnits(TransformedBarrelPos);
//make sure there's no obstacles between the base of the item (or the shoulder of the character) and the end of the barrel
if (Submarine.PickBody(sourcePos, barrelPos, collisionCategory: Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking) == null)
{
//no obstacles -> we start the raycast at the end of the barrel
rayStart = ConvertUnits.ToSimUnits(item.WorldPosition + TransformedBarrelPos);
}
else
{
rayStart = Submarine.LastPickedPosition + Submarine.LastPickedNormal * 0.1f;
if (item.Submarine != null) { rayStart += item.Submarine.SimPosition; }
}
float spread = MathHelper.ToRadians(MathHelper.Lerp(UnskilledSpread, Spread, degreeOfSuccess));
float angle = item.body.Rotation + spread * Rand.Range(-0.5f, 0.5f);
Vector2 rayEnd = rayStart +
ConvertUnits.ToSimUnits(new Vector2(
(float)Math.Cos(angle),
(float)Math.Sin(angle)) * Range * item.body.Dir);
ignoredBodies.Clear();
foreach (Limb limb in character.AnimController.Limbs)
{
if (Rand.Range(0.0f, 0.5f) > degreeOfSuccess) continue;
ignoredBodies.Add(limb.body.FarseerBody);
}
ignoredBodies.Add(character.AnimController.Collider.FarseerBody);
IsActive = true;
activeTimer = 0.1f;
debugRayStartPos = ConvertUnits.ToDisplayUnits(rayStart);
debugRayEndPos = ConvertUnits.ToDisplayUnits(rayEnd);
if (character.Submarine == null)
{
foreach (Submarine sub in Submarine.Loaded)
{
Rectangle subBorders = sub.Borders;
subBorders.Location += new Point((int)sub.WorldPosition.X, (int)sub.WorldPosition.Y - sub.Borders.Height);
if (!MathUtils.CircleIntersectsRectangle(item.WorldPosition, Range * 5.0f, subBorders))
{
continue;
}
Repair(rayStart - sub.SimPosition, rayEnd - sub.SimPosition, deltaTime, character, degreeOfSuccess, ignoredBodies);
}
Repair(rayStart, rayEnd, deltaTime, character, degreeOfSuccess, ignoredBodies);
}
else
{
Repair(rayStart - character.Submarine.SimPosition, rayEnd - character.Submarine.SimPosition, deltaTime, character, degreeOfSuccess, ignoredBodies);
}
UseProjSpecific(deltaTime, rayStart);
return true;
}
partial void UseProjSpecific(float deltaTime, Vector2 raystart);
private readonly HashSet<Character> hitCharacters = new HashSet<Character>();
private readonly List<FireSource> fireSourcesInRange = new List<FireSource>();
private void Repair(Vector2 rayStart, Vector2 rayEnd, float deltaTime, Character user, float degreeOfSuccess, List<Body> ignoredBodies)
{
var collisionCategories = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel | Physics.CollisionRepair;
//if the item can cut off limbs, activate nearby bodies to allow the raycast to hit them
if (statusEffectLists != null && statusEffectLists.ContainsKey(ActionType.OnUse))
{
if (statusEffectLists[ActionType.OnUse].Any(s => s.SeverLimbsProbability > 0.0f))
{
float rangeSqr = ConvertUnits.ToSimUnits(Range);
rangeSqr *= rangeSqr;
foreach (Character c in Character.CharacterList)
{
if (!c.Enabled || !c.AnimController.BodyInRest) { continue; }
//do a broad check first
if (Math.Abs(c.WorldPosition.X - item.WorldPosition.X) > 1000.0f) { continue; }
if (Math.Abs(c.WorldPosition.Y - item.WorldPosition.Y) > 1000.0f) { continue; }
foreach (Limb limb in c.AnimController.Limbs)
{
if (Vector2.DistanceSquared(limb.SimPosition, item.SimPosition) < rangeSqr && Vector2.Dot(rayEnd - rayStart, limb.SimPosition - rayStart) > 0)
{
c.AnimController.BodyInRest = false;
break;
}
}
}
}
}
float lastPickedFraction = 0.0f;
if (RepairMultiple)
{
var bodies = Submarine.PickBodies(rayStart, rayEnd, ignoredBodies, collisionCategories,
ignoreSensors: false,
customPredicate: (Fixture f) =>
{
if (RepairThroughHoles && f.IsSensor && f.Body?.UserData is Structure) { return false; }
if (f.Body?.UserData as string == "ruinroom") { return false; }
return true;
},
allowInsideFixture: true);
lastPickedFraction = Submarine.LastPickedFraction;
Type lastHitType = null;
hitCharacters.Clear();
foreach (Body body in bodies)
{
Type bodyType = body.UserData?.GetType();
if (!RepairThroughWalls && bodyType != null && bodyType != lastHitType)
{
//stop the ray if it already hit a door/wall and is now about to hit some other type of entity
if (lastHitType == typeof(Item) || lastHitType == typeof(Structure)) { break; }
}
Character hitCharacter = null;
if (body.UserData is Limb limb)
{
hitCharacter = limb.character;
}
else if (body.UserData is Character character)
{
hitCharacter = character;
}
//only do damage once to each character even if they ray hit multiple limbs
if (hitCharacter != null)
{
if (hitCharacters.Contains(hitCharacter)) { continue; }
hitCharacters.Add(hitCharacter);
}
if (FixBody(user, deltaTime, degreeOfSuccess, body))
{
lastPickedFraction = Submarine.LastPickedBodyDist(body);
if (bodyType != null) { lastHitType = bodyType; }
}
}
}
else
{
FixBody(user, deltaTime, degreeOfSuccess,
Submarine.PickBody(rayStart, rayEnd,
ignoredBodies, collisionCategories,
ignoreSensors: false,
customPredicate: (Fixture f) =>
{
if (RepairThroughHoles && f.IsSensor && f.Body?.UserData is Structure) { return false; }
if (f.Body?.UserData as string == "ruinroom") { return false; }
return f.Body?.UserData != null;
},
allowInsideFixture: true));
lastPickedFraction = Submarine.LastPickedFraction;
}
if (ExtinguishAmount > 0.0f && item.CurrentHull != null)
{
fireSourcesInRange.Clear();
//step along the ray in 10% intervals, collecting all fire sources in the range
for (float x = 0.0f; x <= lastPickedFraction; x += 0.1f)
{
Vector2 displayPos = ConvertUnits.ToDisplayUnits(rayStart + (rayEnd - rayStart) * x);
if (item.CurrentHull.Submarine != null) { displayPos += item.CurrentHull.Submarine.Position; }
Hull hull = Hull.FindHull(displayPos, item.CurrentHull);
if (hull == null) continue;
foreach (FireSource fs in hull.FireSources)
{
if (fs.IsInDamageRange(displayPos, 100.0f) && !fireSourcesInRange.Contains(fs))
{
fireSourcesInRange.Add(fs);
}
}
}
foreach (FireSource fs in fireSourcesInRange)
{
fs.Extinguish(deltaTime, ExtinguishAmount);
#if SERVER
GameMain.Server.KarmaManager.OnExtinguishingFire(user, deltaTime);
#endif
}
}
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
{
if (Rand.Range(0.0f, 1.0f) < FireProbability * deltaTime)
{
Vector2 displayPos = ConvertUnits.ToDisplayUnits(rayStart + (rayEnd - rayStart) * lastPickedFraction * 0.9f);
if (item.CurrentHull.Submarine != null) { displayPos += item.CurrentHull.Submarine.Position; }
new FireSource(displayPos);
}
}
}
private bool FixBody(Character user, float deltaTime, float degreeOfSuccess, Body targetBody)
{
if (targetBody?.UserData == null) { return false; }
pickedPosition = Submarine.LastPickedPosition;
if (targetBody.UserData is Structure targetStructure)
{
if (targetStructure.IsPlatform) { return false; }
int sectionIndex = targetStructure.FindSectionIndex(ConvertUnits.ToDisplayUnits(pickedPosition));
if (sectionIndex < 0) { return false; }
if (!fixableEntities.Contains("structure") && !fixableEntities.Contains(targetStructure.Prefab.Identifier)) { return true; }
ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, new ISerializableEntity[] { targetStructure });
FixStructureProjSpecific(user, deltaTime, targetStructure, sectionIndex);
targetStructure.AddDamage(sectionIndex, -StructureFixAmount * degreeOfSuccess, user);
//if the next section is small enough, apply the effect to it as well
//(to make it easier to fix a small "left-over" section)
for (int i = -1; i < 2; i += 2)
{
int nextSectionLength = targetStructure.SectionLength(sectionIndex + i);
if ((sectionIndex == 1 && i == -1) ||
(sectionIndex == targetStructure.SectionCount - 2 && i == 1) ||
(nextSectionLength > 0 && nextSectionLength < Structure.WallSectionSize * 0.3f))
{
//targetStructure.HighLightSection(sectionIndex + i);
targetStructure.AddDamage(sectionIndex + i, -StructureFixAmount * degreeOfSuccess);
}
}
return true;
}
else if (targetBody.UserData is Character targetCharacter)
{
if (targetCharacter.Removed) { return false; }
targetCharacter.LastDamageSource = item;
Limb closestLimb = null;
float closestDist = float.MaxValue;
foreach (Limb limb in targetCharacter.AnimController.Limbs)
{
float dist = Vector2.DistanceSquared(item.SimPosition, limb.SimPosition);
if (dist < closestDist)
{
closestLimb = limb;
closestDist = dist;
}
}
if (closestLimb != null && !MathUtils.NearlyEqual(TargetForce, 0.0f))
{
Vector2 dir = closestLimb.WorldPosition - item.WorldPosition;
dir = dir.LengthSquared() < 0.0001f ? Vector2.UnitY : Vector2.Normalize(dir);
closestLimb.body.ApplyForce(dir * TargetForce, maxVelocity: 10.0f);
}
ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse,
closestLimb == null ? new ISerializableEntity[] { targetCharacter } : new ISerializableEntity[] { targetCharacter, closestLimb });
FixCharacterProjSpecific(user, deltaTime, targetCharacter);
return true;
}
else if (targetBody.UserData is Limb targetLimb)
{
if (targetLimb.character == null || targetLimb.character.Removed) { return false; }
if (!MathUtils.NearlyEqual(TargetForce, 0.0f))
{
Vector2 dir = targetLimb.WorldPosition - item.WorldPosition;
dir = dir.LengthSquared() < 0.0001f ? Vector2.UnitY : Vector2.Normalize(dir);
targetLimb.body.ApplyForce(dir * TargetForce, maxVelocity: 10.0f);
}
targetLimb.character.LastDamageSource = item;
ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, new ISerializableEntity[] { targetLimb.character, targetLimb });
FixCharacterProjSpecific(user, deltaTime, targetLimb.character);
return true;
}
else if (targetBody.UserData is Item targetItem)
{
targetItem.IsHighlighted = true;
ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, targetItem.AllPropertyObjects);
if (targetItem.body != null && !MathUtils.NearlyEqual(TargetForce, 0.0f))
{
Vector2 dir = targetItem.WorldPosition - item.WorldPosition;
dir = dir.LengthSquared() < 0.0001f ? Vector2.UnitY : Vector2.Normalize(dir);
targetItem.body.ApplyForce(dir * TargetForce, maxVelocity: 10.0f);
}
var levelResource = targetItem.GetComponent<LevelResource>();
if (levelResource != null && levelResource.Attached &&
levelResource.requiredItems.Any() &&
levelResource.HasRequiredItems(user, addMessage: false))
{
levelResource.DeattachTimer += deltaTime;
#if CLIENT
Character.Controlled?.UpdateHUDProgressBar(
this,
targetItem.WorldPosition,
levelResource.DeattachTimer / levelResource.DeattachDuration,
GUI.Style.Red, GUI.Style.Green);
#endif
}
FixItemProjSpecific(user, deltaTime, targetItem);
return true;
}
return false;
}
partial void FixStructureProjSpecific(Character user, float deltaTime, Structure targetStructure, int sectionIndex);
partial void FixCharacterProjSpecific(Character user, float deltaTime, Character targetCharacter);
partial void FixItemProjSpecific(Character user, float deltaTime, Item targetItem);
private float sinTime;
public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
{
if (!(objective.OperateTarget is Gap leak)) { return true; }
if (leak.Submarine == null) { return true; }
Vector2 fromCharacterToLeak = leak.WorldPosition - character.WorldPosition;
float dist = fromCharacterToLeak.Length();
float reach = Range + ConvertUnits.ToDisplayUnits(((HumanoidAnimController)character.AnimController).ArmLength);
//too far away -> consider this done and hope the AI is smart enough to move closer
if (dist > reach * 2) { return true; }
character.AIController.SteeringManager.Reset();
//steer closer if almost in range
if (dist > reach)
{
if (character.AnimController.InWater)
{
if (character.AIController.SteeringManager is IndoorsSteeringManager indoorSteering)
{
// Swimming inside the sub
if (indoorSteering.CurrentPath != null && !indoorSteering.IsPathDirty && indoorSteering.CurrentPath.Unreachable)
{
Vector2 dir = Vector2.Normalize(fromCharacterToLeak);
character.AIController.SteeringManager.SteeringManual(deltaTime, dir);
}
else
{
character.AIController.SteeringManager.SteeringSeek(character.GetRelativeSimPosition(leak));
}
}
else
{
// Swimming outside the sub
character.AIController.SteeringManager.SteeringSeek(character.GetRelativeSimPosition(leak));
}
}
else
{
// TODO: use the collider size?
if (!character.AnimController.InWater && character.AnimController is HumanoidAnimController &&
Math.Abs(fromCharacterToLeak.X) < 100.0f && fromCharacterToLeak.Y < 0.0f && fromCharacterToLeak.Y > -150.0f)
{
((HumanoidAnimController)character.AnimController).Crouching = true;
}
Vector2 standPos = new Vector2(Math.Sign(-fromCharacterToLeak.X), Math.Sign(-fromCharacterToLeak.Y)) / 2;
if (leak.IsHorizontal)
{
standPos.X *= 2;
standPos.Y = 0;
}
else
{
standPos.X = 0;
}
character.AIController.SteeringManager.SteeringSeek(standPos);
}
}
else
{
if (dist < reach / 2)
{
// Too close -> steer away
character.AIController.SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(character.SimPosition - leak.SimPosition));
}
else if (dist <= reach)
{
// In range
character.CursorPosition = leak.Position;
character.CursorPosition += VectorExtensions.Forward(Item.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, dist / 2);
if (character.AnimController.InWater)
{
var torso = character.AnimController.GetLimb(LimbType.Torso);
// Turn facing the target when not moving (handled in the animcontroller if not moving)
Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition);
Vector2 diff = (mousePos - torso.SimPosition) * character.AnimController.Dir;
float newRotation = MathUtils.VectorToAngle(diff);
character.AnimController.Collider.SmoothRotate(newRotation, 5.0f);
if (VectorExtensions.Angle(VectorExtensions.Forward(torso.body.TransformedRotation), fromCharacterToLeak) < MathHelper.PiOver4)
{
// Swim past
Vector2 moveDir = leak.IsHorizontal ? Vector2.UnitY : Vector2.UnitX;
moveDir *= character.AnimController.Dir;
character.AIController.SteeringManager.SteeringManual(deltaTime, moveDir);
}
}
}
}
if (item.RequireAimToUse)
{
bool isOperatingButtons = false;
if (character.AIController.SteeringManager is IndoorsSteeringManager indoorSteering)
{
var door = indoorSteering.CurrentPath?.CurrentNode?.ConnectedDoor;
if (door != null && !door.IsOpen)
{
isOperatingButtons = door.HasIntegratedButtons || door.Item.GetConnectedComponents<Controller>(true).Any();
}
}
if (!isOperatingButtons)
{
character.SetInput(InputType.Aim, false, true);
}
sinTime += deltaTime * 5;
}
// Press the trigger only when the tool is approximately facing the target.
Vector2 fromItemToLeak = leak.WorldPosition - item.WorldPosition;
var angle = VectorExtensions.Angle(VectorExtensions.Forward(item.body.TransformedRotation), fromItemToLeak);
if (angle < MathHelper.PiOver4)
{
// Check that we don't hit any friendlies
if (Submarine.PickBodies(item.SimPosition, leak.SimPosition, collisionCategory: Physics.CollisionCharacter).None(hit =>
{
if (hit.UserData is Character c)
{
if (c == character) { return false; }
return HumanAIController.IsFriendly(character, c);
}
return false;
}))
{
character.SetInput(InputType.Shoot, false, true);
Use(deltaTime, character);
}
}
bool leakFixed = (leak.Open <= 0.0f || leak.Removed) &&
(leak.ConnectedWall == null || leak.ConnectedWall.Sections.Average(s => s.damage) < 1);
if (leakFixed && leak.FlowTargetHull != null)
{
if (!leak.FlowTargetHull.ConnectedGaps.Any(g => !g.IsRoomToRoom && g.Open > 0.0f))
{
character.Speak(TextManager.GetWithVariable("DialogLeaksFixed", "[roomname]", leak.FlowTargetHull.DisplayName, true), null, 0.0f, "leaksfixed", 10.0f);
}
else
{
character.Speak(TextManager.GetWithVariable("DialogLeakFixed", "[roomname]", leak.FlowTargetHull.DisplayName, true), null, 0.0f, "leakfixed", 10.0f);
}
}
return leakFixed;
}
private void ApplyStatusEffectsOnTarget(Character user, float deltaTime, ActionType actionType, IEnumerable<ISerializableEntity> targets)
{
if (statusEffectLists == null) { return; }
if (!statusEffectLists.TryGetValue(actionType, out List<StatusEffect> statusEffects)) { return; }
foreach (StatusEffect effect in statusEffects)
{
effect.SetUser(user);
if (effect.HasTargetType(StatusEffect.TargetType.UseTarget))
{
effect.Apply(actionType, deltaTime, item, targets);
}
else if (effect.HasTargetType(StatusEffect.TargetType.Character))
{
effect.Apply(actionType, deltaTime, item, targets.Where(t => t is Character));
}
else if (effect.HasTargetType(StatusEffect.TargetType.Limb))
{
effect.Apply(actionType, deltaTime, item, targets.Where(t => t is Limb));
}
#if CLIENT
// Hard-coded progress bars for welding doors stuck.
// A general purpose system could be better, but it would most likely require changes in the way we define the status effects in xml.
foreach (ISerializableEntity target in targets)
{
if (target is Door door)
{
if (!door.CanBeWelded) continue;
for (int i = 0; i < effect.propertyNames.Length; i++)
{
string propertyName = effect.propertyNames[i];
if (propertyName != "stuck") { continue; }
if (door.SerializableProperties == null || !door.SerializableProperties.TryGetValue(propertyName, out SerializableProperty property)) { continue; }
object value = property.GetValue(target);
if (door.Stuck > 0)
{
var progressBar = user.UpdateHUDProgressBar(door, door.Item.WorldPosition, door.Stuck / 100, Color.DarkGray * 0.5f, Color.White);
if (progressBar != null) { progressBar.Size = new Vector2(60.0f, 20.0f); }
}
}
}
}
#endif
}
}
}
}
@@ -0,0 +1,150 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
class Throwable : Holdable
{
private float throwForce, throwPos;
private bool throwing, throwDone;
private bool midAir;
[Serialize(1.0f, false, description: "The impulse applied to the physics body of the item when thrown. Higher values make the item be thrown faster.")]
public float ThrowForce
{
get { return throwForce; }
set { throwForce = value; }
}
public Throwable(Item item, XElement element)
: base(item, element)
{
//throwForce = ToolBox.GetAttributeFloat(element, "throwforce", 1.0f);
if (aimPos == Vector2.Zero)
{
aimPos = new Vector2(0.6f, 0.1f);
}
}
public override bool Use(float deltaTime, Character character = null)
{
return characterUsable || character == null; //We do the actual throwing in Aim because Use might be used by chems
}
public override bool SecondaryUse(float deltaTime, Character character = null)
{
if (!throwDone) return false; //This should only be triggered in update
throwDone = false;
return true;
}
public override void Drop(Character dropper)
{
base.Drop(dropper);
throwing = false;
throwPos = 0.0f;
}
public override void UpdateBroken(float deltaTime, Camera cam)
{
Update(deltaTime, cam);
}
public override void Update(float deltaTime, Camera cam)
{
if (!item.body.Enabled) { return; }
if (midAir)
{
if (item.body.LinearVelocity.LengthSquared() < 0.01f)
{
item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform;
midAir = false;
}
return;
}
if (picker == null || picker.Removed || !picker.HasSelectedItem(item))
{
IsActive = false;
return;
}
if (picker.IsKeyDown(InputType.Aim) && picker.IsKeyHit(InputType.Shoot)) { throwing = true; }
if (!picker.IsKeyDown(InputType.Aim) && !throwing) { throwPos = 0.0f; }
bool aim = picker.IsKeyDown(InputType.Aim) && (picker.SelectedConstruction == null || picker.SelectedConstruction.GetComponent<Ladder>() != null);
if (picker.IsUnconscious || picker.IsDead || !picker.AllowInput)
{
throwing = false;
aim = false;
}
ApplyStatusEffects(ActionType.OnActive, deltaTime, picker);
if (item.body.Dir != picker.AnimController.Dir) { Flip(); }
AnimController ac = picker.AnimController;
item.Submarine = picker.Submarine;
if (!throwing)
{
if (aim)
{
throwPos = MathUtils.WrapAnglePi(System.Math.Min(throwPos + deltaTime * 5.0f, MathHelper.PiOver2));
ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, false, throwPos);
}
else
{
throwPos = 0;
ac.HoldItem(deltaTime, item, handlePos, holdPos, Vector2.Zero, false, holdAngle);
}
}
else
{
throwPos = MathUtils.WrapAnglePi(throwPos - deltaTime * 15.0f);
ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, false, throwPos);
if (throwPos < 0)
{
Vector2 throwVector = Vector2.Normalize(picker.CursorWorldPosition - picker.WorldPosition);
//throw upwards if cursor is at the position of the character
if (!MathUtils.IsValid(throwVector)) { throwVector = Vector2.UnitY; }
#if SERVER
GameServer.Log(picker.LogName + " threw " + item.Name, ServerLog.MessageType.ItemInteraction);
#endif
Character thrower = picker;
item.Drop(thrower, createNetworkEvent: GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer);
item.body.ApplyLinearImpulse(throwVector * throwForce * item.body.Mass * 3.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
//disable platform collisions until the item comes back to rest again
item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel;
midAir = true;
ac.GetLimb(LimbType.Head).body.ApplyLinearImpulse(throwVector * 10.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
ac.GetLimb(LimbType.Torso).body.ApplyLinearImpulse(throwVector * 10.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
Limb rightHand = ac.GetLimb(LimbType.RightHand);
item.body.AngularVelocity = rightHand.body.AngularVelocity;
throwPos = 0;
throwDone = true;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnSecondaryUse, this, thrower.ID });
}
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
{
//Stun grenades, flares, etc. all have their throw-related things handled in "onSecondaryUse"
ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, thrower, user: thrower);
}
throwing = false;
}
}
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,383 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Extensions;
namespace Barotrauma.Items.Components
{
partial class ItemContainer : ItemComponent, IDrawableComponent
{
public ItemInventory Inventory;
private List<Pair<Item, StatusEffect>> itemsWithStatusEffects;
private ushort[] itemIds;
//how many items can be contained
private int capacity;
[Serialize(5, false, description: "How many items can be contained inside this item.")]
public int Capacity
{
get { return capacity; }
set { capacity = Math.Max(value, 1); }
}
private bool hideItems;
[Serialize(true, false, description: "Should the items contained inside this item be hidden."
+ " If set to false, you should use the ItemPos and ItemInterval properties to determine where the items get rendered.")]
public bool HideItems
{
get { return hideItems; }
set
{
hideItems = value;
Drawable = !hideItems;
}
}
[Serialize(true, false, description: "Should the inventory of this item be visible when the item is selected.")]
public bool DrawInventory
{
get;
set;
}
[Serialize(false, false, description: "If set to true, interacting with this item will make the character interact with the contained item(s), automatically picking them up if they can be picked up.")]
public bool AutoInteractWithContained
{
get;
set;
}
[Serialize(5, false, description: "How many inventory slots the inventory has per row.")]
public int SlotsPerRow { get; set; }
private readonly HashSet<string> containableRestrictions = new HashSet<string>();
[Editable, Serialize("", true, description: "Define items (by identifiers or tags) that bots should place inside this container. If empty, no restrictions are applied.")]
public string ContainableRestrictions
{
get { return string.Join(",", containableRestrictions); }
set
{
StringFormatter.ParseCommaSeparatedStringToCollection(value, containableRestrictions);
}
}
[Editable, Serialize(true, true, description: "Should this container be automatically filled with items?")]
public bool AutoFill { get; set; }
private float itemRotation;
[Serialize(0.0f, false, description: "The rotation in which the contained sprites are drawn (in degrees).")]
public float ItemRotation
{
get { return MathHelper.ToDegrees(itemRotation); }
set { itemRotation = MathHelper.ToRadians(value); }
}
public bool ShouldBeContained(string[] identifiersOrTags, out bool isRestrictionsDefined)
{
isRestrictionsDefined = containableRestrictions.Any();
if (ContainableItems.None(ri => ri.MatchesItem(item))) { return false; }
if (!isRestrictionsDefined) { return true; }
return identifiersOrTags.Any(id => containableRestrictions.Any(r => r == id));
}
public bool ShouldBeContained(Item item, out bool isRestrictionsDefined)
{
isRestrictionsDefined = containableRestrictions.Any();
if (ContainableItems.None(ri => ri.MatchesItem(item))) { return false; }
if (!isRestrictionsDefined) { return true; }
return containableRestrictions.Any(id => item.Prefab.Identifier == id || item.HasTag(id));
}
public List<RelatedItem> ContainableItems { get; private set; } = new List<RelatedItem>();
public IEnumerable<string> GetContainableItemIdentifiers => ContainableItems.SelectMany(ri => ri.Identifiers);
public ItemContainer(Item item, XElement element)
: base (item, element)
{
Inventory = new ItemInventory(item, this, capacity, SlotsPerRow);
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "containable":
RelatedItem containable = RelatedItem.Load(subElement, returnEmpty: false, parentDebugName: item.Name);
if (containable == null)
{
DebugConsole.ThrowError("Error in item config \"" + item.ConfigFile + "\" - containable with no identifiers.");
continue;
}
ContainableItems.Add(containable);
break;
}
}
InitProjSpecific(element);
itemsWithStatusEffects = new List<Pair<Item, StatusEffect>>();
}
partial void InitProjSpecific(XElement element);
public void OnItemContained(Item containedItem)
{
item.SetContainedItemPositions();
RelatedItem ri = ContainableItems.Find(x => x.MatchesItem(containedItem));
if (ri != null)
{
itemsWithStatusEffects.RemoveAll(i => i.First == containedItem);
foreach (StatusEffect effect in ri.statusEffects)
{
itemsWithStatusEffects.Add(new Pair<Item, StatusEffect>(containedItem, effect));
}
}
//no need to Update() if this item has no statuseffects and no physics body
IsActive = itemsWithStatusEffects.Count > 0 || containedItem.body != null;
}
public void OnItemRemoved(Item containedItem)
{
itemsWithStatusEffects.RemoveAll(i => i.First == containedItem);
//deactivate if the inventory is empty
IsActive = itemsWithStatusEffects.Count > 0 || containedItem.body != null;
}
public bool CanBeContained(Item item)
{
if (ContainableItems.Count == 0) { return true; }
return (ContainableItems.Find(c => c.MatchesItem(item)) != null);
}
public bool CanBeContained(ItemPrefab itemPrefab)
{
if (ContainableItems.Count == 0) { return true; }
return (ContainableItems.Find(c => c.MatchesItem(itemPrefab)) != null);
}
public override void Update(float deltaTime, Camera cam)
{
if (item.body != null &&
item.body.Enabled &&
item.body.FarseerBody.Awake)
{
item.SetContainedItemPositions();
}
else if (itemsWithStatusEffects.Count == 0)
{
IsActive = false;
return;
}
foreach (Pair<Item, StatusEffect> itemAndEffect in itemsWithStatusEffects)
{
Item contained = itemAndEffect.First;
if (contained.Condition <= 0.0f) continue;
StatusEffect effect = itemAndEffect.Second;
if (effect.HasTargetType(StatusEffect.TargetType.This))
effect.Apply(ActionType.OnContaining, deltaTime, item, item.AllPropertyObjects);
if (effect.HasTargetType(StatusEffect.TargetType.Contained))
effect.Apply(ActionType.OnContaining, deltaTime, item, contained.AllPropertyObjects);
if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
var targets = new List<ISerializableEntity>();
effect.GetNearbyTargets(item.WorldPosition, targets);
effect.Apply(ActionType.OnActive, deltaTime, item, targets);
}
}
}
public override bool Select(Character character)
{
if (item.Container != null) { return false; }
if (AutoInteractWithContained && character.SelectedConstruction == null)
{
foreach (Item contained in Inventory.Items)
{
if (contained == null) continue;
if (contained.TryInteract(character))
{
character.FocusedItem = contained;
return false;
}
}
}
return base.Select(character);
}
public override bool Pick(Character picker)
{
if (AutoInteractWithContained)
{
foreach (Item contained in Inventory.Items)
{
if (contained == null) continue;
if (contained.TryInteract(picker))
{
picker.FocusedItem = contained;
return true;
}
}
}
IsActive = true;
return (picker != null);
}
public override bool Combine(Item item, Character user)
{
if (!ContainableItems.Any(x => x.MatchesItem(item))) { return false; }
if (user != null && !user.CanAccessInventory(Inventory)) { return false; }
if (Inventory.TryPutItem(item, null))
{
IsActive = true;
if (hideItems && item.body != null) item.body.Enabled = false;
return true;
}
return false;
}
public override void Drop(Character dropper)
{
IsActive = true;
}
public override void Equip(Character character)
{
IsActive = true;
}
public void SetContainedItemPositions()
{
Vector2 simPos = item.SimPosition;
Vector2 displayPos = item.Position;
float currentRotation = itemRotation;
if (item.body != null)
{
currentRotation += item.body.Rotation;
}
foreach (Item contained in Inventory.Items)
{
if (contained == null) continue;
if (contained.body != null)
{
try
{
contained.body.FarseerBody.SetTransformIgnoreContacts(ref simPos, currentRotation);
contained.body.SetPrevTransform(contained.body.SimPosition, contained.body.Rotation);
contained.body.UpdateDrawPosition();
}
catch (Exception e)
{
DebugConsole.Log("SetTransformIgnoreContacts threw an exception in SetContainedItemPositions (" + e.Message + ")\n" + e.StackTrace);
GameAnalyticsManager.AddErrorEventOnce("ItemContainer.SetContainedItemPositions.InvalidPosition:" + contained.Name,
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"SetTransformIgnoreContacts threw an exception in SetContainedItemPositions (" + e.Message + ")\n" + e.StackTrace);
}
}
contained.Rect =
new Rectangle(
(int)(displayPos.X - contained.Rect.Width / 2.0f),
(int)(displayPos.Y + contained.Rect.Height / 2.0f),
contained.Rect.Width, contained.Rect.Height);
contained.Submarine = item.Submarine;
contained.CurrentHull = item.CurrentHull;
contained.SetContainedItemPositions();
}
}
public override void OnMapLoaded()
{
if (itemIds == null) return;
for (ushort i = 0; i < itemIds.Length; i++)
{
Item item = Entity.FindEntityByID(itemIds[i]) as Item;
if (item == null) continue;
if (i >= Inventory.Capacity)
{
continue;
}
Inventory.TryPutItem(item, i, false, false, null, false);
}
itemIds = null;
}
protected override void ShallowRemoveComponentSpecific()
{
}
protected override void RemoveComponentSpecific()
{
#if CLIENT
inventoryTopSprite?.Remove();
inventoryBackSprite?.Remove();
inventoryBottomSprite?.Remove();
ContainedStateIndicator?.Remove();
if (Screen.Selected == GameMain.SubEditorScreen && !Submarine.Unloading)
{
GameMain.SubEditorScreen.HandleContainerContentsDeletion(Item, Inventory);
return;
}
#endif
foreach (Item item in Inventory.Items)
{
if (item == null) continue;
item.Drop(null);
}
}
public override void Load(XElement componentElement, bool usePrefabValues)
{
base.Load(componentElement, usePrefabValues);
string containedString = componentElement.GetAttributeString("contained", "");
string[] itemIdStrings = containedString.Split(',');
itemIds = new ushort[itemIdStrings.Length];
for (int i = 0; i < itemIdStrings.Length; i++)
{
if (!ushort.TryParse(itemIdStrings[i], out ushort id)) { continue; }
itemIds[i] = id;
}
}
public override XElement Save(XElement parentElement)
{
XElement componentElement = base.Save(parentElement);
string[] itemIdStrings = new string[Inventory.Items.Length];
for (int i = 0; i < Inventory.Items.Length; i++)
{
itemIdStrings[i] = (Inventory.Items[i] == null) ? "0" : Inventory.Items[i].ID.ToString();
}
componentElement.Add(new XAttribute("contained", string.Join(",", itemIdStrings)));
return componentElement;
}
}
}
@@ -0,0 +1,23 @@
using Microsoft.Xna.Framework;
namespace Barotrauma.Items.Components
{
partial class ItemLabel : ItemComponent, IDrawableComponent
{
public Vector2 DrawSize
{
//use the extents of the item as the draw size
get { return Vector2.Zero; }
}
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0, float signalStrength = 1)
{
switch (connection.Name)
{
case "set_text":
Text = signal;
break;
}
}
}
}
@@ -0,0 +1,35 @@
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
partial class Ladder : ItemComponent
{
public static List<Ladder> List { get; } = new List<Ladder>();
public Ladder(Item item, XElement element)
: base(item, element)
{
InitProjSpecific(element);
List.Add(this);
}
partial void InitProjSpecific(XElement element);
public override bool Select(Character character)
{
if (character == null || character.LockHands || character.Removed || !(character.AnimController is HumanoidAnimController)) return false;
character.AnimController.Anim = AnimController.Animation.Climbing;
return true;
}
protected override void RemoveComponentSpecific()
{
RemoveProjSpecific();
List.Remove(this);
}
partial void RemoveProjSpecific();
}
}
@@ -0,0 +1,405 @@
using FarseerPhysics;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
struct LimbPos
{
public LimbType limbType;
public Vector2 position;
public LimbPos(LimbType limbType, Vector2 position)
{
this.limbType = limbType;
this.position = position;
}
}
partial class Controller : ItemComponent, IServerSerializable
{
//where the limbs of the user should be positioned when using the controller
private readonly List<LimbPos> limbPositions;
private Direction dir;
//the position where the user walks to when using the controller
//(relative to the position of the item)
private Vector2 userPos;
private Camera cam;
private Character user;
private Item focusTarget;
private float targetRotation;
private bool state;
public Vector2 UserPos
{
get { return userPos; }
set { userPos = value; }
}
public Character User
{
get { return user; }
}
public IEnumerable<LimbPos> LimbPositions { get { return limbPositions; } }
[Editable, Serialize(false, false, description: "When enabled, the item will continuously send out a 0/1 signal and interacting with it will flip the signal (making the item behave like a switch). When disabled, the item will simply send out 1 when interacted with.")]
public bool IsToggle
{
get;
set;
}
public Controller(Item item, XElement element)
: base(item, element)
{
limbPositions = new List<LimbPos>();
userPos = element.GetAttributeVector2("UserPos", Vector2.Zero);
Enum.TryParse(element.GetAttributeString("direction", "None"), out dir);
foreach (XElement el in element.Elements())
{
if (el.Name != "limbposition") continue;
LimbPos lp = new LimbPos();
try
{
lp.limbType = (LimbType)Enum.Parse(typeof(LimbType), el.Attribute("limb").Value, true);
}
catch (Exception e)
{
DebugConsole.ThrowError("Error in " + element + ": " + e.Message, e);
}
lp.position = el.GetAttributeVector2("position", Vector2.Zero);
limbPositions.Add(lp);
}
IsActive = true;
}
public override void Update(float deltaTime, Camera cam)
{
this.cam = cam;
if (IsToggle)
{
item.SendSignal(0, state ? "1" : "0", "signal_out", sender: null);
}
if (user == null
|| user.Removed
|| user.SelectedConstruction != item
|| !user.CanInteractWith(item))
{
if (user != null)
{
CancelUsing(user);
user = null;
}
if (!IsToggle) { IsActive = false; }
return;
}
user.AnimController.Anim = AnimController.Animation.UsingConstruction;
if (userPos != Vector2.Zero)
{
Vector2 diff = (item.WorldPosition + userPos) - user.WorldPosition;
if (user.AnimController.InWater)
{
if (diff.LengthSquared() > 30.0f * 30.0f)
{
user.AnimController.TargetMovement = Vector2.Clamp(diff * 0.01f, -Vector2.One, Vector2.One);
user.AnimController.TargetDir = diff.X > 0.0f ? Direction.Right : Direction.Left;
}
else
{
user.AnimController.TargetMovement = Vector2.Zero;
}
}
else
{
diff.Y = 0.0f;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && user != Character.Controlled)
{
if (Math.Abs(diff.X) > 20.0f)
{
//wait for the character to walk to the correct position
return;
}
else if (Math.Abs(diff.X) > 0.1f)
{
//aim to keep the collider at the correct position once close enough
user.AnimController.Collider.LinearVelocity = new Vector2(
diff.X * 0.1f,
user.AnimController.Collider.LinearVelocity.Y);
}
}
else
{
if (Math.Abs(diff.X) > 10.0f)
{
user.AnimController.TargetMovement = Vector2.Normalize(diff);
user.AnimController.TargetDir = diff.X > 0.0f ? Direction.Right : Direction.Left;
return;
}
}
user.AnimController.TargetMovement = Vector2.Zero;
}
}
ApplyStatusEffects(ActionType.OnActive, deltaTime, user);
if (limbPositions.Count == 0) { return; }
user.AnimController.Anim = AnimController.Animation.UsingConstruction;
user.AnimController.ResetPullJoints();
if (dir != 0) user.AnimController.TargetDir = dir;
foreach (LimbPos lb in limbPositions)
{
Limb limb = user.AnimController.GetLimb(lb.limbType);
if (limb == null || !limb.body.Enabled) continue;
limb.Disabled = true;
Vector2 worldPosition = new Vector2(item.WorldRect.X, item.WorldRect.Y) + lb.position * item.Scale;
Vector2 diff = worldPosition - limb.WorldPosition;
limb.PullJointEnabled = true;
limb.PullJointWorldAnchorB = limb.SimPosition + ConvertUnits.ToSimUnits(diff);
}
}
public override bool Use(float deltaTime, Character activator = null)
{
if (activator != user)
{
return false;
}
if (user == null || user.Removed ||
user.SelectedConstruction != item || !user.CanInteractWith(item))
{
user = null;
return false;
}
item.SendSignal(0, "1", "trigger_out", user);
ApplyStatusEffects(ActionType.OnUse, 1.0f, activator);
return true;
}
public override bool SecondaryUse(float deltaTime, Character character = null)
{
if (this.user != character)
{
return false;
}
if (this.user == null || character.Removed ||
this.user.SelectedConstruction != item || !character.CanInteractWith(item))
{
this.user = null;
return false;
}
if (character == null) return false;
focusTarget = GetFocusTarget();
if (focusTarget == null)
{
Vector2 centerPos = new Vector2(item.WorldRect.Center.X, item.WorldRect.Center.Y);
Vector2 offset = character.CursorWorldPosition - centerPos;
offset.Y = -offset.Y;
targetRotation = MathUtils.WrapAngleTwoPi(MathUtils.VectorToAngle(offset));
return false;
}
character.ViewTarget = focusTarget;
#if CLIENT
if (character == Character.Controlled && cam != null)
{
Lights.LightManager.ViewTarget = focusTarget;
cam.TargetPos = focusTarget.WorldPosition;
cam.OffsetAmount = MathHelper.Lerp(cam.OffsetAmount, (focusTarget as Item).Prefab.OffsetOnSelected, deltaTime * 10.0f);
HideHUDs(true);
}
#endif
if (!character.IsRemotePlayer || character.ViewTarget == focusTarget)
{
Vector2 centerPos = new Vector2(item.WorldRect.Center.X, item.WorldRect.Center.Y);
Item targetItem = focusTarget as Item;
if (targetItem != null)
{
Turret turret = targetItem.GetComponent<Turret>();
if (turret != null)
{
centerPos = new Vector2(targetItem.WorldRect.X + turret.TransformedBarrelPos.X, targetItem.WorldRect.Y - turret.TransformedBarrelPos.Y);
}
}
Vector2 offset = character.CursorWorldPosition - centerPos;
offset.Y = -offset.Y;
targetRotation = MathUtils.WrapAngleTwoPi(MathUtils.VectorToAngle(offset));
}
return true;
}
private Item GetFocusTarget()
{
item.SendSignal(0, MathHelper.ToDegrees(targetRotation).ToString("G", CultureInfo.InvariantCulture), "position_out", user);
for (int i = item.LastSentSignalRecipients.Count - 1; i >= 0; i--)
{
if (item.LastSentSignalRecipients[i].Condition <= 0.0f) continue;
if (item.LastSentSignalRecipients[i].Prefab.FocusOnSelected)
{
return item.LastSentSignalRecipients[i];
}
}
return null;
}
public override bool Pick(Character picker)
{
if (IsToggle)
{
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
{
state = !state;
#if SERVER
item.CreateServerEvent(this);
#endif
}
}
else
{
item.SendSignal(0, "1", "signal_out", picker);
}
#if CLIENT
PlaySound(ActionType.OnUse, picker);
#endif
return true;
}
private void CancelUsing(Character character)
{
if (character == null || character.Removed) { return; }
foreach (LimbPos lb in limbPositions)
{
Limb limb = character.AnimController.GetLimb(lb.limbType);
if (limb == null) continue;
limb.Disabled = false;
limb.PullJointEnabled = false;
}
if (character.SelectedConstruction == this.item) { character.SelectedConstruction = null; }
character.AnimController.Anim = AnimController.Animation.None;
if (character == Character.Controlled)
{
HideHUDs(false);
}
#if SERVER
item.CreateServerEvent(this);
#endif
}
public override bool Select(Character activator)
{
if (activator == null || activator.Removed) { return false; }
//someone already using the item
if (user != null && !user.Removed)
{
if (user == activator)
{
IsActive = false;
CancelUsing(user);
user = null;
return false;
}
}
else
{
user = activator;
IsActive = true;
}
#if SERVER
item.CreateServerEvent(this);
#endif
item.SendSignal(0, "1", "signal_out", user);
return true;
}
public override void FlipX(bool relativeToSub)
{
if (dir != Direction.None)
{
dir = dir == Direction.Left ? Direction.Right : Direction.Left;
}
userPos.X = -UserPos.X;
for (int i = 0; i < limbPositions.Count; i++)
{
float diff = (item.Rect.X + limbPositions[i].position.X * item.Scale) - item.Rect.Center.X;
Vector2 flippedPos =
new Vector2(
(item.Rect.Center.X - diff - item.Rect.X) / item.Scale,
limbPositions[i].position.Y);
limbPositions[i] = new LimbPos(limbPositions[i].limbType, flippedPos);
}
}
public override void FlipY(bool relativeToSub)
{
userPos.Y = -UserPos.Y;
for (int i = 0; i < limbPositions.Count; i++)
{
float diff = (item.Rect.Y + limbPositions[i].position.Y) - item.Rect.Center.Y;
Vector2 flippedPos =
new Vector2(
limbPositions[i].position.X,
item.Rect.Center.Y - diff - item.Rect.Y);
limbPositions[i] = new LimbPos(limbPositions[i].limbType, flippedPos);
}
}
partial void HideHUDs(bool value);
}
}
@@ -0,0 +1,220 @@
using Barotrauma.Networking;
using System;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
partial class Deconstructor : Powered, IServerSerializable, IClientSerializable
{
private float progressTimer;
private float progressState;
private bool hasPower;
private ItemContainer inputContainer, outputContainer;
public ItemContainer InputContainer
{
get { return inputContainer; }
}
public ItemContainer OutputContainer
{
get { return outputContainer; }
}
public Deconstructor(Item item, XElement element)
: base(item, element)
{
InitProjSpecific(element);
}
partial void InitProjSpecific(XElement element);
public override void OnItemLoaded()
{
base.OnItemLoaded();
var containers = item.GetComponents<ItemContainer>().ToList();
if (containers.Count < 2)
{
DebugConsole.ThrowError("Error in item \"" + item.Name + "\": Deconstructors must have two ItemContainer components!");
return;
}
inputContainer = containers[0];
outputContainer = containers[1];
OnItemLoadedProjSpecific();
}
partial void OnItemLoadedProjSpecific();
public override void Update(float deltaTime, Camera cam)
{
MoveInputQueue();
if (inputContainer == null || inputContainer.Inventory.Items.All(i => i == null))
{
SetActive(false);
return;
}
hasPower = Voltage >= MinVoltage;
if (!hasPower) { return; }
var repairable = item.GetComponent<Repairable>();
if (repairable != null)
{
repairable.LastActiveTime = (float)Timing.TotalTime + 10.0f;
}
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
if (powerConsumption <= 0.0f) { Voltage = 1.0f; }
progressTimer += deltaTime * Math.Min(Voltage, 1.0f);
var targetItem = inputContainer.Inventory.Items.LastOrDefault(i => i != null);
if (targetItem == null) { return; }
float deconstructTime = targetItem.Prefab.DeconstructItems.Any() ? targetItem.Prefab.DeconstructTime : 1.0f;
progressState = Math.Min(progressTimer / deconstructTime, 1.0f);
if (progressTimer > deconstructTime)
{
int emptySlots = outputContainer.Inventory.Items.Where(i => i == null).Count();
foreach (DeconstructItem deconstructProduct in targetItem.Prefab.DeconstructItems)
{
float percentageHealth = targetItem.Condition / targetItem.Prefab.Health;
if (percentageHealth <= deconstructProduct.MinCondition || percentageHealth > deconstructProduct.MaxCondition) continue;
if (!(MapEntityPrefab.Find(null, deconstructProduct.ItemIdentifier) is ItemPrefab itemPrefab))
{
DebugConsole.ThrowError("Tried to deconstruct item \"" + targetItem.Name + "\" but couldn't find item prefab \"" + deconstructProduct.ItemIdentifier + "\"!");
continue;
}
float condition = deconstructProduct.CopyCondition ?
percentageHealth * itemPrefab.Health :
itemPrefab.Health * deconstructProduct.OutCondition;
//container full, drop the items outside the deconstructor
if (emptySlots <= 0)
{
Entity.Spawner.AddToSpawnQueue(itemPrefab, item.Position, item.Submarine, condition);
}
else
{
Entity.Spawner.AddToSpawnQueue(itemPrefab, outputContainer.Inventory, condition);
emptySlots--;
}
}
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
{
if (targetItem.Prefab.DeconstructItems.Any())
{
//drop all items that are inside the deconstructed item
foreach (ItemContainer ic in targetItem.GetComponents<ItemContainer>())
{
if (ic?.Inventory?.Items == null) { continue; }
foreach (Item containedItem in ic.Inventory.Items)
{
containedItem?.Drop(dropper: null, createNetworkEvent: true);
}
}
inputContainer.Inventory.RemoveItem(targetItem);
Entity.Spawner.AddToRemoveQueue(targetItem);
MoveInputQueue();
PutItemsToLinkedContainer();
}
else
{
if (outputContainer.Inventory.Items.All(i => i != null))
{
targetItem.Drop(dropper: null);
}
else
{
outputContainer.Inventory.TryPutItem(targetItem, user: null, createNetworkEvent: true);
}
}
#if SERVER
item.CreateServerEvent(this);
#endif
progressTimer = 0.0f;
progressState = 0.0f;
}
}
}
private void PutItemsToLinkedContainer()
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
if (outputContainer.Inventory.Items.All(it => it == null)) return;
foreach (MapEntity linkedTo in item.linkedTo)
{
if (linkedTo is Item linkedItem)
{
var fabricator = linkedItem.GetComponent<Fabricator>();
if (fabricator != null) { continue; }
var itemContainer = linkedItem.GetComponent<ItemContainer>();
if (itemContainer == null) { continue; }
foreach (Item containedItem in outputContainer.Inventory.Items)
{
if (containedItem == null) { continue; }
if (itemContainer.Inventory.Items.All(it => it != null)) { break; }
itemContainer.Inventory.TryPutItem(containedItem, user: null, createNetworkEvent: true);
}
}
}
}
/// <summary>
/// Move items towards the last slot in the inventory if there's free slots
/// </summary>
private void MoveInputQueue()
{
for (int i = inputContainer.Inventory.Capacity - 2; i >= 0; i--)
{
if (inputContainer.Inventory.Items[i] != null && inputContainer.Inventory.Items[i + 1] == null)
{
inputContainer.Inventory.TryPutItem(inputContainer.Inventory.Items[i], i + 1, allowSwapping: false, allowCombine: false, user: null, createNetworkEvent: true);
}
}
}
private void SetActive(bool active, Character user = null)
{
PutItemsToLinkedContainer();
if (inputContainer.Inventory.Items.All(i => i == null)) { active = false; }
IsActive = active;
currPowerConsumption = IsActive ? powerConsumption : 0.0f;
#if SERVER
if (user != null)
{
GameServer.Log(user.LogName + (IsActive ? " activated " : " deactivated ") + item.Name, ServerLog.MessageType.ItemInteraction);
}
#endif
if (!IsActive)
{
progressTimer = 0.0f;
progressState = 0.0f;
}
#if CLIENT
activateButton.Text = TextManager.Get(IsActive ? "DeconstructorCancel" : "DeconstructorDeconstruct");
#endif
inputContainer.Inventory.Locked = IsActive;
}
}
}
@@ -0,0 +1,199 @@
using Microsoft.Xna.Framework;
using System;
using System.Globalization;
using System.Xml.Linq;
using Barotrauma.Networking;
namespace Barotrauma.Items.Components
{
partial class Engine : Powered, IServerSerializable, IClientSerializable
{
private float force;
private float targetForce;
private float maxForce;
private Attack propellerDamage;
private float damageTimer;
private bool hasPower;
private float prevVoltage;
private float controlLockTimer;
[Editable(0.0f, 10000000.0f),
Serialize(2000.0f, true, description: "The amount of force exerted on the submarine when the engine is operating at full power.")]
public float MaxForce
{
get { return maxForce; }
set
{
maxForce = Math.Max(0.0f, value);
}
}
[Editable, Serialize("0.0,0.0", true,
description: "The position of the propeller as an offset from the item's center (in pixels)."+
" Determines where the particles spawn and the position that causes characters to take damage from the engine if the PropellerDamage is defined.")]
public Vector2 PropellerPos
{
get;
set;
}
public float Force
{
get { return force;}
set { force = MathHelper.Clamp(value, -100.0f, 100.0f); }
}
public float CurrentVolume
{
get { return Math.Abs((force / 100.0f) * (MinVoltage <= 0.0f ? 1.0f : Math.Min(prevVoltage / MinVoltage, 1.0f))); }
}
public Engine(Item item, XElement element)
: base(item, element)
{
IsActive = true;
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "propellerdamage":
propellerDamage = new Attack(subElement, item.Name + ", Engine");
break;
}
}
InitProjSpecific(element);
}
partial void InitProjSpecific(XElement element);
public override void Update(float deltaTime, Camera cam)
{
UpdateOnActiveEffects(deltaTime);
UpdateAnimation(deltaTime);
controlLockTimer -= deltaTime;
currPowerConsumption = Math.Abs(targetForce) / 100.0f * powerConsumption;
//pumps consume more power when in a bad condition
currPowerConsumption *= MathHelper.Lerp(1.5f, 1.0f, item.Condition / item.MaxCondition);
if (powerConsumption == 0.0f) { Voltage = 1.0f; }
prevVoltage = Voltage;
hasPower = Voltage > MinVoltage;
Force = MathHelper.Lerp(force, (Voltage < MinVoltage) ? 0.0f : targetForce, 0.1f);
if (Math.Abs(Force) > 1.0f)
{
//arbitrary multiplier that was added to changes in submarine mass without having to readjust all engines
float forceMultiplier = 0.1f;
float voltageFactor = MinVoltage <= 0.0f ? 1.0f : Math.Min(Voltage / MinVoltage, 1.0f);
Vector2 currForce = new Vector2(force * maxForce * forceMultiplier * voltageFactor, 0.0f);
//less effective when in a bad condition
currForce *= MathHelper.Lerp(0.5f, 2.0f, item.Condition / item.MaxCondition);
item.Submarine.ApplyForce(currForce);
UpdatePropellerDamage(deltaTime);
float maxChangeSpeed = 0.5f;
float modifier = 2;
float noise = currForce.Length() * forceMultiplier * modifier / maxForce;
float min = Math.Max(1 - maxChangeSpeed, 0);
float max = 1 + maxChangeSpeed;
UpdateAITargets(Math.Clamp(noise, min, max), deltaTime);
#if CLIENT
for (int i = 0; i < 5; i++)
{
GameMain.ParticleManager.CreateParticle("bubbles", item.WorldPosition + PropellerPos,
-currForce / 5.0f + new Vector2(Rand.Range(-100.0f, 100.0f), Rand.Range(-50f, 50f)),
0.0f, item.CurrentHull);
}
#endif
}
}
private void UpdateAITargets(float increaseSpeed, float deltaTime)
{
if (item.AiTarget != null)
{
item.AiTarget.IncreaseSoundRange(deltaTime, increaseSpeed);
if (item.CurrentHull != null && item.CurrentHull.AiTarget != null)
{
// It's possible that some othe item increases the hull's soundrange more than the engine.
item.CurrentHull.AiTarget.SoundRange = Math.Max(item.CurrentHull.AiTarget.SoundRange, item.AiTarget.SoundRange);
}
}
}
private void UpdatePropellerDamage(float deltaTime)
{
damageTimer += deltaTime;
if (damageTimer < 0.5f) return;
damageTimer = 0.1f;
if (propellerDamage == null) return;
Vector2 propellerWorldPos = item.WorldPosition + PropellerPos;
foreach (Character character in Character.CharacterList)
{
if (character.Submarine != null || !character.Enabled || character.Removed) continue;
float dist = Vector2.DistanceSquared(character.WorldPosition, propellerWorldPos);
if (dist > propellerDamage.DamageRange * propellerDamage.DamageRange) continue;
character.LastDamageSource = item;
propellerDamage.DoDamage(null, character, propellerWorldPos, 1.0f, true);
}
}
partial void UpdateAnimation(float deltaTime);
public override void UpdateBroken(float deltaTime, Camera cam)
{
base.UpdateBroken(deltaTime, cam);
force = MathHelper.Lerp(force, 0.0f, 0.1f);
}
public override void FlipX(bool relativeToSub)
{
PropellerPos = new Vector2(-PropellerPos.X, PropellerPos.Y);
}
public override void FlipY(bool relativeToSub)
{
PropellerPos = new Vector2(PropellerPos.X, -PropellerPos.Y);
}
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f)
{
base.ReceiveSignal(stepsTaken, signal, connection, source, sender, power, signalStrength);
if (connection.Name == "set_force")
{
if (float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float tempForce))
{
controlLockTimer = 0.1f;
targetForce = MathHelper.Clamp(tempForce, -100.0f, 100.0f);
}
}
}
public override XElement Save(XElement parentElement)
{
Vector2 prevPropellerPos = PropellerPos;
//undo flipping before saving
if (item.FlippedX) { PropellerPos = new Vector2(-PropellerPos.X, PropellerPos.Y); }
if (item.FlippedY) { PropellerPos = new Vector2(PropellerPos.X, -PropellerPos.Y); }
XElement element = base.Save(parentElement);
PropellerPos = prevPropellerPos;
return element;
}
}
}
@@ -0,0 +1,383 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
partial class Fabricator : Powered, IServerSerializable, IClientSerializable
{
private readonly List<FabricationRecipe> fabricationRecipes = new List<FabricationRecipe>();
private FabricationRecipe fabricatedItem;
private float timeUntilReady;
private float requiredTime;
private bool hasPower;
private Character user;
private ItemContainer inputContainer, outputContainer;
public ItemContainer InputContainer
{
get { return inputContainer; }
}
public ItemContainer OutputContainer
{
get { return outputContainer; }
}
private float progressState;
public Fabricator(Item item, XElement element)
: base(item, element)
{
foreach (XElement subElement in element.Elements())
{
if (subElement.Name.ToString().Equals("fabricableitem", StringComparison.OrdinalIgnoreCase))
{
DebugConsole.ThrowError("Error in item " + item.Name + "! Fabrication recipes should be defined in the craftable item's xml, not in the fabricator.");
break;
}
}
foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs)
{
foreach (FabricationRecipe recipe in itemPrefab.FabricationRecipes)
{
if (recipe.SuitableFabricatorIdentifiers.Length > 0)
{
if (!recipe.SuitableFabricatorIdentifiers.Any(i => item.prefab.Identifier == i || item.HasTag(i)))
{
continue;
}
}
fabricationRecipes.Add(recipe);
}
}
InitProjSpecific();
}
public override void OnItemLoaded()
{
base.OnItemLoaded();
var containers = item.GetComponents<ItemContainer>().ToList();
if (containers.Count < 2)
{
DebugConsole.ThrowError("Error in item \"" + item.Name + "\": Fabricators must have two ItemContainer components!");
return;
}
inputContainer = containers[0];
outputContainer = containers[1];
foreach (var recipe in fabricationRecipes)
{
int ingredientCount = recipe.RequiredItems.Sum(it => it.Amount);
if (ingredientCount > inputContainer.Capacity)
{
DebugConsole.ThrowError("Error in item \"" + item.Name + "\": There's not enough room in the input inventory for the ingredients of \"" + recipe.TargetItem.Name + "\"!");
}
}
OnItemLoadedProjSpecific();
}
partial void OnItemLoadedProjSpecific();
partial void InitProjSpecific();
public override bool Select(Character character)
{
SelectProjSpecific(character);
return base.Select(character);
}
partial void SelectProjSpecific(Character character);
public override bool Pick(Character picker)
{
return (picker != null);
}
public void RemoveFabricationRecipes(List<string> allowedIdentifiers)
{
for (int i = 0; i < fabricationRecipes.Count; i++)
{
if (!allowedIdentifiers.Contains(fabricationRecipes[i].TargetItem.Identifier))
{
fabricationRecipes.RemoveAt(i);
i--;
}
}
CreateRecipes();
}
partial void CreateRecipes();
private void StartFabricating(FabricationRecipe selectedItem, Character user)
{
if (selectedItem == null) return;
if (!outputContainer.Inventory.IsEmpty()) return;
#if CLIENT
itemList.Enabled = false;
activateButton.Text = TextManager.Get("FabricatorCancel");
#endif
IsActive = true;
this.user = user;
fabricatedItem = selectedItem;
MoveIngredientsToInputContainer(selectedItem);
requiredTime = GetRequiredTime(fabricatedItem, user);
timeUntilReady = requiredTime;
inputContainer.Inventory.Locked = true;
outputContainer.Inventory.Locked = true;
currPowerConsumption = powerConsumption;
currPowerConsumption *= MathHelper.Lerp(1.5f, 1.0f, item.Condition / item.MaxCondition);
#if SERVER
if (user != null)
{
GameServer.Log(user.LogName + " started fabricating " + selectedItem.DisplayName + " in " + item.Name, ServerLog.MessageType.ItemInteraction);
}
item.CreateServerEvent(this);
#endif
}
private void CancelFabricating(Character user = null)
{
if (fabricatedItem == null) { return; }
IsActive = false;
fabricatedItem = null;
this.user = null;
currPowerConsumption = 0.0f;
#if CLIENT
itemList.Enabled = true;
if (activateButton != null)
{
activateButton.Text = TextManager.Get("FabricatorCreate");
}
#endif
progressState = 0.0f;
timeUntilReady = 0.0f;
inputContainer.Inventory.Locked = false;
outputContainer.Inventory.Locked = false;
#if SERVER
if (user != null)
{
GameServer.Log(user.LogName + " cancelled the fabrication of " + fabricatedItem.DisplayName + " in " + item.Name, ServerLog.MessageType.ItemInteraction);
}
item.CreateServerEvent(this);
#endif
}
public override void Update(float deltaTime, Camera cam)
{
if (fabricatedItem == null || !CanBeFabricated(fabricatedItem))
{
CancelFabricating();
return;
}
progressState = fabricatedItem == null ? 0.0f : (requiredTime - timeUntilReady) / requiredTime;
hasPower = Voltage >= MinVoltage;
if (!hasPower) { return; }
var repairable = item.GetComponent<Repairable>();
if (repairable != null)
{
repairable.LastActiveTime = (float)Timing.TotalTime + 10.0f;
}
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
if (powerConsumption <= 0) { Voltage = 1.0f; }
timeUntilReady -= deltaTime * Math.Min(Voltage, 1.0f);
if (timeUntilReady > 0.0f) { return; }
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
{
var availableIngredients = GetAvailableIngredients();
foreach (FabricationRecipe.RequiredItem ingredient in fabricatedItem.RequiredItems)
{
for (int i = 0; i < ingredient.Amount; i++)
{
var availableItem = availableIngredients.FirstOrDefault(it => it != null && it.Prefab == ingredient.ItemPrefab && it.Condition >= ingredient.ItemPrefab.Health * ingredient.MinCondition);
if (availableItem == null) { continue; }
//Item4 = use condition bool
if (ingredient.UseCondition && availableItem.Condition - ingredient.ItemPrefab.Health * ingredient.MinCondition > 0.0f) //Leave it behind with reduced condition if it has enough to stay above 0
{
availableItem.Condition -= ingredient.ItemPrefab.Health * ingredient.MinCondition;
continue;
}
availableIngredients.Remove(availableItem);
Entity.Spawner.AddToRemoveQueue(availableItem);
inputContainer.Inventory.RemoveItem(availableItem);
}
}
if (outputContainer.Inventory.Items.All(i => i != null))
{
Entity.Spawner.AddToSpawnQueue(fabricatedItem.TargetItem, item.Position, item.Submarine, fabricatedItem.TargetItem.Health * fabricatedItem.OutCondition);
}
else
{
Entity.Spawner.AddToSpawnQueue(fabricatedItem.TargetItem, outputContainer.Inventory, fabricatedItem.TargetItem.Health * fabricatedItem.OutCondition);
}
if (user != null && !user.Removed)
{
foreach (Skill skill in fabricatedItem.RequiredSkills)
{
float userSkill = user.GetSkillLevel(skill.Identifier);
user.Info.IncreaseSkillLevel(
skill.Identifier,
skill.Level * SkillSettings.Current.SkillIncreasePerFabricatorRequiredSkill / Math.Max(userSkill, 1.0f),
user.WorldPosition + Vector2.UnitY * 150.0f);
}
}
CancelFabricating();
}
}
private bool CanBeFabricated(FabricationRecipe fabricableItem)
{
if (fabricableItem == null) { return false; }
List<Item> availableIngredients = GetAvailableIngredients();
return CanBeFabricated(fabricableItem, availableIngredients);
}
private bool CanBeFabricated(FabricationRecipe fabricableItem, IEnumerable<Item> availableIngredients)
{
if (fabricableItem == null) { return false; }
foreach (FabricationRecipe.RequiredItem requiredItem in fabricableItem.RequiredItems)
{
if (availableIngredients.Count(it => IsItemValidIngredient(it, requiredItem)) < requiredItem.Amount)
{
return false;
}
}
return true;
}
private float GetRequiredTime(FabricationRecipe fabricableItem, Character user)
{
float degreeOfSuccess = DegreeOfSuccess(user, fabricableItem.RequiredSkills);
float t = degreeOfSuccess < 0.5f ? degreeOfSuccess * degreeOfSuccess : degreeOfSuccess * 2;
//fabricating takes 100 times longer if degree of success is close to 0
//characters with a higher skill than required can fabricate up to 100% faster
return fabricableItem.RequiredTime / MathHelper.Clamp(t, 0.01f, 2.0f);
}
/// <summary>
/// Get a list of all items available in the input container and linked containers
/// </summary>
/// <returns></returns>
private List<Item> GetAvailableIngredients()
{
List<Item> availableIngredients = new List<Item>();
availableIngredients.AddRange(inputContainer.Inventory.Items.Where(it => it != null));
foreach (MapEntity linkedTo in item.linkedTo)
{
if (linkedTo is Item linkedItem)
{
var itemContainer = linkedItem.GetComponent<ItemContainer>();
if (itemContainer == null) { continue; }
var deconstructor = linkedItem.GetComponent<Deconstructor>();
if (deconstructor != null)
{
itemContainer = deconstructor.OutputContainer;
}
availableIngredients.AddRange(itemContainer.Inventory.Items.Where(it => it != null));
}
}
#if CLIENT
if (Character.Controlled?.Inventory != null)
{
availableIngredients.AddRange(Character.Controlled.Inventory.Items.Distinct().Where(it => it != null));
}
#else
if (user?.Inventory != null)
{
availableIngredients.AddRange(user.Inventory.Items.Distinct().Where(it => it != null));
}
#endif
return availableIngredients;
}
/// <summary>
/// Move the items required for fabrication into the input container.
/// The method assumes that all the required ingredients are available either in the input container or linked containers.
/// </summary>
private void MoveIngredientsToInputContainer(FabricationRecipe targetItem)
{
//required ingredients that are already present in the input container
List<Item> usedItems = new List<Item>();
bool isClient = GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient;
var availableIngredients = GetAvailableIngredients();
foreach (var requiredItem in targetItem.RequiredItems)
{
for (int i = 0; i < requiredItem.Amount; i++)
{
var matchingItem = availableIngredients.Find(it => !usedItems.Contains(it) && IsItemValidIngredient(it, requiredItem));
if (matchingItem == null) { continue; }
availableIngredients.Remove(matchingItem);
if (matchingItem.ParentInventory == inputContainer.Inventory)
{
//already in input container, all good
usedItems.Add(matchingItem);
}
else //in another inventory, we need to move the item
{
if (inputContainer.Inventory.Items.All(it => it != null))
{
var unneededItem = inputContainer.Inventory.Items.FirstOrDefault(it => !usedItems.Contains(it));
unneededItem?.Drop(null, createNetworkEvent: !isClient);
}
inputContainer.Inventory.TryPutItem(matchingItem, user: null, createNetworkEvent: !isClient);
}
}
}
}
private bool IsItemValidIngredient(Item item, FabricationRecipe.RequiredItem requiredItem)
{
return
item != null &&
item.prefab == requiredItem.ItemPrefab &&
item.Condition / item.Prefab.Health >= requiredItem.MinCondition;
}
}
}
@@ -0,0 +1,130 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
partial class MiniMap : Powered
{
class HullData
{
public float? Oxygen;
public float? Water;
public bool Distort;
public float DistortionTimer;
public List<Hull> LinkedHulls = new List<Hull>();
}
private DateTime resetDataTime;
private bool hasPower;
private readonly Dictionary<Hull, HullData> hullDatas;
[Editable, Serialize(false, true, description: "Does the machine require inputs from water detectors in order to show the water levels inside rooms.")]
public bool RequireWaterDetectors
{
get;
set;
}
[Editable, Serialize(true, true, description: "Does the machine require inputs from oxygen detectors in order to show the oxygen levels inside rooms.")]
public bool RequireOxygenDetectors
{
get;
set;
}
[Editable, Serialize(true, true, description: "Should damaged walls be displayed by the machine.")]
public bool ShowHullIntegrity
{
get;
set;
}
public MiniMap(Item item, XElement element)
: base(item, element)
{
IsActive = true;
hullDatas = new Dictionary<Hull, HullData>();
InitProjSpecific(element);
}
partial void InitProjSpecific(XElement element);
public override void Update(float deltaTime, Camera cam)
{
//periodically reset all hull data
//(so that outdated hull info won't be shown if detectors stop sending signals)
if (DateTime.Now > resetDataTime)
{
foreach (HullData hullData in hullDatas.Values)
{
if (!hullData.Distort)
{
hullData.Oxygen = null;
hullData.Water = null;
}
}
resetDataTime = DateTime.Now + new TimeSpan(0, 0, 1);
}
currPowerConsumption = powerConsumption;
currPowerConsumption *= MathHelper.Lerp(1.5f, 1.0f, item.Condition / item.MaxCondition);
hasPower = Voltage > MinVoltage;
if (hasPower)
{
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
}
}
public override bool Pick(Character picker)
{
return picker != null;
}
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0, float signalStrength = 1.0f)
{
if (source == null || source.CurrentHull == null) { return; }
Hull sourceHull = source.CurrentHull;
if (!hullDatas.TryGetValue(sourceHull, out HullData hullData))
{
hullData = new HullData();
hullDatas.Add(sourceHull, hullData);
}
if (hullData.Distort) return;
switch (connection.Name)
{
case "water_data_in":
//cheating a bit because water detectors don't actually send the water level
if (source.GetComponent<WaterDetector>() == null)
{
hullData.Water = Rand.Range(0.0f, 1.0f);
}
else
{
hullData.Water = Math.Min(sourceHull.WaterVolume / sourceHull.Volume, 1.0f);
}
break;
case "oxygen_data_in":
float oxy;
if (!float.TryParse(signal, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out oxy))
{
oxy = Rand.Range(0.0f, 100.0f);
}
hullData.Oxygen = oxy;
break;
}
}
}
}
@@ -0,0 +1,119 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
class OxygenGenerator : Powered
{
private float powerDownTimer;
private float generatedAmount;
private List<Vent> ventList;
private float totalHullVolume;
public float CurrFlow
{
get;
private set;
}
[Editable, Serialize(400.0f, true, description: "How much oxygen the machine generates when operating at full power.")]
public float GeneratedAmount
{
get { return generatedAmount; }
set { generatedAmount = MathHelper.Clamp(value, -10000.0f, 10000.0f); }
}
public OxygenGenerator(Item item, XElement element)
: base(item, element)
{
IsActive = true;
}
public override void Update(float deltaTime, Camera cam)
{
UpdateOnActiveEffects(deltaTime);
CurrFlow = 0.0f;
currPowerConsumption = powerConsumption;
//consume more power when in a bad condition
currPowerConsumption *= MathHelper.Lerp(1.5f, 1.0f, item.Condition / item.MaxCondition);
if (powerConsumption <= 0.0f)
{
Voltage = 1.0f;
}
if (item.CurrentHull == null) return;
if (Voltage < MinVoltage)
{
powerDownTimer += deltaTime;
return;
}
else
{
powerDownTimer = 0.0f;
}
CurrFlow = Math.Min(Voltage, 1.0f) * generatedAmount * 100.0f;
//less effective when in bad condition
float conditionMult = item.Condition / item.MaxCondition;
//100% condition = 100% oxygen
//50% condition = 25% oxygen
//20% condition = 4%
CurrFlow *= conditionMult * conditionMult;
UpdateVents(CurrFlow);
}
public override void UpdateBroken(float deltaTime, Camera cam)
{
base.UpdateBroken(deltaTime, cam);
powerDownTimer += deltaTime;
CurrFlow = 0.0f;
}
private void GetVents()
{
ventList.Clear();
foreach (MapEntity entity in item.linkedTo)
{
Item linkedItem = entity as Item;
if (linkedItem == null) continue;
Vent vent = linkedItem.GetComponent<Vent>();
if (vent == null) continue;
ventList.Add(vent);
if (linkedItem.CurrentHull != null) totalHullVolume += linkedItem.CurrentHull.Volume;
}
}
private void UpdateVents(float deltaOxygen)
{
if (ventList == null)
{
ventList = new List<Vent>();
GetVents();
}
if (!ventList.Any() || totalHullVolume <= 0.0f) return;
foreach (Vent v in ventList)
{
if (v.Item.CurrentHull == null) continue;
v.OxygenFlow = deltaOxygen * (v.Item.CurrentHull.Volume / totalHullVolume);
v.IsActive = true;
}
}
}
}
@@ -0,0 +1,172 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Globalization;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
partial class Pump : Powered, IServerSerializable, IClientSerializable
{
private float flowPercentage;
private float maxFlow;
private float? targetLevel;
private float pumpSpeedLockTimer, isActiveLockTimer;
[Serialize(0.0f, true, description: "How fast the item is currently pumping water (-100 = full speed out, 100 = full speed in). Intended to be used by StatusEffect conditionals (setting this value in XML has no effect).")]
public float FlowPercentage
{
get { return flowPercentage; }
set
{
if (!MathUtils.IsValid(flowPercentage)) { return; }
flowPercentage = MathHelper.Clamp(value, -100.0f, 100.0f);
flowPercentage = MathUtils.Round(flowPercentage, 1.0f);
}
}
[Editable, Serialize(80.0f, false, description: "How fast the item pumps water in/out when operating at 100%.")]
public float MaxFlow
{
get { return maxFlow; }
set { maxFlow = value; }
}
private float currFlow;
public float CurrFlow
{
get
{
if (!IsActive) { return 0.0f; }
return Math.Abs(currFlow);
}
}
public override bool IsActive
{
get => base.IsActive;
set
{
base.IsActive = value;
if (!IsActive)
{
powerConsumption = 0;
}
}
}
public bool HasPower => IsActive && Voltage >= MinVoltage;
public Pump(Item item, XElement element)
: base(item, element)
{
InitProjSpecific(element);
}
partial void InitProjSpecific(XElement element);
public override void Update(float deltaTime, Camera cam)
{
currFlow = 0.0f;
if (targetLevel != null)
{
pumpSpeedLockTimer -= deltaTime;
float hullPercentage = 0.0f;
if (item.CurrentHull != null) { hullPercentage = (item.CurrentHull.WaterVolume / item.CurrentHull.Volume) * 100.0f; }
FlowPercentage = ((float)targetLevel - hullPercentage) * 10.0f;
if (pumpSpeedLockTimer <= 0.0f)
{
targetLevel = null;
}
}
currPowerConsumption = powerConsumption * Math.Abs(flowPercentage / 100.0f);
//pumps consume more power when in a bad condition
currPowerConsumption *= MathHelper.Lerp(1.5f, 1.0f, item.Condition / item.MaxCondition);
if (!HasPower) { return; }
UpdateProjSpecific(deltaTime);
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
if (item.CurrentHull == null) { return; }
float powerFactor = Math.Min(currPowerConsumption <= 0.0f ? 1.0f : Voltage, 1.0f);
currFlow = flowPercentage / 100.0f * maxFlow * powerFactor;
//less effective when in a bad condition
currFlow *= MathHelper.Lerp(0.5f, 1.0f, item.Condition / item.MaxCondition);
item.CurrentHull.WaterVolume += currFlow;
if (item.CurrentHull.WaterVolume > item.CurrentHull.Volume) { item.CurrentHull.Pressure += 0.5f; }
}
partial void UpdateProjSpecific(float deltaTime);
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f)
{
if (connection.Name == "toggle")
{
IsActive = !IsActive;
isActiveLockTimer = 0.1f;
}
else if (connection.Name == "set_active")
{
IsActive = signal != "0";
isActiveLockTimer = 0.1f;
}
else if (connection.Name == "set_speed")
{
if (float.TryParse(signal, NumberStyles.Any, CultureInfo.InvariantCulture, out float tempSpeed))
{
flowPercentage = MathHelper.Clamp(tempSpeed, -100.0f, 100.0f);
pumpSpeedLockTimer = 0.1f;
}
}
else if (connection.Name == "set_targetlevel")
{
if (float.TryParse(signal, NumberStyles.Any, CultureInfo.InvariantCulture, out float tempTarget))
{
targetLevel = MathHelper.Clamp(tempTarget + 50.0f, 0.0f, 100.0f);
pumpSpeedLockTimer = 0.1f;
}
}
}
public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
{
#if CLIENT
if (GameMain.Client != null) { return false; }
#endif
if (objective.Option.Equals("stoppumping", StringComparison.OrdinalIgnoreCase))
{
#if SERVER
if (FlowPercentage > 0.0f)
{
item.CreateServerEvent(this);
}
#endif
IsActive = false;
FlowPercentage = 0.0f;
}
else
{
#if SERVER
if (!IsActive || FlowPercentage > -100.0f)
{
item.CreateServerEvent(this);
}
#endif
IsActive = true;
FlowPercentage = -100.0f;
}
return true;
}
}
}
@@ -0,0 +1,673 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Extensions;
using System.Globalization;
namespace Barotrauma.Items.Components
{
partial class Reactor : Powered, IServerSerializable, IClientSerializable
{
const float NetworkUpdateInterval = 0.5f;
//the rate at which the reactor is being run on (higher rate -> higher temperature)
private float fissionRate;
//how much of the generated steam is used to spin the turbines and generate power
private float turbineOutput;
private float temperature;
//is automatic temperature control on
//(adjusts the fission rate and turbine output automatically to keep the
//amount of power generated balanced with the load)
private bool autoTemp;
//automatical adjustment to the power output when
//turbine output and temperature are in the optimal range
private float autoAdjustAmount;
private float fuelConsumptionRate;
private float meltDownTimer, meltDownDelay;
private float fireTimer, fireDelay;
private float maxPowerOutput;
private Queue<float> loadQueue = new Queue<float>();
private float load;
private bool unsentChanges;
private float sendUpdateTimer;
private float degreeOfSuccess;
private Vector2 optimalTemperature, allowedTemperature;
private Vector2 optimalFissionRate, allowedFissionRate;
private Vector2 optimalTurbineOutput, allowedTurbineOutput;
private bool _powerOn;
[Serialize(defaultValue: false, isSaveable: true)]
public bool PowerOn
{
get { return _powerOn; }
set
{
_powerOn = value;
#if CLIENT
UpdateUIElementStates();
#endif
}
}
private Character lastAIUser;
private Character lastUser;
private Character LastUser
{
get { return lastUser; }
set
{
if (lastUser == value) return;
lastUser = value;
degreeOfSuccess = lastUser == null ? 0.0f : DegreeOfSuccess(lastUser);
}
}
[Editable(0.0f, float.MaxValue), Serialize(10000.0f, true, description: "How much power (kW) the reactor generates when operating at full capacity.")]
public float MaxPowerOutput
{
get { return maxPowerOutput; }
set
{
maxPowerOutput = Math.Max(0.0f, value);
}
}
[Editable(0.0f, float.MaxValue), Serialize(120.0f, true, description: "How long the temperature has to stay critical until a meltdown occurs.")]
public float MeltdownDelay
{
get { return meltDownDelay; }
set { meltDownDelay = Math.Max(value, 0.0f); }
}
[Editable(0.0f, float.MaxValue), Serialize(30.0f, true, description: "How long the temperature has to stay critical until the reactor catches fire.")]
public float FireDelay
{
get { return fireDelay; }
set { fireDelay = Math.Max(value, 0.0f); }
}
[Serialize(0.0f, true, description: "Current temperature of the reactor (0% - 100%). Indended to be used by StatusEffect conditionals.")]
public float Temperature
{
get { return temperature; }
set
{
if (!MathUtils.IsValid(value)) return;
temperature = MathHelper.Clamp(value, 0.0f, 100.0f);
}
}
[Serialize(0.0f, true, description: "Current fission rate of the reactor (0% - 100%). Intended to be used by StatusEffect conditionals (setting the value from XML is not recommended).")]
public float FissionRate
{
get { return fissionRate; }
set
{
if (!MathUtils.IsValid(value)) return;
fissionRate = MathHelper.Clamp(value, 0.0f, 100.0f);
}
}
[Serialize(0.0f, true, description: "Current turbine output of the reactor (0% - 100%). Intended to be used by StatusEffect conditionals (setting the value from XML is not recommended).")]
public float TurbineOutput
{
get { return turbineOutput; }
set
{
if (!MathUtils.IsValid(value)) return;
turbineOutput = MathHelper.Clamp(value, 0.0f, 100.0f);
}
}
[Serialize(0.2f, true, description: "How fast the condition of the contained fuel rods deteriorates per second."), Editable(0.0f, 1000.0f)]
public float FuelConsumptionRate
{
get { return fuelConsumptionRate; }
set
{
if (!MathUtils.IsValid(value)) return;
fuelConsumptionRate = Math.Max(value, 0.0f);
}
}
[Serialize(false, true, description: "Is the temperature currently critical. Intended to be used by StatusEffect conditionals (setting the value from XML has no effect).")]
public bool TemperatureCritical
{
get { return temperature > allowedTemperature.Y; }
set { /*do nothing*/ }
}
private float correctTurbineOutput;
private float targetFissionRate;
private float targetTurbineOutput;
[Serialize(false, true, description: "Is the automatic temperature control currently on. Indended to be used by StatusEffect conditionals (setting the value from XML is not recommended).")]
public bool AutoTemp
{
get { return autoTemp; }
set
{
autoTemp = value;
#if CLIENT
UpdateUIElementStates();
#endif
}
}
private float prevAvailableFuel;
public float AvailableFuel { get; set; }
public Reactor(Item item, XElement element)
: base(item, element)
{
IsActive = true;
InitProjSpecific(element);
}
partial void InitProjSpecific(XElement element);
public override void Update(float deltaTime, Camera cam)
{
#if SERVER
if (GameMain.Server != null && nextServerLogWriteTime != null)
{
if (Timing.TotalTime >= (float)nextServerLogWriteTime)
{
GameServer.Log(lastUser.LogName + " adjusted reactor settings: " +
"Temperature: " + (int)(temperature * 100.0f) +
", Fission rate: " + (int)targetFissionRate +
", Turbine output: " + (int)targetTurbineOutput +
(autoTemp ? ", Autotemp ON" : ", Autotemp OFF"),
ServerLog.MessageType.ItemInteraction);
nextServerLogWriteTime = null;
lastServerLogWriteTime = (float)Timing.TotalTime;
}
}
#endif
//if an AI character was using the item on the previous frame but not anymore, turn autotemp on
// (= bots turn autotemp back on when leaving the reactor)
if (lastAIUser != null)
{
if (lastAIUser.SelectedConstruction != item && lastAIUser.CanInteractWith(item))
{
AutoTemp = true;
unsentChanges = true;
lastAIUser = null;
}
}
prevAvailableFuel = AvailableFuel;
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
//use a smoothed "correct output" instead of the actual correct output based on the load
//so the player doesn't have to keep adjusting the rate impossibly fast when the load fluctuates heavily
correctTurbineOutput += MathHelper.Clamp((load / MaxPowerOutput * 100.0f) - correctTurbineOutput, -10.0f, 10.0f) * deltaTime;
//calculate tolerances of the meters based on the skills of the user
//more skilled characters have larger "sweet spots", making it easier to keep the power output at a suitable level
float tolerance = MathHelper.Lerp(2.5f, 10.0f, degreeOfSuccess);
optimalTurbineOutput = new Vector2(correctTurbineOutput - tolerance, correctTurbineOutput + tolerance);
tolerance = MathHelper.Lerp(5.0f, 20.0f, degreeOfSuccess);
allowedTurbineOutput = new Vector2(correctTurbineOutput - tolerance, correctTurbineOutput + tolerance);
optimalTemperature = Vector2.Lerp(new Vector2(40.0f, 60.0f), new Vector2(30.0f, 70.0f), degreeOfSuccess);
allowedTemperature = Vector2.Lerp(new Vector2(30.0f, 70.0f), new Vector2(10.0f, 90.0f), degreeOfSuccess);
optimalFissionRate = Vector2.Lerp(new Vector2(30, AvailableFuel - 20), new Vector2(20, AvailableFuel - 10), degreeOfSuccess);
optimalFissionRate.X = Math.Min(optimalFissionRate.X, optimalFissionRate.Y - 10);
allowedFissionRate = Vector2.Lerp(new Vector2(20, AvailableFuel), new Vector2(10, AvailableFuel), degreeOfSuccess);
allowedFissionRate.X = Math.Min(allowedFissionRate.X, allowedFissionRate.Y - 10);
float heatAmount = GetGeneratedHeat(fissionRate);
float temperatureDiff = (heatAmount - turbineOutput) - Temperature;
Temperature += MathHelper.Clamp(Math.Sign(temperatureDiff) * 10.0f * deltaTime, -Math.Abs(temperatureDiff), Math.Abs(temperatureDiff));
//if (item.InWater && AvailableFuel < 100.0f) Temperature -= 12.0f * deltaTime;
FissionRate = MathHelper.Lerp(fissionRate, Math.Min(targetFissionRate, AvailableFuel), deltaTime);
TurbineOutput = MathHelper.Lerp(turbineOutput, targetTurbineOutput, deltaTime);
float temperatureFactor = Math.Min(temperature / 50.0f, 1.0f);
currPowerConsumption = -MaxPowerOutput * Math.Min(turbineOutput / 100.0f, temperatureFactor);
//if the turbine output and coolant flow are the optimal range,
//make the generated power slightly adjust according to the load
// (-> the reactor can automatically handle small changes in load as long as the values are roughly correct)
if (turbineOutput > optimalTurbineOutput.X && turbineOutput < optimalTurbineOutput.Y &&
temperature > optimalTemperature.X && temperature < optimalTemperature.Y)
{
float maxAutoAdjust = maxPowerOutput * 0.1f;
autoAdjustAmount = MathHelper.Lerp(
autoAdjustAmount,
MathHelper.Clamp(-load - currPowerConsumption, -maxAutoAdjust, maxAutoAdjust),
deltaTime * 10.0f);
}
else
{
autoAdjustAmount = MathHelper.Lerp(autoAdjustAmount, 0.0f, deltaTime * 10.0f);
}
currPowerConsumption += autoAdjustAmount;
if (!PowerOn)
{
targetFissionRate = 0.0f;
targetTurbineOutput = 0.0f;
}
else if (autoTemp)
{
UpdateAutoTemp(2.0f, deltaTime);
}
float currentLoad = 0.0f;
List<Connection> connections = item.Connections;
if (connections != null && connections.Count > 0)
{
foreach (Connection connection in connections)
{
if (!connection.IsPower) { continue; }
foreach (Connection recipient in connection.Recipients)
{
if (!(recipient.Item is Item it)) { continue; }
PowerTransfer pt = it.GetComponent<PowerTransfer>();
if (pt == null) { continue; }
//calculate how much external power there is in the grid
//(power coming from somewhere else than this reactor, e.g. batteries)
float externalPower = Math.Max(CurrPowerConsumption - pt.CurrPowerConsumption, 0) * 0.95f;
//reduce the external power from the load to prevent overloading the grid
currentLoad = Math.Max(currentLoad, pt.PowerLoad - externalPower);
}
}
}
loadQueue.Enqueue(currentLoad);
while (loadQueue.Count() > 60.0f)
{
load = loadQueue.Average();
loadQueue.Dequeue();
}
if (fissionRate > 0.0f)
{
foreach (Item item in item.ContainedItems)
{
if (!item.HasTag("reactorfuel")) continue;
item.Condition -= fissionRate / 100.0f * fuelConsumptionRate * deltaTime;
}
if (item.CurrentHull != null)
{
var aiTarget = item.CurrentHull.AiTarget;
float range = Math.Abs(currPowerConsumption) / MaxPowerOutput;
float noise = MathHelper.Lerp(aiTarget.MinSoundRange, aiTarget.MaxSoundRange, range);
aiTarget.SoundRange = Math.Max(aiTarget.SoundRange, noise);
}
if (item.AiTarget != null)
{
var aiTarget = item.AiTarget;
float range = Math.Abs(currPowerConsumption) / MaxPowerOutput;
aiTarget.SoundRange = MathHelper.Lerp(aiTarget.MinSoundRange, aiTarget.MaxSoundRange, range);
}
}
item.SendSignal(0, ((int)(temperature * 100.0f)).ToString(), "temperature_out", null);
UpdateFailures(deltaTime);
#if CLIENT
UpdateGraph(deltaTime);
#endif
AvailableFuel = 0.0f;
sendUpdateTimer = Math.Max(sendUpdateTimer - deltaTime, 0.0f);
if (unsentChanges && sendUpdateTimer <= 0.0f)
{
#if SERVER
if (GameMain.Server != null)
{
item.CreateServerEvent(this);
}
#endif
#if CLIENT
if (GameMain.Client != null)
{
item.CreateClientEvent(this);
}
#endif
sendUpdateTimer = NetworkUpdateInterval;
unsentChanges = false;
}
}
private float GetGeneratedHeat(float fissionRate)
{
return fissionRate * (prevAvailableFuel / 100.0f) * 2.0f;
}
/// <summary>
/// Do we need more fuel to generate enough power to match the current load.
/// </summary>
/// <param name="minimumOutputRatio">How low we allow the output/load ratio to go before loading more fuel.
/// 1.0 = always load more fuel when maximum output is too low, 0.5 = load more if max output is 50% of the load</param>
private bool NeedMoreFuel(float minimumOutputRatio, float minCondition = 0)
{
float remainingFuel = item.ContainedItems.Sum(i => i.Condition);
if (remainingFuel <= minCondition && load > 0.0f)
{
return true;
}
//fission rate is clamped to the amount of available fuel
float maxFissionRate = Math.Min(prevAvailableFuel, 100.0f);
float maxTurbineOutput = 100.0f;
//calculate the maximum output if the fission rate is cranked as high as it goes and turbine output is at max
float theoreticalMaxHeat = GetGeneratedHeat(fissionRate: maxFissionRate);
float temperatureFactor = Math.Min(theoreticalMaxHeat / 50.0f, 1.0f);
float theoreticalMaxOutput = Math.Min(maxTurbineOutput / 100.0f, temperatureFactor) * MaxPowerOutput;
//maximum output not enough, we need more fuel
return theoreticalMaxOutput < load * minimumOutputRatio;
}
private bool TooMuchFuel()
{
var containedItems = item.ContainedItems;
if (containedItems != null && containedItems.Count() <= 1) { return false; }
//get the amount of heat we'd generate if the fission rate was at the low end of the optimal range
float minimumHeat = GetGeneratedHeat(optimalFissionRate.X);
//if we need a very high turbine output to keep the engine from overheating, there's too much fuel
return minimumHeat > Math.Min(correctTurbineOutput * 1.5f, 90);
}
private void UpdateFailures(float deltaTime)
{
if (temperature > allowedTemperature.Y)
{
item.SendSignal(0, "1", "meltdown_warning", null);
//faster meltdown if the item is in a bad condition
meltDownTimer += MathHelper.Lerp(deltaTime * 2.0f, deltaTime, item.Condition / item.MaxCondition);
if (meltDownTimer > MeltdownDelay)
{
MeltDown();
return;
}
}
else
{
item.SendSignal(0, "0", "meltdown_warning", null);
meltDownTimer = Math.Max(0.0f, meltDownTimer - deltaTime);
}
if (temperature > optimalTemperature.Y)
{
float prevFireTimer = fireTimer;
fireTimer += MathHelper.Lerp(deltaTime * 2.0f, deltaTime, item.Condition / item.MaxCondition);
#if SERVER
if (fireTimer > Math.Min(5.0f, FireDelay / 2) && blameOnBroken?.Character?.SelectedConstruction == item)
{
GameMain.Server.KarmaManager.OnReactorOverHeating(blameOnBroken.Character, deltaTime);
}
#endif
if (fireTimer >= FireDelay && prevFireTimer < fireDelay)
{
new FireSource(item.WorldPosition);
}
}
else
{
fireTimer = Math.Max(0.0f, fireTimer - deltaTime);
}
}
private void UpdateAutoTemp(float speed, float deltaTime)
{
float desiredTurbineOutput = (optimalTurbineOutput.X + optimalTurbineOutput.Y) / 2.0f;
targetTurbineOutput += MathHelper.Clamp(desiredTurbineOutput - targetTurbineOutput, -speed, speed) * deltaTime;
targetTurbineOutput = MathHelper.Clamp(targetTurbineOutput, 0.0f, 100.0f);
float desiredFissionRate = (optimalFissionRate.X + optimalFissionRate.Y) / 2.0f;
targetFissionRate += MathHelper.Clamp(desiredFissionRate - targetFissionRate, -speed, speed) * deltaTime;
if (temperature > (optimalTemperature.X + optimalTemperature.Y) / 2.0f)
{
targetFissionRate = Math.Min(targetFissionRate - speed * 2 * deltaTime, allowedFissionRate.Y);
}
else if (-currPowerConsumption < load)
{
targetFissionRate = Math.Min(targetFissionRate + speed * 2 * deltaTime, 100.0f);
}
targetFissionRate = MathHelper.Clamp(targetFissionRate, 0.0f, 100.0f);
//don't push the target too far from the current fission rate
//otherwise we may "overshoot", cranking the target fission rate all the way up because it takes a while
//for the actual fission rate and temperature to follow
targetFissionRate = MathHelper.Clamp(targetFissionRate, FissionRate - 5, FissionRate + 5);
}
public override void UpdateBroken(float deltaTime, Camera cam)
{
base.UpdateBroken(deltaTime, cam);
item.SendSignal(0, ((int)(temperature * 100.0f)).ToString(), "temperature_out", null);
currPowerConsumption = 0.0f;
Temperature -= deltaTime * 1000.0f;
targetFissionRate = Math.Max(targetFissionRate - deltaTime * 10.0f, 0.0f);
targetTurbineOutput = Math.Max(targetTurbineOutput - deltaTime * 10.0f, 0.0f);
#if CLIENT
FissionRateScrollBar.BarScroll = 1.0f - FissionRate / 100.0f;
TurbineOutputScrollBar.BarScroll = 1.0f - TurbineOutput / 100.0f;
UpdateGraph(deltaTime);
#endif
}
private void MeltDown()
{
if (item.Condition <= 0.0f) { return; }
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
item.Condition = 0.0f;
fireTimer = 0.0f;
meltDownTimer = 0.0f;
var containedItems = item.ContainedItems;
if (containedItems != null)
{
foreach (Item containedItem in containedItems)
{
if (containedItem == null) continue;
containedItem.Condition = 0.0f;
}
}
#if SERVER
GameServer.Log("Reactor meltdown!", ServerLog.MessageType.ItemInteraction);
if (GameMain.Server != null)
{
GameMain.Server.KarmaManager.OnReactorMeltdown(blameOnBroken?.Character);
}
#endif
}
public override bool Pick(Character picker)
{
return picker != null;
}
public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return false; }
IsActive = true;
float degreeOfSuccess = DegreeOfSuccess(character);
float refuelLimit = 0.3f;
//characters with insufficient skill levels don't refuel the reactor
if (degreeOfSuccess > refuelLimit)
{
if (objective.SubObjectives.None())
{
if (!AIDecontainEmptyItems(character, objective, equip: false))
{
return false;
}
}
if (aiUpdateTimer > 0.0f)
{
aiUpdateTimer -= deltaTime;
return false;
}
aiUpdateTimer = AIUpdateInterval;
// load more fuel if the current maximum output is only 50% of the current load
// or if the fuel rod is (almost) deplenished
float minCondition = fuelConsumptionRate * MathUtils.Pow((degreeOfSuccess - refuelLimit) * 2, 2);
if (NeedMoreFuel(minimumOutputRatio: 0.5f, minCondition: minCondition))
{
var container = item.GetComponent<ItemContainer>();
if (objective.SubObjectives.None())
{
int itemCount = item.ContainedItems.Count(i => i != null && container.ContainableItems.Any(ri => ri.MatchesItem(i))) + 1;
AIContainItems<Reactor>(container, character, objective, itemCount, equip: false, removeEmpty: true);
character.Speak(TextManager.Get("DialogReactorFuel"), null, 0.0f, "reactorfuel", 30.0f);
}
return false;
}
else if (TooMuchFuel())
{
var container = item.GetComponent<ItemContainer>();
foreach (Item item in item.ContainedItems)
{
if (item != null && container.ContainableItems.Any(ri => ri.MatchesItem(item)))
{
if (!character.Inventory.TryPutItem(item, character, allowedSlots: item.AllowedSlots))
{
item.Drop(character);
}
break;
}
}
}
}
if (objective.Override)
{
if (lastUser != null && lastUser != character && lastUser != lastAIUser)
{
if (lastUser.SelectedConstruction == item)
{
character.Speak(TextManager.Get("DialogReactorTaken"), null, 0.0f, "reactortaken", 10.0f);
}
}
}
LastUser = lastAIUser = character;
bool prevAutoTemp = autoTemp;
bool prevPowerOn = _powerOn;
float prevFissionRate = targetFissionRate;
float prevTurbineOutput = targetTurbineOutput;
switch (objective.Option.ToLowerInvariant())
{
case "powerup":
PowerOn = true;
if (objective.Override || !autoTemp)
{
//characters with insufficient skill levels simply set the autotemp on instead of trying to adjust the temperature manually
if (degreeOfSuccess < 0.5f)
{
AutoTemp = true;
}
else
{
AutoTemp = false;
UpdateAutoTemp(MathHelper.Lerp(0.5f, 2.0f, degreeOfSuccess), 1.0f);
}
}
#if CLIENT
FissionRateScrollBar.BarScroll = FissionRate / 100.0f;
TurbineOutputScrollBar.BarScroll = TurbineOutput / 100.0f;
#endif
break;
case "shutdown":
PowerOn = false;
AutoTemp = false;
targetFissionRate = 0.0f;
targetTurbineOutput = 0.0f;
break;
}
if (autoTemp != prevAutoTemp ||
prevPowerOn != _powerOn ||
Math.Abs(prevFissionRate - targetFissionRate) > 1.0f ||
Math.Abs(prevTurbineOutput - targetTurbineOutput) > 1.0f)
{
unsentChanges = true;
}
aiUpdateTimer = AIUpdateInterval;
return false;
}
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power, float signalStrength = 1.0f)
{
switch (connection.Name)
{
case "shutdown":
if (targetFissionRate > 0.0f || targetTurbineOutput > 0.0f)
{
PowerOn = false;
AutoTemp = false;
targetFissionRate = 0.0f;
targetTurbineOutput = 0.0f;
unsentChanges = true;
}
break;
case "set_fissionrate":
if (float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float newFissionRate))
{
FissionRate = newFissionRate;
unsentChanges = true;
}
break;
case "set_turbineoutput":
if (float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float newTurbineOutput))
{
TurbineOutput = newTurbineOutput;
unsentChanges = true;
}
break;
}
}
}
}
@@ -0,0 +1,392 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
partial class Sonar : Powered, IServerSerializable, IClientSerializable
{
public enum Mode
{
Active,
Passive
};
public const float DefaultSonarRange = 10000.0f;
class ConnectedTransducer
{
public readonly SonarTransducer Transducer;
public float SignalStrength;
public float DisconnectTimer;
public ConnectedTransducer(SonarTransducer transducer, float signalStrength, float disconnectTimer)
{
Transducer = transducer;
SignalStrength = signalStrength;
DisconnectTimer = disconnectTimer;
}
}
private const float DirectionalPingSector = 30.0f;
private static readonly float DirectionalPingDotProduct;
static Sonar()
{
DirectionalPingDotProduct = (float)Math.Cos(MathHelper.ToRadians(DirectionalPingSector) * 0.5f);
}
private float range;
private const float PingFrequency = 0.5f;
private Mode currentMode = Mode.Passive;
private class ActivePing
{
public float State;
public bool IsDirectional;
public Vector2 Direction;
public float PrevPingRadius;
}
// rotating list of currently active pings
private ActivePing[] activePings = new ActivePing[8];
// total number of currently active pings, range [0, activePings.Length[
private int activePingsCount;
// currently active ping index on the above list
private int currentPingIndex = -1;
private const float MinZoom = 1.0f, MaxZoom = 4.0f;
private float zoom = 1.0f;
private bool useDirectionalPing = false;
private Vector2 pingDirection = new Vector2(1.0f, 0.0f);
private bool aiPingCheckPending;
//the float value is a timer used for disconnecting the transducer if no signal is received from it for 1 second
private readonly List<ConnectedTransducer> connectedTransducers;
public IEnumerable<SonarTransducer> ConnectedTransducers
{
get { return connectedTransducers.Select(t => t.Transducer); }
}
[Serialize(DefaultSonarRange, false, description: "The maximum range of the sonar.")]
public float Range
{
get { return range; }
set
{
range = MathHelper.Clamp(value, 0.0f, 100000.0f);
if (item?.AiTarget != null && item.AiTarget.MaxSoundRange <= 0)
{
item.AiTarget.MaxSoundRange = range;
}
}
}
[Serialize(false, false, description: "Should the sonar display the walls of the submarine it is inside.")]
public bool DetectSubmarineWalls
{
get;
set;
}
[Editable, Serialize(false, false, description: "Does the sonar have to be connected to external transducers to work.")]
public bool UseTransducers
{
get;
set;
}
public float Zoom
{
get { return zoom; }
}
public Mode CurrentMode
{
get => currentMode;
set
{
bool changed = currentMode != value;
currentMode = value;
if (value == Mode.Passive)
{
if (item.AiTarget != null)
{
item.AiTarget.SectorDegrees = 360.0f;
}
}
#if CLIENT
if (changed) { prevPassivePingRadius = float.MaxValue; }
UpdateGUIElements();
#endif
}
}
public Sonar(Item item, XElement element)
: base(item, element)
{
connectedTransducers = new List<ConnectedTransducer>();
IsActive = true;
InitProjSpecific(element);
CurrentMode = Mode.Passive;
}
partial void InitProjSpecific(XElement element);
public override void Update(float deltaTime, Camera cam)
{
currPowerConsumption = powerConsumption;
UpdateOnActiveEffects(deltaTime);
if (UseTransducers)
{
foreach (ConnectedTransducer transducer in connectedTransducers)
{
transducer.DisconnectTimer -= deltaTime;
}
connectedTransducers.RemoveAll(t => t.DisconnectTimer <= 0.0f);
}
for (var pingIndex = 0; pingIndex < activePingsCount; ++pingIndex)
{
activePings[pingIndex].State += deltaTime * PingFrequency;
}
if (currentMode == Mode.Active)
{
if ((Voltage >= MinVoltage) &&
(!UseTransducers || connectedTransducers.Count > 0))
{
if (currentPingIndex != -1)
{
var activePing = activePings[currentPingIndex];
if (activePing.State > 1.0f)
{
if (item.AiTarget != null)
{
float range = MathUtils.InverseLerp(item.AiTarget.MinSoundRange, item.AiTarget.MaxSoundRange, Range * activePing.State / zoom);
item.AiTarget.SoundRange = MathHelper.Lerp(item.AiTarget.MinSoundRange, item.AiTarget.MaxSoundRange, range);
item.AiTarget.SectorDegrees = activePing.IsDirectional ? DirectionalPingSector : 360.0f;
item.AiTarget.SectorDir = new Vector2(pingDirection.X, -pingDirection.Y);
}
aiPingCheckPending = true;
currentPingIndex = -1;
}
}
if (currentPingIndex == -1 && activePingsCount < activePings.Length)
{
currentPingIndex = activePingsCount++;
if (activePings[currentPingIndex] == null)
{
activePings[currentPingIndex] = new ActivePing();
}
activePings[currentPingIndex].IsDirectional = useDirectionalPing;
activePings[currentPingIndex].Direction = pingDirection;
activePings[currentPingIndex].State = 0.0f;
activePings[currentPingIndex].PrevPingRadius = 0.0f;
item.Use(deltaTime);
}
}
else
{
if (item.AiTarget != null)
{
item.AiTarget.SectorDegrees = 360.0f;
}
aiPingCheckPending = false;
}
}
for (var pingIndex = 0; pingIndex < activePingsCount;)
{
if (activePings[pingIndex].State > 1.0f)
{
var lastIndex = --activePingsCount;
var oldActivePing = activePings[pingIndex];
activePings[pingIndex] = activePings[lastIndex];
activePings[lastIndex] = oldActivePing;
if (currentPingIndex == lastIndex)
{
currentPingIndex = pingIndex;
}
}
else
{
++pingIndex;
}
}
Voltage -= deltaTime;
}
public override bool Use(float deltaTime, Character character = null)
{
return currentPingIndex != -1;
}
private static readonly Dictionary<string, List<Character>> targetGroups = new Dictionary<string, List<Character>>();
public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
{
if (currentMode == Mode.Passive || !aiPingCheckPending) { return false; }
foreach (List<Character> targetGroup in targetGroups.Values)
{
targetGroup.Clear();
}
foreach (Character c in Character.CharacterList)
{
if (c.AnimController.CurrentHull != null || !c.Enabled) continue;
if (DetectSubmarineWalls && c.AnimController.CurrentHull == null && item.CurrentHull != null) continue;
if (Vector2.DistanceSquared(c.WorldPosition, item.WorldPosition) > range * range) continue;
string directionName = GetDirectionName(c.WorldPosition - item.WorldPosition);
if (!targetGroups.ContainsKey(directionName))
{
targetGroups.Add(directionName, new List<Character>());
}
targetGroups[directionName].Add(c);
}
foreach (KeyValuePair<string, List<Character>> targetGroup in targetGroups)
{
if (!targetGroup.Value.Any()) { continue; }
string dialogTag = "DialogSonarTarget";
if (targetGroup.Value.Count > 1)
{
dialogTag = "DialogSonarTargetMultiple";
}
else if (targetGroup.Value[0].Mass > 100.0f)
{
dialogTag = "DialogSonarTargetLarge";
}
character.Speak(TextManager.GetWithVariables(dialogTag, new string[2] { "[direction]", "[count]" },
new string[2] { targetGroup.Key.ToString(), targetGroup.Value.Count.ToString() },
new bool[2] { true, false }), null, 0, "sonartarget" + targetGroup.Value[0].ID, 60);
//prevent the character from reporting other targets in the group
for (int i = 1; i < targetGroup.Value.Count; i++)
{
character.DisableLine("sonartarget" + targetGroup.Value[i].ID);
}
}
return true;
}
private string GetDirectionName(Vector2 dir)
{
float angle = MathUtils.WrapAngleTwoPi((float)-Math.Atan2(dir.Y, dir.X) + MathHelper.PiOver2);
int clockDir = (int)Math.Round((angle / MathHelper.TwoPi) * 12);
if (clockDir == 0) clockDir = 12;
return TextManager.GetWithVariable("roomname.subdiroclock", "[dir]", clockDir.ToString());
}
private Vector2 GetTransducerPos()
{
if (!UseTransducers || connectedTransducers.Count == 0)
{
//use the position of the sub if the item is static (no body) and inside a sub
return item.Submarine != null && item.body == null ? item.Submarine.WorldPosition : item.WorldPosition;
}
Vector2 transducerPosSum = Vector2.Zero;
foreach (ConnectedTransducer transducer in connectedTransducers)
{
if (transducer.Transducer.Item.Submarine != null)
{
return transducer.Transducer.Item.Submarine.WorldPosition;
}
transducerPosSum += transducer.Transducer.Item.WorldPosition;
}
return transducerPosSum / connectedTransducers.Count;
}
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0, float signalStrength = 1.0f)
{
base.ReceiveSignal(stepsTaken, signal, connection, source, sender, power, signalStrength);
if (connection.Name == "transducer_in")
{
var transducer = source.GetComponent<SonarTransducer>();
if (transducer == null) return;
var connectedTransducer = connectedTransducers.Find(t => t.Transducer == transducer);
if (connectedTransducer == null)
{
connectedTransducers.Add(new ConnectedTransducer(transducer, signalStrength, 1.0f));
}
else
{
connectedTransducer.SignalStrength = signalStrength;
connectedTransducer.DisconnectTimer = 1.0f;
}
}
}
public void ServerRead(ClientNetObject type, IReadMessage msg, Client c)
{
bool isActive = msg.ReadBoolean();
bool directionalPing = useDirectionalPing;
float zoomT = zoom, pingDirectionT = 0.0f;
if (isActive)
{
zoomT = msg.ReadRangedSingle(0.0f, 1.0f, 8);
directionalPing = msg.ReadBoolean();
if (directionalPing)
{
pingDirectionT = msg.ReadRangedSingle(0.0f, 1.0f, 8);
}
}
if (!item.CanClientAccess(c)) { return; }
CurrentMode = isActive ? Mode.Active : Mode.Passive;
if (isActive)
{
zoom = MathHelper.Lerp(MinZoom, MaxZoom, zoomT);
useDirectionalPing = directionalPing;
if (useDirectionalPing)
{
float pingAngle = MathHelper.Lerp(0.0f, MathHelper.TwoPi, pingDirectionT);
pingDirection = new Vector2((float)Math.Cos(pingAngle), (float)Math.Sin(pingAngle));
}
#if CLIENT
zoomSlider.BarScroll = zoomT;
directionalModeSwitch.Selected = useDirectionalPing;
#endif
}
#if SERVER
item.CreateServerEvent(this);
#endif
}
public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
{
msg.Write(currentMode == Mode.Active);
if (currentMode == Mode.Active)
{
msg.WriteRangedSingle(zoom, MinZoom, MaxZoom, 8);
msg.Write(useDirectionalPing);
if (useDirectionalPing)
{
float pingAngle = MathUtils.WrapAngleTwoPi(MathUtils.VectorToAngle(pingDirection));
msg.WriteRangedSingle(MathUtils.InverseLerp(0.0f, MathHelper.TwoPi, pingAngle), 0.0f, 1.0f, 8);
}
}
}
}
}
@@ -0,0 +1,33 @@
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
class SonarTransducer : Powered
{
const float SendSignalInterval = 0.5f;
private float sendSignalTimer;
public SonarTransducer(Item item, XElement element) : base(item, element)
{
IsActive = true;
}
public override void Update(float deltaTime, Camera cam)
{
UpdateOnActiveEffects(deltaTime);
CurrPowerConsumption = powerConsumption;
if (Voltage >= MinVoltage)
{
sendSignalTimer += deltaTime;
if (sendSignalTimer > SendSignalInterval)
{
item.SendSignal(0, "0101101101101011010", "data_out", sender: null);
sendSignalTimer = SendSignalInterval;
}
}
}
}
}
@@ -0,0 +1,619 @@
using Barotrauma.Networking;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;
using Voronoi2;
namespace Barotrauma.Items.Components
{
partial class Steering : Powered, IServerSerializable, IClientSerializable
{
private const float AutopilotRayCastInterval = 0.5f;
private const float RecalculatePathInterval = 5.0f;
private const float AutopilotMinDistToPathNode = 30.0f;
private const float AutoPilotSteeringLerp = 0.1f;
private const float AutoPilotMaxSpeed = 0.5f;
private const float AIPilotMaxSpeed = 1.0f;
private Vector2 currVelocity;
private Vector2 targetVelocity;
private Vector2 steeringInput;
private bool autoPilot;
private Vector2? posToMaintain;
private SteeringPath steeringPath;
private PathFinder pathFinder;
private float networkUpdateTimer;
private bool unsentChanges;
private float autopilotRayCastTimer;
private float autopilotRecalculatePathTimer;
private Vector2 avoidStrength;
private float neutralBallastLevel;
private float steeringAdjustSpeed = 1.0f;
private Character user;
private Sonar sonar;
private Submarine controlledSub;
public bool AutoPilot
{
get { return autoPilot; }
set
{
if (value == autoPilot) { return; }
autoPilot = value;
#if CLIENT
UpdateGUIElements();
#endif
if (autoPilot)
{
if (pathFinder == null)
{
pathFinder = new PathFinder(WayPoint.WayPointList, false);
}
MaintainPos = true;
if (posToMaintain == null)
{
posToMaintain = controlledSub != null ?
controlledSub.WorldPosition :
item.Submarine == null ? item.WorldPosition : item.Submarine.WorldPosition;
}
}
else
{
PosToMaintain = null;
MaintainPos = false;
LevelEndSelected = false;
LevelStartSelected = false;
}
}
}
[Editable(0.0f, 1.0f, decimals: 3),
Serialize(0.5f, true, description: "How full the ballast tanks should be when the submarine is not being steered upwards/downwards."
+ " Can be used to compensate if the ballast tanks are too large/small relative to the size of the submarine.")]
public float NeutralBallastLevel
{
get { return neutralBallastLevel; }
set
{
neutralBallastLevel = MathHelper.Clamp(value, 0.0f, 1.0f);
}
}
[Serialize(1000.0f, true, description: "How close the docking port has to be to another docking port for the docking mode to become active.")]
public float DockingAssistThreshold
{
get;
set;
}
public Vector2 TargetVelocity
{
get { return targetVelocity;}
set
{
if (!MathUtils.IsValid(value)) return;
targetVelocity.X = MathHelper.Clamp(value.X, -100.0f, 100.0f);
targetVelocity.Y = MathHelper.Clamp(value.Y, -100.0f, 100.0f);
}
}
public Vector2 SteeringInput
{
get { return steeringInput; }
set
{
if (!MathUtils.IsValid(value)) return;
steeringInput.X = MathHelper.Clamp(value.X, -100.0f, 100.0f);
steeringInput.Y = MathHelper.Clamp(value.Y, -100.0f, 100.0f);
}
}
public SteeringPath SteeringPath
{
get { return steeringPath; }
}
public Vector2? PosToMaintain
{
get { return posToMaintain; }
set { posToMaintain = value; }
}
struct ObstacleDebugInfo
{
public Vector2 Point1;
public Vector2 Point2;
public Vector2? Intersection;
public float Dot;
public Vector2 AvoidStrength;
public ObstacleDebugInfo(GraphEdge edge, Vector2? intersection, float dot, Vector2 avoidStrength, Vector2 translation)
{
Point1 = edge.Point1 + translation;
Point2 = edge.Point2 + translation;
Intersection = intersection;
Dot = dot;
AvoidStrength = avoidStrength;
}
}
//edge point 1, edge point 2, avoid strength
private List<ObstacleDebugInfo> debugDrawObstacles = new List<ObstacleDebugInfo>();
#region Docking
public List<DockingPort> DockingSources = new List<DockingPort>();
public DockingPort ActiveDockingSource, DockingTarget;
private bool searchedConnectedDockingPort;
private bool dockingModeEnabled;
public bool DockingModeEnabled
{
get { return UseAutoDocking && dockingModeEnabled; }
set { dockingModeEnabled = value; }
}
public bool UseAutoDocking
{
get;
set;
} = true;
private void FindConnectedDockingPort()
{
searchedConnectedDockingPort = true;
foreach (MapEntity linkedTo in item.linkedTo)
{
if (linkedTo is Item item)
{
var port = item.GetComponent<DockingPort>();
if (port != null)
{
DockingSources.Add(port);
}
}
}
var dockingConnection = item.Connections.FirstOrDefault(c => c.Name == "toggle_docking");
if (dockingConnection != null)
{
var connectedPorts = item.GetConnectedComponentsRecursive<DockingPort>(dockingConnection);
DockingSources.AddRange(connectedPorts.Where(p => p.Item.Submarine != null && !p.Item.Submarine.IsOutpost));
}
}
#endregion
public Steering(Item item, XElement element)
: base(item, element)
{
IsActive = true;
InitProjSpecific(element);
}
partial void InitProjSpecific(XElement element);
public override void OnItemLoaded()
{
base.OnItemLoaded();
sonar = item.GetComponent<Sonar>();
}
public override bool Select(Character character)
{
if (!CanBeSelected) return false;
user = character;
return true;
}
public override void Update(float deltaTime, Camera cam)
{
if (!searchedConnectedDockingPort)
{
FindConnectedDockingPort();
}
networkUpdateTimer -= deltaTime;
if (unsentChanges)
{
if (networkUpdateTimer <= 0.0f)
{
#if CLIENT
if (GameMain.Client != null)
{
item.CreateClientEvent(this);
correctionTimer = CorrectionDelay;
}
else
#endif
#if SERVER
if (GameMain.Server != null)
{
item.CreateServerEvent(this);
}
#endif
networkUpdateTimer = 0.1f;
unsentChanges = false;
}
}
controlledSub = item.Submarine;
var sonar = item.GetComponent<Sonar>();
if (sonar != null && sonar.UseTransducers)
{
controlledSub = sonar.ConnectedTransducers.Any() ? sonar.ConnectedTransducers.First().Item.Submarine : null;
}
currPowerConsumption = powerConsumption;
if (Voltage < MinVoltage) { return; }
if (user != null && user.Removed)
{
user = null;
}
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
float userSkill = 0.0f;
if (user != null && (user.SelectedConstruction == item || item.linkedTo.Contains(user.SelectedConstruction)))
{
userSkill = user.GetSkillLevel("helm") / 100.0f;
}
if (AutoPilot)
{
UpdateAutoPilot(deltaTime);
targetVelocity = targetVelocity.ClampLength(MathHelper.Lerp(AutoPilotMaxSpeed, AIPilotMaxSpeed, userSkill) * 100.0f);
}
else
{
if (user != null && user.Info != null && user.SelectedConstruction == item)
{
user.Info.IncreaseSkillLevel(
"helm",
SkillSettings.Current.SkillIncreasePerSecondWhenSteering / Math.Max(userSkill, 1.0f) * deltaTime,
user.WorldPosition + Vector2.UnitY * 150.0f);
}
Vector2 velocityDiff = steeringInput - targetVelocity;
if (velocityDiff != Vector2.Zero)
{
if (steeringAdjustSpeed >= 0.99f)
{
TargetVelocity = steeringInput;
}
else
{
float steeringChange = 1.0f / (1.0f - steeringAdjustSpeed);
steeringChange *= steeringChange * 10.0f;
TargetVelocity += Vector2.Normalize(velocityDiff) *
Math.Min(steeringChange * deltaTime, velocityDiff.Length());
}
}
}
item.SendSignal(0, targetVelocity.X.ToString(CultureInfo.InvariantCulture), "velocity_x_out", null);
float targetLevel = -targetVelocity.Y;
targetLevel += (neutralBallastLevel - 0.5f) * 100.0f;
item.SendSignal(0, targetLevel.ToString(CultureInfo.InvariantCulture), "velocity_y_out", null);
}
private void UpdateAutoPilot(float deltaTime)
{
if (controlledSub == null) { return; }
if (posToMaintain != null)
{
Vector2 steeringVel = GetSteeringVelocity((Vector2)posToMaintain, 10.0f);
TargetVelocity = Vector2.Lerp(TargetVelocity, steeringVel, AutoPilotSteeringLerp);
return;
}
autopilotRayCastTimer -= deltaTime;
autopilotRecalculatePathTimer -= deltaTime;
if (autopilotRecalculatePathTimer <= 0.0f)
{
//periodically recalculate the path in case the sub ends up to a position
//where it can't keep traversing the initially calculated path
UpdatePath();
autopilotRecalculatePathTimer = RecalculatePathInterval;
}
steeringPath.CheckProgress(ConvertUnits.ToSimUnits(controlledSub.WorldPosition), 10.0f);
if (autopilotRayCastTimer <= 0.0f && steeringPath.NextNode != null)
{
Vector2 diff = ConvertUnits.ToSimUnits(steeringPath.NextNode.Position - controlledSub.WorldPosition);
//if the node is close enough, check if it's visible
float lengthSqr = diff.LengthSquared();
if (lengthSqr > 0.001f && lengthSqr < AutopilotMinDistToPathNode * AutopilotMinDistToPathNode)
{
diff = Vector2.Normalize(diff);
//check if the next waypoint is visible from all corners of the sub
//(i.e. if we can navigate directly towards it or if there's obstacles in the way)
bool nextVisible = true;
for (int x = -1; x < 2; x += 2)
{
for (int y = -1; y < 2; y += 2)
{
Vector2 cornerPos =
new Vector2(controlledSub.Borders.Width * x, controlledSub.Borders.Height * y) / 2.0f;
cornerPos = ConvertUnits.ToSimUnits(cornerPos * 1.1f + controlledSub.WorldPosition);
float dist = Vector2.Distance(cornerPos, steeringPath.NextNode.SimPosition);
if (Submarine.PickBody(cornerPos, cornerPos + diff * dist, null, Physics.CollisionLevel) == null) { continue; }
nextVisible = false;
x = 2;
y = 2;
}
}
if (nextVisible) steeringPath.SkipToNextNode();
}
autopilotRayCastTimer = AutopilotRayCastInterval;
}
Vector2 newVelocity = Vector2.Zero;
if (steeringPath.CurrentNode != null)
{
newVelocity = GetSteeringVelocity(steeringPath.CurrentNode.WorldPosition, 2.0f);
}
Vector2 avoidDist = new Vector2(
Math.Max(1000.0f * Math.Abs(controlledSub.Velocity.X), controlledSub.Borders.Width * 0.75f),
Math.Max(1000.0f * Math.Abs(controlledSub.Velocity.Y), controlledSub.Borders.Height * 0.75f));
float avoidRadius = avoidDist.Length();
Vector2 newAvoidStrength = Vector2.Zero;
debugDrawObstacles.Clear();
//steer away from nearby walls
var closeCells = Level.Loaded.GetCells(controlledSub.WorldPosition, 4);
foreach (VoronoiCell cell in closeCells)
{
foreach (GraphEdge edge in cell.Edges)
{
if (MathUtils.GetLineIntersection(edge.Point1 + cell.Translation, edge.Point2 + cell.Translation, controlledSub.WorldPosition, cell.Center, out Vector2 intersection))
{
Vector2 diff = controlledSub.WorldPosition - intersection;
//far enough -> ignore
if (Math.Abs(diff.X) > avoidDist.X && Math.Abs(diff.Y) > avoidDist.Y)
{
debugDrawObstacles.Add(new ObstacleDebugInfo(edge, intersection, 0.0f, Vector2.Zero, Vector2.Zero));
continue;
}
if (diff.LengthSquared() < 1.0f) diff = Vector2.UnitY;
Vector2 normalizedDiff = Vector2.Normalize(diff);
float dot = controlledSub.Velocity == Vector2.Zero ?
0.0f : Vector2.Dot(controlledSub.Velocity, -normalizedDiff);
//not heading towards the wall -> ignore
if (dot < 1.0)
{
debugDrawObstacles.Add(new ObstacleDebugInfo(edge, intersection, dot, Vector2.Zero, cell.Translation));
continue;
}
Vector2 change = (normalizedDiff * Math.Max((avoidRadius - diff.Length()), 0.0f)) / avoidRadius;
if (change.LengthSquared() < 0.001f) { continue; }
newAvoidStrength += change * (dot - 1.0f);
debugDrawObstacles.Add(new ObstacleDebugInfo(edge, intersection, dot - 1.0f, change * (dot - 1.0f), cell.Translation));
}
}
}
avoidStrength = Vector2.Lerp(avoidStrength, newAvoidStrength, deltaTime * 10.0f);
TargetVelocity = Vector2.Lerp(TargetVelocity, newVelocity + avoidStrength * 100.0f, AutoPilotSteeringLerp);
//steer away from other subs
foreach (Submarine sub in Submarine.Loaded)
{
if (sub == controlledSub) continue;
if (controlledSub.DockedTo.Contains(sub)) continue;
float thisSize = Math.Max(controlledSub.Borders.Width, controlledSub.Borders.Height);
float otherSize = Math.Max(sub.Borders.Width, sub.Borders.Height);
Vector2 diff = controlledSub.WorldPosition - sub.WorldPosition;
float dist = diff == Vector2.Zero ? 0.0f : diff.Length();
//far enough -> ignore
if (dist > thisSize + otherSize) continue;
Vector2 dir = dist <= 0.0001f ? Vector2.UnitY : diff / dist;
float dot = controlledSub.Velocity == Vector2.Zero ?
0.0f : Vector2.Dot(Vector2.Normalize(controlledSub.Velocity), -dir);
//heading away -> ignore
if (dot < 0.0f) continue;
targetVelocity += diff * 200.0f;
}
//clamp velocity magnitude to 100.0f
float velMagnitude = targetVelocity.Length();
if (velMagnitude > 100.0f)
{
targetVelocity *= 100.0f / velMagnitude;
}
}
private void UpdatePath()
{
if (pathFinder == null) pathFinder = new PathFinder(WayPoint.WayPointList, false);
Vector2 target;
if (LevelEndSelected)
{
target = ConvertUnits.ToSimUnits(Level.Loaded.EndPosition);
}
else
{
target = ConvertUnits.ToSimUnits(Level.Loaded.StartPosition);
}
steeringPath = pathFinder.FindPath(ConvertUnits.ToSimUnits(controlledSub == null ? item.WorldPosition : controlledSub.WorldPosition), target, errorMsgStr: "(Autopilot, target: " + target + ")");
}
public void SetDestinationLevelStart()
{
AutoPilot = true;
MaintainPos = false;
posToMaintain = null;
LevelEndSelected = false;
if (!LevelStartSelected)
{
LevelStartSelected = true;
UpdatePath();
}
}
public void SetDestinationLevelEnd()
{
AutoPilot = true;
MaintainPos = false;
posToMaintain = null;
LevelStartSelected = false;
if (!LevelEndSelected)
{
LevelEndSelected = true;
UpdatePath();
}
}
/// <summary>
/// Get optimal velocity for moving towards a position
/// </summary>
/// <param name="worldPosition">Position to steer towards to</param>
/// <param name="slowdownAmount">How heavily the sub slows down when approaching the target</param>
/// <returns></returns>
private Vector2 GetSteeringVelocity(Vector2 worldPosition, float slowdownAmount)
{
Vector2 futurePosition = ConvertUnits.ToDisplayUnits(controlledSub.Velocity) * slowdownAmount;
Vector2 targetSpeed = ((worldPosition - controlledSub.WorldPosition) - futurePosition);
if (targetSpeed.LengthSquared() > 500.0f * 500.0f)
{
return Vector2.Normalize(targetSpeed) * 100.0f;
}
else
{
return targetSpeed / 5.0f;
}
}
private bool aiDockingToggled;
public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
{
if (objective.Override)
{
if (user != character && user != null && user.SelectedConstruction == item)
{
character.Speak(TextManager.Get("DialogSteeringTaken"), null, 0.0f, "steeringtaken", 10.0f);
}
}
user = character;
if (!AutoPilot)
{
unsentChanges = true;
AutoPilot = true;
}
switch (objective.Option.ToLowerInvariant())
{
case "maintainposition":
if (objective.Override)
{
if (!MaintainPos)
{
unsentChanges = true;
MaintainPos = true;
}
if (!posToMaintain.HasValue)
{
unsentChanges = true;
posToMaintain = controlledSub != null ?
controlledSub.WorldPosition :
item.Submarine == null ? item.WorldPosition : item.Submarine.WorldPosition;
}
}
break;
case "navigateback":
if (!aiDockingToggled && DockingSources.Any(d => d.Docked))
{
item.SendSignal(0, "1", "toggle_docking", sender: null);
}
if (objective.Override)
{
if (MaintainPos || LevelEndSelected || !LevelStartSelected)
{
unsentChanges = true;
}
SetDestinationLevelStart();
}
break;
case "navigatetodestination":
if (!aiDockingToggled && DockingSources.Any(d => d.Docked))
{
item.SendSignal(0, "1", "toggle_docking", sender: null);
}
if (objective.Override)
{
if (MaintainPos || !LevelEndSelected || LevelStartSelected)
{
unsentChanges = true;
}
SetDestinationLevelEnd();
}
break;
}
sonar?.AIOperate(deltaTime, character, objective);
return false;
}
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f)
{
if (connection.Name == "velocity_in")
{
currVelocity = XMLExtensions.ParseVector2(signal, false);
}
else
{
base.ReceiveSignal(stepsTaken, signal, connection, source, sender, power, signalStrength);
}
}
}
}
@@ -0,0 +1,32 @@
using System;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
class Vent : ItemComponent
{
private float oxygenFlow;
public float OxygenFlow
{
get { return oxygenFlow; }
set { oxygenFlow = Math.Max(value, 0.0f); }
}
public Vent (Item item, XElement element)
: base(item, element)
{
}
public override void Update(float deltaTime, Camera cam)
{
if (item.CurrentHull == null) return;
if (item.InWater) return;
item.CurrentHull.Oxygen += oxygenFlow * deltaTime;
OxygenFlow -= deltaTime * 1000.0f;
}
}
}
@@ -0,0 +1,281 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
partial class PowerContainer : Powered, IDrawableComponent, IServerSerializable, IClientSerializable
{
//[power/min]
private float capacity;
private float charge;
//private float rechargeVoltage;
//how fast the battery can be recharged
private float maxRechargeSpeed;
//how fast it's currently being recharged (can be changed, so that
//charging can be slowed down or disabled if there's a shortage of power)
private float rechargeSpeed;
private float lastSentCharge;
//charge indicator description
protected Vector2 indicatorPosition, indicatorSize;
protected bool isHorizontal;
public float CurrPowerOutput
{
get;
private set;
}
[Serialize("0,0", true, description: "The position of the progress bar indicating the charge of the item. In pixels as an offset from the upper left corner of the sprite.")]
public Vector2 IndicatorPosition
{
get { return indicatorPosition; }
set { indicatorPosition = value; }
}
[Serialize("0,0", true, description: "The size of the progress bar indicating the charge of the item (in pixels).")]
public Vector2 IndicatorSize
{
get { return indicatorSize; }
set { indicatorSize = value; }
}
[Serialize(false, true, description: "Should the progress bar indicating the charge of the item fill up horizontally or vertically.")]
public bool IsHorizontal
{
get { return isHorizontal; }
set { isHorizontal = value; }
}
[Editable, Serialize(10.0f, true, description: "Maximum output of the device when fully charged (kW).")]
public float MaxOutPut { set; get; }
[Editable, Serialize(10.0f, true, description: "The maximum capacity of the device (kW * min). For example, a value of 1000 means the device can output 100 kilowatts of power for 10 minutes, or 1000 kilowatts for 1 minute.")]
public float Capacity
{
get { return capacity; }
set { capacity = Math.Max(value, 1.0f); }
}
[Editable, Serialize(0.0f, true, description: "The current charge of the device.")]
public float Charge
{
get { return charge; }
set
{
if (!MathUtils.IsValid(value)) return;
charge = MathHelper.Clamp(value, 0.0f, capacity);
//send a network event if the charge has changed by more than 5%
if (Math.Abs(charge - lastSentCharge) / capacity > 0.05f)
{
#if SERVER
if (GameMain.Server != null) item.CreateServerEvent(this);
#endif
lastSentCharge = charge;
}
}
}
public float ChargePercentage => MathUtils.Percentage(Charge, Capacity);
[Editable, Serialize(10.0f, true, description: "How fast the device can be recharged. For example, a recharge speed of 100 kW and a capacity of 1000 kW*min would mean it takes 10 minutes to fully charge the device.")]
public float MaxRechargeSpeed
{
get { return maxRechargeSpeed; }
set { maxRechargeSpeed = Math.Max(value, 1.0f); }
}
[Editable, Serialize(10.0f, true, description: "The current recharge speed of the device.")]
public float RechargeSpeed
{
get { return rechargeSpeed; }
set
{
if (!MathUtils.IsValid(value)) return;
rechargeSpeed = MathHelper.Clamp(value, 0.0f, maxRechargeSpeed);
rechargeSpeed = MathUtils.RoundTowardsClosest(rechargeSpeed, Math.Max(maxRechargeSpeed * 0.1f, 1.0f));
if (isRunning)
{
HasBeenTuned = true;
}
}
}
public float RechargeRatio => RechargeSpeed / MaxRechargeSpeed;
public const float aiRechargeTargetRatio = 0.5f;
private bool isRunning;
public bool HasBeenTuned { get; private set; }
public PowerContainer(Item item, XElement element)
: base(item, element)
{
IsActive = true;
InitProjSpecific();
}
partial void InitProjSpecific();
public override bool Pick(Character picker)
{
return picker != null;
}
public override void Update(float deltaTime, Camera cam)
{
isRunning = true;
float chargeRatio = charge / capacity;
float gridPower = 0.0f;
float gridLoad = 0.0f;
foreach (Connection c in item.Connections)
{
if (!c.IsPower || !c.IsOutput) { continue; }
foreach (Connection c2 in c.Recipients)
{
if (c2.Item.Condition <= 0.0f) { continue; }
PowerTransfer pt = c2.Item.GetComponent<PowerTransfer>();
if (pt == null)
{
foreach (Powered powered in c2.Item.GetComponents<Powered>())
{
if (!powered.IsActive) continue;
gridLoad += powered.CurrPowerConsumption;
}
continue;
}
if (!pt.IsActive || !pt.CanTransfer) { continue; }
gridPower -= pt.CurrPowerConsumption;
gridLoad += pt.PowerLoad;
}
}
if (chargeRatio > 0.0f)
{
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
}
if (charge >= capacity)
{
//rechargeVoltage = 0.0f;
charge = capacity;
CurrPowerConsumption = 0.0f;
}
else
{
currPowerConsumption = MathHelper.Lerp(currPowerConsumption, rechargeSpeed, 0.05f);
Charge += currPowerConsumption * Math.Min(Voltage, 1.0f) / 3600.0f;
}
if (charge <= 0.0f)
{
CurrPowerOutput = 0.0f;
charge = 0.0f;
return;
}
//output starts dropping when the charge is less than 10%
float maxOutputRatio = 1.0f;
if (chargeRatio < 0.1f)
{
maxOutputRatio = Math.Max(chargeRatio * 10.0f, 0.0f);
}
CurrPowerOutput += (gridLoad - gridPower) * deltaTime;
float maxOutput = Math.Min(MaxOutPut * maxOutputRatio, gridLoad);
CurrPowerOutput = MathHelper.Clamp(CurrPowerOutput, 0.0f, maxOutput);
Charge -= CurrPowerOutput / 3600.0f;
item.SendSignal(0, ((int)Math.Round(Charge)).ToString(), "charge", null);
item.SendSignal(0, ((int)Math.Round((Charge / capacity) * 100)).ToString(), "charge_%", null);
item.SendSignal(0, ((int)Math.Round((RechargeSpeed / maxRechargeSpeed) * 100)).ToString(), "charge_rate", null);
}
public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return false; }
if (objective.Override)
{
HasBeenTuned = false;
}
if (HasBeenTuned) { return true; }
if (string.IsNullOrEmpty(objective.Option) || objective.Option.Equals("charge", StringComparison.OrdinalIgnoreCase))
{
if (Math.Abs(rechargeSpeed - maxRechargeSpeed * aiRechargeTargetRatio) > 0.05f)
{
#if SERVER
item.CreateServerEvent(this);
#endif
RechargeSpeed = maxRechargeSpeed * aiRechargeTargetRatio;
#if CLIENT
if (rechargeSpeedSlider != null)
{
rechargeSpeedSlider.BarScroll = RechargeSpeed / Math.Max(maxRechargeSpeed, 1.0f);
}
#endif
character.Speak(TextManager.GetWithVariables("DialogChargeBatteries", new string[2] { "[itemname]", "[rate]" },
new string[2] { item.Name, ((int)(rechargeSpeed / maxRechargeSpeed * 100.0f)).ToString() },
new bool[2] { true, false }), null, 1.0f, "chargebattery", 10.0f);
}
}
else
{
if (rechargeSpeed > 0.0f)
{
#if SERVER
item.CreateServerEvent(this);
#endif
RechargeSpeed = 0.0f;
#if CLIENT
if (rechargeSpeedSlider != null)
{
rechargeSpeedSlider.BarScroll = RechargeSpeed / Math.Max(maxRechargeSpeed, 1.0f);
}
#endif
character.Speak(TextManager.GetWithVariables("DialogStopChargingBatteries", new string[2] { "[itemname]", "[rate]" },
new string[2] { item.Name, ((int)(rechargeSpeed / maxRechargeSpeed * 100.0f)).ToString() },
new bool[2] { true, false }), null, 1.0f, "chargebattery", 10.0f);
}
}
return true;
}
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power, float signalStrength = 1.0f)
{
if (connection.IsPower) { return; }
if (connection.Name == "set_rate")
{
if (float.TryParse(signal, NumberStyles.Any, CultureInfo.InvariantCulture, out float tempSpeed))
{
if (!MathUtils.IsValid(tempSpeed)) { return; }
float rechargeRate = MathHelper.Clamp(tempSpeed / 100.0f, 0.0f, 1.0f);
RechargeSpeed = rechargeRate * MaxRechargeSpeed;
#if CLIENT
if (rechargeSpeedSlider != null)
{
rechargeSpeedSlider.BarScroll = rechargeRate;
}
#endif
}
}
}
}
}
@@ -0,0 +1,335 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
partial class PowerTransfer : Powered
{
public List<Connection> PowerConnections { get; private set; }
private readonly Dictionary<Connection, bool> connectionDirty = new Dictionary<Connection, bool>();
//a list of connections a given connection is connected to, either directly or via other power transfer components
private readonly Dictionary<Connection, HashSet<Connection>> connectedRecipients = new Dictionary<Connection, HashSet<Connection>>();
protected float powerLoad;
protected bool isBroken;
public float PowerLoad
{
get { return powerLoad; }
set { powerLoad = value; }
}
[Editable, Serialize(true, true, description: "Can the item be damaged if too much power is supplied to the power grid.")]
public bool CanBeOverloaded
{
get;
set;
}
[Editable(MinValueFloat = 1.0f), Serialize(2.0f, true, description:
"How much power has to be supplied to the grid relative to the load before item starts taking damage. "
+ "E.g. a value of 2 means that the grid has to be receiving twice as much power as the devices in the grid are consuming.")]
public float OverloadVoltage
{
get;
set;
}
[Serialize(0.15f, true, description: "The probability for a fire to start when the item breaks."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)]
public float FireProbability
{
get;
set;
}
[Serialize(false, false, description: "Is the item currently overloaded. Intended to be used by StatusEffect conditionals (setting the value from XML is not recommended).")]
public bool Overload
{
get;
set;
}
//can the component transfer power
private bool canTransfer;
public bool CanTransfer
{
get { return canTransfer; }
set
{
if (canTransfer == value) return;
canTransfer = value;
SetAllConnectionsDirty();
}
}
public override bool IsActive
{
get
{
return base.IsActive;
}
set
{
if (base.IsActive == value) return;
base.IsActive = value;
powerLoad = 0.0f;
currPowerConsumption = 0.0f;
SetAllConnectionsDirty();
if (!base.IsActive)
{
//we need to refresh the connections here because Update won't be called on inactive components
RefreshConnections();
}
}
}
public PowerTransfer(Item item, XElement element)
: base(item, element)
{
IsActive = true;
canTransfer = true;
InitProjectSpecific(element);
}
partial void InitProjectSpecific(XElement element);
public override void UpdateBroken(float deltaTime, Camera cam)
{
base.UpdateBroken(deltaTime, cam);
Overload = false;
if (!isBroken)
{
powerLoad = 0.0f;
currPowerConsumption = 0.0f;
SetAllConnectionsDirty();
RefreshConnections();
isBroken = true;
}
}
public override void Update(float deltaTime, Camera cam)
{
RefreshConnections();
if (!CanTransfer) { return; }
if (isBroken)
{
SetAllConnectionsDirty();
isBroken = false;
}
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
//if the item can't be fixed, don't allow it to break
if (!item.Repairables.Any() || !CanBeOverloaded) { return; }
float maxOverVoltage = Math.Max(OverloadVoltage, 1.0f);
Overload = -currPowerConsumption > Math.Max(powerLoad, 200.0f) * maxOverVoltage;
if (Overload && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer))
{
//damage the item if voltage is too high (except if running as a client)
float prevCondition = item.Condition;
item.Condition -= deltaTime * 10.0f;
if (item.Condition <= 0.0f && prevCondition > 0.0f)
{
#if CLIENT
SoundPlayer.PlaySound("zap", item.WorldPosition, hullGuess: item.CurrentHull);
Vector2 baseVel = Rand.Vector(300.0f);
for (int i = 0; i < 10; i++)
{
var particle = GameMain.ParticleManager.CreateParticle("spark", item.WorldPosition,
baseVel + Rand.Vector(100.0f), 0.0f, item.CurrentHull);
if (particle != null) particle.Size *= Rand.Range(0.5f, 1.0f);
}
#endif
float currentIntensity = GameMain.GameSession?.EventManager != null ?
GameMain.GameSession.EventManager.CurrentIntensity : 0.5f;
//higher probability for fires if the current intensity is low
if (FireProbability > 0.0f &&
Rand.Range(0.0f, 1.0f) < MathHelper.Lerp(FireProbability, FireProbability * 0.1f, currentIntensity))
{
new FireSource(item.WorldPosition);
}
}
}
}
public override bool Pick(Character picker)
{
return picker != null;
}
protected void RefreshConnections()
{
var connections = item.Connections;
foreach (Connection c in connections)
{
if (!connectionDirty.ContainsKey(c))
{
connectionDirty[c] = true;
}
else if (!connectionDirty[c])
{
continue;
}
HashSet<Connection> connected = new HashSet<Connection>();
if (!connectedRecipients.ContainsKey(c))
{
connectedRecipients.Add(c, connected);
}
else
{
//mark all previous recipients as dirty
foreach (Connection recipient in connectedRecipients[c])
{
var pt = recipient.Item.GetComponent<PowerTransfer>();
if (pt != null) pt.connectionDirty[recipient] = true;
}
}
//find all connections that are connected to this one (directly or via another PowerTransfer)
connected.Add(c);
GetConnected(c, connected);
connectedRecipients[c] = connected;
//go through all the PowerTransfers and we're connected to and set their connections to match the ones we just calculated
//(no need to go through the recursive GetConnected method again)
foreach (Connection recipient in connected)
{
var recipientPowerTransfer = recipient.Item.GetComponent<PowerTransfer>();
if (recipientPowerTransfer == null) continue;
if (!connectedRecipients.ContainsKey(recipient))
{
connectedRecipients.Add(recipient, connected);
}
recipientPowerTransfer.connectedRecipients[recipient] = connected;
recipientPowerTransfer.connectionDirty[recipient] = false;
}
}
}
//Finds all the connections that can receive a signal sent into the given connection and stores them in the hashset.
private void GetConnected(Connection c, HashSet<Connection> connected)
{
var recipients = c.Recipients;
foreach (Connection recipient in recipients)
{
if (recipient == null || connected.Contains(recipient)) continue;
Item it = recipient.Item;
if (it == null || it.Condition <= 0.0f) continue;
connected.Add(recipient);
var powerTransfer = it.GetComponent<PowerTransfer>();
if (powerTransfer != null && powerTransfer.CanTransfer && powerTransfer.IsActive)
{
GetConnected(recipient, connected);
}
}
}
public void SetAllConnectionsDirty()
{
if (item.Connections == null) return;
foreach (Connection c in item.Connections)
{
connectionDirty[c] = true;
}
}
public void SetConnectionDirty(Connection connection)
{
var connections = item.Connections;
if (connections == null || !connections.Contains(connection)) return;
connectionDirty[connection] = true;
}
public override void OnItemLoaded()
{
base.OnItemLoaded();
var connections = Item.Connections;
PowerConnections = connections == null ? new List<Connection>() : connections.FindAll(c => c.IsPower);
if (connections == null)
{
IsActive = false;
return;
}
if (!(this is RelayComponent))
{
if (PowerConnections.Any(p => !p.IsOutput) && PowerConnections.Any(p => p.IsOutput))
{
DebugConsole.ThrowError("Error in item \"" + Name + "\" - PowerTransfer components should not have separate power inputs and outputs, but transfer power between wires connected to the same power connection. " +
"If you want power to pass from input to output, change the component to a RelayComponent.");
}
}
SetAllConnectionsDirty();
}
public override void ReceivePowerProbeSignal(Connection connection, Item source, float power)
{
//we've already received this signal
if (lastPowerProbeRecipients.Contains(this)) { return; }
if (item.Condition <= 0.0f) { return; }
lastPowerProbeRecipients.Add(this);
if (power < 0.0f)
{
powerLoad -= power;
}
else
{
currPowerConsumption -= power;
}
powerOut?.SendPowerProbeSignal(source, power);
}
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power, float signalStrength = 1.0f)
{
if (item.Condition <= 0.0f || connection.IsPower) { return; }
if (!connectedRecipients.ContainsKey(connection)) { return; }
if (connection.Name.Length > 5 && connection.Name.Substring(0, 6) == "signal")
{
foreach (Connection recipient in connectedRecipients[connection])
{
if (recipient.Item == item || recipient.Item == source) { continue; }
foreach (ItemComponent ic in recipient.Item.Components)
{
//other junction boxes don't need to receive the signal in the pass-through signal connections
//because we relay it straight to the connected items without going through the whole chain of junction boxes
if (ic is PowerTransfer && !(ic is RelayComponent) && connection.Name.Contains("signal")) { continue; }
ic.ReceiveSignal(stepsTaken, signal, recipient, source, sender, 0.0f, signalStrength);
}
foreach (StatusEffect effect in recipient.Effects)
{
recipient.Item.ApplyStatusEffect(effect, ActionType.OnUse, 1.0f);
}
}
}
}
}
}
@@ -0,0 +1,311 @@
using System;
using System.Xml.Linq;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
#if CLIENT
using Barotrauma.Sounds;
#endif
namespace Barotrauma.Items.Components
{
partial class Powered : ItemComponent
{
private static float updateTimer;
protected static float UpdateInterval = 0.2f;
/// <summary>
/// List of all powered ItemComponents
/// </summary>
private static readonly List<Powered> poweredList = new List<Powered>();
/// <summary>
/// Items that have already received the "probe signal" that's used to distribute power and load across the grid
/// </summary>
protected static HashSet<PowerTransfer> lastPowerProbeRecipients = new HashSet<PowerTransfer>();
/// <summary>
/// The amount of power currently consumed by the item. Negative values mean that the item is providing power to connected items
/// </summary>
protected float currPowerConsumption;
/// <summary>
/// Current voltage of the item (load / power)
/// </summary>
private float voltage;
/// <summary>
/// The minimum voltage required for the item to work
/// </summary>
private float minVoltage;
/// <summary>
/// The maximum amount of power the item can draw from connected items
/// </summary>
protected float powerConsumption;
protected Connection powerIn, powerOut;
[Editable, Serialize(0.5f, true, description: "The minimum voltage required for the device to function. " +
"The voltage is calculated as power / powerconsumption, meaning that a device " +
"with a power consumption of 1000 kW would need at least 500 kW of power to work if the minimum voltage is set to 0.5.")]
public float MinVoltage
{
get { return powerConsumption <= 0.0f ? 0.0f : minVoltage; }
set { minVoltage = value; }
}
[Editable, Serialize(0.0f, true, description: "How much power the device draws (or attempts to draw) from the electrical grid when active.")]
public float PowerConsumption
{
get { return powerConsumption; }
set { powerConsumption = value; }
}
[Serialize(false, true, description: "Is the device currently active. Inactive devices don't consume power.")]
public override bool IsActive
{
get { return base.IsActive; }
set
{
base.IsActive = value;
if (!value)
{
currPowerConsumption = 0.0f;
}
}
}
[Serialize(0.0f, true, description: "The current power consumption of the device. Intended to be used by StatusEffect conditionals (setting the value from XML is not recommended).")]
public float CurrPowerConsumption
{
get {return currPowerConsumption; }
set { currPowerConsumption = value; }
}
[Serialize(0.0f, true, description: "The current voltage of the item (calculated as power consumption / available power). Intended to be used by StatusEffect conditionals (setting the value from XML is not recommended).")]
public float Voltage
{
get { return voltage; }
set { voltage = Math.Max(0.0f, value); }
}
[Editable, Serialize(true, true, description: "Can the item be damaged by electomagnetic pulses.")]
public bool VulnerableToEMP
{
get;
set;
}
public Powered(Item item, XElement element)
: base(item, element)
{
poweredList.Add(this);
InitProjectSpecific(element);
}
partial void InitProjectSpecific(XElement element);
protected void UpdateOnActiveEffects(float deltaTime)
{
if (currPowerConsumption <= 0.0f)
{
//if the item consumes no power, ignore the voltage requirement and
//apply OnActive statuseffects as long as this component is active
if (powerConsumption <= 0.0f)
{
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
}
return;
}
if (voltage > minVoltage)
{
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
}
#if CLIENT
if (voltage > minVoltage)
{
if (!powerOnSoundPlayed && powerOnSound != null)
{
SoundPlayer.PlaySound(powerOnSound.Sound, item.WorldPosition, powerOnSound.Volume, powerOnSound.Range, item.CurrentHull);
powerOnSoundPlayed = true;
}
}
else if (voltage < 0.1f)
{
powerOnSoundPlayed = false;
}
#endif
}
public override void Update(float deltaTime, Camera cam)
{
UpdateOnActiveEffects(deltaTime);
}
public override void OnItemLoaded()
{
if (item.Connections == null) { return; }
foreach (Connection c in item.Connections)
{
if (!c.IsPower) { continue; }
if (this is PowerTransfer pt)
{
if (c.Name == "power_in")
{
powerIn = c;
}
else if (c.Name == "power_out")
{
powerOut = c;
}
else if (c.Name == "power")
{
powerIn = powerOut = c;
}
}
else
{
if (c.IsOutput)
{
if (c.Name == "power_in")
{
#if DEBUG
DebugConsole.ThrowError($"Item \"{item.Name}\" has a power output connection called power_in. If the item is supposed to receive power through the connection, change it to an input connection.");
#else
DebugConsole.NewMessage($"Item \"{item.Name}\" has a power output connection called power_in. If the item is supposed to receive power through the connection, change it to an input connection.", Color.Orange);
#endif
}
powerOut = c;
}
else
{
if (c.Name == "power_out")
{
#if DEBUG
DebugConsole.ThrowError($"Item \"{item.Name}\" has a power input connection called power_out. If the item is supposed to output power through the connection, change it to an output connection.");
#else
DebugConsole.NewMessage($"Item \"{item.Name}\" has a power input connection called power_out. If the item is supposed to output power through the connection, change it to an output connection.", Color.Orange);
#endif
}
powerIn = c;
}
}
}
}
public virtual void ReceivePowerProbeSignal(Connection connection, Item source, float power) { }
public static void UpdatePower(float deltaTime)
{
if (updateTimer > 0.0f)
{
updateTimer -= deltaTime;
return;
}
updateTimer = UpdateInterval;
//reset power first
foreach (Powered powered in poweredList)
{
if (powered is PowerTransfer pt)
{
powered.CurrPowerConsumption = 0.0f;
pt.PowerLoad = 0.0f;
if (pt is RelayComponent relay)
{
relay.DisplayLoad = 0.0f;
}
}
//only reset voltage if the item has a power connector
//(other items, such as handheld devices, get power through other means and shouldn't be updated here)
if (powered.powerIn != null || powered.powerOut != null) { powered.voltage = 0.0f; }
}
//go through all the devices that are consuming/providing power
//and send out a "probe signal" which the PowerTransfer components use to add up the grid power/load
foreach (Powered powered in poweredList)
{
if (powered is PowerTransfer) { continue; }
if (powered.currPowerConsumption > 0.0f)
{
//consuming power
lastPowerProbeRecipients.Clear();
powered.powerIn?.SendPowerProbeSignal(powered.item, -powered.currPowerConsumption);
}
}
foreach (Powered powered in poweredList)
{
if (powered is PowerTransfer) { continue; }
else if (powered.currPowerConsumption < 0.0f)
{
//providing power
lastPowerProbeRecipients.Clear();
powered.powerOut?.SendPowerProbeSignal(powered.item, -powered.currPowerConsumption);
}
if (powered is PowerContainer pc)
{
if (pc.CurrPowerOutput <= 0.0f) { continue; }
//providing power
lastPowerProbeRecipients.Clear();
powered.powerOut?.SendPowerProbeSignal(powered.item, pc.CurrPowerOutput);
}
}
//go through powered items and calculate their current voltage
foreach (Powered powered in poweredList)
{
if (powered is PowerTransfer pt1 || (pt1 = powered.Item.GetComponent<PowerTransfer>()) != null)
{
powered.voltage = -pt1.CurrPowerConsumption / Math.Max(pt1.PowerLoad, 1.0f);
continue;
}
if (powered.powerConsumption <= 0.0f && !(powered is PowerContainer))
{
powered.voltage = 1.0f;
continue;
}
if (powered.powerIn == null) { continue; }
foreach (Connection powerSource in powered.powerIn.Recipients)
{
if (!powerSource.IsPower || !powerSource.IsOutput) { continue; }
var pt = powerSource.Item.GetComponent<PowerTransfer>();
if (pt != null)
{
float voltage = -pt.CurrPowerConsumption / Math.Max(pt.PowerLoad, 1.0f);
powered.voltage = Math.Max(powered.voltage, voltage);
continue;
}
var pc = powerSource.Item.GetComponent<PowerContainer>();
if (pc != null)
{
float voltage = pc.CurrPowerOutput / Math.Max(powered.CurrPowerConsumption, 1.0f);
powered.voltage += voltage;
}
}
}
}
/// <summary>
/// Returns the amount of power that can be supplied by batteries directly connected to the item
/// </summary>
protected float GetAvailableBatteryPower()
{
var batteries = item.GetConnectedComponents<PowerContainer>();
float availablePower = 0.0f;
foreach (PowerContainer battery in batteries)
{
float batteryPower = Math.Min(battery.Charge * 3600.0f, battery.MaxOutPut);
availablePower += batteryPower;
}
return availablePower;
}
protected override void RemoveComponentSpecific()
{
poweredList.Remove(this);
}
}
}
@@ -0,0 +1,656 @@
using Barotrauma.Networking;
using FarseerPhysics;
using FarseerPhysics.Dynamics;
using FarseerPhysics.Dynamics.Contacts;
using FarseerPhysics.Dynamics.Joints;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
class Projectile : ItemComponent
{
struct HitscanResult
{
public Fixture Fixture;
public Vector2 Point;
public Vector2 Normal;
public float Fraction;
public HitscanResult(Fixture fixture, Vector2 point, Vector2 normal, float fraction)
{
Fixture = fixture;
Point = point;
Normal = normal;
Fraction = fraction;
}
}
struct Impact
{
public Fixture Fixture;
public Vector2 Normal;
public Impact(Fixture fixture, Vector2 normal)
{
Fixture = fixture;
Normal = normal;
}
}
private readonly Queue<Impact> impactQueue = new Queue<Impact>();
//continuous collision detection is used while the projectile is moving faster than this
const float ContinuousCollisionThreshold = 5.0f;
//a duration during which the projectile won't drop from the body it's stuck to
private const float PersistentStickJointDuration = 1.0f;
private float launchImpulse;
private PrismaticJoint stickJoint;
private Body stickTarget;
private readonly Attack attack;
private Vector2 launchPos;
public List<Body> IgnoredBodies;
private Character user;
public Character User
{
get { return user; }
set
{
user = value;
attack?.SetUser(user);
}
}
private float persistentStickJointTimer;
[Serialize(10.0f, false, description: "The impulse applied to the physics body of the item when it's launched. Higher values make the projectile faster.")]
public float LaunchImpulse
{
get { return launchImpulse; }
set { launchImpulse = value; }
}
[Serialize(0.0f, false, description: "The rotation of the item relative to the rotation of the weapon when launched (in degrees).")]
public float LaunchRotation
{
get { return MathHelper.ToDegrees(LaunchRotationRadians); }
set { LaunchRotationRadians = MathHelper.ToRadians(value); }
}
public float LaunchRotationRadians
{
get;
private set;
}
[Serialize(false, false, description: "When set to true, the item can stick to any target it hits.")]
//backwards compatibility, can stick to anything
public bool DoesStick
{
get;
set;
}
[Serialize(false, false, description: "Can the item stick to the character it hits.")]
public bool StickToCharacters
{
get;
set;
}
[Serialize(false, false, description: "Can the item stick to the structure it hits.")]
public bool StickToStructures
{
get;
set;
}
[Serialize(false, false, description: "Can the item stick to the item it hits.")]
public bool StickToItems
{
get;
set;
}
[Serialize(false, false, description: "Hitscan projectiles cast a ray forwards and immediately hit whatever the ray hits. "+
"It is recommended to use hitscans for very fast-moving projectiles such as bullets, because using extremely fast launch velocities may cause physics glitches.")]
public bool Hitscan
{
get;
set;
}
[Serialize(1, false, description: "How many hitscans should be done when the projectile is launched. "
+ "Multiple hitscans can be used to simulate weapons that fire multiple projectiles at the same time" +
" without having to actually use multiple projectile items, for example shotguns.")]
public int HitScanCount
{
get;
set;
}
[Serialize(false, false, description: "Should the item be deleted when it hits something.")]
public bool RemoveOnHit
{
get;
set;
}
[Serialize(0.0f, false, description: "Random spread applied to the launch angle of the projectile (in degrees).")]
public float Spread
{
get;
set;
}
public Projectile(Item item, XElement element)
: base (item, element)
{
IgnoredBodies = new List<Body>();
foreach (XElement subElement in element.Elements())
{
if (!subElement.Name.ToString().Equals("attack", StringComparison.OrdinalIgnoreCase)) { continue; }
attack = new Attack(subElement, item.Name + ", Projectile");
}
}
public override void OnItemLoaded()
{
if (attack != null && attack.DamageRange <= 0.0f && item.body != null)
{
switch (item.body.BodyShape)
{
case PhysicsBody.Shape.Circle:
attack.DamageRange = item.body.radius;
break;
case PhysicsBody.Shape.Capsule:
attack.DamageRange = item.body.height / 2 + item.body.radius;
break;
case PhysicsBody.Shape.Rectangle:
attack.DamageRange = new Vector2(item.body.width / 2.0f, item.body.height / 2.0f).Length();
break;
}
attack.DamageRange = ConvertUnits.ToDisplayUnits(attack.DamageRange);
}
}
public override bool Use(float deltaTime, Character character = null)
{
if (character != null && !characterUsable) { return false; }
for (int i = 0; i < HitScanCount; i++)
{
float launchAngle = item.body.Rotation + MathHelper.ToRadians(Spread * Rand.Range(-0.5f, 0.5f));
Vector2 launchDir = new Vector2((float)Math.Cos(launchAngle), (float)Math.Sin(launchAngle));
if (Hitscan)
{
Vector2 prevSimpos = item.SimPosition;
DoHitscan(launchDir);
if (i < HitScanCount - 1)
{
item.SetTransform(prevSimpos, item.body.Rotation);
}
}
else
{
Launch(launchDir * launchImpulse * item.body.Mass);
}
}
User = character;
return true;
}
private void Launch(Vector2 impulse)
{
if (item.AiTarget != null)
{
item.AiTarget.SightRange = item.AiTarget.MaxSightRange;
item.AiTarget.SoundRange = item.AiTarget.MaxSoundRange;
}
item.Drop(null);
launchPos = item.SimPosition;
item.body.Enabled = true;
item.body.ApplyLinearImpulse(impulse, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
item.body.FarseerBody.OnCollision += OnProjectileCollision;
item.body.FarseerBody.IsBullet = true;
item.body.CollisionCategories = Physics.CollisionProjectile;
item.body.CollidesWith = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel;
IsActive = true;
if (stickJoint == null) { return; }
stickTarget = null;
GameMain.World.Remove(stickJoint);
stickJoint = null;
}
private void DoHitscan(Vector2 dir)
{
float rotation = item.body.Rotation;
Vector2 simPositon = item.SimPosition;
item.Drop(null);
item.body.Enabled = true;
//set the velocity of the body because the OnProjectileCollision method
//uses it to determine the direction from which the projectile hit
item.body.LinearVelocity = dir;
IsActive = true;
Vector2 rayStart = simPositon;
Vector2 rayEnd = simPositon + dir * 1000.0f;
List<HitscanResult> hits = new List<HitscanResult>();
hits.AddRange(DoRayCast(rayStart, rayEnd));
if (item.Submarine != null)
{
//shooting indoors, do a hitscan outside as well
hits.AddRange(DoRayCast(rayStart + item.Submarine.SimPosition, rayEnd + item.Submarine.SimPosition));
}
else
{
//shooting outdoors, see if we can hit anything inside a sub
foreach (Submarine submarine in Submarine.Loaded)
{
var inSubHits = DoRayCast(rayStart - submarine.SimPosition, rayEnd - submarine.SimPosition);
//transform back to world coordinates
for (int i = 0; i < inSubHits.Count; i++)
{
inSubHits[i] = new HitscanResult(
inSubHits[i].Fixture,
inSubHits[i].Point + submarine.SimPosition,
inSubHits[i].Normal,
inSubHits[i].Fraction);
}
hits.AddRange(inSubHits);
}
}
bool hitSomething = false;
hits = hits.OrderBy(h => h.Fraction).ToList();
foreach (HitscanResult h in hits)
{
item.body.SetTransform(h.Point, rotation);
if (HandleProjectileCollision(h.Fixture, h.Normal))
{
hitSomething = true;
break;
}
}
//the raycast didn't hit anything -> the projectile flew somewhere outside the level and is permanently lost
if (!hitSomething)
{
if (Entity.Spawner == null)
{
item.Remove();
}
else
{
Entity.Spawner.AddToRemoveQueue(item);
}
}
}
private List<HitscanResult> DoRayCast(Vector2 rayStart, Vector2 rayEnd)
{
List<HitscanResult> hits = new List<HitscanResult>();
Vector2 dir = rayEnd - rayStart;
dir = dir.LengthSquared() < 0.00001f ? Vector2.UnitY : Vector2.Normalize(dir);
//do an AABB query first to see if the start of the ray is inside a fixture
var aabb = new FarseerPhysics.Collision.AABB(rayStart - Vector2.One * 0.001f, rayStart + Vector2.One * 0.001f);
GameMain.World.QueryAABB((fixture) =>
{
//ignore sensors and items
if (fixture?.Body == null || fixture.IsSensor) { return true; }
if (fixture.Body.UserData is Item) { return true; }
if (fixture.Body?.UserData as string == "ruinroom") { return true; }
//ignore everything else than characters, sub walls and level walls
if (!fixture.CollisionCategories.HasFlag(Physics.CollisionCharacter) &&
!fixture.CollisionCategories.HasFlag(Physics.CollisionWall) &&
!fixture.CollisionCategories.HasFlag(Physics.CollisionLevel)) { return true; }
fixture.Body.GetTransform(out FarseerPhysics.Common.Transform transform);
if (!fixture.Shape.TestPoint(ref transform, ref rayStart)) { return true; }
hits.Add(new HitscanResult(fixture, rayStart, -dir, 0.0f));
return true;
}, ref aabb);
GameMain.World.RayCast((fixture, point, normal, fraction) =>
{
//ignore sensors and items
if (fixture?.Body == null || fixture.IsSensor) { return -1; }
if (fixture.Body.UserData is Item item && item.GetComponent<Door>() == null) { return -1; }
if (fixture.Body?.UserData as string == "ruinroom") { return -1; }
//ignore everything else than characters, sub walls and level walls
if (!fixture.CollisionCategories.HasFlag(Physics.CollisionCharacter) &&
!fixture.CollisionCategories.HasFlag(Physics.CollisionWall) &&
!fixture.CollisionCategories.HasFlag(Physics.CollisionLevel)) { return -1; }
hits.Add(new HitscanResult(fixture, point, normal, fraction));
return hits.Count < 25 ? 1 : 0;
}, rayStart, rayEnd, Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel);
return hits;
}
public override void Update(float deltaTime, Camera cam)
{
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
while (impactQueue.Count > 0)
{
var impact = impactQueue.Dequeue();
HandleProjectileCollision(impact.Fixture, impact.Normal);
}
if (item.body != null && item.body.FarseerBody.IsBullet)
{
if (item.body.LinearVelocity.LengthSquared() < ContinuousCollisionThreshold * ContinuousCollisionThreshold)
{
item.body.FarseerBody.IsBullet = false;
//projectiles with a stickjoint don't become inactive until the stickjoint is detached
if (stickJoint == null) { IsActive = false; }
}
}
if (stickJoint == null) { return; }
if (persistentStickJointTimer > 0.0f)
{
persistentStickJointTimer -= deltaTime;
return;
}
if (stickJoint.JointTranslation < stickJoint.LowerLimit * 0.9f || stickJoint.JointTranslation > stickJoint.UpperLimit * 0.9f)
{
stickTarget = null;
if (stickJoint != null)
{
if (GameMain.World.JointList.Contains(stickJoint))
{
GameMain.World.Remove(stickJoint);
}
stickJoint = null;
}
if (!item.body.FarseerBody.IsBullet) { IsActive = false; }
}
}
private bool OnProjectileCollision(Fixture f1, Fixture target, Contact contact)
{
if (User != null && User.Removed) { User = null; return false; }
if (IgnoredBodies.Contains(target.Body)) { return false; }
if (target.Body.UserData is Submarine sub)
{
Vector2 dir = item.body.LinearVelocity.LengthSquared() < 0.001f ?
contact.Manifold.LocalNormal : Vector2.Normalize(item.body.LinearVelocity);
//do a raycast in the sub's coordinate space to see if it hit a structure
var wallBody = Submarine.PickBody(
item.body.SimPosition - ConvertUnits.ToSimUnits(sub.Position) - dir,
item.body.SimPosition - ConvertUnits.ToSimUnits(sub.Position) + dir,
collisionCategory: Physics.CollisionWall);
if (wallBody?.FixtureList?.First() != null && wallBody.UserData is Structure structure &&
//ignore the hit if it's behind the position the item was launched from, and the projectile is travelling in the opposite direction
Vector2.Dot(item.body.SimPosition - launchPos, dir) > 0)
{
target = wallBody.FixtureList.First();
}
else
{
return false;
}
}
else if (target.Body.UserData is Limb limb)
{
//severed limbs don't deactivate the projectile (but may still slow it down enough to make it inactive)
if (limb.IsSevered)
{
target.Body.ApplyLinearImpulse(item.body.LinearVelocity * item.body.Mass);
return true;
}
}
//ignore character colliders (the projectile only hits limbs)
if (target.CollisionCategories == Physics.CollisionCharacter && target.Body.UserData is Character)
{
return false;
}
impactQueue.Enqueue(new Impact(target, contact.Manifold.LocalNormal));
item.body.FarseerBody.OnCollision -= OnProjectileCollision;
return true;
}
private bool HandleProjectileCollision(Fixture target, Vector2 collisionNormal)
{
if (User != null && User.Removed) { User = null; }
if (IgnoredBodies.Contains(target.Body)) { return false; }
//ignore character colliders (the projectile only hits limbs)
if (target.CollisionCategories == Physics.CollisionCharacter && target.Body.UserData is Character)
{
return false;
}
AttackResult attackResult = new AttackResult();
Character character = null;
if (target.Body.UserData is Submarine submarine)
{
item.Move(-submarine.Position);
item.Submarine = submarine;
item.body.Submarine = submarine;
return !Hitscan;
}
else if (target.Body.UserData is Limb limb)
{
//severed limbs don't deactivate the projectile (but may still slow it down enough to make it inactive)
if (limb.IsSevered)
{
target.Body.ApplyLinearImpulse(item.body.LinearVelocity * item.body.Mass);
return true;
}
limb.character.LastDamageSource = item;
if (attack != null) { attackResult = attack.DoDamageToLimb(User, limb, item.WorldPosition, 1.0f); }
if (limb.character != null) { character = limb.character; }
}
else if (target.Body.UserData is Item targetItem)
{
if (attack != null && targetItem.Prefab.DamagedByProjectiles)
{
attackResult = attack.DoDamage(User, targetItem, item.WorldPosition, 1.0f);
}
}
else if (target.Body.UserData is IDamageable damageable)
{
if (attack != null) { attackResult = attack.DoDamage(User, damageable, item.WorldPosition, 1.0f); }
}
if (character != null) { character.LastDamageSource = item; }
#if CLIENT
PlaySound(ActionType.OnUse, user: user);
PlaySound(ActionType.OnImpact, user: user);
#endif
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
{
if (target.Body.UserData is Limb targetLimb)
{
ApplyStatusEffects(ActionType.OnUse, 1.0f, character, targetLimb, user: user);
ApplyStatusEffects(ActionType.OnImpact, 1.0f, character, targetLimb, user: user);
var attack = targetLimb.attack;
if (attack != null)
{
// Apply the status effects defined in the limb's attack that was hit
foreach (var effect in attack.StatusEffects)
{
if (effect.type == ActionType.OnImpact)
{
//effect.Apply(effect.type, 1.0f, targetLimb.character, targetLimb.character, targetLimb.WorldPosition);
if (effect.HasTargetType(StatusEffect.TargetType.This))
{
effect.Apply(effect.type, 1.0f, targetLimb.character, targetLimb.character, targetLimb.WorldPosition);
}
if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
var targets = new List<ISerializableEntity>();
effect.GetNearbyTargets(targetLimb.WorldPosition, targets);
effect.Apply(ActionType.OnActive, 1.0f, targetLimb.character, targets);
}
}
}
}
#if SERVER
if (GameMain.NetworkMember.IsServer)
{
GameMain.Server?.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnUse, this, targetLimb.character.ID, targetLimb, (ushort)0, item.WorldPosition });
GameMain.Server?.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnImpact, this, targetLimb.character.ID, targetLimb, (ushort)0, item.WorldPosition });
}
#endif
}
else
{
ApplyStatusEffects(ActionType.OnUse, 1.0f, useTarget: target.Body.UserData as Entity, user: user);
ApplyStatusEffects(ActionType.OnImpact, 1.0f, useTarget: target.Body.UserData as Entity, user: user);
#if SERVER
if (GameMain.NetworkMember.IsServer)
{
GameMain.Server?.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnUse, this, (ushort)0, null, (target.Body.UserData as Entity)?.ID ?? 0, item.WorldPosition });
GameMain.Server?.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnImpact, this, (ushort)0, null, (target.Body.UserData as Entity)?.ID ?? 0, item.WorldPosition });
}
#endif
}
}
item.body.FarseerBody.OnCollision -= OnProjectileCollision;
item.body.CollisionCategories = Physics.CollisionItem;
item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel;
IgnoredBodies.Clear();
target.Body.ApplyLinearImpulse(item.body.LinearVelocity * item.body.Mass);
if (attackResult.AppliedDamageModifiers != null &&
attackResult.AppliedDamageModifiers.Any(dm => dm.DeflectProjectiles))
{
item.body.LinearVelocity *= 0.1f;
}
else if (Vector2.Dot(item.body.LinearVelocity, collisionNormal) < 0.0f &&
(DoesStick ||
(StickToCharacters && target.Body.UserData is Limb) ||
(StickToStructures && target.Body.UserData is Structure) ||
(StickToItems && target.Body.UserData is Item)))
{
Vector2 dir = new Vector2(
(float)Math.Cos(item.body.Rotation),
(float)Math.Sin(item.body.Rotation));
StickToTarget(target.Body, dir);
item.body.LinearVelocity *= 0.5f;
return Hitscan;
}
else
{
item.body.LinearVelocity *= 0.5f;
}
var containedItems = item.ContainedItems;
if (containedItems != null)
{
foreach (Item contained in containedItems)
{
if (contained.body != null)
{
contained.SetTransform(item.SimPosition, contained.body.Rotation);
}
}
}
if (RemoveOnHit)
{
Entity.Spawner.AddToRemoveQueue(item);
}
return true;
}
private void StickToTarget(Body targetBody, Vector2 axis)
{
if (stickJoint != null) return;
stickJoint = new PrismaticJoint(targetBody, item.body.FarseerBody, item.body.SimPosition, axis, true)
{
MotorEnabled = true,
MaxMotorForce = 30.0f,
LimitEnabled = true
};
if (item.Sprite != null)
{
stickJoint.LowerLimit = ConvertUnits.ToSimUnits(item.Sprite.size.X * -0.3f);
stickJoint.UpperLimit = ConvertUnits.ToSimUnits(item.Sprite.size.X * 0.3f);
}
persistentStickJointTimer = PersistentStickJointDuration;
stickTarget = targetBody;
GameMain.World.Add(stickJoint);
IsActive = true;
}
protected override void RemoveComponentSpecific()
{
if (stickJoint != null)
{
try
{
GameMain.World.Remove(stickJoint);
}
catch
{
//the body that the projectile was stuck to has been removed
}
stickJoint = null;
}
}
}
}
@@ -0,0 +1,437 @@
using Barotrauma.Extensions;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
partial class Repairable : ItemComponent, IServerSerializable, IClientSerializable
{
private string header;
private float deteriorationTimer;
private float deteriorateAlwaysResetTimer;
bool wasBroken;
bool wasGoodCondition;
public float LastActiveTime;
[Serialize(0.0f, true, description: "How fast the condition of the item deteriorates per second."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 2)]
public float DeteriorationSpeed
{
get;
set;
}
[Serialize(0.0f, true, description: "Minimum initial delay before the item starts to deteriorate."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f, DecimalCount = 2)]
public float MinDeteriorationDelay
{
get;
set;
}
[Serialize(0.0f, true, description: "Maximum initial delay before the item starts to deteriorate."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f, DecimalCount = 2)]
public float MaxDeteriorationDelay
{
get;
set;
}
[Serialize(50.0f, true, description: "The item won't deteriorate spontaneously if the condition is below this value. For example, if set to 10, the condition will spontaneously drop to 10 and then stop dropping (unless the item is damaged further by external factors). Percentages of max condition."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)]
public float MinDeteriorationCondition
{
get;
set;
}
[Serialize(0f, true, description: "How low a traitor must get the item's condition for it to start breaking down.")]
public float MinSabotageCondition
{
get;
set;
}
[Serialize(80.0f, true, description: "The condition of the item has to be below this for AI characters to repair it. Percentages of max condition."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)]
public float AIRepairThreshold
{
get;
set;
}
[Serialize(100.0f, true, description: "The amount of time it takes to fix the item with insufficient skill levels."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)]
public float FixDurationLowSkill
{
get;
set;
}
[Serialize(10.0f, true, description: "The amount of time it takes to fix the item with sufficient skill levels."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)]
public float FixDurationHighSkill
{
get;
set;
}
[Serialize(false, false, description: "If set to true, the deterioration timer will always run regardless if the item is being used or not.")]
public bool DeteriorateAlways
{
get;
set;
}
public Character CurrentFixer { get; private set; }
public enum FixActions : int
{
None = 0,
Repair = 1,
Sabotage = 2
}
private FixActions currentFixerAction = FixActions.None;
public FixActions CurrentFixerAction
{
get => currentFixerAction;
private set { currentFixerAction = value; }
}
public Repairable(Item item, XElement element)
: base(item, element)
{
IsActive = true;
canBeSelected = true;
this.item = item;
header =
TextManager.Get(element.GetAttributeString("header", ""), returnNull: true) ??
TextManager.Get(item.Prefab.ConfigElement.GetAttributeString("header", ""), returnNull: true) ??
element.GetAttributeString("name", "");
//backwards compatibility
var showRepairUIAttribute = element.Attributes().FirstOrDefault(a => a.Name.ToString().Equals("showrepairuithreshold", StringComparison.OrdinalIgnoreCase));
if (showRepairUIAttribute != null)
{
float repairThreshold;
if (Single.TryParse(showRepairUIAttribute.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out repairThreshold))
{
AIRepairThreshold = repairThreshold;
}
}
InitProjSpecific(element);
}
public override void OnItemLoaded()
{
deteriorationTimer = Rand.Range(MinDeteriorationDelay, MaxDeteriorationDelay);
}
partial void InitProjSpecific(XElement element);
/// <summary>
/// Check if the character manages to succesfully repair the item
/// </summary>
public bool CheckCharacterSuccess(Character character)
{
if (character == null) { return false; }
// Only check for success when repairing electrical devices
if (requiredSkills.None(s => s != null && s.Identifier.Equals("electrical", StringComparison.OrdinalIgnoreCase))) { return true; }
//unpowered items can be repaired without a risk of electrical shock
if (item.GetComponent<Powered>() is Powered powered && powered.Voltage < 0.1f) { return true; }
if (Rand.Range(0.0f, 0.5f) < DegreeOfSuccess(character)) { return true; }
item.ApplyStatusEffects(ActionType.OnFailure, 1.0f, character);
return false;
}
public bool StartRepairing(Character character, FixActions action)
{
if (character == null || character.IsDead || action == FixActions.None)
{
DebugConsole.ThrowError("Invalid repair command!");
return false;
}
else
{
#if SERVER
if (CurrentFixer != character || currentFixerAction != action)
{
if (!CheckCharacterSuccess(character))
{
GameMain.Server?.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnFailure, this, character.ID });
return false;
}
item.CreateServerEvent(this);
}
#else
if (GameMain.Client == null && (CurrentFixer != character || currentFixerAction != action) && !CheckCharacterSuccess(character)) { return false; }
#endif
CurrentFixer = character;
CurrentFixerAction = action;
return true;
}
}
public bool StopRepairing(Character character)
{
if (CurrentFixer == character)
{
#if SERVER
if (CurrentFixer != character || currentFixerAction != FixActions.None)
{
item.CreateServerEvent(this);
}
#endif
CurrentFixer.AnimController.Anim = AnimController.Animation.None;
CurrentFixer = null;
currentFixerAction = FixActions.None;
#if CLIENT
repairSoundChannel?.FadeOutAndDispose();
repairSoundChannel = null;
#endif
return true;
}
else
{
return false;
}
}
public override void UpdateBroken(float deltaTime, Camera cam)
{
Update(deltaTime, cam);
}
public void ResetDeterioration()
{
deteriorationTimer = Rand.Range(MinDeteriorationDelay, MaxDeteriorationDelay);
item.Condition = item.Prefab.Health;
#if SERVER
//let the clients know the deterioration delay
item.CreateServerEvent(this);
#endif
}
public override void Update(float deltaTime, Camera cam)
{
UpdateProjSpecific(deltaTime);
if (CurrentFixer == null)
{
if (deteriorateAlwaysResetTimer > 0.0f)
{
deteriorateAlwaysResetTimer -= deltaTime;
if (deteriorateAlwaysResetTimer <= 0.0f)
{
DeteriorateAlways = false;
#if SERVER
//let the clients know the deterioration delay
item.CreateServerEvent(this);
#endif
}
}
if (!ShouldDeteriorate()) { return; }
if (item.Condition > 0.0f)
{
if (deteriorationTimer > 0.0f)
{
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
{
deteriorationTimer -= deltaTime * GetDeteriorationDelayMultiplier();
#if SERVER
if (deteriorationTimer <= 0.0f) { item.CreateServerEvent(this); }
#endif
}
return;
}
if (item.ConditionPercentage > MinDeteriorationCondition)
{
item.Condition -= DeteriorationSpeed * deltaTime;
}
}
return;
}
UpdateFixAnimation(CurrentFixer);
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
if (CurrentFixer != null && (CurrentFixer.SelectedConstruction != item || !CurrentFixer.CanInteractWith(item) || CurrentFixer.IsDead))
{
StopRepairing(CurrentFixer);
return;
}
float successFactor = requiredSkills.Count == 0 ? 1.0f : DegreeOfSuccess(CurrentFixer, requiredSkills);
//item must have been below the repair threshold for the player to get an achievement or XP for repairing it
if (!item.IsFullCondition)
{
wasBroken = true;
}
if (item.ConditionPercentage > MinSabotageCondition)
{
wasGoodCondition = true;
}
float fixDuration = MathHelper.Lerp(FixDurationLowSkill, FixDurationHighSkill, successFactor);
if (currentFixerAction == FixActions.Repair)
{
if (fixDuration <= 0.0f)
{
item.Condition = item.MaxCondition;
}
else
{
float conditionIncrease = deltaTime / (fixDuration / item.MaxCondition);
item.Condition += conditionIncrease;
#if SERVER
GameMain.Server.KarmaManager.OnItemRepaired(CurrentFixer, this, conditionIncrease);
#endif
}
if (item.IsFullCondition)
{
if (wasBroken)
{
foreach (Skill skill in requiredSkills)
{
float characterSkillLevel = CurrentFixer.GetSkillLevel(skill.Identifier);
CurrentFixer.Info.IncreaseSkillLevel(skill.Identifier,
SkillSettings.Current.SkillIncreasePerRepair / Math.Max(characterSkillLevel, 1.0f),
CurrentFixer.WorldPosition + Vector2.UnitY * 100.0f);
}
SteamAchievementManager.OnItemRepaired(item, CurrentFixer);
deteriorationTimer = Rand.Range(MinDeteriorationDelay, MaxDeteriorationDelay);
wasBroken = false;
}
StopRepairing(CurrentFixer);
}
}
else if (currentFixerAction == FixActions.Sabotage)
{
if (fixDuration <= 0.0f)
{
item.Condition = item.MaxCondition * (MinSabotageCondition / 100);
}
else
{
float conditionDecrease = deltaTime / (fixDuration / item.MaxCondition);
item.Condition -= conditionDecrease;
}
if (item.ConditionPercentage <= MinSabotageCondition)
{
if (wasGoodCondition)
{
foreach (Skill skill in requiredSkills)
{
float characterSkillLevel = CurrentFixer.GetSkillLevel(skill.Identifier);
CurrentFixer.Info.IncreaseSkillLevel(skill.Identifier,
SkillSettings.Current.SkillIncreasePerSabotage / Math.Max(characterSkillLevel, 1.0f),
CurrentFixer.WorldPosition + Vector2.UnitY * 100.0f);
}
deteriorationTimer = 0.0f;
deteriorateAlwaysResetTimer = item.Condition / DeteriorationSpeed;
DeteriorateAlways = true;
item.Condition = item.MaxCondition * (MinSabotageCondition / 100);
wasGoodCondition = false;
}
StopRepairing(CurrentFixer);
}
}
else
{
throw new NotImplementedException(currentFixerAction.ToString());
}
}
partial void UpdateProjSpecific(float deltaTime);
private bool ShouldDeteriorate()
{
if (LastActiveTime > Timing.TotalTime) { return true; }
foreach (ItemComponent ic in item.Components)
{
if (ic is Fabricator || ic is Deconstructor)
{
//fabricators and deconstructors rely on LastActiveTime
return false;
}
else if (ic is PowerTransfer pt)
{
//power transfer items (junction boxes, relays) don't deteriorate if they're no carrying any power
if (Math.Abs(pt.CurrPowerConsumption) > 0.1f) { return true; }
}
else if (ic is Engine engine)
{
//engines don't deteriorate if they're not running
if (Math.Abs(engine.Force) > 1.0f) { return true; }
}
else if (ic is Pump pump)
{
//pumps don't deteriorate if they're not running
if (Math.Abs(pump.FlowPercentage) > 1.0f && pump.IsActive) { return true; }
}
else if (ic is Reactor reactor)
{
//reactors don't deteriorate if they're not powered up
if (reactor.Temperature > 0.1f) { return true; }
}
else if (ic is OxygenGenerator oxyGenerator)
{
//oxygen generators don't deteriorate if they're not running
if (oxyGenerator.CurrFlow > 0.1f) { return true; }
}
else if (ic is Powered powered)
{
if (powered.Voltage >= powered.MinVoltage) { return true; }
}
}
return DeteriorateAlways;
}
private float GetDeteriorationDelayMultiplier()
{
foreach (ItemComponent ic in item.Components)
{
if (ic is Engine engine)
{
return Math.Abs(engine.Force) / 100.0f;
}
else if (ic is Pump pump)
{
return Math.Abs(pump.FlowPercentage) / 100.0f;
}
else if (ic is Reactor reactor)
{
return (reactor.FissionRate + reactor.TurbineOutput) / 200.0f;
}
}
return 1.0f;
}
private void UpdateFixAnimation(Character character)
{
character.AnimController.UpdateUseItem(false, item.WorldPosition + new Vector2(0.0f, 100.0f) * ((item.Condition / item.MaxCondition) % 0.1f));
}
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0, float signalStrength = 1)
{
//do nothing
//Repairables should always stay active, so we don't want to use the default behavior
//where set_active/set_state signals can disable the component
}
}
}
@@ -0,0 +1,17 @@
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
class AdderComponent : ArithmeticComponent
{
public AdderComponent(Item item, XElement element)
: base(item, element)
{
}
protected override float Calculate(float signal1, float signal2)
{
return signal1 + signal2;
}
}
}
@@ -0,0 +1,80 @@
using System;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
class AndComponent : ItemComponent
{
protected string output, falseOutput;
//an array to keep track of how long ago a non-zero signal was received on both inputs
protected float[] timeSinceReceived;
//the output is sent if both inputs have received a signal within the timeframe
protected float timeFrame;
[InGameEditable(DecimalCount = 2), Serialize(0.0f, true, description: "The item sends the output if both inputs have received a non-zero signal within the timeframe. If set to 0, the inputs must receive a signal at the same time.")]
public float TimeFrame
{
get { return timeFrame; }
set
{
timeFrame = Math.Max(0.0f, value);
}
}
[InGameEditable, Serialize("1", true, description: "The signal sent when both inputs have received a non-zero signal.")]
public string Output
{
get { return output; }
set { output = value; }
}
[InGameEditable, Serialize("", true, description: "The signal sent when both inputs have not received a non-zero signal (if empty, no signal is sent).")]
public string FalseOutput
{
get { return falseOutput; }
set { falseOutput = value; }
}
public AndComponent(Item item, XElement element)
: base(item, element)
{
timeSinceReceived = new float[] { Math.Max(timeFrame * 2.0f, 0.1f), Math.Max(timeFrame * 2.0f, 0.1f) };
IsActive = true;
}
public override void Update(float deltaTime, Camera cam)
{
bool sendOutput = true;
for (int i = 0; i < timeSinceReceived.Length; i++)
{
if (timeSinceReceived[i] > timeFrame) sendOutput = false;
timeSinceReceived[i] += deltaTime;
}
string signalOut = sendOutput ? output : falseOutput;
if (string.IsNullOrEmpty(signalOut)) return;
item.SendSignal(0, signalOut, "signal_out", null);
}
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f)
{
switch (connection.Name)
{
case "signal_in1":
if (signal == "0") return;
timeSinceReceived[0] = 0.0f;
break;
case "signal_in2":
if (signal == "0") return;
timeSinceReceived[1] = 0.0f;
break;
case "set_output":
output = signal;
break;
}
}
}
}
@@ -0,0 +1,87 @@
using Microsoft.Xna.Framework;
using System;
using System.Globalization;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
abstract class ArithmeticComponent : ItemComponent
{
//an array to keep track of how long ago a signal was received on both inputs
protected float[] timeSinceReceived;
protected float[] receivedSignal;
//the output is sent if both inputs have received a signal within the timeframe
protected float timeFrame;
[Serialize(999999.0f, true, description: "The output of the item is restricted below this value."),
InGameEditable(MinValueFloat = -999999.0f, MaxValueFloat = 999999.0f)]
public float ClampMax
{
get;
set;
}
[Serialize(-999999.0f, true, description: "The output of the item is restricted above this value."),
InGameEditable(MinValueFloat = -999999.0f, MaxValueFloat = 999999.0f)]
public float ClampMin
{
get;
set;
}
[InGameEditable(DecimalCount = 2),
Serialize(0.0f, true, description: "The item must have received signals to both inputs within this timeframe to output the sum of the signals." +
" If set to 0, the inputs must be received at the same time.")]
public float TimeFrame
{
get { return timeFrame; }
set
{
timeFrame = Math.Max(0.0f, value);
}
}
public ArithmeticComponent(Item item, XElement element)
: base(item, element)
{
timeSinceReceived = new float[] { Math.Max(timeFrame * 2.0f, 0.1f), Math.Max(timeFrame * 2.0f, 0.1f) };
receivedSignal = new float[2];
}
sealed public override void Update(float deltaTime, Camera cam)
{
for (int i = 0; i < timeSinceReceived.Length; i++)
{
if (timeSinceReceived[i] > timeFrame)
{
IsActive = false;
return;
}
timeSinceReceived[i] += deltaTime;
}
float output = Calculate(receivedSignal[0], receivedSignal[1]);
item.SendSignal(0, MathHelper.Clamp(output, ClampMin, ClampMax).ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
}
protected abstract float Calculate(float signal1, float signal2);
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f)
{
switch (connection.Name)
{
case "signal_in1":
float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out receivedSignal[0]);
timeSinceReceived[0] = 0.0f;
IsActive = true;
break;
case "signal_in2":
float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out receivedSignal[1]);
timeSinceReceived[1] = 0.0f;
IsActive = true;
break;
}
}
}
}
@@ -0,0 +1,56 @@
using System;
using System.Globalization;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
class ColorComponent : ItemComponent
{
protected float[] receivedSignal;
private string output = "0,0,0,0";
public ColorComponent(Item item, XElement element)
: base(item, element)
{
receivedSignal = new float[4];
IsActive = true;
}
public override void Update(float deltaTime, Camera cam)
{
item.SendSignal(0, output, "signal_out", null);
}
private void UpdateOutput()
{
output = receivedSignal[0].ToString("G", CultureInfo.InvariantCulture);
output += "," + receivedSignal[1].ToString("G", CultureInfo.InvariantCulture);
output += "," + receivedSignal[2].ToString("G", CultureInfo.InvariantCulture);
output += "," + receivedSignal[3].ToString("G", CultureInfo.InvariantCulture);
}
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f)
{
switch (connection.Name)
{
case "signal_r":
float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out receivedSignal[0]);
UpdateOutput();
break;
case "signal_g":
float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out receivedSignal[1]);
UpdateOutput();
break;
case "signal_b":
float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out receivedSignal[2]);
UpdateOutput();
break;
case "signal_a":
float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out receivedSignal[3]);
UpdateOutput();
break;
}
}
}
}
@@ -0,0 +1,333 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
partial class Connection
{
//how many wires can be linked to a single connector
public const int MaxLinked = 5;
public readonly string Name;
public readonly string DisplayName;
private Wire[] wires;
public IEnumerable<Wire> Wires
{
get { return wires; }
}
private Item item;
public readonly bool IsOutput;
public readonly List<StatusEffect> Effects;
public readonly ushort[] wireId;
public bool IsPower
{
get;
private set;
}
private bool recipientsDirty = true;
private List<Connection> recipients = new List<Connection>();
public List<Connection> Recipients
{
get
{
if (recipientsDirty) RefreshRecipients();
return recipients;
}
}
public Item Item
{
get { return item; }
}
public ConnectionPanel ConnectionPanel
{
get;
private set;
}
public override string ToString()
{
return "Connection (" + item.Name + ", " + Name + ")";
}
public Connection(XElement element, ConnectionPanel connectionPanel)
{
#if CLIENT
if (connector == null)
{
connector = GUI.Style.GetComponentStyle("ConnectionPanelConnector").Sprites[GUIComponent.ComponentState.None][0].Sprite;
wireVertical = GUI.Style.GetComponentStyle("ConnectionPanelWire").Sprites[GUIComponent.ComponentState.None][0].Sprite;
connectionSprite = GUI.Style.GetComponentStyle("ConnectionPanelConnection").Sprites[GUIComponent.ComponentState.None][0].Sprite;
connectionSpriteHighlight = GUI.Style.GetComponentStyle("ConnectionPanelConnection").Sprites[GUIComponent.ComponentState.Hover][0].Sprite;
screwSprites = GUI.Style.GetComponentStyle("ConnectionPanelScrew").Sprites[GUIComponent.ComponentState.None].Select(s => s.Sprite).ToList();
}
#endif
ConnectionPanel = connectionPanel;
item = connectionPanel.Item;
wires = new Wire[MaxLinked];
IsOutput = element.Name.ToString() == "output";
Name = element.GetAttributeString("name", IsOutput ? "output" : "input");
string displayNameTag = "", fallbackTag = "";
//if displayname is not present, attempt to find it from the prefab
if (element.Attribute("displayname") == null)
{
foreach (XElement subElement in item.Prefab.ConfigElement.Elements())
{
if (!subElement.Name.ToString().Equals("connectionpanel", StringComparison.OrdinalIgnoreCase)) { continue; }
foreach (XElement connectionElement in subElement.Elements())
{
string prefabConnectionName = element.GetAttributeString("name", null);
if (prefabConnectionName == Name)
{
displayNameTag = connectionElement.GetAttributeString("displayname", "");
fallbackTag = connectionElement.GetAttributeString("fallbackdisplayname", "");
}
}
}
}
else
{
displayNameTag = element.GetAttributeString("displayname", "");
fallbackTag = element.GetAttributeString("fallbackdisplayname", null);
}
if (!string.IsNullOrEmpty(displayNameTag))
{
//extract the tag parts in case the tags contains variables
string tagWithoutVariables = displayNameTag?.Split('~')?.FirstOrDefault();
string fallbackTagWithoutVariables = fallbackTag?.Split('~')?.FirstOrDefault();
//use displayNameTag if found, otherwise fallBack
if (TextManager.ContainsTag(tagWithoutVariables))
{
DisplayName = TextManager.GetServerMessage(displayNameTag);
}
else if (TextManager.ContainsTag(fallbackTagWithoutVariables))
{
DisplayName = TextManager.GetServerMessage(fallbackTag);
}
}
if (string.IsNullOrEmpty(DisplayName))
{
#if DEBUG
DebugConsole.ThrowError("Missing display name in connection " + item.Name + ": " + Name);
#endif
DisplayName = Name;
}
IsPower = Name == "power_in" || Name == "power" || Name == "power_out";
Effects = new List<StatusEffect>();
wireId = new ushort[MaxLinked];
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "link":
int index = -1;
for (int i = 0; i < MaxLinked; i++)
{
if (wireId[i] < 1) index = i;
}
if (index == -1) break;
int id = subElement.GetAttributeInt("w", 0);
if (id < 0) id = 0;
wireId[index] = (ushort)id;
break;
case "statuseffect":
Effects.Add(StatusEffect.Load(subElement, item.Name + ", connection " + Name));
break;
}
}
}
private void RefreshRecipients()
{
recipients.Clear();
for (int i = 0; i < MaxLinked; i++)
{
if (wires[i] == null) continue;
Connection recipient = wires[i].OtherConnection(this);
if (recipient != null) recipients.Add(recipient);
}
recipientsDirty = false;
}
public int FindEmptyIndex()
{
for (int i = 0; i < MaxLinked; i++)
{
if (wires[i] == null) return i;
}
return -1;
}
public int FindWireIndex(Wire wire)
{
for (int i = 0; i < MaxLinked; i++)
{
if (wires[i] == wire) return i;
}
return -1;
}
public int FindWireIndex(Item wireItem)
{
for (int i = 0; i < MaxLinked; i++)
{
if (wires[i] == null && wireItem == null) return i;
if (wires[i] != null && wires[i].Item == wireItem) return i;
}
return -1;
}
public void TryAddLink(Wire wire)
{
for (int i = 0; i < MaxLinked; i++)
{
if (wires[i] == null)
{
SetWire(i, wire);
return;
}
}
}
public void SetWire(int index, Wire wire)
{
Wire previousWire = wires[index];
if (wire != previousWire && previousWire != null)
{
var otherConnection = previousWire.OtherConnection(this);
if (otherConnection != null)
{
otherConnection.recipientsDirty = true;
}
}
wires[index] = wire;
recipientsDirty = true;
if (wire != null)
{
ConnectionPanel.DisconnectedWires.Remove(wire);
var otherConnection = wire.OtherConnection(this);
if (otherConnection != null)
{
otherConnection.recipientsDirty = true;
}
}
}
public void SendSignal(int stepsTaken, string signal, Item source, Character sender, float power, float signalStrength = 1.0f)
{
for (int i = 0; i < MaxLinked; i++)
{
if (wires[i] == null) { continue; }
Connection recipient = wires[i].OtherConnection(this);
if (recipient == null) { continue; }
if (recipient.item == this.item || recipient.item == source) { continue; }
source?.LastSentSignalRecipients.Add(recipient.item);
foreach (ItemComponent ic in recipient.item.Components)
{
ic.ReceiveSignal(stepsTaken, signal, recipient, source, sender, power, signalStrength);
}
foreach (StatusEffect effect in recipient.Effects)
{
recipient.Item.ApplyStatusEffect(effect, ActionType.OnUse, (float)Timing.Step);
}
}
}
public void SendPowerProbeSignal(Item source, float power)
{
for (int i = 0; i < MaxLinked; i++)
{
if (wires[i] == null) { continue; }
Connection recipient = wires[i].OtherConnection(this);
if (recipient == null) { continue; }
recipient.item.GetComponent<Powered>()?.ReceivePowerProbeSignal(recipient, source, power);
}
}
public void ClearConnections()
{
for (int i = 0; i < MaxLinked; i++)
{
if (wires[i] == null) continue;
wires[i].RemoveConnection(this);
wires[i] = null;
recipientsDirty = true;
}
}
public void ConnectLinked()
{
if (wireId == null) return;
for (int i = 0; i < MaxLinked; i++)
{
if (wireId[i] == 0) { continue; }
if (!(Entity.FindEntityByID(wireId[i]) is Item wireItem)) { continue; }
wires[i] = wireItem.GetComponent<Wire>();
recipientsDirty = true;
if (wires[i] != null)
{
if (wires[i].Item.body != null) wires[i].Item.body.Enabled = false;
wires[i].Connect(this, false, false);
}
}
}
public void Save(XElement parentElement)
{
XElement newElement = new XElement(IsOutput ? "output" : "input", new XAttribute("name", Name));
Array.Sort(wires, delegate (Wire wire1, Wire wire2)
{
if (wire1 == null) return 1;
if (wire2 == null) return -1;
return wire1.Item.ID.CompareTo(wire2.Item.ID);
});
for (int i = 0; i < MaxLinked; i++)
{
if (wires[i] == null) continue;
newElement.Add(new XElement("link",
new XAttribute("w", wires[i].Item.ID.ToString())));
}
parentElement.Add(newElement);
}
}
}
@@ -0,0 +1,314 @@
using Barotrauma.Networking;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
partial class ConnectionPanel : ItemComponent, IServerSerializable, IClientSerializable
{
public List<Connection> Connections;
private Character user;
/// <summary>
/// Wires that have been disconnected from the panel, but not removed completely (visible at the bottom of the connection panel).
/// </summary>
public readonly HashSet<Wire> DisconnectedWires = new HashSet<Wire>();
private List<ushort> disconnectedWireIds;
[Editable, Serialize(false, true, description: "Locked connection panels cannot be rewired in-game.")]
public bool Locked
{
get;
set;
}
//connection panels can't be deactivated externally (by signals or status effects)
public override bool IsActive
{
get { return base.IsActive; }
set { /*do nothing*/ }
}
public Character User
{
get { return user; }
}
public ConnectionPanel(Item item, XElement element)
: base(item, element)
{
Connections = new List<Connection>();
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString())
{
case "input":
Connections.Add(new Connection(subElement, this));
break;
case "output":
Connections.Add(new Connection(subElement, this));
break;
}
}
base.IsActive = true;
InitProjSpecific(element);
}
partial void InitProjSpecific(XElement element);
public override void OnMapLoaded()
{
foreach (Connection c in Connections)
{
c.ConnectLinked();
}
if (disconnectedWireIds != null)
{
foreach (ushort disconnectedWireId in disconnectedWireIds)
{
if (!(Entity.FindEntityByID(disconnectedWireId) is Item wireItem)) { continue; }
Wire wire = wireItem.GetComponent<Wire>();
if (wire != null)
{
DisconnectedWires.Add(wire);
base.IsActive = true;
}
}
}
}
public override void OnItemLoaded()
{
if (item.body != null)
{
var holdable = item.GetComponent<Holdable>();
if (holdable == null || !holdable.Attachable)
{
DebugConsole.ThrowError("Item \"" + item.Name + "\" has a ConnectionPanel component," +
" but cannot be wired because it has an active physics body that cannot be attached to a wall." +
" Remove the physics body or add a Holdable component with the Attachable attribute set to true.");
}
}
}
public void MoveConnectedWires(Vector2 amount)
{
Vector2 wireNodeOffset = item.Submarine == null ? Vector2.Zero : item.Submarine.HiddenSubPosition + amount;
foreach (Connection c in Connections)
{
foreach (Wire wire in c.Wires)
{
if (wire == null) continue;
#if CLIENT
if (wire.Item.IsSelected) continue;
#endif
var wireNodes = wire.GetNodes();
if (wireNodes.Count == 0) continue;
if (Submarine.RectContains(item.Rect, wireNodes[0] + wireNodeOffset))
{
wire.MoveNode(0, amount);
}
else if (Submarine.RectContains(item.Rect, wireNodes[wireNodes.Count - 1] + wireNodeOffset))
{
wire.MoveNode(wireNodes.Count - 1, amount);
}
}
}
}
public override void Update(float deltaTime, Camera cam)
{
UpdateProjSpecific(deltaTime);
if (user == null || user.SelectedConstruction != item)
{
#if SERVER
if (user != null) { item.CreateServerEvent(this); }
#endif
user = null;
if (DisconnectedWires.Count == 0) { base.IsActive = false; }
return;
}
if (!user.Enabled || !HasRequiredItems(user, addMessage: false))
{
user = null;
base.IsActive = false;
return;
}
user.AnimController.UpdateUseItem(true, item.WorldPosition + new Vector2(0.0f, 100.0f) * (((float)Timing.TotalTime / 10.0f) % 0.1f));
}
public override void UpdateBroken(float deltaTime, Camera cam)
{
Update(deltaTime, cam);
}
partial void UpdateProjSpecific(float deltaTime);
public override bool Select(Character picker)
{
//attaching wires to items with a body is not allowed
//(signal items remove their bodies when attached to a wall)
if (item.body != null)
{
return false;
}
user = picker;
#if SERVER
if (user != null) { item.CreateServerEvent(this); }
#endif
base.IsActive = true;
return true;
}
public override bool Use(float deltaTime, Character character = null)
{
if (character == null || character != user) { return false; }
return true;
}
/// <summary>
/// Check if the character manages to succesfully rewire the panel, and if not, apply OnFailure effects
/// </summary>
public bool CheckCharacterSuccess(Character character)
{
if (character == null) { return false; }
var powered = item.GetComponent<Powered>();
if (powered != null)
{
//unpowered panels can be rewired without a risk of electrical shock
if (powered.Voltage < 0.1f) { return true; }
}
float degreeOfSuccess = DegreeOfSuccess(character);
if (Rand.Range(0.0f, 0.5f) < degreeOfSuccess) { return true; }
item.ApplyStatusEffects(ActionType.OnFailure, 1.0f, character);
return false;
}
public override void Load(XElement element, bool usePrefabValues)
{
base.Load(element, usePrefabValues);
List<Connection> loadedConnections = new List<Connection>();
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString())
{
case "input":
loadedConnections.Add(new Connection(subElement, this));
break;
case "output":
loadedConnections.Add(new Connection(subElement, this));
break;
}
}
for (int i = 0; i < loadedConnections.Count && i < Connections.Count; i++)
{
loadedConnections[i].wireId.CopyTo(Connections[i].wireId, 0);
}
disconnectedWireIds = element.GetAttributeUshortArray("disconnectedwires", new ushort[0]).ToList();
}
public override XElement Save(XElement parentElement)
{
XElement componentElement = base.Save(parentElement);
foreach (Connection c in Connections)
{
c.Save(componentElement);
}
if (DisconnectedWires.Count > 0)
{
componentElement.Add(new XAttribute("disconnectedwires", string.Join(",", DisconnectedWires.Select(w => w.Item.ID))));
}
return componentElement;
}
protected override void ShallowRemoveComponentSpecific()
{
//do nothing
}
protected override void RemoveComponentSpecific()
{
foreach (Wire wire in DisconnectedWires.ToList())
{
if (wire.OtherConnection(null) == null) //wire not connected to anything else
{
wire.Item.Drop(null);
}
}
DisconnectedWires.Clear();
foreach (Connection c in Connections)
{
foreach (Wire wire in c.Wires)
{
if (wire == null) { continue; }
if (wire.OtherConnection(c) == null) //wire not connected to anything else
{
wire.Item.Drop(null);
}
else
{
wire.RemoveConnection(item);
}
}
}
#if CLIENT
rewireSoundChannel?.FadeOutAndDispose();
rewireSoundChannel = null;
#endif
}
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0, float signalStrength = 1)
{
//do nothing
}
public void ClientWrite(IWriteMessage msg, object[] extraData = null)
{
#if CLIENT
TriggerRewiringSound();
#endif
foreach (Connection connection in Connections)
{
foreach (Wire wire in connection.Wires)
{
msg.Write(wire?.Item == null ? (ushort)0 : wire.Item.ID);
}
}
msg.Write((ushort)DisconnectedWires.Count());
foreach (Wire disconnectedWire in DisconnectedWires)
{
msg.Write(disconnectedWire.Item.ID);
}
}
}
}
@@ -0,0 +1,199 @@
using Barotrauma.Networking;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
partial class CustomInterface : ItemComponent, IClientSerializable, IServerSerializable
{
class CustomInterfaceElement : ISerializableEntity
{
public bool ContinuousSignal;
public bool State;
public string ConnectionName;
public Connection Connection;
[Serialize("", false, translationTextTag: "Label.", description: "The text displayed on this button/tickbox."), Editable]
public string Label { get; set; }
[Serialize("1", false, description: "The signal sent out when this button is pressed or this tickbox checked."), Editable]
public string Signal { get; set; }
public string Name => "CustomInterfaceElement";
public Dictionary<string, SerializableProperty> SerializableProperties { get; set; }
public List<StatusEffect> StatusEffects = new List<StatusEffect>();
public CustomInterfaceElement(XElement element)
{
Label = element.GetAttributeString("text", "");
ConnectionName = element.GetAttributeString("connection", "");
Signal = element.GetAttributeString("signal", "1");
foreach (XElement subElement in element.Elements())
{
if (subElement.Name.ToString().Equals("statuseffect", System.StringComparison.OrdinalIgnoreCase))
{
StatusEffects.Add(StatusEffect.Load(subElement, parentDebugName: "custom interface element (label " + Label + ")"));
}
}
}
}
private string[] labels;
[Serialize("", true, description: "The texts displayed on the buttons/tickboxes, separated by commas.")]
public string Labels
{
get { return string.Join(",", labels); }
set
{
if (value == null) { return; }
string[] splitValues = value == "" ? new string[0] : value.Split(',');
if (customInterfaceElementList.Count > 0)
{
UpdateLabels(splitValues);
}
}
}
private string[] signals;
[Serialize("", true, description: "The signals sent when the buttons are pressed or the tickboxes checked, separated by commas.")]
public string Signals
{
//use semicolon as a separator because comma may be needed in the signals (for color or vector values for example)
//kind of hacky, we should probably add support for (string) arrays to SerializableEntityEditor so this wouldn't be needed
get { return signals == null ? "" : string.Join(";", signals); }
set
{
if (value == null) { return; }
string[] splitValues = value == "" ? new string[0] : value.Split(';');
if (customInterfaceElementList.Count > 0)
{
signals = new string[customInterfaceElementList.Count];
for (int i = 0; i < customInterfaceElementList.Count; i++)
{
signals[i] = i < splitValues.Length ? splitValues[i] : customInterfaceElementList[i].Signal;
customInterfaceElementList[i].Signal = signals[i];
}
}
}
}
private List<CustomInterfaceElement> customInterfaceElementList = new List<CustomInterfaceElement>();
public CustomInterface(Item item, XElement element)
: base(item, element)
{
int i = 0;
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "button":
var button = new CustomInterfaceElement(subElement)
{
ContinuousSignal = false
};
if (string.IsNullOrEmpty(button.Label))
{
button.Label = "Signal out " + customInterfaceElementList.Count(e => !e.ContinuousSignal);
}
customInterfaceElementList.Add(button);
break;
case "tickbox":
var tickBox = new CustomInterfaceElement(subElement)
{
ContinuousSignal = true
};
if (string.IsNullOrEmpty(tickBox.Label))
{
tickBox.Label = "Signal out " + customInterfaceElementList.Count(e => !e.ContinuousSignal);
}
customInterfaceElementList.Add(tickBox);
break;
}
i++;
}
IsActive = true;
InitProjSpecific(element);
Labels = element.GetAttributeString("labels", "");
Signals = element.GetAttributeString("signals", "");
}
private void UpdateLabels(string[] newLabels)
{
labels = new string[customInterfaceElementList.Count];
for (int i = 0; i < labels.Length; i++)
{
labels[i] = i < newLabels.Length ? newLabels[i] : customInterfaceElementList[i].Label;
if (Screen.Selected != GameMain.SubEditorScreen)
{
customInterfaceElementList[i].Label = TextManager.Get(labels[i], returnNull: true) ?? labels[i];
}
else
{
customInterfaceElementList[i].Label = labels[i];
}
}
UpdateLabelsProjSpecific();
}
public override void OnItemLoaded()
{
foreach (CustomInterfaceElement ciElement in customInterfaceElementList)
{
ciElement.Connection = item.Connections?.FirstOrDefault(c => c.Name == ciElement.ConnectionName);
}
}
partial void UpdateLabelsProjSpecific();
partial void InitProjSpecific(XElement element);
private void ButtonClicked(CustomInterfaceElement btnElement)
{
if (btnElement == null) return;
if (btnElement.Connection != null)
{
item.SendSignal(0, btnElement.Signal, btnElement.Connection, sender: null, source: item);
}
foreach (StatusEffect effect in btnElement.StatusEffects)
{
item.ApplyStatusEffect(effect, ActionType.OnUse, 1.0f);
}
}
private void TickBoxToggled(CustomInterfaceElement tickBoxElement, bool state)
{
if (tickBoxElement == null) { return; }
tickBoxElement.State = state;
}
public override void Update(float deltaTime, Camera cam)
{
UpdateProjSpecific();
foreach (CustomInterfaceElement ciElement in customInterfaceElementList)
{
if (!ciElement.ContinuousSignal) { continue; }
//TODO: allow changing output when a tickbox is not selected
if (!string.IsNullOrEmpty(ciElement.Signal) && ciElement.Connection != null)
{
item.SendSignal(0, ciElement.State ? ciElement.Signal : "0", ciElement.Connection, sender: null, source: item);
}
foreach (StatusEffect effect in ciElement.StatusEffects)
{
item.ApplyStatusEffect(effect, ciElement.State ? ActionType.OnUse : ActionType.OnSecondaryUse, 1.0f, null, null, null, true, false);
}
}
}
partial void UpdateProjSpecific();
public override XElement Save(XElement parentElement)
{
labels = customInterfaceElementList.Select(ci => ci.Label).ToArray();
signals = customInterfaceElementList.Select(ci => ci.Signal).ToArray();
return base.Save(parentElement);
}
}
}
@@ -0,0 +1,114 @@
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
class DelayComponent : ItemComponent
{
class DelayedSignal
{
public readonly string Signal;
public readonly float SignalStrength;
//in number of frames
public int SendTimer;
//in number of frames
public int SendDuration;
public DelayedSignal(string signal, float signalStrength, int sendTimer)
{
Signal = signal;
SignalStrength = signalStrength;
SendTimer = sendTimer;
}
}
private int signalQueueSize;
private int delayTicks;
private Queue<DelayedSignal> signalQueue;
private DelayedSignal prevQueuedSignal;
private float delay;
[InGameEditable(MinValueFloat = 0.0f, MaxValueFloat = 60.0f, DecimalCount = 2), Serialize(1.0f, true, description: "How long the item delays the signals (in seconds).")]
public float Delay
{
get { return delay; }
set
{
if (value == delay) { return; }
delay = value;
delayTicks = (int)(delay / Timing.Step);
signalQueueSize = delayTicks * 2;
}
}
[InGameEditable, Serialize(false, true, description: "Should the component discard previously received signals when a new one is received.")]
public bool ResetWhenSignalReceived
{
get;
set;
}
[InGameEditable, Serialize(false, true, description: "Should the component discard previously received signals when the incoming signal changes.")]
public bool ResetWhenDifferentSignalReceived
{
get;
set;
}
public DelayComponent(Item item, XElement element)
: base (item, element)
{
signalQueue = new Queue<DelayedSignal>();
IsActive = true;
}
public override void Update(float deltaTime, Camera cam)
{
foreach (var val in signalQueue)
{
val.SendTimer -= 1;
}
while (signalQueue.Count > 0 && signalQueue.Peek().SendTimer <= 0)
{
var signalOut = signalQueue.Peek();
signalOut.SendDuration -= 1;
item.SendSignal(0, signalOut.Signal, "signal_out", null, signalStrength: signalOut.SignalStrength);
if (signalOut.SendDuration <= 0) { signalQueue.Dequeue(); } else { break; }
}
}
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f)
{
switch (connection.Name)
{
case "signal_in":
if (signalQueue.Count >= signalQueueSize) { return; }
if (ResetWhenSignalReceived) { prevQueuedSignal = null; signalQueue.Clear(); }
if (ResetWhenDifferentSignalReceived && signalQueue.Count > 0 && signalQueue.Peek().Signal != signal)
{
prevQueuedSignal = null;
signalQueue.Clear();
}
if (prevQueuedSignal != null &&
prevQueuedSignal.Signal == signal &&
MathUtils.NearlyEqual(prevQueuedSignal.SignalStrength, signalStrength) &&
((prevQueuedSignal.SendTimer + prevQueuedSignal.SendDuration == delayTicks) || (prevQueuedSignal.SendTimer <= 0 && prevQueuedSignal.SendDuration > 0)))
{
prevQueuedSignal.SendDuration += 1;
return;
}
prevQueuedSignal = new DelayedSignal(signal, signalStrength, delayTicks)
{
SendDuration = 1
};
signalQueue.Enqueue(prevQueuedSignal);
break;
}
}
}
}
@@ -0,0 +1,19 @@
using System;
using System.Globalization;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
class DivideComponent : ArithmeticComponent
{
public DivideComponent(Item item, XElement element)
: base(item, element)
{
}
protected override float Calculate(float signal1, float signal2)
{
return signal1 / signal2;
}
}
}
@@ -0,0 +1,83 @@
using System;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
class EqualsComponent : ItemComponent
{
protected string output, falseOutput;
//an array to keep track of how long ago a signal was received on both inputs
protected float[] timeSinceReceived;
protected string[] receivedSignal;
//the output is sent if both inputs have received a signal within the timeframe
protected float timeFrame;
[InGameEditable, Serialize("1", true, description: "The signal this item outputs when the received signals are equal.")]
public string Output
{
get { return output; }
set { output = value; }
}
[InGameEditable, Serialize("", true, description: "The signal this item outputs when the received signals are not equal.")]
public string FalseOutput
{
get { return falseOutput; }
set { falseOutput = value; }
}
[InGameEditable(DecimalCount = 2), Serialize(0.0f, true, description: "The maximum amount of time between the received signals. If set to 0, the signals must be received at the same time.")]
public float TimeFrame
{
get { return timeFrame; }
set
{
timeFrame = Math.Max(0.0f, value);
}
}
public EqualsComponent(Item item, XElement element)
: base(item, element)
{
timeSinceReceived = new float[] { Math.Max(timeFrame * 2.0f, 0.1f), Math.Max(timeFrame * 2.0f, 0.1f) };
receivedSignal = new string[2];
IsActive = true;
}
public override void Update(float deltaTime, Camera cam)
{
bool sendOutput = false;
for (int i = 0; i < timeSinceReceived.Length; i++)
{
if (timeSinceReceived[i] <= timeFrame) sendOutput = true;
timeSinceReceived[i] += deltaTime;
}
if (sendOutput)
{
string signalOut = receivedSignal[0] == receivedSignal[1] ? output : falseOutput;
if (string.IsNullOrEmpty(signalOut)) return;
item.SendSignal(0, signalOut, "signal_out", null);
}
}
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f)
{
switch (connection.Name)
{
case "signal_in1":
receivedSignal[0] = signal;
timeSinceReceived[0] = 0.0f;
break;
case "signal_in2":
receivedSignal[1] = signal;
timeSinceReceived[1] = 0.0f;
break;
}
}
}
}
@@ -0,0 +1,43 @@
using System.Globalization;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
class ExponentiationComponent : ItemComponent
{
private float exponent;
[InGameEditable, Serialize(1.0f, false, description: "The exponent of the operation.")]
public float Exponent
{
get
{
return exponent;
}
set
{
exponent = value;
}
}
public ExponentiationComponent(Item item, XElement element)
: base(item, element)
{
IsActive = true;
}
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0, float signalStrength = 1)
{
switch (connection.Name)
{
case "set_exponent":
case "exponent":
float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out exponent);
break;
case "signal_in":
float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float value);
item.SendSignal(0, MathUtils.Pow(value, Exponent).ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
break;
}
}
}
}
@@ -0,0 +1,67 @@
using System;
using System.Globalization;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
class FunctionComponent : ItemComponent
{
public enum FunctionType
{
Round,
Ceil,
Floor,
Factorial,
AbsoluteValue,
SquareRoot
}
[Serialize(FunctionType.Round, false, description: "Which kind of function to run the input through.")]
public FunctionType Function
{
get; set;
}
public FunctionComponent(Item item, XElement element)
: base(item, element)
{
IsActive = true;
}
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0, float signalStrength = 1)
{
if (connection.Name != "signal_in") return;
if (!float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float value)) return;
switch (Function)
{
case FunctionType.Round:
item.SendSignal(0, Math.Round(value).ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
break;
case FunctionType.Ceil:
item.SendSignal(0, Math.Ceiling(value).ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
break;
case FunctionType.Floor:
item.SendSignal(0, Math.Floor(value).ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
break;
case FunctionType.Factorial:
int intVal = (int)Math.Min(value, 20);
ulong factorial = 1;
for (int i = intVal; i > 0; i--)
{
factorial *= (ulong)i;
}
item.SendSignal(0, factorial.ToString(), "signal_out", null);
break;
case FunctionType.AbsoluteValue:
item.SendSignal(0, Math.Abs(value).ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
break;
case FunctionType.SquareRoot:
double square = value > 0 ? Math.Sqrt(value) : 0;
item.SendSignal(0, square.ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
break;
default:
throw new NotImplementedException($"Function {Function} has not been implemented.");
}
}
}
}
@@ -0,0 +1,41 @@
using System.Globalization;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
class GreaterComponent : EqualsComponent
{
private float val1, val2;
public GreaterComponent(Item item, XElement element)
: base(item, element)
{
IsActive = true;
}
public override void Update(float deltaTime, Camera cam)
{
bool sendOutput = false;
for (int i = 0; i < timeSinceReceived.Length; i++)
{
if (timeSinceReceived[i] <= timeFrame) sendOutput = true;
timeSinceReceived[i] += deltaTime;
}
if (sendOutput)
{
string signalOut = val1 > val2 ? output : falseOutput;
if (string.IsNullOrEmpty(signalOut)) return;
item.SendSignal(0, signalOut, "signal_out", null);
}
}
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f)
{
base.ReceiveSignal(stepsTaken, signal, connection, source, sender, power, signalStrength);
float.TryParse(receivedSignal[0], NumberStyles.Float, CultureInfo.InvariantCulture, out val1);
float.TryParse(receivedSignal[1], NumberStyles.Float, CultureInfo.InvariantCulture, out val2);
}
}
}
@@ -0,0 +1,285 @@
using Microsoft.Xna.Framework;
using System;
using System.Xml.Linq;
using Barotrauma.Networking;
#if CLIENT
using Microsoft.Xna.Framework.Graphics;
using Barotrauma.Lights;
#endif
namespace Barotrauma.Items.Components
{
partial class LightComponent : Powered, IServerSerializable, IDrawableComponent
{
private Color lightColor;
private float lightBrightness;
private float blinkFrequency;
private float range;
private float flicker;
private bool castShadows;
private bool drawBehindSubs;
private float blinkTimer;
private bool itemLoaded;
public PhysicsBody ParentBody;
[Serialize(100.0f, true, description: "The range of the emitted light. Higher values are more performance-intensive."),
Editable(MinValueFloat = 0.0f, MaxValueFloat = 2048.0f)]
public float Range
{
get { return range; }
set
{
range = MathHelper.Clamp(value, 0.0f, 4096.0f);
#if CLIENT
if (light != null) { light.Range = range; }
#endif
}
}
public float Rotation;
[Editable, Serialize(true, true, description: "Should structures cast shadows when light from this light source hits them. " +
"Disabling shadows increases the performance of the game, and is recommended for lights with a short range.")]
public bool CastShadows
{
get { return castShadows; }
set
{
castShadows = value;
#if CLIENT
if (light != null) light.CastShadows = value;
#endif
}
}
[Editable, Serialize(false, true, description: "Lights drawn behind submarines don't cast any shadows and are much faster to draw than shadow-casting lights. " +
"It's recommended to enable this on decorative lights outside the submarine's hull.")]
public bool DrawBehindSubs
{
get { return drawBehindSubs; }
set
{
drawBehindSubs = value;
#if CLIENT
if (light != null) light.IsBackground = drawBehindSubs;
#endif
}
}
[Editable, Serialize(false, true, description: "Is the light currently on.")]
public bool IsOn
{
get { return IsActive; }
set
{
if (IsActive == value) { return; }
IsActive = value;
#if SERVER
if (GameMain.Server != null && itemLoaded) { item.CreateServerEvent(this); }
#endif
}
}
[Serialize(0.0f, false, description: "How heavily the light flickers. 0 = no flickering, 1 = the light will alternate between completely dark and full brightness.")]
public float Flicker
{
get { return flicker; }
set
{
flicker = MathHelper.Clamp(value, 0.0f, 1.0f);
}
}
[Editable, Serialize(0.0f, true, description: "How rapidly the light blinks on and off (in Hz). 0 = no blinking.")]
public float BlinkFrequency
{
get { return blinkFrequency; }
set
{
blinkFrequency = MathHelper.Clamp(value, 0.0f, 60.0f);
}
}
[InGameEditable, Serialize("255,255,255,255", true, description: "The color of the emitted light (R,G,B,A).")]
public Color LightColor
{
get { return lightColor; }
set
{
lightColor = value;
#if CLIENT
if (light != null) light.Color = IsActive ? lightColor : Color.Transparent;
#endif
}
}
public override void Move(Vector2 amount)
{
#if CLIENT
light.Position += amount;
#endif
}
public override bool IsActive
{
get
{
return base.IsActive;
}
set
{
if (base.IsActive == value) { return; }
base.IsActive = value;
SetLightSourceState(value, value ? lightBrightness : 0.0f);
}
}
public LightComponent(Item item, XElement element)
: base(item, element)
{
#if CLIENT
light = new LightSource(element)
{
ParentSub = item.CurrentHull?.Submarine,
Position = item.Position,
CastShadows = castShadows,
IsBackground = drawBehindSubs,
SpriteScale = Vector2.One * item.Scale,
Range = range
};
#endif
IsActive = IsOn;
item.AddTag("light");
}
public override void OnItemLoaded()
{
base.OnItemLoaded();
itemLoaded = true;
SetLightSourceState(IsActive, lightBrightness);
}
public override void Update(float deltaTime, Camera cam)
{
if (item.AiTarget != null)
{
UpdateAITarget(item.AiTarget);
}
UpdateOnActiveEffects(deltaTime);
#if CLIENT
light.ParentSub = item.Submarine;
#endif
if (item.Container != null)
{
SetLightSourceState(false, 0.0f);
return;
}
#if CLIENT
light.Position = ParentBody != null ? ParentBody.Position : item.Position;
#endif
PhysicsBody body = ParentBody ?? item.body;
if (body != null)
{
#if CLIENT
light.Rotation = body.Dir > 0.0f ? body.DrawRotation : body.DrawRotation - MathHelper.Pi;
light.LightSpriteEffect = (body.Dir > 0.0f) ? SpriteEffects.None : SpriteEffects.FlipVertically;
#endif
if (!body.Enabled)
{
SetLightSourceState(false, 0.0f);
return;
}
}
else
{
#if CLIENT
light.Rotation = -Rotation;
#endif
}
currPowerConsumption = powerConsumption;
if (Rand.Range(0.0f, 1.0f) < 0.05f && Voltage < Rand.Range(0.0f, MinVoltage))
{
#if CLIENT
if (Voltage > 0.1f)
{
SoundPlayer.PlaySound("zap", item.WorldPosition, hullGuess: item.CurrentHull);
}
#endif
lightBrightness = 0.0f;
}
else
{
lightBrightness = MathHelper.Lerp(lightBrightness, Math.Min(Voltage, 1.0f), 0.1f);
}
if (blinkFrequency > 0.0f)
{
blinkTimer = (blinkTimer + deltaTime * blinkFrequency) % 1.0f;
}
if (blinkTimer > 0.5f)
{
SetLightSourceState(false, lightBrightness);
}
else
{
SetLightSourceState(true, lightBrightness * (1.0f - Rand.Range(0.0f, flicker)));
}
if (powerIn == null && powerConsumption > 0.0f) { Voltage -= deltaTime; }
}
public override void UpdateBroken(float deltaTime, Camera cam)
{
SetLightSourceState(false, 0.0f);
}
public override bool Use(float deltaTime, Character character = null)
{
return true;
}
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f)
{
switch (connection.Name)
{
case "toggle":
IsActive = !IsActive;
break;
case "set_state":
IsActive = (signal != "0");
break;
case "set_color":
LightColor = XMLExtensions.ParseColor(signal, false);
break;
}
}
public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
{
msg.Write(IsOn);
}
private void UpdateAITarget(AITarget target)
{
if (!IsActive) { return; }
if (target.MaxSightRange <= 0)
{
target.MaxSightRange = Range * 5;
}
target.SightRange = Math.Max(target.SightRange, target.MaxSightRange * lightBrightness);
}
partial void SetLightSourceState(bool enabled, float brightness);
}
}
@@ -0,0 +1,40 @@
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
class MemoryComponent : ItemComponent
{
[InGameEditable, Serialize("", true, description: "The currently stored signal the item outputs.")]
public string Value
{
get;
set;
}
protected bool writeable = true;
public MemoryComponent(Item item, XElement element)
: base(item, element)
{
IsActive = true;
}
public override void Update(float deltaTime, Camera cam)
{
item.SendSignal(0, Value, "signal_out", null);
}
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f)
{
switch (connection.Name)
{
case "signal_in":
if (writeable) { Value = signal; }
break;
case "signal_store":
writeable = (signal == "1");
break;
}
}
}
}
@@ -0,0 +1,41 @@
using System.Globalization;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
class ModuloComponent : ItemComponent
{
private float modulus;
[InGameEditable, Serialize(1.0f, false, description: "The modulus of the operation. Must be non-zero.")]
public float Modulus
{
get { return modulus; }
set
{
modulus = MathUtils.NearlyEqual(value, 0.0f) ? 1.0f : value;
}
}
public ModuloComponent(Item item, XElement element) : base(item, element)
{
IsActive = true;
}
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0, float signalStrength = 1)
{
switch (connection.Name)
{
case "set_modulus":
case "modulus":
float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float newModulus);
Modulus = newModulus;
break;
case "signal_in":
float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float value);
item.SendSignal(0, (value % modulus).ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
break;
}
}
}
}
@@ -0,0 +1,161 @@
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
partial class MotionSensor : ItemComponent
{
private const float UpdateInterval = 0.1f;
private float rangeX, rangeY;
private Vector2 detectOffset;
private float updateTimer;
[Serialize(false, false, description: "Has the item currently detected movement. Intended to be used by StatusEffect conditionals (setting this value in XML has no effect).")]
public bool MotionDetected { get; set; }
[Editable, Serialize(false, true, description: "Should the sensor only detect the movement of humans?")]
public bool OnlyHumans
{
get;
set;
}
[Editable, Serialize(false, true, description: "Should the sensor ignore the bodies of dead characters?")]
public bool IgnoreDead
{
get;
set;
}
[InGameEditable, Serialize(0.0f, true, description: "Horizontal detection range.")]
public float RangeX
{
get { return rangeX; }
set
{
rangeX = MathHelper.Clamp(value, 0.0f, 1000.0f);
}
}
[InGameEditable, Serialize(0.0f, true, description: "Vertical movement detection range.")]
public float RangeY
{
get { return rangeY; }
set
{
rangeY = MathHelper.Clamp(value, 0.0f, 1000.0f);
}
}
[Editable, Serialize("0,0", true, description: "The position to detect the movement at relative to the item. For example, 0,100 would detect movement 100 units above the item.")]
public Vector2 DetectOffset
{
get { return detectOffset; }
set
{
detectOffset = value;
detectOffset.X = MathHelper.Clamp(value.X, -rangeX, rangeX);
detectOffset.Y = MathHelper.Clamp(value.Y, -rangeY, rangeY);
}
}
[InGameEditable, Serialize("1", true, description: "The signal the item outputs when it has detected movement.")]
public string Output { get; set; }
[InGameEditable, Serialize("", true, description: "The signal the item outputs when it has not detected movement.")]
public string FalseOutput { get; set; }
[Editable(DecimalCount = 3), Serialize(0.01f, true, description: "How fast the objects within the detector's range have to be moving (in m/s).")]
public float MinimumVelocity
{
get;
set;
}
public MotionSensor(Item item, XElement element)
: base(item, element)
{
IsActive = true;
//backwards compatibility
if (element.Attribute("range") != null)
{
rangeX = rangeY = element.GetAttributeFloat("range", 0.0f);
}
}
public override void Update(float deltaTime, Camera cam)
{
string signalOut = MotionDetected ? Output : FalseOutput;
if (!string.IsNullOrEmpty(signalOut)) item.SendSignal(1, signalOut, "state_out", null);
updateTimer -= deltaTime;
if (updateTimer > 0.0f) return;
MotionDetected = false;
updateTimer = UpdateInterval;
if (item.body != null && item.body.Enabled)
{
if (Math.Abs(item.body.LinearVelocity.X) > MinimumVelocity || Math.Abs(item.body.LinearVelocity.Y) > MinimumVelocity)
{
MotionDetected = true;
}
}
Vector2 detectPos = item.WorldPosition + detectOffset;
Rectangle detectRect = new Rectangle((int)(detectPos.X - rangeX), (int)(detectPos.Y - rangeY), (int)(rangeX * 2), (int)(rangeY * 2));
float broadRangeX = Math.Max(rangeX * 2, 500);
float broadRangeY = Math.Max(rangeY * 2, 500);
foreach (Character c in Character.CharacterList)
{
if (IgnoreDead && c.IsDead) { continue; }
if (OnlyHumans && !c.IsHuman) { continue; }
//do a rough check based on the position of the character's collider first
//before the more accurate limb-based check
if (Math.Abs(c.WorldPosition.X - detectPos.X) > broadRangeX || Math.Abs(c.WorldPosition.Y - detectPos.Y) > broadRangeY)
{
continue;
}
foreach (Limb limb in c.AnimController.Limbs)
{
if (limb.LinearVelocity.LengthSquared() <= MinimumVelocity * MinimumVelocity) continue;
if (MathUtils.CircleIntersectsRectangle(limb.WorldPosition, ConvertUnits.ToDisplayUnits(limb.body.GetMaxExtent()), detectRect))
{
MotionDetected = true;
break;
}
}
}
}
public override void FlipX(bool relativeToSub)
{
detectOffset.X = -detectOffset.X;
}
public override void FlipY(bool relativeToSub)
{
detectOffset.Y = -detectOffset.Y;
}
public override XElement Save(XElement parentElement)
{
Vector2 prevDetectOffset = detectOffset;
//undo flipping before saving
if (item.FlippedX) { detectOffset.X = -detectOffset.X; }
if (item.FlippedY) { detectOffset.Y = -detectOffset.Y; }
XElement element = base.Save(parentElement);
detectOffset = prevDetectOffset;
return element;
}
}
}
@@ -0,0 +1,17 @@
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
class MultiplyComponent : ArithmeticComponent
{
public MultiplyComponent(Item item, XElement element)
: base(item, element)
{
}
protected override float Calculate(float signal1, float signal2)
{
return signal1 * signal2;
}
}
}
@@ -0,0 +1,19 @@
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
class NotComponent : ItemComponent
{
public NotComponent(Item item, XElement element)
: base (item, element)
{
}
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f)
{
if (connection.Name != "signal_in") return;
item.SendSignal(stepsTaken, signal == "0" ? "1" : "0", "signal_out", sender, 0.0f, source, signalStrength);
}
}
}
@@ -0,0 +1,28 @@
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
class OrComponent : AndComponent
{
public OrComponent(Item item, XElement element)
: base(item, element)
{
IsActive = true;
}
public override void Update(float deltaTime, Camera cam)
{
bool sendOutput = false;
for (int i = 0; i < timeSinceReceived.Length; i++)
{
if (timeSinceReceived[i] <= timeFrame) sendOutput = true;
timeSinceReceived[i] += deltaTime;
}
string signalOut = sendOutput ? output : falseOutput;
if (string.IsNullOrEmpty(signalOut)) return;
item.SendSignal(0, signalOut, "signal_out", null);
}
}
}
@@ -0,0 +1,101 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Xml.Linq;
using Microsoft.Xna.Framework;
namespace Barotrauma.Items.Components
{
class OscillatorComponent : ItemComponent
{
public enum WaveType
{
Pulse,
Sine,
Square,
}
private float frequency;
private float phase;
[InGameEditable, Serialize(WaveType.Pulse, true, description: "What kind of a signal the item outputs." +
" Pulse: periodically sends out a signal of 1." +
" Sine: sends out a sine wave oscillating between -1 and 1." +
" Square: sends out a signal that alternates between 0 and 1.")]
public WaveType OutputType
{
get;
set;
}
[InGameEditable(DecimalCount = 2), Serialize(1.0f, true, description: "How fast the signal oscillates, or how fast the pulses are sent (in Hz).")]
public float Frequency
{
get { return frequency; }
set
{
//capped to 240 Hz (= 4 signals per frame) to prevent players
//from wrecking the performance by setting the value too high
frequency = MathHelper.Clamp(value, 0.0f, 240.0f);
}
}
public OscillatorComponent(Item item, XElement element) :
base(item, element)
{
IsActive = true;
}
public override void Update(float deltaTime, Camera cam)
{
switch (OutputType)
{
case WaveType.Pulse:
if (frequency <= 0.0f) return;
phase += deltaTime;
float pulseInterval = 1.0f / frequency;
while (phase >= pulseInterval)
{
item.SendSignal(0, "1", "signal_out", null);
phase -= pulseInterval;
}
break;
case WaveType.Square:
phase = (phase + deltaTime * frequency) % 1.0f;
item.SendSignal(0, phase < 0.5f ? "0" : "1", "signal_out", null);
break;
case WaveType.Sine:
phase = (phase + deltaTime * frequency) % 1.0f;
item.SendSignal(0, Math.Sin(phase * MathHelper.TwoPi).ToString(CultureInfo.InvariantCulture), "signal_out", null);
break;
}
}
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f)
{
switch (connection.Name)
{
case "set_frequency":
case "frequency_in":
float newFrequency;
if (float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out newFrequency))
{
Frequency = newFrequency;
}
IsActive = true;
break;
case "set_outputtype":
case "set_wavetype":
WaveType newOutputType;
if (Enum.TryParse(signal, out newOutputType))
{
OutputType = newOutputType;
}
break;
}
}
}
}
@@ -0,0 +1,21 @@
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
class OxygenDetector : ItemComponent
{
public OxygenDetector(Item item, XElement element)
: base (item, element)
{
IsActive = true;
}
public override void Update(float deltaTime, Camera cam)
{
if (item.CurrentHull == null) return;
item.SendSignal(0, ((int)item.CurrentHull.OxygenPercentage).ToString(), "signal_out", null);
}
}
}
@@ -0,0 +1,104 @@
using System.Text.RegularExpressions;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
class RegExFindComponent : ItemComponent
{
private string expression;
private string receivedSignal;
private string previousReceivedSignal;
private bool previousResult;
private Regex regex;
private bool nonContinuousOutputSent;
[InGameEditable, Serialize("1", true, description: "The signal this item outputs when the received signal matches the regular expression.")]
public string Output { get; set; }
[Serialize("0", true, description: "The signal this item outputs when the received signal does not match the regular expression.")]
public string FalseOutput { get; set; }
[InGameEditable, Serialize(true, true, description: "Should the component keep sending the output even after it stops receiving a signal, or only send an output when it receives a signal.")]
public bool ContinuousOutput { get; set; }
[InGameEditable, Serialize("", true, description: "The regular expression used to check the incoming signals.")]
public string Expression
{
get { return expression; }
set
{
if (expression == value) return;
expression = value;
previousReceivedSignal = "";
try
{
regex = new Regex(@expression);
}
catch
{
item.SendSignal(0, "ERROR", "signal_out", null);
return;
}
}
}
public RegExFindComponent(Item item, XElement element)
: base(item, element)
{
IsActive = true;
}
public override void Update(float deltaTime, Camera cam)
{
if (string.IsNullOrWhiteSpace(expression) || regex == null) return;
if (receivedSignal != previousReceivedSignal && receivedSignal != null)
{
try
{
Match match = regex.Match(receivedSignal);
previousResult = match.Success;
previousReceivedSignal = receivedSignal;
}
catch
{
item.SendSignal(0, "ERROR", "signal_out", null);
previousResult = false;
return;
}
}
string signalOut = previousResult ? Output : FalseOutput;
if (ContinuousOutput)
{
if (!string.IsNullOrEmpty(signalOut)) { item.SendSignal(0, signalOut, "signal_out", null); }
}
else if (!nonContinuousOutputSent)
{
if (!string.IsNullOrEmpty(signalOut)) { item.SendSignal(0, signalOut, "signal_out", null); }
nonContinuousOutputSent = true;
}
}
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f)
{
switch (connection.Name)
{
case "signal_in":
receivedSignal = signal;
nonContinuousOutputSent = false;
break;
case "set_output":
Output = signal;
break;
}
}
}
}
@@ -0,0 +1,217 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
class RelayComponent : PowerTransfer, IServerSerializable
{
private float maxPower;
private bool isOn;
private float throttlePowerOutput;
private static readonly Dictionary<string, string> connectionPairs = new Dictionary<string, string>
{
{ "power_in", "power_out"},
{ "signal_in", "signal_out" },
{ "signal_in1", "signal_out1" },
{ "signal_in2", "signal_out2" },
{ "signal_in3", "signal_out3" },
{ "signal_in4", "signal_out4" },
{ "signal_in5", "signal_out5" }
};
public float DisplayLoad { get; set; }
[Editable, Serialize(1000.0f, true, description: "The maximum amount of power that can pass through the item.")]
public float MaxPower
{
get { return maxPower; }
set
{
maxPower = Math.Max(0.0f, value);
}
}
[Editable, Serialize(false, true, description: "Can the relay currently pass power and signals through it.")]
public bool IsOn
{
get
{
return isOn;
}
set
{
isOn = value;
CanTransfer = value;
if (!isOn)
{
currPowerConsumption = 0.0f;
}
}
}
public RelayComponent(Item item, XElement element)
: base(item, element)
{
IsActive = true;
throttlePowerOutput = MaxPower;
}
public override void OnItemLoaded()
{
base.OnItemLoaded();
var connections = Item.Connections;
if (connections != null)
{
foreach (KeyValuePair<string, string> connectionPair in connectionPairs)
{
if (connections.Any(c => c.Name == connectionPair.Key) && !connections.Any(c => c.Name == connectionPair.Value))
{
DebugConsole.ThrowError("Error in item \"" + Name + "\" - matching connection pair not found for the connection \"" + connectionPair.Key + "\" (expecting \"" + connectionPair.Value + "\").");
}
else if (connections.Any(c => c.Name == connectionPair.Value) && !connections.Any(c => c.Name == connectionPair.Key))
{
DebugConsole.ThrowError("Error in item \"" + Name + "\" - matching connection pair not found for the connection \"" + connectionPair.Value + "\" (expecting \"" + connectionPair.Key + "\").");
}
}
}
}
public override void Update(float deltaTime, Camera cam)
{
RefreshConnections();
item.SendSignal(0, IsOn ? "1" : "0", "state_out", null);
if (!CanTransfer) { Voltage = 0.0f; return; }
if (isBroken)
{
SetAllConnectionsDirty();
isBroken = false;
}
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
if (powerOut != null)
{
bool overloaded = false;
foreach (Connection recipient in powerOut.Recipients)
{
var pt = recipient.Item.GetComponent<PowerTransfer>();
if (pt != null)
{
float overload = -pt.CurrPowerConsumption - pt.PowerLoad;
throttlePowerOutput += overload * deltaTime * 0.5f;
overloaded = overload > 1.0f;
}
}
throttlePowerOutput = overloaded ?
MathHelper.Clamp(throttlePowerOutput, 0.0f, MaxPower):
Math.Max(throttlePowerOutput - MaxPower * 0.1f * deltaTime, 0.0f);
}
if (Math.Min(-currPowerConsumption, PowerLoad) > maxPower && CanBeOverloaded)
{
item.Condition = 0.0f;
}
}
public override void ReceivePowerProbeSignal(Connection connection, Item source, float power)
{
if (!IsOn || item.Condition <= 0.0f) { return; }
//we've already received this signal
if (lastPowerProbeRecipients.Contains(this)) { return; }
lastPowerProbeRecipients.Add(this);
if (power < 0.0f)
{
if (!connection.IsOutput || powerIn == null) { return; }
//power being drawn from the power_out connection
DisplayLoad -= Math.Min(power, 0.0f);
powerLoad -= Math.Min(power + throttlePowerOutput, 0.0f);
//pass the load to items connected to the input
powerIn.SendPowerProbeSignal(source, Math.Max(power, -MaxPower));
}
else
{
if (connection.IsOutput || powerOut == null) { return; }
//power being supplied to the power_in connection
if (currPowerConsumption - power < -MaxPower)
{
power += MaxPower + (currPowerConsumption - power);
}
currPowerConsumption -= power;
foreach (Connection recipient in powerOut.Recipients)
{
if (!recipient.IsPower) { continue; }
var powered = recipient.Item.GetComponent<Powered>();
if (powered == null) { continue; }
float load = powered.CurrPowerConsumption;
var powerTransfer = powered as PowerTransfer;
if (powerTransfer != null) { load = powerTransfer.PowerLoad; }
float powerOut = power * (load / Math.Max(powerLoad + throttlePowerOutput, 0.01f));
powered.ReceivePowerProbeSignal(recipient, source, Math.Min(powerOut, power));
}
}
}
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f)
{
if (item.Condition <= 0.0f || connection.IsPower) { return; }
if (connectionPairs.TryGetValue(connection.Name, out string outConnection))
{
if (!IsOn) { return; }
item.SendSignal(stepsTaken, signal, outConnection, sender, power, source, signalStrength);
}
else if (connection.Name == "toggle")
{
SetState(!IsOn, false);
}
else if (connection.Name == "set_state")
{
SetState(signal != "0", false);
}
}
public void SetState(bool on, bool isNetworkMessage)
{
#if CLIENT
if (GameMain.Client != null && !isNetworkMessage) return;
#endif
#if SERVER
if (on != IsOn && GameMain.Server != null)
{
item.CreateServerEvent(this);
}
#endif
IsOn = on;
}
public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
{
msg.Write(isOn);
}
public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime)
{
SetState(msg.ReadBoolean(), true);
}
}
}
@@ -0,0 +1,40 @@
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
class SignalCheckComponent : ItemComponent
{
[InGameEditable, Serialize("1", true, description: "The signal this item outputs when the received signal matches the target signal.")]
public string Output { get; set; }
[InGameEditable, Serialize("0", true, description: "The signal this item outputs when the received signal does not match the target signal.")]
public string FalseOutput { get; set; }
[InGameEditable, Serialize("", true, description: "The value to compare the received signals against.")]
public string TargetSignal { get; set; }
public SignalCheckComponent(Item item, XElement element)
: base(item, element)
{
}
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f)
{
switch (connection.Name)
{
case "signal_in":
string signalOut = (signal == TargetSignal) ? Output : FalseOutput;
if (string.IsNullOrWhiteSpace(signalOut)) return;
item.SendSignal(stepsTaken, signalOut, "signal_out", sender, signalStrength);
break;
case "set_output":
Output = signal;
break;
case "set_targetsignal":
TargetSignal = signal;
break;
}
}
}
}
@@ -0,0 +1,25 @@
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
class SmokeDetector : ItemComponent
{
[Serialize(50.0f, false, description: "How large the fire has to be for the detector to react to it.")]
public float FireSizeThreshold
{
get; set;
}
public SmokeDetector(Item item, XElement element)
: base (item, element)
{
IsActive = true;
}
public override void Update(float deltaTime, Camera cam)
{
item.SendSignal(0, item.CurrentHull != null && item.CurrentHull.FireSources.Any(fs => fs.Size.X > FireSizeThreshold) ? "1" : "0", "signal_out", null);
}
}
}
@@ -0,0 +1,17 @@
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
class SubtractComponent : ArithmeticComponent
{
public SubtractComponent(Item item, XElement element)
: base(item, element)
{
}
protected override float Calculate(float signal1, float signal2)
{
return signal1 - signal2;
}
}
}
@@ -0,0 +1,51 @@
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
partial class Terminal : ItemComponent
{
private const int MaxMessageLength = 150;
public string DisplayedWelcomeMessage
{
get;
private set;
}
private string welcomeMessage;
[InGameEditable, Serialize("", true, "Message to be displayed on the terminal display when it is first opened.", translationTextTag = "terminalwelcomemsg.")]
public string WelcomeMessage
{
get { return welcomeMessage; }
set
{
if (welcomeMessage == value) { return; }
welcomeMessage = value;
DisplayedWelcomeMessage = TextManager.Get(welcomeMessage, returnNull: true) ?? welcomeMessage;
}
}
private string OutputValue { get; set; }
public Terminal(Item item, XElement element)
: base(item, element)
{
IsActive = true;
InitProjSpecific(element);
}
partial void InitProjSpecific(XElement element);
partial void ShowOnDisplay(string input);
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0, float signalStrength = 1)
{
if (connection.Name != "signal_in") { return; }
if (signal.Length > MaxMessageLength)
{
signal = signal.Substring(0, MaxMessageLength);
}
ShowOnDisplay(signal);
}
}
}
@@ -0,0 +1,108 @@
using Microsoft.Xna.Framework;
using System;
using System.Globalization;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
class TrigonometricFunctionComponent : ItemComponent
{
public enum FunctionType
{
Sin,
Cos,
Tan,
Asin,
Acos,
Atan,
}
protected float[] receivedSignal = new float[2];
[Serialize(FunctionType.Sin, false, description: "Which kind of function to run the input through.")]
public FunctionType Function
{
get; set;
}
[InGameEditable, Serialize(false, true, description: "If set to true, the trigonometric function uses radians instead of degrees.")]
public bool UseRadians
{
get; set;
}
public TrigonometricFunctionComponent(Item item, XElement element)
: base(item, element)
{
IsActive = true;
}
public override void Update(float deltaTime, Camera cam)
{
//reset received signals
receivedSignal[0] = float.NaN;
receivedSignal[1] = float.NaN;
}
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0, float signalStrength = 1)
{
float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float value);
switch (Function)
{
case FunctionType.Sin:
if (!UseRadians) { value = MathHelper.ToRadians(value); }
item.SendSignal(0, ((float)Math.Sin(value)).ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
break;
case FunctionType.Cos:
if (!UseRadians) { value = MathHelper.ToRadians(value); }
item.SendSignal(0, ((float)Math.Cos(value)).ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
break;
case FunctionType.Tan:
if (!UseRadians) { value = MathHelper.ToRadians(value); }
item.SendSignal(0, ((float)Math.Tan(value)).ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
break;
case FunctionType.Asin:
{
float angle = (float)Math.Asin(value);
if (!UseRadians) { angle = MathHelper.ToDegrees(angle); }
item.SendSignal(0, angle.ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
}
break;
case FunctionType.Acos:
{
float angle = (float)Math.Acos(value);
if (!UseRadians) { angle = MathHelper.ToDegrees(angle); }
item.SendSignal(0, angle.ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
}
break;
case FunctionType.Atan:
if (connection.Name == "signal_in_x")
{
float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out receivedSignal[0]);
}
else if (connection.Name == "signal_in_y")
{
float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out receivedSignal[1]);
if (!float.IsNaN(receivedSignal[0]) && !float.IsNaN(receivedSignal[1]))
{
float angle = (float)Math.Atan2(receivedSignal[1], receivedSignal[0]);
if (!UseRadians) { angle = MathHelper.ToDegrees(angle); }
item.SendSignal(0, angle.ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
}
}
else
{
float angle = (float)Math.Atan(value);
if (!UseRadians) { angle = MathHelper.ToDegrees(angle); }
item.SendSignal(0, angle.ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
}
break;
default:
throw new NotImplementedException($"Function {Function} has not been implemented.");
}
}
}
}
@@ -0,0 +1,64 @@
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
class WaterDetector : ItemComponent
{
//how often the detector can switch from state to another
const float StateSwitchInterval = 1.0f;
private bool isInWater;
private float stateSwitchDelay;
[InGameEditable, Serialize("1", true, description: "The signal the item sends out when it's underwater.")]
public string Output { get; set; }
[InGameEditable, Serialize("0", true, description: "The signal the item sends out when it's not underwater.")]
public string FalseOutput { get; set; }
public WaterDetector(Item item, XElement element)
: base(item, element)
{
IsActive = true;
}
public override void Update(float deltaTime, Camera cam)
{
if (stateSwitchDelay > 0.0f)
{
stateSwitchDelay -= deltaTime;
}
else
{
bool prevState = isInWater;
isInWater = false;
if (item.InWater)
{
//item in water -> we definitely want to send the True output
isInWater = true;
}
else if (item.CurrentHull != null)
{
//item in not water -> check if there's water anywhere within the rect of the item
if (item.CurrentHull.Surface > item.CurrentHull.Rect.Y - item.CurrentHull.Rect.Height + 1 &&
item.CurrentHull.Surface > item.Rect.Y - item.Rect.Height)
{
isInWater = true;
}
}
if (prevState != isInWater)
{
stateSwitchDelay = StateSwitchInterval;
}
}
string signalOut = isInWater ? Output : FalseOutput;
if (!string.IsNullOrEmpty(signalOut))
{
item.SendSignal(0, signalOut, "signal_out", null);
}
}
}
}
@@ -0,0 +1,191 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
partial class WifiComponent : ItemComponent
{
private static List<WifiComponent> list = new List<WifiComponent>();
private float range;
private int channel;
private float chatMsgCooldown;
private string prevSignal;
[Serialize(Character.TeamType.None, true, description: "WiFi components can only communicate with components that have the same Team ID.")]
public Character.TeamType TeamID { get; set; }
[Editable, Serialize(20000.0f, false, description: "How close the recipient has to be to receive a signal from this WiFi component.")]
public float Range
{
get { return range; }
set { range = Math.Max(value, 0.0f); }
}
[InGameEditable, Serialize(1, true, description: "WiFi components can only communicate with components that use the same channel.")]
public int Channel
{
get { return channel; }
set
{
channel = MathHelper.Clamp(value, 0, 10000);
}
}
[Serialize(false, false, description: "Can the component communicate with wifi components in another team's submarine (e.g. enemy sub in Combat missions, respawn shuttle). Needs to be enabled on both the component transmitting the signal and the component receiving it.")]
public bool AllowCrossTeamCommunication
{
get;
set;
}
[Editable, Serialize(false, false, description: "If enabled, any signals received from another chat-linked wifi component are displayed " +
"as chat messages in the chatbox of the player holding the item.")]
public bool LinkToChat
{
get;
set;
}
[Editable, Serialize(1.0f, true, description: "How many seconds have to pass between signals for a message to be displayed in the chatbox. " +
"Setting this to a very low value is not recommended, because it may cause an excessive amount of chat messages to be created " +
"if there are chat-linked wifi components that transmit a continuous signal.")]
public float MinChatMessageInterval
{
get;
set;
}
[Editable, Serialize(false, true, description: "If set to true, the component will only create chat messages when the received signal changes.")]
public bool DiscardDuplicateChatMessages
{
get;
set;
}
public WifiComponent(Item item, XElement element)
: base (item, element)
{
list.Add(this);
IsActive = true;
}
public bool CanTransmit()
{
return HasRequiredContainedItems(user: null, addMessage: false);
}
public IEnumerable<WifiComponent> GetReceiversInRange()
{
return list.Where(w => w != this && w.CanReceive(this));
}
public bool CanReceive(WifiComponent sender)
{
if (sender == null || sender.channel != channel) { return false; }
if (sender.TeamID != TeamID && !AllowCrossTeamCommunication)
{
return false;
}
if (Vector2.DistanceSquared(item.WorldPosition, sender.item.WorldPosition) > sender.range * sender.range) { return false; }
return HasRequiredContainedItems(user: null, addMessage: false);
}
public override void Update(float deltaTime, Camera cam)
{
chatMsgCooldown -= deltaTime;
}
public void TransmitSignal(int stepsTaken, string signal, Item source, Character sender, bool sendToChat, float signalStrength = 1.0f)
{
var senderComponent = source?.GetComponent<WifiComponent>();
if (senderComponent != null && !CanReceive(senderComponent)) return;
bool chatMsgSent = false;
var receivers = GetReceiversInRange();
foreach (WifiComponent wifiComp in receivers)
{
//signal strength diminishes by distance
float sentSignalStrength = signalStrength *
MathHelper.Clamp(1.0f - (Vector2.Distance(item.WorldPosition, wifiComp.item.WorldPosition) / wifiComp.range), 0.0f, 1.0f);
wifiComp.item.SendSignal(stepsTaken, signal, "signal_out", sender, 0, source, sentSignalStrength);
if (source != null)
{
foreach (Item receiverItem in wifiComp.item.LastSentSignalRecipients)
{
if (!source.LastSentSignalRecipients.Contains(receiverItem))
{
source.LastSentSignalRecipients.Add(receiverItem);
}
}
}
if (DiscardDuplicateChatMessages && signal == prevSignal) continue;
if (LinkToChat && wifiComp.LinkToChat && chatMsgCooldown <= 0.0f && sendToChat)
{
if (wifiComp.item.ParentInventory != null &&
wifiComp.item.ParentInventory.Owner != null &&
GameMain.NetworkMember != null)
{
string chatMsg = signal;
if (senderComponent != null)
{
chatMsg = ChatMessage.ApplyDistanceEffect(chatMsg, 1.0f - sentSignalStrength);
}
if (chatMsg.Length > ChatMessage.MaxLength) chatMsg = chatMsg.Substring(0, ChatMessage.MaxLength);
if (string.IsNullOrEmpty(chatMsg)) continue;
#if CLIENT
if (wifiComp.item.ParentInventory.Owner == Character.Controlled)
{
if (GameMain.Client == null)
GameMain.NetworkMember.AddChatMessage(signal, ChatMessageType.Radio, source == null ? "" : source.Name);
}
#endif
#if SERVER
if (GameMain.Server != null)
{
Client recipientClient = GameMain.Server.ConnectedClients.Find(c => c.Character == wifiComp.item.ParentInventory.Owner);
if (recipientClient != null)
{
GameMain.Server.SendDirectChatMessage(
ChatMessage.Create(source == null ? "" : source.Name, chatMsg, ChatMessageType.Radio, null), recipientClient);
}
}
#endif
chatMsgSent = true;
}
}
}
if (chatMsgSent) chatMsgCooldown = MinChatMessageInterval;
prevSignal = signal;
}
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f)
{
if (connection == null || connection.Name != "signal_in") return;
TransmitSignal(stepsTaken, signal, source, sender, true, signalStrength);
}
protected override void RemoveComponentSpecific()
{
list.Remove(this);
}
}
}
@@ -0,0 +1,766 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
partial class Wire : ItemComponent, IDrawableComponent, IServerSerializable, IClientSerializable
{
partial class WireSection
{
private Vector2 start;
private Vector2 end;
private readonly float angle;
private readonly float length;
public Vector2 Start
{
get { return start; }
}
public Vector2 End
{
get { return end; }
}
public WireSection(Vector2 start, Vector2 end)
{
this.start = start;
this.end = end;
angle = MathUtils.VectorToAngle(end - start);
length = Vector2.Distance(start, end);
}
}
const float MaxAttachDistance = 150.0f;
const float MinNodeDistance = 15.0f;
const int MaxNodeCount = 255;
const int MaxNodesPerNetworkEvent = 30;
private List<Vector2> nodes;
private readonly List<WireSection> sections;
private Connection[] connections;
private bool canPlaceNode;
private Vector2 newNodePos;
private Vector2 sectionExtents;
public bool Hidden;
private float removeNodeDelay;
private bool locked;
public bool Locked
{
get
{
if (GameMain.NetworkMember?.ServerSettings != null && !GameMain.NetworkMember.ServerSettings.AllowRewiring) { return false; }
return locked || connections.Any(c => c != null && c.ConnectionPanel.Locked);
}
set { locked = value; }
}
public Connection[] Connections
{
get { return connections; }
}
[Serialize(5000.0f, false, description: "The maximum distance the wire can extend (in pixels).")]
public float MaxLength
{
get;
set;
}
public Wire(Item item, XElement element)
: base(item, element)
{
nodes = new List<Vector2>();
sections = new List<WireSection>();
connections = new Connection[2];
IsActive = false;
item.IsShootable = true;
InitProjSpecific(element);
}
partial void InitProjSpecific(XElement element);
public Connection OtherConnection(Connection connection)
{
if (connection == connections[0]) { return connections[1]; }
if (connection == connections[1]) { return connections[0]; }
return null;
}
public bool IsConnectedTo(Item item)
{
if (connections[0] != null && connections[0].Item == item) return true;
return (connections[1] != null && connections[1].Item == item);
}
public void RemoveConnection(Item item)
{
for (int i = 0; i < 2; i++)
{
if (connections[i] == null || connections[i].Item != item) continue;
foreach (Wire wire in connections[i].Wires)
{
if (wire != this) continue;
SetConnectedDirty();
connections[i].SetWire(connections[i].FindWireIndex(wire), null);
}
connections[i] = null;
}
}
public void RemoveConnection(Connection connection)
{
if (connection == connections[0]) { connections[0] = null; }
if (connection == connections[1]) { connections[1] = null; }
SetConnectedDirty();
}
public bool Connect(Connection newConnection, bool addNode = true, bool sendNetworkEvent = false)
{
for (int i = 0; i < 2; i++)
{
if (connections[i] == newConnection) { return false; }
}
if (!connections.Any(c => c == null)) { return false; }
for (int i = 0; i < 2; i++)
{
if (connections[i] != null && connections[i].Item == newConnection.Item)
{
addNode = false;
break;
}
}
if (item.body != null) { item.Submarine = newConnection.Item.Submarine; }
newConnection.ConnectionPanel.DisconnectedWires.Remove(this);
for (int i = 0; i < 2; i++)
{
if (connections[i] != null) { continue; }
connections[i] = newConnection;
FixNodeEnds();
if (!addNode) { break; }
Submarine refSub = newConnection.Item.Submarine;
if (refSub == null)
{
Structure attachTarget = Structure.GetAttachTarget(newConnection.Item.WorldPosition);
if (attachTarget == null) { continue; }
refSub = attachTarget.Submarine;
}
Vector2 nodePos = refSub == null ?
newConnection.Item.Position :
newConnection.Item.Position - refSub.HiddenSubPosition;
if (nodes.Count > 0 && nodes[0] == nodePos) { break; }
if (nodes.Count > 1 && nodes[nodes.Count - 1] == nodePos) { break; }
//make sure we place the node at the correct end of the wire (the end that's closest to the new node pos)
int newNodeIndex = 0;
if (nodes.Count > 1)
{
if (Vector2.DistanceSquared(nodes[nodes.Count - 1], nodePos) < Vector2.DistanceSquared(nodes[0], nodePos))
{
newNodeIndex = nodes.Count;
}
}
if (newNodeIndex == 0 && nodes.Count > 1)
{
nodes.Insert(0, nodePos);
}
else
{
nodes.Add(nodePos);
}
break;
}
SetConnectedDirty();
if (connections[0] != null && connections[1] != null)
{
foreach (ItemComponent ic in item.Components)
{
if (ic == this) continue;
ic.Drop(null);
}
if (item.Container != null) item.Container.RemoveContained(this.item);
if (item.body != null) item.body.Enabled = false;
IsActive = false;
CleanNodes();
}
if (item.body != null) item.Submarine = newConnection.Item.Submarine;
if (sendNetworkEvent)
{
#if SERVER
if (GameMain.Server != null)
{
CreateNetworkEvent();
}
#endif
//the wire is active if only one end has been connected
IsActive = connections[0] == null ^ connections[1] == null;
}
Drawable = IsActive || nodes.Any();
UpdateSections();
return true;
}
public override void Equip(Character character)
{
ClearConnections(character);
IsActive = true;
}
public override void Unequip(Character character)
{
ClearConnections(character);
IsActive = false;
}
public override void Drop(Character dropper)
{
ClearConnections(dropper);
IsActive = false;
}
public override void Update(float deltaTime, Camera cam)
{
if (nodes.Count == 0) { return; }
Character user = item.ParentInventory?.Owner as Character;
removeNodeDelay = (user?.SelectedConstruction == null) ? removeNodeDelay - deltaTime : 0.5f;
Submarine sub = item.Submarine;
if (connections[0] != null && connections[0].Item.Submarine != null) { sub = connections[0].Item.Submarine; }
if (connections[1] != null && connections[1].Item.Submarine != null) { sub = connections[1].Item.Submarine; }
if (Screen.Selected != GameMain.SubEditorScreen)
{
//cannot run wires from sub to another
if (item.Submarine != sub && sub != null && item.Submarine != null)
{
ClearConnections();
return;
}
if (item.CurrentHull == null)
{
Structure attachTarget = Structure.GetAttachTarget(item.WorldPosition);
canPlaceNode = attachTarget != null;
sub = sub ?? attachTarget?.Submarine;
Vector2 attachPos = GetAttachPosition(user);
newNodePos = sub == null ?
attachPos :
attachPos - sub.Position - sub.HiddenSubPosition;
}
else
{
newNodePos = GetAttachPosition(user);
if (sub != null) { newNodePos -= sub.HiddenSubPosition; }
canPlaceNode = true;
}
//prevent the wire from extending too far when rewiring
if (nodes.Count > 0)
{
if (user == null) { return; }
Vector2 prevNodePos = nodes[nodes.Count - 1];
if (sub != null) { prevNodePos += sub.HiddenSubPosition; }
float currLength = 0.0f;
for (int i = 0; i < nodes.Count - 1; i++)
{
currLength += Vector2.Distance(nodes[i], nodes[i + 1]);
}
currLength += Vector2.Distance(nodes[nodes.Count - 1], newNodePos);
if (currLength > MaxLength)
{
Vector2 diff = nodes[nodes.Count - 1] - newNodePos;
Vector2 pullBackDir = diff == Vector2.Zero ? Vector2.Zero : Vector2.Normalize(diff);
user.AnimController.Collider.ApplyForce(pullBackDir * user.Mass * 50.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
user.AnimController.UpdateUseItem(true, user.WorldPosition + pullBackDir * 200.0f);
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
{
if (currLength > MaxLength * 1.5f)
{
ClearConnections();
#if SERVER
CreateNetworkEvent();
#endif
return;
}
}
}
}
}
else
{
newNodePos = RoundNode(item.Position);
if (sub != null) { newNodePos -= sub.HiddenSubPosition; }
canPlaceNode = true;
}
if (item != null)
{
Vector2 relativeNodePos = newNodePos - item.Position;
if (sub != null)
{
relativeNodePos += sub.HiddenSubPosition;
}
sectionExtents = new Vector2(
Math.Max(Math.Abs(relativeNodePos.X), sectionExtents.X),
Math.Max(Math.Abs(relativeNodePos.Y), sectionExtents.Y));
}
}
private Vector2 GetAttachPosition(Character user)
{
if (user == null) { return item.Position; }
Vector2 mouseDiff = user.CursorWorldPosition - user.WorldPosition;
mouseDiff = mouseDiff.ClampLength(MaxAttachDistance);
return new Vector2(
MathUtils.RoundTowardsClosest(user.Position.X + mouseDiff.X, Submarine.GridSize.X),
MathUtils.RoundTowardsClosest(user.Position.Y + mouseDiff.Y, Submarine.GridSize.Y));
}
public override bool Use(float deltaTime, Character character = null)
{
if (character == null || character != Character.Controlled) { return false; }
if (character.SelectedConstruction != null) { return false; }
#if CLIENT
if (Screen.Selected == GameMain.SubEditorScreen && !PlayerInput.PrimaryMouseButtonClicked())
{
return false;
}
#endif
//clients communicate node addition/removal with network events
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { return false; }
if (newNodePos != Vector2.Zero && canPlaceNode && nodes.Count > 0 && Vector2.Distance(newNodePos, nodes[nodes.Count - 1]) > MinNodeDistance)
{
if (nodes.Count >= MaxNodeCount)
{
nodes.RemoveAt(nodes.Count - 1);
}
nodes.Add(newNodePos);
CleanNodes();
UpdateSections();
Drawable = true;
newNodePos = Vector2.Zero;
#if CLIENT
if (GameMain.NetworkMember != null)
{
GameMain.Client.CreateEntityEvent(item, new object[]
{
NetEntityEvent.Type.ComponentState,
item.GetComponentIndex(this),
nodes.Count
});
}
#endif
}
return true;
}
public override bool SecondaryUse(float deltaTime, Character character = null)
{
if (character == null || character != Character.Controlled) { return false; }
//clients communicate node addition/removal with network events
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { return false; }
if (nodes.Count > 1 && removeNodeDelay <= 0.0f)
{
nodes.RemoveAt(nodes.Count - 1);
UpdateSections();
#if CLIENT
if (GameMain.NetworkMember != null)
{
GameMain.Client.CreateEntityEvent(item, new object[]
{
NetEntityEvent.Type.ComponentState,
item.GetComponentIndex(this),
nodes.Count
});
}
#endif
}
removeNodeDelay = 0.1f;
Drawable = IsActive || sections.Count > 0;
return true;
}
public override bool Pick(Character picker)
{
ClearConnections(picker);
return true;
}
public override void Move(Vector2 amount)
{
#if CLIENT
if (item.IsSelected) MoveNodes(amount);
#endif
}
public List<Vector2> GetNodes()
{
return new List<Vector2>(nodes);
}
public void SetNodes(List<Vector2> nodes)
{
this.nodes = new List<Vector2>(nodes);
UpdateSections();
}
public void MoveNode(int index, Vector2 amount)
{
if (index < 0 || index >= nodes.Count) return;
nodes[index] += amount;
UpdateSections();
}
public void MoveNodes(Vector2 amount)
{
for (int i = 0; i < nodes.Count; i++)
{
nodes[i] += amount;
}
UpdateSections();
}
public void UpdateSections()
{
sections.Clear();
for (int i = 0; i < nodes.Count - 1; i++)
{
sections.Add(new WireSection(nodes[i], nodes[i + 1]));
}
Drawable = IsActive || sections.Count > 0;
CalculateExtents();
}
private void CalculateExtents()
{
sectionExtents = Vector2.Zero;
if (sections.Count > 0)
{
for (int i = 0; i < nodes.Count; i++)
{
sectionExtents.X = Math.Max(Math.Abs(nodes[i].X - item.Position.X), sectionExtents.X);
sectionExtents.Y = Math.Max(Math.Abs(nodes[i].Y - item.Position.Y), sectionExtents.Y);
}
}
}
public void ClearConnections(Character user = null)
{
nodes.Clear();
sections.Clear();
foreach (Item item in Item.ItemList)
{
var connectionPanel = item.GetComponent<ConnectionPanel>();
if (connectionPanel != null && connectionPanel.DisconnectedWires.Contains(this))
{
#if SERVER
item.CreateServerEvent(connectionPanel);
#endif
connectionPanel.DisconnectedWires.Remove(this);
}
}
#if SERVER
if (user != null)
{
if (connections[0] != null || connections[1] != null)
{
GameMain.Server.KarmaManager.OnWireDisconnected(user, this);
}
if (connections[0] != null && connections[1] != null)
{
GameServer.Log(user.LogName + " disconnected a wire from " +
connections[0].Item.Name + " (" + connections[0].Name + ") to "+
connections[1].Item.Name + " (" + connections[1].Name + ")", ServerLog.MessageType.ItemInteraction);
}
else if (connections[0] != null)
{
GameServer.Log(user.LogName + " disconnected a wire from " +
connections[0].Item.Name + " (" + connections[0].Name + ")", ServerLog.MessageType.ItemInteraction);
}
else if (connections[1] != null)
{
GameServer.Log(user.LogName + " disconnected a wire from " +
connections[1].Item.Name + " (" + connections[1].Name + ")", ServerLog.MessageType.ItemInteraction);
}
}
#endif
SetConnectedDirty();
for (int i = 0; i < 2; i++)
{
if (connections[i] == null) { continue; }
int wireIndex = connections[i].FindWireIndex(item);
if (wireIndex == -1) { continue; }
#if SERVER
if (!connections[i].Item.Removed)
{
connections[i].Item.CreateServerEvent(connections[i].Item.GetComponent<ConnectionPanel>());
}
#endif
connections[i].SetWire(wireIndex, null);
connections[i] = null;
}
Drawable = sections.Count > 0;
}
private Vector2 RoundNode(Vector2 position)
{
position.X = MathUtils.Round(position.X, Submarine.GridSize.X / 2.0f);
position.Y = MathUtils.Round(position.Y, Submarine.GridSize.Y / 2.0f);
return position;
}
public void SetConnectedDirty()
{
for (int i = 0; i < 2; i++)
{
if (connections[i]?.Item != null)
{
var pt = connections[i].Item.GetComponent<PowerTransfer>();
if (pt != null) pt.SetConnectionDirty(connections[i]);
}
}
}
private void CleanNodes()
{
bool removed;
do
{
removed = false;
for (int i = nodes.Count - 2; i > 0; i--)
{
if (Math.Abs(nodes[i - 1].X - nodes[i].X) < 1.0f && Math.Abs(nodes[i + 1].X - nodes[i].X) < 1.0f &&
Math.Sign(nodes[i - 1].Y - nodes[i].Y) != Math.Sign(nodes[i + 1].Y - nodes[i].Y))
{
nodes.RemoveAt(i);
removed = true;
}
else if (Math.Abs(nodes[i - 1].Y - nodes[i].Y) < 1.0f && Math.Abs(nodes[i + 1].Y - nodes[i].Y) < 1.0f &&
Math.Sign(nodes[i - 1].X - nodes[i].X) != Math.Sign(nodes[i + 1].X - nodes[i].X))
{
nodes.RemoveAt(i);
removed = true;
}
}
} while (removed);
}
private void FixNodeEnds()
{
if (connections[0] == null || connections[1] == null || nodes.Count == 0) { return; }
Vector2 nodePos = nodes[0];
Submarine refSub = connections[0].Item.Submarine ?? connections[1].Item.Submarine;
if (refSub != null) { nodePos += refSub.HiddenSubPosition; }
float dist1 = Vector2.DistanceSquared(connections[0].Item.Position, nodePos);
float dist2 = Vector2.DistanceSquared(connections[1].Item.Position, nodePos);
//first node is closer to the second item
//= the nodes are "backwards", need to reverse them
if (dist1 > dist2)
{
nodes.Reverse();
UpdateSections();
}
}
private int GetClosestNodeIndex(Vector2 pos, float maxDist, out float closestDist)
{
closestDist = 0.0f;
int closestIndex = -1;
for (int i = 0; i < nodes.Count; i++)
{
float dist = Vector2.Distance(nodes[i], pos);
if (dist > maxDist) continue;
if (closestIndex == -1 || dist < closestDist)
{
closestIndex = i;
closestDist = dist;
}
}
return closestIndex;
}
private int GetClosestSectionIndex(Vector2 mousePos, float maxDist, out float closestDist)
{
closestDist = 0.0f;
int closestIndex = -1;
for (int i = 0; i < nodes.Count-1; i++)
{
if ((Math.Abs(nodes[i].X - nodes[i + 1].X)<5 || Math.Sign(mousePos.X - nodes[i].X) != Math.Sign(mousePos.X - nodes[i + 1].X)) &&
(Math.Abs(nodes[i].Y - nodes[i + 1].Y)<5 || Math.Sign(mousePos.Y - nodes[i].Y) != Math.Sign(mousePos.Y - nodes[i + 1].Y)))
{
float dist = MathUtils.LineToPointDistance(nodes[i], nodes[i + 1], mousePos);
if (dist > maxDist) continue;
if (closestIndex == -1 || dist < closestDist)
{
closestIndex = i;
closestDist = dist;
}
}
}
return closestIndex;
}
public override void FlipX(bool relativeToSub)
{
Vector2 refPos = item.Submarine == null ?
Vector2.Zero :
item.Position - item.Submarine.HiddenSubPosition;
for (int i = 0; i < nodes.Count; i++)
{
nodes[i] = relativeToSub ?
new Vector2(-nodes[i].X, nodes[i].Y) :
new Vector2(refPos.X - (nodes[i].X - refPos.X), nodes[i].Y);
}
UpdateSections();
}
public override void FlipY(bool relativeToSub)
{
Vector2 refPos = item.Submarine == null ?
Vector2.Zero :
item.Position - item.Submarine.HiddenSubPosition;
for (int i = 0; i < nodes.Count; i++)
{
nodes[i] = relativeToSub ?
new Vector2(nodes[i].X, -nodes[i].Y) :
new Vector2(nodes[i].X, refPos.Y - (nodes[i].Y - refPos.Y));
}
UpdateSections();
}
public override void Load(XElement componentElement, bool usePrefabValues)
{
base.Load(componentElement, usePrefabValues);
string nodeString = componentElement.GetAttributeString("nodes", "");
if (nodeString == "") return;
string[] nodeCoords = nodeString.Split(';');
for (int i = 0; i < nodeCoords.Length / 2; i++)
{
float.TryParse(nodeCoords[i * 2], NumberStyles.Float, CultureInfo.InvariantCulture, out float x);
float.TryParse(nodeCoords[i * 2 + 1], NumberStyles.Float, CultureInfo.InvariantCulture, out float y);
nodes.Add(new Vector2(x, y));
}
Drawable = nodes.Any();
}
public override XElement Save(XElement parentElement)
{
XElement componentElement = base.Save(parentElement);
if (nodes == null || nodes.Count == 0) return componentElement;
string[] nodeCoords = new string[nodes.Count * 2];
for (int i = 0; i < nodes.Count; i++)
{
nodeCoords[i * 2] = nodes[i].X.ToString(CultureInfo.InvariantCulture);
nodeCoords[i * 2 + 1] = nodes[i].Y.ToString(CultureInfo.InvariantCulture);
}
componentElement.Add(new XAttribute("nodes", string.Join(";", nodeCoords)));
return componentElement;
}
protected override void ShallowRemoveComponentSpecific()
{
/*for (int i = 0; i < 2; i++)
{
if (connections[i] == null) continue;
int wireIndex = connections[i].FindWireIndex(item);
if (wireIndex > -1)
{
connections[i].AddLink(wireIndex, null);
}
}*/
}
protected override void RemoveComponentSpecific()
{
ClearConnections();
base.RemoveComponentSpecific();
#if CLIENT
overrideSprite?.Remove();
overrideSprite = null;
wireSprite = null;
#endif
}
}
}
@@ -0,0 +1,28 @@
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
class XorComponent : AndComponent
{
public XorComponent(Item item, XElement element)
: base(item, element)
{
IsActive = true;
}
public override void Update(float deltaTime, Camera cam)
{
int sendOutput = 0;
for (int i = 0; i < timeSinceReceived.Length; i++)
{
if (timeSinceReceived[i] <= timeFrame) sendOutput += 1;
timeSinceReceived[i] += deltaTime;
}
string signalOut = sendOutput == 1 ? output : falseOutput;
if (string.IsNullOrEmpty(signalOut)) return;
item.SendSignal(0, signalOut, "signal_out", null);
}
}
}
@@ -0,0 +1,12 @@
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
partial class StatusHUD : ItemComponent
{
public StatusHUD(Item item, XElement element)
: base(item, element)
{
}
}
}
@@ -0,0 +1,732 @@
using Barotrauma.Networking;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Extensions;
namespace Barotrauma.Items.Components
{
partial class Turret : Powered, IDrawableComponent, IServerSerializable
{
private Sprite barrelSprite, railSprite;
private Vector2 barrelPos;
private Vector2 transformedBarrelPos;
private LightComponent lightComponent;
private float rotation, targetRotation;
private float reload, reloadTime;
private float minRotation, maxRotation;
private float launchImpulse;
private Camera cam;
private float angularVelocity;
private int failedLaunchAttempts;
private Character user;
[Serialize("0,0", false, description: "The position of the barrel relative to the upper left corner of the base sprite (in pixels).")]
public Vector2 BarrelPos
{
get
{
return barrelPos;
}
set
{
barrelPos = value;
UpdateTransformedBarrelPos();
}
}
public Vector2 TransformedBarrelPos
{
get
{
return transformedBarrelPos;
}
}
[Serialize(0.0f, false, description: "The impulse applied to the physics body of the projectile (the higher the impulse, the faster the projectiles are launched).")]
public float LaunchImpulse
{
get { return launchImpulse; }
set { launchImpulse = value; }
}
[Editable(0.0f, 1000.0f), Serialize(5.0f, false, description: "The period of time the user has to wait between shots.")]
public float Reload
{
get { return reloadTime; }
set { reloadTime = value; }
}
[Serialize(1, false, description: "How projectiles the weapon launches when fired once.")]
public int ProjectileCount
{
get;
set;
}
[Editable, Serialize("0.0,0.0", true, description: "The range at which the barrel can rotate. TODO")]
public Vector2 RotationLimits
{
get
{
return new Vector2(MathHelper.ToDegrees(minRotation), MathHelper.ToDegrees(maxRotation));
}
set
{
minRotation = MathHelper.ToRadians(Math.Min(value.X, value.Y));
maxRotation = MathHelper.ToRadians(Math.Max(value.X, value.Y));
rotation = (minRotation + maxRotation) / 2;
#if CLIENT
if (lightComponent != null)
{
lightComponent.Rotation = rotation;
lightComponent.Light.Rotation = -rotation;
}
#endif
}
}
[Editable(0.0f, 1000.0f, DecimalCount = 2),
Serialize(5.0f, false, description: "How much torque is applied to rotate the barrel when the item is used by a character"
+ " with insufficient skills to operate it. Higher values make the barrel rotate faster.")]
public float SpringStiffnessLowSkill
{
get;
private set;
}
[Editable(0.0f, 1000.0f, DecimalCount = 2),
Serialize(2.0f, false, description: "How much torque is applied to rotate the barrel when the item is used by a character"
+ " with sufficient skills to operate it. Higher values make the barrel rotate faster.")]
public float SpringStiffnessHighSkill
{
get;
private set;
}
[Editable(0.0f, 1000.0f, DecimalCount = 2),
Serialize(50.0f, false, description: "How much torque is applied to resist the movement of the barrel when the item is used by a character"
+ " with insufficient skills to operate it. Higher values make the aiming more \"snappy\", stopping the barrel from swinging around the direction it's being aimed at.")]
public float SpringDampingLowSkill
{
get;
private set;
}
[Editable(0.0f, 1000.0f, DecimalCount = 2),
Serialize(10.0f, false, description: "How much torque is applied to resist the movement of the barrel when the item is used by a character"
+ " with sufficient skills to operate it. Higher values make the aiming more \"snappy\", stopping the barrel from swinging around the direction it's being aimed at.")]
public float SpringDampingHighSkill
{
get;
private set;
}
[Editable(0.0f, 100.0f, DecimalCount = 2),
Serialize(1.0f, false, description: "Maximum angular velocity of the barrel when used by a character with insufficient skills to operate it.")]
public float RotationSpeedLowSkill
{
get;
private set;
}
[Editable(0.0f, 100.0f, DecimalCount = 2),
Serialize(5.0f, false, description: "Maximum angular velocity of the barrel when used by a character with sufficient skills to operate it."),]
public float RotationSpeedHighSkill
{
get;
private set;
}
private float baseRotationRad;
[Editable(0.0f, 360.0f), Serialize(0.0f, true, description: "The angle of the turret's base in degrees.")]
public float BaseRotation
{
get { return MathHelper.ToDegrees(baseRotationRad); }
set
{
baseRotationRad = MathHelper.ToRadians(value);
UpdateTransformedBarrelPos();
}
}
public Turret(Item item, XElement element)
: base(item, element)
{
IsActive = true;
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "barrelsprite":
barrelSprite = new Sprite(subElement);
break;
case "railsprite":
railSprite = new Sprite(subElement);
break;
}
}
item.IsShootable = true;
item.RequireAimToUse = false;
InitProjSpecific(element);
}
partial void InitProjSpecific(XElement element);
private void UpdateTransformedBarrelPos()
{
float flippedRotation = BaseRotation;
if (item.FlippedX) flippedRotation = -flippedRotation;
//if (item.FlippedY) flippedRotation = 180.0f - flippedRotation;
transformedBarrelPos = MathUtils.RotatePointAroundTarget(barrelPos * item.Scale, new Vector2(item.Rect.Width / 2, item.Rect.Height / 2), flippedRotation);
#if CLIENT
item.SpriteRotation = MathHelper.ToRadians(flippedRotation);
#endif
}
public override void OnItemLoaded()
{
base.OnItemLoaded();
var lightComponents = item.GetComponents<LightComponent>();
if (lightComponents != null && lightComponents.Count() > 0)
{
lightComponent = lightComponents.FirstOrDefault(lc => lc.Parent == this);
#if CLIENT
if (lightComponent != null)
{
lightComponent.Parent = null;
lightComponent.Rotation = rotation;
lightComponent.Light.Rotation = -rotation;
}
#endif
}
}
public override void Update(float deltaTime, Camera cam)
{
this.cam = cam;
if (reload > 0.0f) { reload -= deltaTime; }
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
UpdateProjSpecific(deltaTime);
if (minRotation == maxRotation) { return; }
float targetMidDiff = MathHelper.WrapAngle(targetRotation - (minRotation + maxRotation) / 2.0f);
float maxDist = (maxRotation - minRotation) / 2.0f;
if (Math.Abs(targetMidDiff) > maxDist)
{
targetRotation = (targetMidDiff < 0.0f) ? minRotation : maxRotation;
}
float degreeOfSuccess = user == null ? 0.5f : DegreeOfSuccess(user);
if (degreeOfSuccess < 0.5f) { degreeOfSuccess *= degreeOfSuccess; } //the ease of aiming drops quickly with insufficient skill levels
float springStiffness = MathHelper.Lerp(SpringStiffnessLowSkill, SpringStiffnessHighSkill, degreeOfSuccess);
float springDamping = MathHelper.Lerp(SpringDampingLowSkill, SpringDampingHighSkill, degreeOfSuccess);
float rotationSpeed = MathHelper.Lerp(RotationSpeedLowSkill, RotationSpeedHighSkill, degreeOfSuccess);
if (user?.Info != null)
{
user.Info.IncreaseSkillLevel("weapons",
SkillSettings.Current.SkillIncreasePerSecondWhenOperatingTurret * deltaTime / Math.Max(user.GetSkillLevel("weapons"), 1.0f),
user.WorldPosition + Vector2.UnitY * 150.0f);
}
angularVelocity +=
(MathHelper.WrapAngle(targetRotation - rotation) * springStiffness - angularVelocity * springDamping) * deltaTime;
angularVelocity = MathHelper.Clamp(angularVelocity, -rotationSpeed, rotationSpeed);
rotation += angularVelocity * deltaTime;
float rotMidDiff = MathHelper.WrapAngle(rotation - (minRotation + maxRotation) / 2.0f);
if (rotMidDiff < -maxDist)
{
rotation = minRotation;
angularVelocity *= -0.5f;
}
else if (rotMidDiff > maxDist)
{
rotation = maxRotation;
angularVelocity *= -0.5f;
}
if (lightComponent != null)
{
lightComponent.Rotation = rotation;
}
}
partial void UpdateProjSpecific(float deltaTime);
public override bool Use(float deltaTime, Character character = null)
{
if (!characterUsable && character != null) { return false; }
return TryLaunch(deltaTime, character);
}
private bool TryLaunch(float deltaTime, Character character = null)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return false; }
if (reload > 0.0f) { return false; }
if (GetAvailableBatteryPower() < powerConsumption)
{
#if CLIENT
if (!flashLowPower && character != null && character == Character.Controlled)
{
flashLowPower = true;
GUI.PlayUISound(GUISoundType.PickItemFail);
}
#endif
return false;
}
Projectile launchedProjectile = null;
for (int i = 0; i < ProjectileCount; i++)
{
foreach (MapEntity e in item.linkedTo)
{
//use linked projectile containers in case they have to react to the turret being launched somehow
//(play a sound, spawn more projectiles)
if (!(e is Item linkedItem)) { continue; }
ItemContainer projectileContainer = linkedItem.GetComponent<ItemContainer>();
if (projectileContainer != null)
{
linkedItem.Use(deltaTime, null);
var repairable = linkedItem.GetComponent<Repairable>();
if (repairable != null)
{
repairable.LastActiveTime = (float)Timing.TotalTime + 1.0f;
}
}
}
var projectiles = GetLoadedProjectiles(true);
if (projectiles.Count == 0)
{
//coilguns spawns ammo in the ammo boxes with the OnUse statuseffect when the turret is launched,
//causing a one frame delay before the gun can be launched (or more in multiplayer where there may be a longer delay)
// -> attempt to launch the gun multiple times before showing the "no ammo" flash
failedLaunchAttempts++;
#if CLIENT
if (!flashNoAmmo && character != null && character == Character.Controlled && failedLaunchAttempts > 20)
{
flashNoAmmo = true;
failedLaunchAttempts = 0;
GUI.PlayUISound(GUISoundType.PickItemFail);
}
#endif
return false;
}
failedLaunchAttempts = 0;
var batteries = item.GetConnectedComponents<PowerContainer>();
float neededPower = powerConsumption;
while (neededPower > 0.0001f && batteries.Count > 0)
{
batteries.RemoveAll(b => b.Charge <= 0.0001f || b.MaxOutPut <= 0.0001f);
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
}
}
launchedProjectile = projectiles[0];
Launch(projectiles[0].Item, character);
}
#if SERVER
if (character != null && launchedProjectile != null)
{
string msg = character.LogName + " launched " + item.Name + " (projectile: " + launchedProjectile.Item.Name;
var containedItems = launchedProjectile.Item.ContainedItems;
if (containedItems == null || !containedItems.Any())
{
msg += ")";
}
else
{
msg += ", contained items: " + string.Join(", ", containedItems.Select(i => i.Name)) + ")";
}
GameServer.Log(msg, ServerLog.MessageType.ItemInteraction);
}
#endif
return true;
}
private void Launch(Item projectile, Character user = null)
{
reload = reloadTime;
projectile.Drop(null);
projectile.body.Dir = 1.0f;
projectile.body.ResetDynamics();
projectile.body.Enabled = true;
projectile.SetTransform(ConvertUnits.ToSimUnits(new Vector2(item.WorldRect.X + transformedBarrelPos.X, item.WorldRect.Y - transformedBarrelPos.Y)), -rotation);
projectile.UpdateTransform();
projectile.Submarine = projectile.body.Submarine;
Projectile projectileComponent = projectile.GetComponent<Projectile>();
if (projectileComponent != null)
{
projectileComponent.Use((float)Timing.Step);
projectileComponent.User = user;
}
if (projectile.Container != null) projectile.Container.RemoveContained(projectile);
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ComponentState, item.GetComponentIndex(this), projectile });
}
ApplyStatusEffects(ActionType.OnUse, 1.0f, user: user);
LaunchProjSpecific();
}
partial void LaunchProjSpecific();
public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
{
if (character.AIController.SelectedAiTarget?.Entity is Character previousTarget &&
previousTarget.IsDead)
{
character?.Speak(TextManager.Get("DialogTurretTargetDead"), null, 0.0f, "killedtarget" + previousTarget.ID, 30.0f);
character.AIController.SelectTarget(null);
}
if (GetAvailableBatteryPower() < powerConsumption)
{
var batteries = item.GetConnectedComponents<PowerContainer>();
float lowestCharge = 0.0f;
PowerContainer batteryToLoad = null;
foreach (PowerContainer battery in batteries)
{
if (batteryToLoad == null || battery.Charge < lowestCharge)
{
batteryToLoad = battery;
lowestCharge = battery.Charge;
}
}
if (batteryToLoad == null) return true;
if (batteryToLoad.RechargeSpeed < batteryToLoad.MaxRechargeSpeed * 0.4f)
{
objective.AddSubObjective(new AIObjectiveOperateItem(batteryToLoad, character, objective.objectiveManager, option: "", requireEquip: false));
return false;
}
}
int usableProjectileCount = 0;
int maxProjectileCount = 0;
foreach (MapEntity e in item.linkedTo)
{
if (!(e is Item projectileContainer)) continue;
var containedItems = projectileContainer.ContainedItems;
if (containedItems != null)
{
var container = projectileContainer.GetComponent<ItemContainer>();
maxProjectileCount += container.Capacity;
int projectiles = containedItems.Count(it => it.Condition > 0.0f);
usableProjectileCount += projectiles;
}
}
if (usableProjectileCount == 0 || (usableProjectileCount < maxProjectileCount && objective.Option.Equals("fireatwill", StringComparison.OrdinalIgnoreCase)))
{
ItemContainer container = null;
Item containerItem = null;
foreach (MapEntity e in item.linkedTo)
{
containerItem = e as Item;
if (containerItem == null) { continue; }
container = containerItem.GetComponent<ItemContainer>();
if (container != null) { break; }
}
if (container == null || container.ContainableItems.Count == 0) { return true; }
if (objective.SubObjectives.None())
{
if (!AIDecontainEmptyItems(character, objective, equip: true, sourceContainer: container))
{
return false;
}
}
if (objective.SubObjectives.None())
{
var loadItemsObjective = AIContainItems<Turret>(container, character, objective, usableProjectileCount + 1, equip: true, removeEmpty: true);
loadItemsObjective.ignoredContainerIdentifiers = new string[] { containerItem.prefab.Identifier };
character.Speak(TextManager.GetWithVariable("DialogLoadTurret", "[itemname]", item.Name, true), null, 0.0f, "loadturret", 30.0f);
}
return false;
}
//enough shells and power
Character closestEnemy = null;
float closestDist = 3000 * 3000;
foreach (Character enemy in Character.CharacterList)
{
// Ignore dead, friendly, and those that are inside the same sub
if (enemy.IsDead || !enemy.Enabled || enemy.Submarine == character.Submarine) { continue; }
if (HumanAIController.IsFriendly(character, enemy)) { continue; }
float dist = Vector2.DistanceSquared(enemy.WorldPosition, item.WorldPosition);
if (dist > closestDist) { continue; }
float angle = -MathUtils.VectorToAngle(enemy.WorldPosition - item.WorldPosition);
float midRotation = (minRotation + maxRotation) / 2.0f;
while (midRotation - angle < -MathHelper.Pi) { angle -= MathHelper.TwoPi; }
while (midRotation - angle > MathHelper.Pi) { angle += MathHelper.TwoPi; }
if (angle < minRotation || angle > maxRotation) { continue; }
closestEnemy = enemy;
closestDist = dist;
}
if (closestEnemy == null) { return false; }
character.AIController.SelectTarget(closestEnemy.AiTarget);
character.CursorPosition = closestEnemy.WorldPosition;
if (item.Submarine != null) { character.CursorPosition -= item.Submarine.Position; }
float enemyAngle = MathUtils.VectorToAngle(closestEnemy.WorldPosition - item.WorldPosition);
float turretAngle = -rotation;
if (Math.Abs(MathUtils.GetShortestAngle(enemyAngle, turretAngle)) > 0.15f) { return false; }
Vector2 start = ConvertUnits.ToSimUnits(item.WorldPosition);
Vector2 end = ConvertUnits.ToSimUnits(closestEnemy.WorldPosition);
if (closestEnemy.Submarine != null)
{
start -= closestEnemy.Submarine.SimPosition;
end -= closestEnemy.Submarine.SimPosition;
}
var collisionCategories = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel;
var pickedBody = Submarine.PickBody(start, end, null, collisionCategories);
if (pickedBody == null) { return false; }
Character targetCharacter = null;
if (pickedBody.UserData is Character c)
{
targetCharacter = c;
}
else if (pickedBody.UserData is Limb limb)
{
targetCharacter = limb.character;
}
if (targetCharacter != null && HumanAIController.IsFriendly(character, targetCharacter))
{
// Don't shoot friendly characters
return false;
}
else if (targetCharacter == null && !(pickedBody.UserData is Structure) && !(pickedBody.UserData is Item))
{
// Hit something else than a wall or an item (probably a level wall)
return false;
}
if (objective.Option.Equals("fireatwill", StringComparison.OrdinalIgnoreCase))
{
character?.Speak(TextManager.GetWithVariable("DialogFireTurret", "[itemname]", item.Name, true), null, 0.0f, "fireturret", 5.0f);
character.SetInput(InputType.Shoot, true, true);
}
return false;
}
private void GetAvailablePower(out float availableCharge, out float availableCapacity)
{
var batteries = item.GetConnectedComponents<PowerContainer>();
availableCharge = 0.0f;
availableCapacity = 0.0f;
foreach (PowerContainer battery in batteries)
{
availableCharge += battery.Charge;
availableCapacity += battery.Capacity;
}
}
protected override void RemoveComponentSpecific()
{
base.RemoveComponentSpecific();
barrelSprite?.Remove(); barrelSprite = null;
railSprite?.Remove(); railSprite = null;
#if CLIENT
crosshairSprite?.Remove(); crosshairSprite = null;
crosshairPointerSprite?.Remove(); crosshairPointerSprite = null;
moveSoundChannel?.Dispose(); moveSoundChannel = null;
#endif
}
private List<Projectile> GetLoadedProjectiles(bool returnFirst = false)
{
List<Projectile> projectiles = new List<Projectile>();
//check the item itself first
CheckProjectileContainer(item, projectiles, returnFirst);
foreach (MapEntity e in item.linkedTo)
{
if (e is Item projectileContainer) { CheckProjectileContainer(projectileContainer, projectiles, returnFirst); }
if (returnFirst && projectiles.Any()) return projectiles;
}
return projectiles;
}
private void CheckProjectileContainer(Item projectileContainer, List<Projectile> projectiles, bool returnFirst)
{
var containedItems = projectileContainer.ContainedItems;
if (containedItems == null) return;
foreach (Item containedItem in containedItems)
{
var projectileComponent = containedItem.GetComponent<Projectile>();
if (projectileComponent != null)
{
projectiles.Add(projectileComponent);
if (returnFirst) return;
}
else
{
//check if the contained item is another itemcontainer with projectiles inside it
if (containedItem.ContainedItems == null) continue;
foreach (Item subContainedItem in containedItem.ContainedItems)
{
projectileComponent = subContainedItem.GetComponent<Projectile>();
if (projectileComponent != null)
{
projectiles.Add(projectileComponent);
if (returnFirst) return;
}
}
}
}
}
public override void FlipX(bool relativeToSub)
{
minRotation = MathHelper.Pi - minRotation;
maxRotation = MathHelper.Pi - maxRotation;
var temp = minRotation;
minRotation = maxRotation;
maxRotation = temp;
barrelPos.X = item.Rect.Width / item.Scale - barrelPos.X;
while (minRotation < 0)
{
minRotation += MathHelper.TwoPi;
maxRotation += MathHelper.TwoPi;
}
rotation = (minRotation + maxRotation) / 2;
UpdateTransformedBarrelPos();
}
public override void FlipY(bool relativeToSub)
{
baseRotationRad = MathUtils.WrapAngleTwoPi(baseRotationRad - MathHelper.Pi);
UpdateTransformedBarrelPos();
/*minRotation = -minRotation;
maxRotation = -maxRotation;
var temp = minRotation;
minRotation = maxRotation;
maxRotation = temp;
barrelPos.Y = item.Rect.Height / item.Scale - barrelPos.Y;
while (minRotation < 0)
{
minRotation += MathHelper.TwoPi;
maxRotation += MathHelper.TwoPi;
}
rotation = (minRotation + maxRotation) / 2;
UpdateTransformedBarrelPos();*/
}
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power, float signalStrength = 1.0f)
{
switch (connection.Name)
{
case "position_in":
if (float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float newRotation))
{
targetRotation = MathHelper.ToRadians(newRotation);
IsActive = true;
}
user = sender;
break;
case "trigger_in":
item.Use((float)Timing.Step, sender);
user = sender;
//triggering the Use method through item.Use will fail if the item is not characterusable and the signal was sent by a character
//so lets do it manually
if (!characterUsable && sender != null)
{
TryLaunch((float)Timing.Step, sender);
}
break;
case "toggle":
case "toggle_light":
if (lightComponent != null)
{
lightComponent.IsOn = !lightComponent.IsOn;
}
break;
}
}
public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
{
Item item = (Item)extraData[2];
msg.Write(item.Removed ? (ushort)0 : item.ID);
}
}
}
@@ -0,0 +1,463 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using Barotrauma.Networking;
namespace Barotrauma
{
public enum WearableType
{
Item,
Hair,
Beard,
Moustache,
FaceAttachment,
JobIndicator,
Husk,
Herpes
}
class WearableSprite
{
public string UnassignedSpritePath { get; private set; }
public string SpritePath { get; private set; }
public XElement SourceElement { get; private set; }
public WearableType Type { get; private set; }
private Sprite _sprite;
public Sprite Sprite
{
get { return _sprite; }
set
{
if (value == _sprite) { return; }
if (_sprite != null)
{
_sprite.Remove();
}
_sprite = value;
}
}
public LimbType Limb { get; private set; }
public bool HideLimb { get; private set; }
public bool HideOtherWearables { get; private set; }
public List<WearableType> HideWearablesOfType { get; private set; }
public bool InheritLimbDepth { get; private set; }
public bool InheritTextureScale { get; private set; }
public bool InheritOrigin { get; private set; }
public bool InheritSourceRect { get; private set; }
public LimbType DepthLimb { get; private set; }
private Wearable _wearableComponent;
public Wearable WearableComponent
{
get { return _wearableComponent; }
set
{
if (value == _wearableComponent) { return; }
if (_wearableComponent != null)
{
_wearableComponent.Remove();
}
_wearableComponent = value;
}
}
public string Sound { get; private set; }
public Point? SheetIndex { get; private set; }
public LightComponent LightComponent { get; set; }
public int Variant { get; set; }
private Gender _gender;
/// <summary>
/// None = Any/Not Defined -> no effect.
/// Changing the gender forces re-initialization, because the textures can be different for male and female characters.
/// </summary>
public Gender Gender
{
get { return _gender; }
set
{
if (value == _gender) { return; }
_gender = value;
IsInitialized = false;
UnassignedSpritePath = ParseSpritePath(SourceElement.GetAttributeString("texture", string.Empty));
Init(_gender);
}
}
public WearableSprite(XElement subElement, WearableType type)
{
Type = type;
SourceElement = subElement;
UnassignedSpritePath = subElement.GetAttributeString("texture", string.Empty);
Init();
switch (type)
{
case WearableType.Hair:
case WearableType.Beard:
case WearableType.Moustache:
case WearableType.FaceAttachment:
case WearableType.JobIndicator:
case WearableType.Husk:
case WearableType.Herpes:
Limb = LimbType.Head;
HideLimb = type == WearableType.Husk || type == WearableType.Herpes;
HideOtherWearables = false;
InheritLimbDepth = true;
InheritTextureScale = true;
InheritOrigin = true;
InheritSourceRect = true;
break;
}
}
/// <summary>
/// Note: this constructor cannot initialize automatically, because the gender is unknown at this point. We only know it when the item is equipped.
/// </summary>
public WearableSprite(XElement subElement, Wearable wearable, int variant = 0)
{
Type = WearableType.Item;
WearableComponent = wearable;
Variant = Math.Max(variant, 0);
UnassignedSpritePath = ParseSpritePath(subElement.GetAttributeString("texture", string.Empty));
SourceElement = subElement;
}
private string ParseSpritePath(string texturePath) => texturePath.Contains("/") ? texturePath : $"{Path.GetDirectoryName(WearableComponent.Item.Prefab.FilePath)}/{texturePath}";
public void ParsePath(bool parseSpritePath)
{
string tempPath = UnassignedSpritePath;
if (_gender != Gender.None)
{
tempPath = tempPath.Replace("[GENDER]", (_gender == Gender.Female) ? "female" : "male");
}
SpritePath = tempPath.Replace("[VARIANT]", Variant.ToString());
if (!File.Exists(SpritePath))
{
// If the variant does not exist, parse the path so that it uses first variant.
SpritePath = tempPath.Replace("[VARIANT]", "1");
}
if (parseSpritePath)
{
Sprite.ParseTexturePath(file: SpritePath);
}
}
public bool IsInitialized { get; private set; }
public void Init(Gender gender = Gender.None)
{
if (IsInitialized) { return; }
_gender = UnassignedSpritePath.Contains("[GENDER]") ? gender : Gender.None;
ParsePath(false);
if (Sprite != null)
{
Sprite.Remove();
}
Sprite = new Sprite(SourceElement, file: SpritePath);
Limb = (LimbType)Enum.Parse(typeof(LimbType), SourceElement.GetAttributeString("limb", "Head"), true);
HideLimb = SourceElement.GetAttributeBool("hidelimb", false);
HideOtherWearables = SourceElement.GetAttributeBool("hideotherwearables", false);
InheritLimbDepth = SourceElement.GetAttributeBool("inheritlimbdepth", true);
InheritTextureScale = SourceElement.GetAttributeBool("inherittexturescale", false);
InheritOrigin = SourceElement.GetAttributeBool("inheritorigin", false);
InheritSourceRect = SourceElement.GetAttributeBool("inheritsourcerect", false);
DepthLimb = (LimbType)Enum.Parse(typeof(LimbType), SourceElement.GetAttributeString("depthlimb", "None"), true);
Sound = SourceElement.GetAttributeString("sound", "");
var index = SourceElement.GetAttributePoint("sheetindex", new Point(-1, -1));
if (index.X > -1 && index.Y > -1)
{
SheetIndex = index;
}
HideWearablesOfType = new List<WearableType>();
var wearableTypes = SourceElement.GetAttributeStringArray("hidewearablesoftype", null);
if (wearableTypes != null && wearableTypes.Length > 0)
{
foreach (var value in wearableTypes)
{
if (Enum.TryParse(value, ignoreCase: true, out WearableType wearableType))
{
HideWearablesOfType.Add(wearableType);
}
}
}
IsInitialized = true;
}
}
}
namespace Barotrauma.Items.Components
{
class Wearable : Pickable, IServerSerializable
{
private readonly XElement[] wearableElements;
private readonly WearableSprite[] wearableSprites;
private readonly LimbType[] limbType;
private readonly Limb[] limb;
private readonly List<DamageModifier> damageModifiers;
public IEnumerable<DamageModifier> DamageModifiers
{
get { return damageModifiers; }
}
public bool AutoEquipWhenFull { get; private set; }
public bool DisplayContainedStatus { get; private set; }
public readonly int Variants;
private int variant;
public int Variant
{
get { return variant; }
set
{
#if SERVER
variant = value;
item.CreateServerEvent(this);
#elif CLIENT
if (variant == value) { return; }
Character character = picker;
if (character != null)
{
Unequip(character);
}
for (int i = 0; i < wearableSprites.Length; i++)
{
var subElement = wearableElements[i];
wearableSprites[i]?.Sprite?.Remove();
wearableSprites[i] = new WearableSprite(subElement, this, value);
}
if (character != null)
{
Equip(character);
}
variant = value;
#endif
}
}
public Wearable(Item item, XElement element) : base(item, element)
{
this.item = item;
damageModifiers = new List<DamageModifier>();
int spriteCount = element.Elements().Count(x => x.Name.ToString() == "sprite");
Variants = element.GetAttributeInt("variants", 0);
variant = Rand.Range(1, Variants + 1, Rand.RandSync.Server);
wearableSprites = new WearableSprite[spriteCount];
wearableElements = new XElement[spriteCount];
limbType = new LimbType[spriteCount];
limb = new Limb[spriteCount];
AutoEquipWhenFull = element.GetAttributeBool("autoequipwhenfull", true);
DisplayContainedStatus = element.GetAttributeBool("displaycontainedstatus", false);
int i = 0;
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLower())
{
case "sprite":
if (subElement.Attribute("texture") == null)
{
DebugConsole.ThrowError("Item \"" + item.Name + "\" doesn't have a texture specified!");
return;
}
limbType[i] = (LimbType)Enum.Parse(typeof(LimbType),
subElement.GetAttributeString("limb", "Head"), true);
wearableSprites[i] = new WearableSprite(subElement, this, variant);
wearableElements[i] = subElement;
foreach (XElement lightElement in subElement.Elements())
{
if (!lightElement.Name.ToString().Equals("lightcomponent", StringComparison.OrdinalIgnoreCase)) { continue; }
wearableSprites[i].LightComponent = new LightComponent(item, lightElement)
{
Parent = this
};
item.AddComponent(wearableSprites[i].LightComponent);
}
i++;
break;
case "damagemodifier":
damageModifiers.Add(new DamageModifier(subElement, item.Name + ", Wearable"));
break;
}
}
}
public override void Equip(Character character)
{
picker = character;
for (int i = 0; i < wearableSprites.Length; i++ )
{
var wearableSprite = wearableSprites[i];
if (!wearableSprite.IsInitialized) { wearableSprite.Init(picker.Info?.Gender ?? Gender.None); }
if (picker.Info?.Gender != Gender.None && (wearableSprite.Gender != Gender.None))
{
// If the item is gender specific (it has a different textures for male and female), we have to change the gender here so that the texture is updated.
wearableSprite.Gender = picker.Info.Gender;
}
Limb equipLimb = character.AnimController.GetLimb(limbType[i]);
if (equipLimb == null) { continue; }
if (item.body != null)
{
item.body.Enabled = false;
}
IsActive = true;
if (wearableSprite.LightComponent != null)
{
wearableSprite.LightComponent.ParentBody = equipLimb.body;
}
limb[i] = equipLimb;
if (!equipLimb.WearingItems.Contains(wearableSprite))
{
equipLimb.WearingItems.Add(wearableSprite);
equipLimb.WearingItems.Sort((i1, i2) => { return i2.Sprite.Depth.CompareTo(i1.Sprite.Depth); });
equipLimb.WearingItems.Sort((i1, i2) =>
{
if (i1?.WearableComponent == null && i2?.WearableComponent == null)
{
return 0;
}
else if (i1?.WearableComponent == null)
{
return -1;
}
else if (i2?.WearableComponent == null)
{
return 1;
}
return i1.WearableComponent.AllowedSlots.Contains(InvSlotType.OuterClothes).CompareTo(i2.WearableComponent.AllowedSlots.Contains(InvSlotType.OuterClothes));
});
}
#if CLIENT
equipLimb.UpdateWearableTypesToHide();
#endif
}
}
public override void Drop(Character dropper)
{
Unequip(picker);
base.Drop(dropper);
picker = null;
IsActive = false;
}
public override void Unequip(Character character)
{
if (picker == null) return;
for (int i = 0; i < wearableSprites.Length; i++)
{
Limb equipLimb = character.AnimController.GetLimb(limbType[i]);
if (equipLimb == null) continue;
if (wearableSprites[i].LightComponent != null)
{
wearableSprites[i].LightComponent.ParentBody = null;
}
equipLimb.WearingItems.RemoveAll(w => w != null && w == wearableSprites[i]);
#if CLIENT
equipLimb.UpdateWearableTypesToHide();
#endif
limb[i] = null;
}
IsActive = false;
}
public override void UpdateBroken(float deltaTime, Camera cam)
{
Update(deltaTime, cam);
}
public override void Update(float deltaTime, Camera cam)
{
if (picker.Removed)
{
IsActive = false;
return;
}
item.SetTransform(picker.SimPosition, 0.0f);
item.SetContainedItemPositions();
item.ApplyStatusEffects(ActionType.OnWearing, deltaTime, picker);
#if CLIENT
PlaySound(ActionType.OnWearing, picker);
#endif
}
protected override void RemoveComponentSpecific()
{
base.RemoveComponentSpecific();
foreach (WearableSprite wearableSprite in wearableSprites)
{
if (wearableSprite != null && wearableSprite.Sprite != null) wearableSprite.Sprite.Remove();
}
}
public override XElement Save(XElement parentElement)
{
XElement componentElement = base.Save(parentElement);
componentElement.Add(new XAttribute("variant", variant));
return componentElement;
}
private int loadedVariant = -1;
public override void Load(XElement componentElement, bool usePrefabValues)
{
base.Load(componentElement, usePrefabValues);
loadedVariant = componentElement.GetAttributeInt("variant", -1);
}
public override void OnItemLoaded()
{
base.OnItemLoaded();
//do this here to prevent creating a network event before the item has been fully initialized
if (loadedVariant > 0 && loadedVariant < Variants + 1)
{
Variant = loadedVariant;
}
}
public override void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
{
msg.Write((byte)Variant);
base.ServerWrite(msg, c, extraData);
}
public override void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime)
{
Variant = (int)msg.ReadByte();
base.ClientRead(type, msg, sendingTime);
}
}
}