Unstable v0.1300.0.0 (February 19th 2021)

This commit is contained in:
Joonas Rikkonen
2021-02-25 13:44:23 +02:00
parent b772654326
commit 24cbef485a
441 changed files with 21343 additions and 8562 deletions
@@ -13,7 +13,16 @@ namespace Barotrauma.Items.Components
{
partial class DockingPort : ItemComponent, IDrawableComponent, IServerSerializable
{
private static List<DockingPort> list = new List<DockingPort>();
public enum DirectionType
{
None,
Top,
Bottom,
Left,
Right
}
private static readonly List<DockingPort> list = new List<DockingPort>();
public static IEnumerable<DockingPort> List
{
get { return list; }
@@ -29,15 +38,17 @@ namespace Barotrauma.Items.Components
private Fixture outsideBlocker;
private Body doorBody;
private float dockingCooldown;
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;
const float ForceLockDelay = 1.0f;
public int DockingDir { get; private set; }
public int DockingDir { get; 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; }
@@ -63,6 +74,10 @@ namespace Barotrauma.Items.Components
set;
}
[Editable, Serialize(DirectionType.None, false, description: "Which direction the port is allowed to dock in. For example, \"Top\" would mean the port can dock to another port above it.\n"+
"Normally there's no need to touch this setting, but if you notice the docking position is incorrect (for example due to some unusual docking port configuration without hulls or doors), you can use this to enforce the direction.")]
public DirectionType ForceDockingDirection { get; set; }
public DockingPort DockingTarget { get; private set; }
public Door Door { get; private set; }
@@ -89,6 +104,11 @@ namespace Barotrauma.Items.Components
}
}
public bool IsLocked
{
get { return joint is WeldJoint || DockingTarget?.joint is WeldJoint; }
}
/// <summary>
/// Automatically cleared after docking -> no need to unregister
/// </summary>
@@ -129,20 +149,12 @@ namespace Barotrauma.Items.Components
DockingDir = GetDir(DockingTarget);
DockingTarget.DockingDir = -DockingDir;
}
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();
}
//undock and redock to recreate the hulls, gaps and physics bodies
var prevDockingTarget = DockingTarget;
Undock(applyEffects: false);
Dock(prevDockingTarget);
Lock(true, applyEffects: false);
}
}
@@ -169,15 +181,15 @@ namespace Barotrauma.Items.Components
private void AttemptDock()
{
var adjacentPort = FindAdjacentPort();
if (adjacentPort != null) Dock(adjacentPort);
if (adjacentPort != null) { Dock(adjacentPort); }
}
public void Dock(DockingPort target)
{
if (item.Submarine.DockedTo.Contains(target.item.Submarine)) return;
if (item.Submarine.DockedTo.Contains(target.item.Submarine)) { return; }
forceLockTimer = 0.0f;
dockingCooldown = 0.1f;
if (DockingTarget != null)
{
@@ -219,7 +231,6 @@ namespace Barotrauma.Items.Components
#if SERVER
if (GameMain.Server != null && (!item.Submarine?.Loading ?? true))
{
originalDockingTargetID = DockingTarget.item.ID;
item.CreateServerEvent(this);
}
#endif
@@ -229,7 +240,7 @@ namespace Barotrauma.Items.Components
}
public void Lock(bool isNetworkMessage, bool forcePosition = false)
public void Lock(bool isNetworkMessage, bool forcePosition = false, bool applyEffects = true)
{
#if CLIENT
if (GameMain.Client != null && !isNetworkMessage) { return; }
@@ -246,18 +257,24 @@ namespace Barotrauma.Items.Components
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.Info.IsOutpost)
if (applyEffects)
{
item.Submarine.SubBody.SetPosition(item.Submarine.SubBody.Position + ConvertUnits.ToDisplayUnits(jointDiff));
ApplyStatusEffects(ActionType.OnUse, 1.0f);
}
else if (DockingTarget.item.Submarine.PhysicsBody.Mass < item.Submarine.PhysicsBody.Mass ||
item.Submarine.Info.IsOutpost)
if (forcePosition)
{
DockingTarget.item.Submarine.SubBody.SetPosition(DockingTarget.item.Submarine.SubBody.Position - ConvertUnits.ToDisplayUnits(jointDiff));
Vector2 jointDiff = joint.WorldAnchorB - joint.WorldAnchorA;
if (item.Submarine.PhysicsBody.Mass < DockingTarget.item.Submarine.PhysicsBody.Mass ||
DockingTarget.item.Submarine.Info.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.Info.IsOutpost)
{
DockingTarget.item.Submarine.SubBody.SetPosition(DockingTarget.item.Submarine.SubBody.Position - ConvertUnits.ToDisplayUnits(jointDiff));
}
}
ConnectWireBetweenPorts();
@@ -266,7 +283,6 @@ namespace Barotrauma.Items.Components
#if SERVER
if (GameMain.Server != null && (!item.Submarine?.Loading ?? true))
{
originalDockingTargetID = DockingTarget.item.ID;
item.CreateServerEvent(this);
}
#else
@@ -311,10 +327,10 @@ namespace Barotrauma.Items.Components
joint = null;
}
Vector2 offset = (IsHorizontal ?
Vector2 offset = IsHorizontal ?
Vector2.UnitX * DockingDir :
Vector2.UnitY * DockingDir);
offset *= DockedDistance * 0.5f;
Vector2.UnitY * DockingDir;
offset *= DockedDistance * 0.5f * item.Scale;
Vector2 pos1 = item.WorldPosition + offset;
@@ -346,6 +362,14 @@ namespace Barotrauma.Items.Components
public int GetDir(DockingPort dockingTarget = null)
{
int forcedDockingDir = GetForcedDockingDir();
if (forcedDockingDir != 0) { return forcedDockingDir; }
if (dockingTarget != null)
{
forcedDockingDir = -dockingTarget.GetForcedDockingDir();
if (forcedDockingDir != 0) { return forcedDockingDir; }
}
if (DockingDir != 0) { return DockingDir; }
if (Door != null && Door.LinkedGap.linkedTo.Count > 0)
@@ -390,9 +414,10 @@ namespace Barotrauma.Items.Components
}
if (dockingTarget != null)
{
return IsHorizontal ?
int dir = IsHorizontal ?
Math.Sign(dockingTarget.item.WorldPosition.X - item.WorldPosition.X) :
Math.Sign(dockingTarget.item.WorldPosition.Y - item.WorldPosition.Y);
if (dir != 0) { return dir; }
}
if (item.Submarine != null)
{
@@ -404,6 +429,22 @@ namespace Barotrauma.Items.Components
return 0;
}
private int GetForcedDockingDir()
{
switch (ForceDockingDirection)
{
case DirectionType.Left:
return -1;
case DirectionType.Right:
return 1;
case DirectionType.Top:
return 1;
case DirectionType.Bottom:
return -1;
}
return 0;
}
private void ConnectWireBetweenPorts()
{
Wire wire = item.GetComponent<Wire>();
@@ -491,8 +532,9 @@ namespace Barotrauma.Items.Components
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);
int scaledDockedDistance = (int)(DockedDistance / 2 * item.Scale);
hullRects[0] = new Rectangle(hullRects[0].Center.X, hullRects[0].Y, scaledDockedDistance, hullRects[0].Height);
hullRects[1] = new Rectangle(hullRects[1].Center.X - scaledDockedDistance, hullRects[1].Y, scaledDockedDistance, 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;
@@ -588,8 +630,9 @@ namespace Barotrauma.Items.Components
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));
int scaledDockedDistance = (int)(DockedDistance / 2 * item.Scale);
hullRects[0] = new Rectangle(hullRects[0].X, hullRects[0].Y - hullRects[0].Height / 2 + scaledDockedDistance, hullRects[0].Width, scaledDockedDistance);
hullRects[1] = new Rectangle(hullRects[1].X, hullRects[1].Y - hullRects[1].Height / 2, hullRects[1].Width, scaledDockedDistance);
//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;
@@ -801,13 +844,17 @@ namespace Barotrauma.Items.Components
}
}
public void Undock()
public void Undock(bool applyEffects = true)
{
if (DockingTarget == null || !docked) return;
if (DockingTarget == null || !docked) { return; }
forceLockTimer = 0.0f;
dockingCooldown = 0.1f;
ApplyStatusEffects(ActionType.OnSecondaryUse, 1.0f);
if (applyEffects)
{
ApplyStatusEffects(ActionType.OnSecondaryUse, 1.0f);
}
DockingTarget.item.Submarine.ConnectedDockingPorts.Remove(item.Submarine);
item.Submarine.ConnectedDockingPorts.Remove(DockingTarget.item.Submarine);
@@ -879,7 +926,6 @@ namespace Barotrauma.Items.Components
#if SERVER
if (GameMain.Server != null && (!item.Submarine?.Loading ?? true))
{
originalDockingTargetID = Entity.NullEntityID;
item.CreateServerEvent(this);
}
#endif
@@ -889,6 +935,7 @@ namespace Barotrauma.Items.Components
public override void Update(float deltaTime, Camera cam)
{
dockingCooldown -= deltaTime;
if (DockingTarget == null)
{
dockingState = MathHelper.Lerp(dockingState, 0.0f, deltaTime * 10.0f);
@@ -896,7 +943,6 @@ namespace Barotrauma.Items.Components
item.SendSignal(0, "0", "state_out", null);
item.SendSignal(0, (FindAdjacentPort() != null) ? "1" : "0", "proximity_sensor", null);
}
else
{
@@ -908,7 +954,6 @@ namespace Barotrauma.Items.Components
if (joint is DistanceJoint)
{
item.SendSignal(0, "0", "state_out", null);
dockingState = MathHelper.Lerp(dockingState, 0.5f, deltaTime * 10.0f);
forceLockTimer += deltaTime;
@@ -918,9 +963,7 @@ namespace Barotrauma.Items.Components
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;
float massRatio1, massRatio2;
if (item.Submarine.PhysicsBody.BodyType != BodyType.Dynamic)
{
massRatio1 = 0.0f;
@@ -954,11 +997,10 @@ namespace Barotrauma.Items.Components
{
doorBody.Enabled = DockingTarget.Door.Body.Enabled;
}
item.SendSignal(0, "1", "state_out", null);
dockingState = MathHelper.Lerp(dockingState, 1.0f, deltaTime * 10.0f);
}
item.SendSignal(0, IsLocked ? "1" : "0", "state_out", null);
}
if (!obstructedWayPointsDisabled && dockingState >= 0.99f)
{
@@ -985,7 +1027,8 @@ namespace Barotrauma.Items.Components
if (initialized) { return; }
initialized = true;
float closestDist = 30.0f * 30.0f;
float maxXDist = (item.Prefab.sprite.size.X * item.Prefab.Scale) / 2;
float closestYDist = (item.Prefab.sprite.size.Y * item.Prefab.Scale) / 2;
foreach (Item it in Item.ItemList)
{
if (it.Submarine != item.Submarine) { continue; }
@@ -993,11 +1036,22 @@ namespace Barotrauma.Items.Components
var doorComponent = it.GetComponent<Door>();
if (doorComponent == null || doorComponent.IsHorizontal == IsHorizontal) { continue; }
float distSqr = Vector2.DistanceSquared(item.Position, it.Position);
if (distSqr < closestDist)
float yDist = Math.Abs(it.Position.Y - item.Position.Y);
if (item.linkedTo.Contains(it))
{
// If there's a door linked to the docking port, always treat it close enough.
yDist = Math.Min(closestYDist, yDist);
}
else if (Math.Abs(it.Position.X - item.Position.X) > maxXDist)
{
// Too far left/right
continue;
}
if (yDist <= closestYDist)
{
Door = doorComponent;
closestDist = distSqr;
closestYDist = yDist;
}
}
@@ -1055,6 +1109,8 @@ namespace Barotrauma.Items.Components
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
if (dockingCooldown > 0.0f) { return; }
bool wasDocked = docked;
DockingPort prevDockingTarget = DockingTarget;
@@ -66,6 +66,8 @@ namespace Barotrauma.Items.Components
private Rectangle doorRect;
private bool isBroken;
public bool CanBeTraversed => (IsOpen || IsBroken) && !IsJammed && !IsStuck;
public bool IsBroken
{
@@ -283,12 +285,6 @@ namespace Barotrauma.Items.Components
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; }
@@ -642,6 +638,19 @@ namespace Barotrauma.Items.Components
partial void OnFailedToOpen();
public override bool HasAccess(Character character)
{
if (!item.IsInteractable(character)) { return false; }
if (HasIntegratedButtons)
{
return base.HasAccess(character);
}
else
{
return Item.GetConnectedComponents<Controller>(true).Any(b => b.HasAccess(character));
}
}
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f)
{
if (IsStuck || IsJammed) { return; }
@@ -62,7 +62,7 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(0.25f, true, description: "The duration of an individual discharge (in seconds)."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)]
[Serialize(0.25f, true, description: "The duration of an individual discharge (in seconds)."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 60.0f, ValueStep = 0.1f, DecimalCount = 2)]
public float Duration
{
get;
@@ -2,15 +2,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Xml.Linq;
using Barotrauma.Extensions;
using Barotrauma.MapCreatures.Behavior;
using Barotrauma.Networking;
using FarseerPhysics;
using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
using Vector2 = Microsoft.Xna.Framework.Vector2;
using Vector4 = Microsoft.Xna.Framework.Vector4;
namespace Barotrauma.Items.Components
{
@@ -286,8 +285,19 @@ namespace Barotrauma.Items.Components
}
}
int value = pool[Growable.RandomInt(0, possible, random)];
int value;
if (Parent == null)
{
value = pool[Growable.RandomInt(0, possible, random)];
}
else
{
var (x, y, z, w) = Parent.GrowthWeights;
float[] weights = { x, y, z, w };
value = pool.RandomElementByWeight(i => weights[i]);
}
return (TileSide) (1 << value);
}
@@ -382,6 +392,12 @@ namespace Barotrauma.Items.Components
[Serialize("0.26,0.27,0.29,1.0", true, "Tint of a dead plant.")]
public Color DeadTint { get; set; }
[Serialize("1,1,1,1", true, "Probability for the plant to grow in a direction.")]
public Vector4 GrowthWeights { get; set; }
[Serialize(0.0f, true, "How much damage is taken from fires.")]
public float FireVulnerability { get; set; }
private const float increasedDeathSpeed = 10f;
private bool accelerateDeath;
private float health;
@@ -405,6 +421,7 @@ namespace Barotrauma.Items.Components
private int productDelay;
private int vineDelay;
private float fireCheckCooldown;
public readonly List<ProducedItem> ProducedItems = new List<ProducedItem>();
public readonly List<VineTile> Vines = new List<VineTile>();
@@ -476,11 +493,15 @@ namespace Barotrauma.Items.Components
if (Health > 0)
{
GrowVines(planter, slot);
Health -= accelerateDeath ? Hardiness * increasedDeathSpeed : Hardiness;
// fertilizer makes the plant tick faster, compensate by halving water requirement
float multipler = planter.Fertilizer > 0 ? 0.5f : 1f;
Health -= (accelerateDeath ? Hardiness * increasedDeathSpeed : Hardiness) * multipler;
if (planter.Item.InWater)
{
Health -= FloodTolerance;
Health -= FloodTolerance * multipler;
}
#if SERVER
if (FullyGrown)
@@ -617,6 +638,8 @@ namespace Barotrauma.Items.Components
{
base.Update(deltaTime, cam);
UpdateFires(deltaTime);
#if CLIENT
foreach (VineTile vine in Vines)
{
@@ -627,6 +650,29 @@ namespace Barotrauma.Items.Components
CheckPlantState();
}
private void UpdateFires(float deltaTime)
{
if (!Decayed && item.CurrentHull?.FireSources is { } fireSources && FireVulnerability > 0f)
{
if (fireCheckCooldown <= 0)
{
foreach (FireSource source in fireSources)
{
if (source.IsInDamageRange(item.WorldPosition, source.DamageRange))
{
Health -= FireVulnerability;
}
}
fireCheckCooldown = 5f;
}
else
{
fireCheckCooldown -= deltaTime;
}
}
}
private void GrowVines(Planter planter, PlantSlot slot)
{
if (FullyGrown) { return; }
@@ -677,7 +723,23 @@ namespace Barotrauma.Items.Components
TileSide side = oldVines.GetRandomFreeSide(random);
if (side == TileSide.None) { continue; }
if (side == TileSide.None)
{
oldVines.FailedGrowthAttempts++;
continue;
}
if (GrowthWeights != Vector4.One)
{
var (x, y, z, w) = GrowthWeights;
float[] weights = { x, y, z, w };
int index = (int) Math.Log2((int) side);
if (MathUtils.NearlyEqual(weights[index], 0f))
{
oldVines.FailedGrowthAttempts++;
continue;
}
}
Vector2 pos = oldVines.AdjacentPositions[side];
Rectangle rect = VineTile.CreatePlantRect(pos);
@@ -18,7 +18,7 @@ namespace Barotrauma.Items.Components
protected Vector2[] handlePos;
private readonly Vector2[] scaledHandlePos;
private InputType prevPickKey;
private readonly InputType prevPickKey;
private string prevMsg;
private Dictionary<RelatedItem.RelationType, List<RelatedItem>> prevRequiredItems;
@@ -29,13 +29,22 @@ namespace Barotrauma.Items.Components
private float swingState;
private Character prevEquipper;
private bool attachable, attached, attachedByDefault;
private Voronoi2.VoronoiCell attachTargetCell;
private readonly PhysicsBody body;
public PhysicsBody Pusher
{
get;
private set;
}
[Serialize(true, true, description: "Is the item currently able to push characters around? True by default. Only valid if blocksplayers is set to true.")]
public bool CanPush
{
get;
set;
}
//the angle in which the Character holds the item
protected float holdAngle;
@@ -205,6 +214,7 @@ namespace Barotrauma.Items.Components
if (other.Body.UserData is Character character)
{
if (!IsActive) { return false; }
if (!CanPush) { return false; }
return character != picker;
}
else
@@ -255,6 +265,7 @@ namespace Barotrauma.Items.Components
if (Pusher != null) { Pusher.Enabled = false; }
if (item.body != null) { item.body.Enabled = true; }
IsActive = false;
attachTargetCell = null;
if (picker == null || picker.Removed)
{
@@ -299,11 +310,9 @@ namespace Barotrauma.Items.Components
{
item.SetTransform(picker.SimPosition, 0.0f);
}
}
}
}
picker.DeselectItem(item);
picker.Inventory.RemoveItem(item);
picker = null;
}
@@ -338,34 +347,33 @@ namespace Barotrauma.Items.Components
}
bool alreadyEquipped = character.HasEquippedItem(item);
bool canSelect = picker.TrySelectItem(item);
if (canSelect || picker.HasEquippedItem(item))
if (picker.HasEquippedItem(item))
{
if (!canSelect)
{
character.DeselectItem(item);
}
item.body.Enabled = true;
item.body.PhysEnabled = false;
IsActive = true;
#if SERVER
if (!alreadyEquipped) GameServer.Log(GameServer.CharacterLogName(character) + " equipped " + item.Name, ServerLog.MessageType.ItemInteraction);
if (picker != prevEquipper) { GameServer.Log(GameServer.CharacterLogName(character) + " equipped " + item.Name, ServerLog.MessageType.ItemInteraction); }
#endif
prevEquipper = picker;
}
else
{
prevEquipper = null;
}
}
public override void Unequip(Character character)
{
if (picker == null) return;
picker.DeselectItem(item);
#if SERVER
GameServer.Log(GameServer.CharacterLogName(character) + " unequipped " + item.Name, ServerLog.MessageType.ItemInteraction);
if (prevEquipper != null)
{
GameServer.Log(GameServer.CharacterLogName(character) + " unequipped " + item.Name, ServerLog.MessageType.ItemInteraction);
}
#endif
prevEquipper = null;
if (picker == null) { return; }
item.body.PhysEnabled = true;
item.body.Enabled = false;
IsActive = false;
@@ -383,9 +391,9 @@ namespace Barotrauma.Items.Components
//can be attached anywhere inside hulls
if (item.CurrentHull != null && Submarine.RectContains(item.CurrentHull.WorldRect, attachPos)) { return true; }
return Structure.GetAttachTarget(attachPos) != null;
return Structure.GetAttachTarget(attachPos) != null || GetAttachTargetCell(100.0f) != null;
}
public bool CanBeDeattached()
{
if (!attachable || !attached) { return true; }
@@ -399,14 +407,14 @@ namespace Barotrauma.Items.Components
//if the item has a connection panel and rewiring is disabled, don't allow deattaching
var connectionPanel = item.GetComponent<ConnectionPanel>();
if (connectionPanel != null && (connectionPanel.Locked || !(GameMain.NetworkMember?.ServerSettings?.AllowRewiring ?? true)))
if (connectionPanel != null && !connectionPanel.AlwaysAllowRewiring && (connectionPanel.Locked || !(GameMain.NetworkMember?.ServerSettings?.AllowRewiring ?? true)))
{
return false;
}
if (item.CurrentHull == null)
{
return Structure.GetAttachTarget(item.WorldPosition) != null;
return attachTargetCell != null && Structure.GetAttachTarget(item.WorldPosition) != null;
}
else
{
@@ -464,7 +472,7 @@ namespace Barotrauma.Items.Components
public void AttachToWall()
{
if (!attachable) return;
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)
@@ -479,15 +487,19 @@ namespace Barotrauma.Items.Components
}
item.Submarine = attachTarget.Submarine;
}
else
{
attachTargetCell = GetAttachTargetCell(150.0f);
if (attachTargetCell != null) { IsActive = true; }
}
}
var containedItems = item.OwnInventory?.Items;
var containedItems = item.OwnInventory?.AllItems;
if (containedItems != null)
{
foreach (Item contained in containedItems)
{
if (contained == null) { continue; }
if (contained.body == null) { continue; }
if (contained?.body == null) { continue; }
contained.SetTransform(item.SimPosition, contained.body.Rotation);
}
}
@@ -507,6 +519,7 @@ namespace Barotrauma.Items.Components
if (!attachable) return;
Attached = false;
attachTargetCell = null;
//make the item pickable with the default pick key and with no specific tools/items when it's deattached
requiredItems.Clear();
@@ -568,9 +581,48 @@ namespace Barotrauma.Items.Components
Vector2 userPos = useWorldCoordinates ? user.WorldPosition : user.Position;
return new Vector2(
MathUtils.RoundTowardsClosest(userPos.X + mouseDiff.X, Submarine.GridSize.X),
MathUtils.RoundTowardsClosest(userPos.Y + mouseDiff.Y, Submarine.GridSize.Y));
Vector2 attachPos = userPos + mouseDiff;
if (user.Submarine == null && Level.Loaded != null)
{
bool edgeFound = false;
foreach (var cell in Level.Loaded.GetCells(attachPos))
{
if (cell.CellType != Voronoi2.CellType.Solid) { continue; }
foreach (var edge in cell.Edges)
{
if (!edge.IsSolid) { continue; }
if (MathUtils.GetLineIntersection(edge.Point1, edge.Point2, user.WorldPosition, attachPos, out Vector2 intersection))
{
attachPos = intersection;
edgeFound = true;
break;
}
}
if (edgeFound) { break; }
}
}
return
new Vector2(
MathUtils.RoundTowardsClosest(attachPos.X, Submarine.GridSize.X),
MathUtils.RoundTowardsClosest(attachPos.Y, Submarine.GridSize.Y));
}
private Voronoi2.VoronoiCell GetAttachTargetCell(float maxDist)
{
if (Level.Loaded == null) { return null; }
foreach (var cell in Level.Loaded.GetCells(item.WorldPosition, searchDepth: 1))
{
if (cell.CellType != Voronoi2.CellType.Solid) { continue; }
Vector2 diff = cell.Center - item.WorldPosition;
if (diff.LengthSquared() > 0.0001f) { diff = Vector2.Normalize(diff); }
if (cell.IsPointInside(item.WorldPosition + diff * maxDist))
{
return cell;
}
}
return null;
}
public override void UpdateBroken(float deltaTime, Camera cam)
@@ -580,14 +632,28 @@ namespace Barotrauma.Items.Components
public override void Update(float deltaTime, Camera cam)
{
if (attachTargetCell != null)
{
if (attachTargetCell.CellType != Voronoi2.CellType.Solid)
{
Drop(dropConnectedWires: true, dropper: null);
}
return;
}
if (item.body == null || !item.body.Enabled) { return; }
if (picker == null || !picker.HasEquippedItem(item))
{
if (Pusher != null) { Pusher.Enabled = false; }
IsActive = false;
if (attachTargetCell == null) { IsActive = false; }
return;
}
if (picker == Character.Controlled && picker.IsKeyDown(InputType.Aim) && CanBeAttached(picker))
{
Drawable = true;
}
Vector2 swing = Vector2.Zero;
if (swingAmount != Vector2.Zero && !picker.IsUnconscious && picker.Stun <= 0.0f)
{
@@ -612,7 +678,7 @@ namespace Barotrauma.Items.Components
item.Submarine = picker.Submarine;
if (picker.HasSelectedItem(item))
if (picker.HeldItems.Contains(item))
{
scaledHandlePos[0] = handlePos[0] * item.Scale;
scaledHandlePos[1] = handlePos[1] * item.Scale;
@@ -42,13 +42,13 @@ namespace Barotrauma.Items.Components
public override void Equip(Character character)
{
base.Equip(character);
character.Info.CheckDisguiseStatus(true, this);
character.Info?.CheckDisguiseStatus(true, this);
}
public override void Unequip(Character character)
{
base.Unequip(character);
character.Info.CheckDisguiseStatus(true, this);
character.Info?.CheckDisguiseStatus(true, this);
}
}
}
@@ -58,6 +58,13 @@ namespace Barotrauma.Items.Components
}
}
[Serialize(1.0f, false, description: "How much the position of the item can vary from the wall the item spawns on.")]
public float RandomOffsetFromWall
{
get;
set;
}
public bool Attached
{
get { return holdable != null && holdable.Attached; }
@@ -77,7 +84,7 @@ namespace Barotrauma.Items.Components
}
else
{
if (Vector2.DistanceSquared(item.SimPosition, trigger.SimPosition) > 0.01f)
if (trigger != null && Vector2.DistanceSquared(item.SimPosition, trigger.SimPosition) > 0.01f)
{
trigger.SetTransform(item.SimPosition, 0.0f);
}
@@ -50,6 +50,11 @@ namespace Barotrauma.Items.Components
set;
}
/// <summary>
/// Defines items that boost the weapon functionality, like battery cell for stun batons.
/// </summary>
public readonly string[] PreferredContainedItems;
public MeleeWeapon(Item item, XElement element)
: base(item, element)
{
@@ -61,6 +66,7 @@ namespace Barotrauma.Items.Components
item.IsShootable = true;
// TODO: should define this in xml if we have melee weapons that don't require aim to use
item.RequireAimToUse = true;
PreferredContainedItems = element.GetAttributeStringArray("preferredcontaineditems", new string[0], convertToLowerInvariant: true);
}
public override void Equip(Character character)
@@ -76,11 +82,9 @@ namespace Barotrauma.Items.Components
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++ )
foreach (Item heldItem in character.HeldItems)
{
if (character.SelectedItems[i] == null || character.SelectedItems[i] == Item) { continue; }
var otherWeapon = character.SelectedItems[i].GetComponent<MeleeWeapon>();
var otherWeapon = heldItem.GetComponent<MeleeWeapon>();
if (otherWeapon == null) { continue; }
if (otherWeapon.hitting) { return false; }
}
@@ -143,13 +147,15 @@ namespace Barotrauma.Items.Components
public override void Update(float deltaTime, Camera cam)
{
if (!item.body.Enabled) { impactQueue.Clear(); return; }
if (!picker.HasSelectedItem(item)) { impactQueue.Clear(); IsActive = false; }
if (picker == null && !picker.HeldItems.Contains(item)) { impactQueue.Clear(); IsActive = false; }
while (impactQueue.Count > 0)
{
var impact = impactQueue.Dequeue();
HandleImpact(impact.Body);
}
//in case handling the impact does something to the picker
if (picker == null) { return; }
reloadTimer -= deltaTime;
if (reloadTimer < 0) { reloadTimer = 0; }
@@ -242,12 +248,14 @@ namespace Barotrauma.Items.Components
return true;
}
//ignore collision if there's a wall between the user and the weapon to prevent hitting through walls
contact.GetWorldManifold(out Vector2 normal, out var points);
//ignore collision if there's a wall between the user and the contact point to prevent hitting through walls
if (Submarine.PickBody(User.AnimController.AimSourceSimPos,
item.SimPosition,
points[0],
collisionCategory: Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking,
allowInsideFixture: true,
customPredicate: (Fixture fixture) => { return fixture.CollidesWith.HasFlag(Physics.CollisionItem); }) != null)
customPredicate: (Fixture fixture) => { return fixture.CollidesWith.HasFlag(Physics.CollisionItem) && fixture.Body != f2.Body; }) != null)
{
return false;
}
@@ -2,6 +2,7 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
@@ -91,7 +92,7 @@ namespace Barotrauma.Items.Components
{
if (picker.Inventory.TryPutItemWithAutoEquipCheck(item, picker, allowedSlots))
{
if (!picker.HasSelectedItem(item) && item.body != null) item.body.Enabled = false;
if (!picker.HeldItems.Contains(item) && item.body != null) { item.body.Enabled = false; }
this.picker = picker;
for (int i = item.linkedTo.Count - 1; i >= 0; i--)
@@ -71,11 +71,11 @@ namespace Barotrauma.Items.Components
character.AnimController.Collider.ApplyForce(propulsion, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
if (character.SelectedItems[0] == item)
if (character.Inventory.IsInLimbSlot(item, InvSlotType.RightHand))
{
character.AnimController.GetLimb(LimbType.RightHand)?.body.ApplyForce(propulsion, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
}
if (character.SelectedItems[1] == item)
if (character.Inventory.IsInLimbSlot(item, InvSlotType.LeftHand))
{
character.AnimController.GetLimb(LimbType.LeftHand)?.body.ApplyForce(propulsion, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
}
@@ -152,7 +152,7 @@ namespace Barotrauma.Items.Components
public Projectile FindProjectile(bool triggerOnUseOnContainers = false)
{
var containedItems = item.OwnInventory?.Items;
var containedItems = item.OwnInventory?.AllItemsMod;
if (containedItems == null) { return null; }
foreach (Item item in containedItems)
@@ -166,7 +166,7 @@ namespace Barotrauma.Items.Components
foreach (Item it in containedItems)
{
if (it == null) { continue; }
var containedSubItems = it.OwnInventory?.Items;
var containedSubItems = it.OwnInventory?.AllItemsMod;
if (containedSubItems == null) { continue; }
foreach (Item subItem in containedSubItems)
{
@@ -87,6 +87,13 @@ namespace Barotrauma.Items.Components
[Serialize(false, false, description: "Can the item repair things through holes in walls.")]
public bool RepairThroughHoles { get; set; }
[Serialize(100.0f, false, description: "How far two walls need to not be considered overlapping and to stop the ray.")]
public float MaxOverlappingWallDist
{
get; set;
}
[Serialize(true, false, description: "Can the item hit broken doors.")]
public bool HitItems { get; set; }
@@ -109,10 +116,11 @@ namespace Barotrauma.Items.Components
{
get
{
if (item.body == null) { return BarrelPos; }
Matrix bodyTransform = Matrix.CreateRotationZ(item.body.Rotation + MathHelper.ToRadians(BarrelRotation));
Vector2 flippedPos = BarrelPos;
if (item.body.Dir < 0.0f) { flippedPos.X = -flippedPos.X; }
return (Vector2.Transform(flippedPos, bodyTransform));
return Vector2.Transform(flippedPos, bodyTransform);
}
}
@@ -228,11 +236,15 @@ namespace Barotrauma.Items.Components
}
float spread = MathHelper.ToRadians(MathHelper.Lerp(UnskilledSpread, Spread, degreeOfSuccess));
float angle = item.body.Rotation + MathHelper.ToRadians(BarrelRotation) + spread * Rand.Range(-0.5f, 0.5f);
Vector2 rayEnd = rayStartWorld +
ConvertUnits.ToSimUnits(new Vector2(
(float)Math.Cos(angle),
(float)Math.Sin(angle)) * Range * item.body.Dir);
float angle = MathHelper.ToRadians(BarrelRotation) + spread * Rand.Range(-0.5f, 0.5f);
float dir = 1;
if (item.body != null)
{
angle += item.body.Rotation;
dir = item.body.Dir;
}
Vector2 rayEnd = rayStartWorld + ConvertUnits.ToSimUnits(new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle)) * Range * dir);
ignoredBodies.Clear();
if (character != null)
@@ -315,9 +327,14 @@ namespace Barotrauma.Items.Components
{
var bodies = Submarine.PickBodies(rayStart, rayEnd, ignoredBodies, collisionCategories,
ignoreSensors: false,
customPredicate: (Fixture f) =>
customPredicate: (Fixture f) =>
{
if (RepairThroughHoles && f.IsSensor && f.Body?.UserData is Structure || (f.Body?.UserData is Item it && it.GetComponent<Planter>() != null)) { return false; }
if (f.IsSensor)
{
if (RepairThroughHoles && f.Body?.UserData is Structure) { return false; }
if (f.Body?.UserData is PhysicsBody) { return false; }
}
if (f.Body?.UserData is Item it && it.GetComponent<Planter>() != null) { return false; }
if (f.Body?.UserData as string == "ruinroom") { return false; }
if (f.Body?.UserData is VineTile && !(FireDamage > 0)) { return false; }
return true;
@@ -356,12 +373,13 @@ namespace Barotrauma.Items.Components
}
//if repairing through walls is not allowed and the next wall is more than 100 pixels away from the previous one, stop here
//(= repairing multiple overlapping walls is allowed as long as the edges of the walls are less than 100 pixels apart)
//(= repairing multiple overlapping walls is allowed as long as the edges of the walls are less than MaxOverlappingWallDist pixels apart)
float thisBodyFraction = Submarine.LastPickedBodyDist(body);
if (!RepairThroughWalls && lastHitType == typeof(Structure) && Range * (thisBodyFraction - lastPickedFraction) > 100.0f)
if (!RepairThroughWalls && lastHitType == typeof(Structure) && Range * (thisBodyFraction - lastPickedFraction) > MaxOverlappingWallDist)
{
break;
}
pickedPosition = rayStart + (rayEnd - rayStart) * thisBodyFraction;
if (FixBody(user, deltaTime, degreeOfSuccess, body))
{
lastPickedFraction = thisBodyFraction;
@@ -371,13 +389,16 @@ namespace Barotrauma.Items.Components
}
else
{
FixBody(user, deltaTime, degreeOfSuccess,
Submarine.PickBody(rayStart, rayEnd,
ignoredBodies, collisionCategories,
var pickedBody = Submarine.PickBody(rayStart, rayEnd,
ignoredBodies, collisionCategories,
ignoreSensors: false,
customPredicate: (Fixture f) =>
customPredicate: (Fixture f) =>
{
if (RepairThroughHoles && f.IsSensor && f.Body?.UserData is Structure) { return false; }
if (f.IsSensor)
{
if (RepairThroughHoles && f.Body?.UserData is Structure) { return false; }
if (f.Body?.UserData is PhysicsBody) { return false; }
}
if (f.Body?.UserData as string == "ruinroom") { return false; }
if (f.Body?.UserData is VineTile && !(FireDamage > 0)) { return false; }
@@ -393,9 +414,11 @@ namespace Barotrauma.Items.Components
if (targetItem.Condition <= 0) { return false; }
}
}
return f.Body?.UserData != null;
return f.Body?.UserData != null;
},
allowInsideFixture: true));
allowInsideFixture: true);
pickedPosition = Submarine.LastPickedPosition;
FixBody(user, deltaTime, degreeOfSuccess, pickedBody);
lastPickedFraction = Submarine.LastPickedFraction;
}
@@ -438,7 +461,7 @@ namespace Barotrauma.Items.Components
}
}
if (WaterAmount > 0.0f && item.CurrentHull?.Submarine != null)
if (WaterAmount > 0.0f && item.Submarine != null)
{
Vector2 pos = ConvertUnits.ToDisplayUnits(rayStart + item.Submarine.SimPosition);
@@ -466,7 +489,7 @@ namespace Barotrauma.Items.Components
#if CLIENT
float barOffset = 10f * GUI.Scale;
Vector2 offset = planter.PlantSlots.ContainsKey(i) ? planter.PlantSlots[i].Offset : Vector2.Zero;
user.UpdateHUDProgressBar(planter, planter.Item.DrawPosition + new Vector2(barOffset, 0) + offset, seed.Health / seed.MaxHealth, GUI.Style.Blue, GUI.Style.Blue, "progressbar.watering");
user?.UpdateHUDProgressBar(planter, planter.Item.DrawPosition + new Vector2(barOffset, 0) + offset, seed.Health / seed.MaxHealth, GUI.Style.Blue, GUI.Style.Blue, "progressbar.watering");
#endif
}
}
@@ -490,8 +513,6 @@ namespace Barotrauma.Items.Components
{
if (targetBody?.UserData == null) { return false; }
pickedPosition = Submarine.LastPickedPosition;
if (targetBody.UserData is Structure targetStructure)
{
if (targetStructure.IsPlatform) { return false; }
@@ -519,10 +540,9 @@ namespace Barotrauma.Items.Components
}
return true;
}
else if (targetBody.UserData is Voronoi2.VoronoiCell cell)
else if (targetBody.UserData is Voronoi2.VoronoiCell cell && cell.IsDestructible)
{
var levelWall = Level.Loaded?.ExtraWalls.Find(w => w.Body == cell.Body) as DestructibleLevelWall;
if (levelWall != null)
if (Level.Loaded?.ExtraWalls.Find(w => w.Body == cell.Body) is DestructibleLevelWall levelWall)
{
levelWall.AddDamage(-LevelWallFixAmount * deltaTime, item.WorldPosition);
}
@@ -574,7 +594,7 @@ namespace Barotrauma.Items.Components
}
else if (targetBody.UserData is Item targetItem)
{
if (!HitItems || targetItem.NonInteractable) { return false; }
if (!HitItems || !targetItem.IsInteractable(user)) { return false; }
var levelResource = targetItem.GetComponent<LevelResource>();
if (levelResource != null && levelResource.Attached &&
@@ -589,7 +609,7 @@ namespace Barotrauma.Items.Components
levelResource.DeattachTimer / levelResource.DeattachDuration,
GUI.Style.Red, GUI.Style.Green, "progressbar.deattaching");
#endif
FixItemProjSpecific(user, deltaTime, targetItem);
FixItemProjSpecific(user, deltaTime, targetItem, showProgressBar: false);
return true;
}
@@ -615,7 +635,7 @@ namespace Barotrauma.Items.Components
targetItem.body.ApplyForce(dir * TargetForce, maxVelocity: 10.0f);
}
FixItemProjSpecific(user, deltaTime, targetItem);
FixItemProjSpecific(user, deltaTime, targetItem, showProgressBar: true);
return true;
}
else if (targetBody.UserData is BallastFloraBranch branch)
@@ -630,7 +650,7 @@ namespace Barotrauma.Items.Components
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);
partial void FixItemProjSpecific(Character user, float deltaTime, Item targetItem, bool showProgressBar);
private float sinTime;
private float repairTimer;
@@ -638,74 +658,71 @@ namespace Barotrauma.Items.Components
private readonly float repairTimeOut = 5;
public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
{
if (!(objective.OperateTarget is Gap leak)) { return true; }
if (leak.Submarine == null) { return true; }
if (!(objective.OperateTarget is Gap leak))
{
Reset();
return true;
}
if (leak.Submarine == null)
{
Reset();
return true;
}
if (leak != previousGap)
{
sinTime = 0;
repairTimer = 0;
Reset();
previousGap = leak;
}
Vector2 fromCharacterToLeak = leak.WorldPosition - character.WorldPosition;
float dist = fromCharacterToLeak.Length();
float reach = AIObjectiveFixLeak.CalculateReach(this, character);
//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 (dist > reach * 3)
{
if (character.AnimController.InWater)
// Too far away -> consider this done and hope the AI is smart enough to move closer
Reset();
return true;
}
character.AIController.SteeringManager.Reset();
if (!character.AnimController.InWater)
{
// 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)
{
if (character.AIController.SteeringManager is IndoorsSteeringManager indoorSteering)
((HumanoidAnimController)character.AnimController).Crouching = true;
}
}
if (dist > reach * 0.8f || dist > reach * 0.5f && character.AnimController.Limbs.Any(l => l.inWater))
{
// Steer closer
if (character.AIController.SteeringManager is IndoorsSteeringManager indoorSteering)
{
// Swimming inside the sub
if (indoorSteering.CurrentPath != null && !indoorSteering.IsPathDirty && (indoorSteering.CurrentPath.Unreachable || indoorSteering.CurrentPath.Finished))
{
// 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));
}
Vector2 dir = Vector2.Normalize(fromCharacterToLeak);
character.AIController.SteeringManager.SteeringManual(deltaTime, dir);
}
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);
// Swimming outside the sub
character.AIController.SteeringManager.SteeringSeek(character.GetRelativeSimPosition(leak));
}
}
if (dist < reach / 2)
else if (dist < reach * 0.25f)
{
// Too close -> steer away
character.AIController.SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(character.SimPosition - leak.SimPosition));
}
else if (dist < reach * 2)
if (dist <= reach)
{
// In or almost in range
// In range
character.CursorPosition = leak.WorldPosition;
if (character.Submarine != null)
{
@@ -729,51 +746,57 @@ namespace Barotrauma.Items.Components
character.AIController.SteeringManager.SteeringManual(deltaTime, moveDir);
}
}
}
if (item.RequireAimToUse)
{
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)
{
if (Submarine.PickBody(item.SimPosition, leak.SimPosition, collisionCategory: Physics.CollisionWall, allowInsideFixture: true)?.UserData is Item i)
if (item.RequireAimToUse)
{
var door = i.GetComponent<Door>();
// Hit a door, abandon so that we don't weld it shut.
return door != null && !door.IsOpen && !door.IsBroken;
character.SetInput(InputType.Aim, false, true);
sinTime += deltaTime * 5;
}
// Check that we don't hit any friendlies
if (Submarine.PickBodies(item.SimPosition, leak.SimPosition, collisionCategory: Physics.CollisionCharacter).None(hit =>
// 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)
{
if (hit.UserData is Character c)
if (Submarine.PickBody(item.SimPosition, leak.SimPosition, collisionCategory: Physics.CollisionWall, allowInsideFixture: true)?.UserData is Item i)
{
if (c == character) { return false; }
return HumanAIController.IsFriendly(character, c);
var door = i.GetComponent<Door>();
// Hit a door, abandon so that we don't weld it shut.
return door != null && !door.CanBeTraversed;
}
return false;
}))
{
character.SetInput(InputType.Shoot, false, true);
Use(deltaTime, character);
repairTimer += deltaTime;
if (repairTimer > repairTimeOut)
// 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);
repairTimer += deltaTime;
if (repairTimer > repairTimeOut)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: timed out while welding a leak in {leak.FlowTargetHull.DisplayName}.", color: Color.Yellow);
DebugConsole.NewMessage($"{character.Name}: timed out while welding a leak in {leak.FlowTargetHull.DisplayName}.", color: Color.Yellow);
#endif
return true;
Reset();
return true;
}
}
}
}
else
{
// Reset the timer so that we don't time out if the water forces push us away
repairTimer = 0;
}
bool leakFixed = (leak.Open <= 0.0f || leak.Removed) &&
(leak.ConnectedWall == null || leak.ConnectedWall.Sections.Average(s => s.damage) < 1);
if (leakFixed && leak.FlowTargetHull?.DisplayName != null)
if (leakFixed && leak.FlowTargetHull?.DisplayName != null && character.IsOnPlayerTeam)
{
if (!leak.FlowTargetHull.ConnectedGaps.Any(g => !g.IsRoomToRoom && g.Open > 0.0f))
{
@@ -786,6 +809,12 @@ namespace Barotrauma.Items.Components
}
return leakFixed;
void Reset()
{
sinTime = 0;
repairTimer = 0;
}
}
private void ApplyStatusEffectsOnTarget(Character user, float deltaTime, ActionType actionType, IEnumerable<ISerializableEntity> targets)
@@ -816,7 +845,7 @@ namespace Barotrauma.Items.Components
foreach (ISerializableEntity target in targets)
{
if (!(target is Door door)) { continue; }
if (!door.CanBeWelded || door.Item.NonInteractable) { continue; }
if (!door.CanBeWelded || !door.Item.IsInteractable(user)) { continue; }
for (int i = 0; i < effect.propertyNames.Length; i++)
{
string propertyName = effect.propertyNames[i];
@@ -1,5 +1,6 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
@@ -83,7 +84,7 @@ namespace Barotrauma.Items.Components
return;
}
if (picker == null || picker.Removed || !picker.HasSelectedItem(item))
if (picker == null || picker.Removed || !picker.HeldItems.Contains(item))
{
IsActive = false;
return;
@@ -235,6 +235,12 @@ namespace Barotrauma.Items.Components
[Serialize(0f, false, description: "How useful the item is in combat? Used by AI to decide which item it should use as a weapon. For the sake of clarity, use a value between 0 and 100 (not enforced).")]
public float CombatPriority { get; private set; }
/// <summary>
/// Which sound should be played when manual sound selection type is selected? Not [Editable] because we don't want this visible in the editor for every component.
/// </summary>
[Serialize(0, true, alwaysUseInstanceValues: true)]
public int ManuallySelectedSound { get; private set; }
public ItemComponent(Item item, XElement element)
{
this.item = item;
@@ -394,7 +400,10 @@ namespace Barotrauma.Items.Components
}
//called when isActive is true and condition > 0.0f
public virtual void Update(float deltaTime, Camera cam) { }
public virtual void Update(float deltaTime, Camera cam)
{
ApplyStatusEffects(ActionType.OnActive, deltaTime);
}
//called when isActive is true and condition == 0.0f
public virtual void UpdateBroken(float deltaTime, Camera cam)
@@ -450,22 +459,20 @@ namespace Barotrauma.Items.Components
public virtual bool Combine(Item item, Character user)
{
if (canBeCombined && this.item.Prefab == item.Prefab && item.Condition > 0.0f && this.item.Condition > 0.0f)
if (canBeCombined && this.item.Prefab == item.Prefab &&
item.Condition > 0.0f && this.item.Condition > 0.0f &&
!item.IsFullCondition && !this.item.IsFullCondition)
{
float transferAmount = 0.0f;
if (this.Item.Condition <= item.Condition)
transferAmount = Math.Min(item.Condition, this.item.MaxCondition - this.item.Condition);
else
transferAmount = -Math.Min(this.item.Condition, item.MaxCondition - item.Condition);
float transferAmount = Math.Min(item.Condition, this.item.MaxCondition - this.item.Condition);
if (transferAmount == 0.0f) { return false; }
if (MathUtils.NearlyEqual(transferAmount, 0.0f)) { return false; }
if (removeOnCombined)
{
if (item.Condition - transferAmount <= 0.0f)
{
if (item.ParentInventory != null)
{
if (item.ParentInventory.Owner is Character owner && owner.HasSelectedItem(item))
if (item.ParentInventory.Owner is Character owner && owner.HeldItems.Contains(item))
{
item.Unequip(owner);
}
@@ -481,7 +488,7 @@ namespace Barotrauma.Items.Components
{
if (this.Item.ParentInventory != null)
{
if (this.Item.ParentInventory.Owner is Character owner && owner.HasSelectedItem(this.Item))
if (this.Item.ParentInventory.Owner is Character owner && owner.HeldItems.Contains(this.Item))
{
this.Item.Unequip(owner);
}
@@ -651,16 +658,18 @@ namespace Barotrauma.Items.Components
/// <summary>
/// Only checks if any of the Picked requirements are matched (used for checking id card(s)). Much simpler and a bit different than HasRequiredItems.
/// </summary>
public bool HasAccess(Character character)
public virtual bool HasAccess(Character character)
{
if (character.Inventory == null) { return false; }
if (!item.IsInteractable(character)) { return false; }
if (requiredItems.None()) { return true; }
foreach (Item item in character.Inventory.Items)
if (character.Inventory != null)
{
if (requiredItems.Any(ri => ri.Value.Any(r => r.Type == RelatedItem.RelationType.Picked && r.MatchesItem(item))))
foreach (Item item in character.Inventory.AllItems)
{
return true;
if (requiredItems.Any(ri => ri.Value.Any(r => r.Type == RelatedItem.RelationType.Picked && r.MatchesItem(item))))
{
return true;
}
}
}
return false;
@@ -669,6 +678,7 @@ namespace Barotrauma.Items.Components
public virtual bool HasRequiredItems(Character character, bool addMessage, string msg = null)
{
if (requiredItems.None()) { return true; }
if (!character.IsPlayer && character.Params.AI != null && character.Params.AI.Infiltrate) { return true; }
if (character.Inventory == null) { return false; }
bool hasRequiredItems = false;
bool canContinue = true;
@@ -676,7 +686,7 @@ namespace Barotrauma.Items.Components
{
foreach (RelatedItem ri in requiredItems[RelatedItem.RelationType.Equipped])
{
canContinue = CheckItems(ri, character.SelectedItems);
canContinue = CheckItems(ri, character.HeldItems);
if (!canContinue) { break; }
}
}
@@ -686,7 +696,7 @@ namespace Barotrauma.Items.Components
{
foreach (RelatedItem ri in requiredItems[RelatedItem.RelationType.Picked])
{
if (!CheckItems(ri, character.Inventory.Items)) { break; }
if (!CheckItems(ri, character.Inventory.AllItems)) { break; }
}
}
}
@@ -942,33 +952,13 @@ namespace Barotrauma.Items.Components
#region AI related
protected const float AIUpdateInterval = 0.2f;
protected float aiUpdateTimer;
private int itemIndex;
private Character previousUser;
protected bool FindSuitableContainer(Character character, Func<Item, float> priority, out Item suitableContainer)
{
suitableContainer = null;
if (character.AIController is HumanAIController aiController)
{
if (previousUser != character)
{
previousUser = character;
itemIndex = 0;
}
if (character.FindItem(ref itemIndex, out Item targetContainer, ignoredItems: aiController.IgnoredItems, customPriorityFunction: priority))
{
suitableContainer = targetContainer;
return true;
}
}
return false;
}
protected AIObjectiveContainItem AIContainItems<T>(ItemContainer container, Character character, AIObjective objective, int itemCount, bool equip, bool removeEmpty, bool spawnItemIfNotFound = false) where T : ItemComponent
protected AIObjectiveContainItem AIContainItems<T>(ItemContainer container, Character character, AIObjective currentObjective, int itemCount, bool equip, bool removeEmpty, bool spawnItemIfNotFound = false, bool dropItemOnDeselected = false) where T : ItemComponent
{
AIObjectiveContainItem containObjective = null;
if (character.AIController is HumanAIController aiController)
{
containObjective = new AIObjectiveContainItem(character, container.GetContainableItemIdentifiers.ToArray(), container, objective.objectiveManager, spawnItemIfNotFound: spawnItemIfNotFound)
containObjective = new AIObjectiveContainItem(character, container.GetContainableItemIdentifiers.ToArray(), container, currentObjective.objectiveManager, spawnItemIfNotFound: spawnItemIfNotFound)
{
targetItemCount = itemCount,
Equip = equip,
@@ -986,91 +976,24 @@ namespace Barotrauma.Items.Components
return 1.0f;
}
};
containObjective.Abandoned += () =>
containObjective.Abandoned += () => aiController.IgnoredItems.Add(container.Item);
if (dropItemOnDeselected)
{
aiController.IgnoredItems.Add(container.Item);
};
objective.AddSubObjective(containObjective);
currentObjective.Deselected += () =>
{
if (containObjective == null) { return; }
if (containObjective.IsCompleted) { return; }
Item item = containObjective.ItemToContain;
if (item != null && character.CanInteractWith(item, checkLinked: false))
{
item.Drop(character);
}
};
}
currentObjective.AddSubObjective(containObjective);
}
return containObjective;
}
/// <summary>
/// Returns true when done seeking the suitable container.
/// </summary>
protected bool AIDecontainEmptyItems(Character character, AIObjective objective, bool equip, ItemContainer sourceContainer = null)
{
if (character.AIController is HumanAIController aiController)
{
ItemContainer sourceC = sourceContainer ?? (item.OwnInventory?.Owner is Item it ? it.GetComponent<ItemContainer>() : null);
var containedItems = sourceContainer != null ? sourceContainer.Inventory.Items : item.OwnInventory.Items;
foreach (Item containedItem in containedItems)
{
if (containedItem != null && containedItem.Condition <= 0.0f)
{
if (FindSuitableContainer(character,
i =>
{
if (i.IsThisOrAnyContainerIgnoredByAI()) { return 0; }
var container = i.GetComponent<ItemContainer>();
if (container == null) { return 0; }
if (container.Inventory.IsFull()) { return 0; }
// Ignore containers that are identical to the source container
if (sourceC != null && container.Item.Prefab == sourceC.Item.Prefab) { return 0; }
if (container.ShouldBeContained(containedItem, out bool isRestrictionsDefined))
{
if (isRestrictionsDefined)
{
return 4;
}
else
{
if (containedItem.Prefab.IsContainerPreferred(container, out bool isPreferencesDefined, out bool isSecondary))
{
return isPreferencesDefined ? isSecondary ? 2 : 3 : 1;
}
else
{
return isPreferencesDefined ? 0 : 1;
}
}
}
else
{
return 0;
}
}, out Item targetContainer))
{
var decontainObjective = new AIObjectiveDecontainItem(character, containedItem, objective.objectiveManager, sourceC, targetContainer?.GetComponent<ItemContainer>())
{
Equip = equip
};
decontainObjective.Abandoned += () =>
{
itemIndex = 0;
if (targetContainer != null)
{
aiController.IgnoredItems.Add(targetContainer);
}
};
decontainObjective.Completed += () =>
{
if (targetContainer == null)
{
itemIndex = 0;
}
};
objective.AddSubObjectiveInQueue(decontainObjective);
}
else
{
return false;
}
}
}
}
return true;
}
#endregion
}
}
@@ -9,11 +9,24 @@ namespace Barotrauma.Items.Components
{
partial class ItemContainer : ItemComponent, IDrawableComponent
{
class ActiveContainedItem
{
public readonly Item Item;
public readonly StatusEffect StatusEffect;
public readonly bool ExcludeBroken;
public ActiveContainedItem(Item item, StatusEffect statusEffect, bool excludeBroken)
{
Item = item;
StatusEffect = statusEffect;
ExcludeBroken = excludeBroken;
}
}
public ItemInventory Inventory;
private List<Pair<Item, StatusEffect>> itemsWithStatusEffects;
private readonly List<ActiveContainedItem> activeContainedItems = new List<ActiveContainedItem>();
private ushort[] itemIds;
private List<ushort>[] itemIds;
//how many items can be contained
private int capacity;
@@ -24,6 +37,15 @@ namespace Barotrauma.Items.Components
set { capacity = Math.Max(value, 1); }
}
//how many items can be contained
private int maxStackSize;
[Serialize(64, false, description: "How many items can be stacked in one slot. Does not increase the maximum stack size of the items themselves, e.g. a stack of bullets could have a maximum size of 8 but the number of bullets in a specific weapon could be restricted to 6.")]
public int MaxStackSize
{
get { return maxStackSize; }
set { maxStackSize = 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.")]
@@ -94,6 +116,9 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(false, false)]
public bool RemoveContainedItemsOnDeconstruct { get; set; }
public bool ShouldBeContained(string[] identifiersOrTags, out bool isRestrictionsDefined)
{
isRestrictionsDefined = containableRestrictions.Any();
@@ -138,8 +163,6 @@ namespace Barotrauma.Items.Components
}
InitProjSpecific(element);
itemsWithStatusEffects = new List<Pair<Item, StatusEffect>>();
}
partial void InitProjSpecific(XElement element);
@@ -151,23 +174,23 @@ namespace Barotrauma.Items.Components
RelatedItem ri = ContainableItems.Find(x => x.MatchesItem(containedItem));
if (ri != null)
{
itemsWithStatusEffects.RemoveAll(i => i.First == containedItem);
activeContainedItems.RemoveAll(i => i.Item == containedItem);
foreach (StatusEffect effect in ri.statusEffects)
{
itemsWithStatusEffects.Add(new Pair<Item, StatusEffect>(containedItem, effect));
activeContainedItems.Add(new ActiveContainedItem(containedItem, effect, ri.ExcludeBroken));
}
}
//no need to Update() if this item has no statuseffects and no physics body
IsActive = itemsWithStatusEffects.Count > 0 || Inventory.Items.Any(it => it?.body != null);
IsActive = activeContainedItems.Count > 0 || Inventory.AllItems.Any(it => it.body != null);
}
public void OnItemRemoved(Item containedItem)
{
itemsWithStatusEffects.RemoveAll(i => i.First == containedItem);
activeContainedItems.RemoveAll(i => i.Item == containedItem);
//deactivate if the inventory is empty
IsActive = itemsWithStatusEffects.Count > 0 || Inventory.Items.Any(it => it?.body != null);
IsActive = activeContainedItems.Count > 0 || Inventory.AllItems.Any(it => it.body != null);
}
public bool CanBeContained(Item item)
@@ -193,18 +216,17 @@ namespace Barotrauma.Items.Components
{
item.SetContainedItemPositions();
}
else if (itemsWithStatusEffects.Count == 0)
else if (activeContainedItems.Count == 0)
{
IsActive = false;
return;
}
foreach (Pair<Item, StatusEffect> itemAndEffect in itemsWithStatusEffects)
foreach (var activeContainedItem in activeContainedItems)
{
Item contained = itemAndEffect.First;
if (contained.Condition <= 0.0f) continue;
StatusEffect effect = itemAndEffect.Second;
Item contained = activeContainedItem.Item;
if (activeContainedItem.ExcludeBroken && contained.Condition <= 0.0f) { continue; }
StatusEffect effect = activeContainedItem.StatusEffect;
if (effect.HasTargetType(StatusEffect.TargetType.This))
effect.Apply(ActionType.OnContaining, deltaTime, item, item.AllPropertyObjects);
@@ -237,9 +259,8 @@ namespace Barotrauma.Items.Components
}
if (AutoInteractWithContained && character.SelectedConstruction == null)
{
foreach (Item contained in Inventory.Items)
foreach (Item contained in Inventory.AllItems)
{
if (contained == null) continue;
if (contained.TryInteract(character))
{
character.FocusedItem = contained;
@@ -261,9 +282,8 @@ namespace Barotrauma.Items.Components
}
if (AutoInteractWithContained)
{
foreach (Item contained in Inventory.Items)
foreach (Item contained in Inventory.AllItems)
{
if (contained == null) continue;
if (contained.TryInteract(picker))
{
picker.FocusedItem = contained;
@@ -274,20 +294,19 @@ namespace Barotrauma.Items.Components
IsActive = true;
return (picker != null);
return picker != null;
}
public override bool Combine(Item item, Character user)
{
if (!AllowDragAndDrop && user != null) { return false; }
if (!ContainableItems.Any(x => x.MatchesItem(item))) { return false; }
if (!ContainableItems.Any(it => it.MatchesItem(item))) { return false; }
if (user != null && !user.CanAccessInventory(Inventory)) { return false; }
if (Inventory.TryPutItem(item, null))
if (Inventory.TryPutItem(item, user))
{
IsActive = true;
if (hideItems && item.body != null) item.body.Enabled = false;
if (hideItems && item.body != null) { item.body.Enabled = false; }
return true;
}
@@ -315,9 +334,8 @@ namespace Barotrauma.Items.Components
currentRotation += item.body.Rotation;
}
foreach (Item contained in Inventory.Items)
foreach (Item contained in Inventory.AllItems)
{
if (contained == null) { continue; }
if (contained.body != null)
{
try
@@ -359,13 +377,22 @@ namespace Barotrauma.Items.Components
public override void OnMapLoaded()
{
if (itemIds != null)
{
if (itemIds != null)
{
for (ushort i = 0; i < itemIds.Length; i++)
{
if (!(Entity.FindEntityByID(itemIds[i]) is Item item)) { continue; }
if (i >= Inventory.Capacity) { continue; }
Inventory.TryPutItem(item, i, false, false, null, false);
if (i >= Inventory.Capacity)
{
//legacy support: before item stacking was implemented, revolver for example had a separate slot for each bullet
//now there's just one, try to put the extra items where they fit (= stack them)
Inventory.TryPutItem(item, user: null, createNetworkEvent: false);
continue;
}
foreach (ushort id in itemIds[i])
{
if (!(Entity.FindEntityByID(id) is Item item)) { continue; }
Inventory.TryPutItem(item, i, false, false, null, false);
}
}
itemIds = null;
}
@@ -377,12 +404,9 @@ namespace Barotrauma.Items.Components
if (SpawnWithId.Length > 0)
{
ItemPrefab prefab = ItemPrefab.Prefabs.Find(m => m.Identifier == SpawnWithId);
if (prefab != null)
if (prefab != null && Inventory != null && Inventory.CanBePut(prefab))
{
if (Inventory != null && Inventory.Items.Any(it => it == null))
{
Entity.Spawner?.AddToSpawnQueue(prefab, Inventory);
}
Entity.Spawner?.AddToSpawnQueue(prefab, Inventory, spawnIfInventoryFull: false);
}
}
}
@@ -407,13 +431,8 @@ namespace Barotrauma.Items.Components
return;
}
#endif
foreach (Item item in Inventory.Items)
{
if (item == null) continue;
item.Drop(null);
}
}
Inventory.AllItemsMod.ForEach(it => it.Drop(null));
}
public override void Load(XElement componentElement, bool usePrefabValues, IdRemap idRemap)
{
@@ -421,26 +440,28 @@ namespace Barotrauma.Items.Components
string containedString = componentElement.GetAttributeString("contained", "");
string[] itemIdStrings = containedString.Split(',');
itemIds = new ushort[itemIdStrings.Length];
itemIds = new List<ushort>[itemIdStrings.Length];
for (int i = 0; i < itemIdStrings.Length; i++)
{
if (!int.TryParse(itemIdStrings[i], out int id)) { continue; }
itemIds[i] = idRemap.GetOffsetId(id);
itemIds[i] ??= new List<ushort>();
foreach (string idStr in itemIdStrings[i].Split(';'))
{
if (!int.TryParse(idStr, out int id)) { continue; }
itemIds[i].Add(idRemap.GetOffsetId(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++)
string[] itemIdStrings = new string[Inventory.Capacity];
for (int i = 0; i < Inventory.Capacity; i++)
{
itemIdStrings[i] = (Inventory.Items[i] == null) ? "0" : Inventory.Items[i].ID.ToString();
var items = Inventory.GetItemsAt(i);
itemIdStrings[i] = string.Join(';', items.Select(it => it.ID.ToString()));
}
componentElement.Add(new XAttribute("contained", string.Join(",", itemIdStrings)));
componentElement.Add(new XAttribute("contained", string.Join(',', itemIdStrings)));
return componentElement;
}
}
@@ -90,6 +90,13 @@ namespace Barotrauma.Items.Components
[Serialize(UseEnvironment.Both, false, description: "Can the item be selected in air, underwater or both.")]
public UseEnvironment UsableIn { get; set; }
[Serialize(false, false, description: "Should the character using the item be drawn behind the item.")]
public bool DrawUserBehind
{
get;
set;
}
public bool ControlCharacterPose
{
get { return limbPositions.Count > 0; }
@@ -236,12 +243,12 @@ namespace Barotrauma.Items.Components
case LimbType.RightHand:
case LimbType.RightForearm:
case LimbType.RightArm:
if (user.SelectedItems[0] != null) { continue; }
if (user.Inventory.GetItemInLimbSlot(InvSlotType.RightHand) != null) { continue; }
break;
case LimbType.LeftHand:
case LimbType.LeftForearm:
case LimbType.LeftArm:
if ( user.SelectedItems[1] != null) { continue; }
if (user.Inventory.GetItemInLimbSlot(InvSlotType.LeftHand) != null) { continue; }
break;
}
}
@@ -388,6 +395,12 @@ namespace Barotrauma.Items.Components
limb.PullJointEnabled = false;
}
//disable flipping for 0.5 seconds, because flipping the character when it's in a weird pose (e.g. lying in bed) can mess up the ragdoll
if (character.AnimController is HumanoidAnimController humanoidAnim)
{
humanoidAnim.LockFlippingUntil = (float)Timing.TotalTime + 0.5f;
}
if (character.SelectedConstruction == this.item) { character.SelectedConstruction = null; }
character.AnimController.Anim = AnimController.Animation.None;
@@ -470,6 +483,12 @@ namespace Barotrauma.Items.Components
}
}
public override bool HasAccess(Character character)
{
if (!item.IsInteractable(character)) { return false; }
return base.HasAccess(character);
}
partial void HideHUDs(bool value);
}
}
@@ -1,5 +1,7 @@
using Barotrauma.Networking;
using Barotrauma.Extensions;
using Barotrauma.Networking;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
@@ -59,7 +61,7 @@ namespace Barotrauma.Items.Components
{
MoveInputQueue();
if (inputContainer == null || inputContainer.Inventory.Items.All(i => i == null))
if (inputContainer == null || inputContainer.Inventory.IsEmpty())
{
SetActive(false);
return;
@@ -79,7 +81,7 @@ namespace Barotrauma.Items.Components
if (powerConsumption <= 0.0f) { Voltage = 1.0f; }
progressTimer += deltaTime * Math.Min(Voltage, 1.0f);
var targetItem = inputContainer.Inventory.Items.LastOrDefault(i => i != null);
var targetItem = inputContainer.Inventory.LastOrDefault();
if (targetItem == null) { return; }
float deconstructTime = targetItem.Prefab.DeconstructItems.Any() ? targetItem.Prefab.DeconstructTime / DeconstructionSpeed : 1.0f;
@@ -87,78 +89,107 @@ namespace Barotrauma.Items.Components
progressState = Math.Min(progressTimer / deconstructTime, 1.0f);
if (progressTimer > deconstructTime)
{
int emptySlots = outputContainer.Inventory.Items.Where(i => i == null).Count();
// In multiplayer, the server handles the deconstruction into new items
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
foreach (DeconstructItem deconstructProduct in targetItem.Prefab.DeconstructItems)
if (targetItem.Prefab.RandomDeconstructionOutput)
{
int amount = targetItem.Prefab.RandomDeconstructionOutputAmount;
List<int> deconstructItemIndexes = new List<int>();
for (int i = 0; i < targetItem.Prefab.DeconstructItems.Count; i++)
{
deconstructItemIndexes.Add(i);
}
List<float> commonness = targetItem.Prefab.DeconstructItems.Select(i => i.Commonness).ToList();
List<DeconstructItem> products = new List<DeconstructItem>();
for (int i = 0; i < amount; i++)
{
if (deconstructItemIndexes.Count < 1) { break; }
var itemIndex = ToolBox.SelectWeightedRandom(deconstructItemIndexes, commonness, Rand.RandSync.Unsynced);
products.Add(targetItem.Prefab.DeconstructItems[itemIndex]);
var removeIndex = deconstructItemIndexes.IndexOf(itemIndex);
deconstructItemIndexes.RemoveAt(removeIndex);
commonness.RemoveAt(removeIndex);
}
foreach (DeconstructItem deconstructProduct in products)
{
CreateDeconstructProduct(deconstructProduct);
}
}
else
{
foreach (DeconstructItem deconstructProduct in targetItem.Prefab.DeconstructItems)
{
CreateDeconstructProduct(deconstructProduct);
}
}
void CreateDeconstructProduct(DeconstructItem deconstructProduct)
{
float percentageHealth = targetItem.Condition / targetItem.Prefab.Health;
if (percentageHealth <= deconstructProduct.MinCondition || percentageHealth > deconstructProduct.MaxCondition) continue;
if (percentageHealth <= deconstructProduct.MinCondition || percentageHealth > deconstructProduct.MaxCondition) { return; }
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;
return;
}
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, outputContainer.Inventory, condition, onSpawned: (Item spawnedItem) =>
{
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.AllowDeconstruct)
{
//drop all items that are inside the deconstructed item
foreach (ItemContainer ic in targetItem.GetComponents<ItemContainer>())
for (int i = 0; i < outputContainer.Capacity; i++)
{
if (ic?.Inventory?.Items == null) { continue; }
foreach (Item containedItem in ic.Inventory.Items)
var containedItem = outputContainer.Inventory.GetItemAt(i);
if (containedItem?.Combine(spawnedItem, null) ?? false)
{
containedItem?.Drop(dropper: null, createNetworkEvent: true);
break;
}
}
inputContainer.Inventory.RemoveItem(targetItem);
Entity.Spawner.AddToRemoveQueue(targetItem);
MoveInputQueue();
PutItemsToLinkedContainer();
});
}
if (targetItem.Prefab.AllowDeconstruct)
{
//drop all items that are inside the deconstructed item
foreach (ItemContainer ic in targetItem.GetComponents<ItemContainer>())
{
if (ic?.Inventory == null || ic.RemoveContainedItemsOnDeconstruct) { continue; }
ic.Inventory.AllItemsMod.ForEach(containedItem => outputContainer.Inventory.TryPutItem(containedItem, user: null));
}
inputContainer.Inventory.RemoveItem(targetItem);
Entity.Spawner.AddToRemoveQueue(targetItem);
MoveInputQueue();
PutItemsToLinkedContainer();
}
else
{
if (!outputContainer.Inventory.CanBePut(targetItem))
{
targetItem.Drop(dropper: null);
}
else
{
if (outputContainer.Inventory.Items.All(i => i != null))
{
targetItem.Drop(dropper: null);
}
else
{
outputContainer.Inventory.TryPutItem(targetItem, user: null, createNetworkEvent: true);
}
outputContainer.Inventory.TryPutItem(targetItem, user: null, createNetworkEvent: true);
}
#if SERVER
item.CreateServerEvent(this);
#endif
progressTimer = 0.0f;
progressState = 0.0f;
}
#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;
if (outputContainer.Inventory.IsEmpty()) { return; }
foreach (MapEntity linkedTo in item.linkedTo)
{
@@ -168,13 +199,7 @@ namespace Barotrauma.Items.Components
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);
}
outputContainer.Inventory.AllItemsMod.ForEach(containedItem => itemContainer.Inventory.TryPutItem(containedItem, user: null, createNetworkEvent: true));
}
}
}
@@ -186,9 +211,12 @@ namespace Barotrauma.Items.Components
{
for (int i = inputContainer.Inventory.Capacity - 2; i >= 0; i--)
{
if (inputContainer.Inventory.Items[i] != null && inputContainer.Inventory.Items[i + 1] == null)
while (inputContainer.Inventory.GetItemAt(i) is Item item1 && inputContainer.Inventory.CanBePut(item1, i + 1))
{
inputContainer.Inventory.TryPutItem(inputContainer.Inventory.Items[i], i + 1, allowSwapping: false, allowCombine: false, user: null, createNetworkEvent: true);
if (!inputContainer.Inventory.TryPutItem(item1, i + 1, allowSwapping: false, allowCombine: false, user: null, createNetworkEvent: true))
{
break;
}
}
}
}
@@ -197,18 +225,16 @@ namespace Barotrauma.Items.Components
{
PutItemsToLinkedContainer();
if (inputContainer.Inventory.Items.All(i => i == null)) { active = false; }
if (inputContainer.Inventory.IsEmpty()) { active = false; }
IsActive = active;
currPowerConsumption = IsActive ? powerConsumption : 0.0f;
#if SERVER
if (user != null)
{
GameServer.Log(GameServer.CharacterLogName(user) + (IsActive ? " activated " : " deactivated ") + item.Name, ServerLog.MessageType.ItemInteraction);
}
#endif
if (!IsActive)
{
progressTimer = 0.0f;
@@ -27,7 +27,7 @@ namespace Barotrauma.Items.Components
public Character User;
[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.")]
Serialize(500.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; }
@@ -46,6 +46,13 @@ namespace Barotrauma.Items.Components
set;
}
[Editable, Serialize(false, true)]
public bool DisablePropellerDamage
{
get;
set;
}
public float Force
{
get { return force;}
@@ -117,6 +124,7 @@ namespace Barotrauma.Items.Components
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);
if (item.Submarine.FlippedX) { currForce *= -1; }
item.Submarine.ApplyForce(currForce);
UpdatePropellerDamage(deltaTime);
float maxChangeSpeed = 0.5f;
@@ -130,7 +138,7 @@ namespace Barotrauma.Items.Components
if (particleTimer <= 0.0f)
{
Vector2 particleVel = -currForce.ClampLength(5000.0f) / 5.0f;
GameMain.ParticleManager.CreateParticle("bubbles", item.WorldPosition + PropellerPos,
GameMain.ParticleManager.CreateParticle("bubbles", item.WorldPosition + PropellerPos * item.Scale,
particleVel * Rand.Range(0.9f, 1.1f),
0.0f, item.CurrentHull);
particleTimer = 1.0f / particlesPerSec;
@@ -154,19 +162,22 @@ namespace Barotrauma.Items.Components
private void UpdatePropellerDamage(float deltaTime)
{
if (DisablePropellerDamage) { return; }
damageTimer += deltaTime;
if (damageTimer < 0.5f) return;
if (damageTimer < 0.5f) { return; }
damageTimer = 0.1f;
if (propellerDamage == null) return;
Vector2 propellerWorldPos = item.WorldPosition + PropellerPos;
if (propellerDamage == null) { return; }
float scaledDamageRange = propellerDamage.DamageRange * item.Scale;
Vector2 propellerWorldPos = item.WorldPosition + PropellerPos * item.Scale;
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;
if (character.Submarine != null || !character.Enabled || character.Removed) { continue; }
float distSqr = Vector2.DistanceSquared(character.WorldPosition, propellerWorldPos);
if (distSqr > scaledDamageRange * scaledDamageRange) { continue; }
character.LastDamageSource = item;
propellerDamage.DoDamage(null, character, propellerWorldPos, 1.0f, true);
}
@@ -4,6 +4,7 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Security.Cryptography;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
@@ -96,6 +97,12 @@ namespace Barotrauma.Items.Components
fabricationRecipes.Add(recipe);
}
}
fabricationRecipes.Sort((r1, r2) =>
{
int hash1 = (int)r1.TargetItem.UIntIdentifier;
int hash2 = (int)r2.TargetItem.UIntIdentifier;
return hash1 - hash2;
});
state = FabricatorState.Stopped;
@@ -142,7 +149,7 @@ namespace Barotrauma.Items.Components
public override bool Pick(Character picker)
{
return (picker != null);
return picker != null;
}
public void RemoveFabricationRecipes(List<string> allowedIdentifiers)
@@ -161,10 +168,10 @@ namespace Barotrauma.Items.Components
partial void CreateRecipes();
private void StartFabricating(FabricationRecipe selectedItem, Character user)
private void StartFabricating(FabricationRecipe selectedItem, Character user, bool addToServerLog = true)
{
if (selectedItem == null) { return; }
if (!outputContainer.Inventory.IsEmpty()) { return; }
if (!outputContainer.Inventory.CanBePut(selectedItem.TargetItem)) { return; }
#if CLIENT
itemList.Enabled = false;
@@ -190,7 +197,7 @@ namespace Barotrauma.Items.Components
State = FabricatorState.Active;
}
#if SERVER
if (user != null)
if (user != null && addToServerLog)
{
GameServer.Log(GameServer.CharacterLogName(user) + " started fabricating " + selectedItem.DisplayName + " in " + item.Name, ServerLog.MessageType.ItemInteraction);
}
@@ -298,20 +305,24 @@ namespace Barotrauma.Items.Components
}
Character tempUser = user;
if (outputContainer.Inventory.Items.All(i => i != null))
int amountFittingContainer = outputContainer.Inventory.HowManyCanBePut(fabricatedItem.TargetItem);
for (int i = 0; i < fabricatedItem.Amount; i++)
{
Entity.Spawner.AddToSpawnQueue(fabricatedItem.TargetItem, item.Position, item.Submarine, fabricatedItem.TargetItem.Health * fabricatedItem.OutCondition,
onSpawned: (Item spawnedItem) => { onItemSpawned(spawnedItem, tempUser); });
}
else
{
Entity.Spawner.AddToSpawnQueue(fabricatedItem.TargetItem, outputContainer.Inventory, fabricatedItem.TargetItem.Health * fabricatedItem.OutCondition,
onSpawned: (Item spawnedItem) => { onItemSpawned(spawnedItem, tempUser); });
if (i < amountFittingContainer)
{
Entity.Spawner.AddToSpawnQueue(fabricatedItem.TargetItem, outputContainer.Inventory, fabricatedItem.TargetItem.Health * fabricatedItem.OutCondition,
onSpawned: (Item spawnedItem) => { onItemSpawned(spawnedItem, tempUser); });
}
else
{
Entity.Spawner.AddToSpawnQueue(fabricatedItem.TargetItem, item.Position, item.Submarine, fabricatedItem.TargetItem.Health * fabricatedItem.OutCondition,
onSpawned: (Item spawnedItem) => { onItemSpawned(spawnedItem, tempUser); });
}
}
static void onItemSpawned(Item spawnedItem, Character user)
{
if (user != null && user.TeamID != Character.TeamType.None)
if (user != null && user.TeamID != CharacterTeamType.None)
{
foreach (WifiComponent wifiComponent in spawnedItem.GetComponents<WifiComponent>())
{
@@ -328,10 +339,23 @@ namespace Barotrauma.Items.Components
user.Info.IncreaseSkillLevel(
skill.Identifier,
skill.Level * SkillSettings.Current.SkillIncreasePerFabricatorRequiredSkill / Math.Max(userSkill, 1.0f),
user.WorldPosition + Vector2.UnitY * 150.0f);
user.Position + Vector2.UnitY * 150.0f);
}
}
//disabled "continuous fabrication" for now
//before we enable it, there should be some UI controls for fabricating a specific number of items
/*var prevFabricatedItem = fabricatedItem;
var prevUser = user;
CancelFabricating();
if (CanBeFabricated(prevFabricatedItem))
{
//keep fabricating if we can fabricate more
StartFabricating(prevFabricatedItem, prevUser, addToServerLog: false);
}*/
CancelFabricating();
}
}
@@ -375,7 +399,7 @@ namespace Barotrauma.Items.Components
float skillSum = (from t in skills let characterLevel = character.GetSkillLevel(t.Identifier) select (characterLevel - (t.Level * SkillRequirementMultiplier))).Sum();
float average = skillSum / skills.Count;
return ((average + 100.0f) / 2.0f) / 100.0f;
return (average + 100.0f) / 2.0f / 100.0f;
}
public override float GetSkillMultiplier()
@@ -390,7 +414,7 @@ namespace Barotrauma.Items.Components
private List<Item> GetAvailableIngredients()
{
List<Item> availableIngredients = new List<Item>();
availableIngredients.AddRange(inputContainer.Inventory.Items.Where(it => it != null));
availableIngredients.AddRange(inputContainer.Inventory.AllItems);
foreach (MapEntity linkedTo in item.linkedTo)
{
if (linkedTo is Item linkedItem)
@@ -404,18 +428,18 @@ namespace Barotrauma.Items.Components
itemContainer = deconstructor.OutputContainer;
}
availableIngredients.AddRange(itemContainer.Inventory.Items.Where(it => it != null));
availableIngredients.AddRange(itemContainer.Inventory.AllItems);
}
}
#if CLIENT
if (Character.Controlled?.Inventory != null)
{
availableIngredients.AddRange(Character.Controlled.Inventory.Items.Distinct().Where(it => it != null));
availableIngredients.AddRange(Character.Controlled.Inventory.AllItems);
}
#else
if (user?.Inventory != null)
{
availableIngredients.AddRange(user.Inventory.Items.Distinct().Where(it => it != null));
availableIngredients.AddRange(user.Inventory.AllItems);
}
#endif
@@ -450,9 +474,9 @@ namespace Barotrauma.Items.Components
}
else //in another inventory, we need to move the item
{
if (inputContainer.Inventory.Items.All(it => it != null))
if (!inputContainer.Inventory.CanBePut(matchingItem))
{
var unneededItem = inputContainer.Inventory.Items.FirstOrDefault(it => !usedItems.Contains(it));
var unneededItem = inputContainer.Inventory.AllItems.FirstOrDefault(it => !usedItems.Contains(it));
unneededItem?.Drop(null, createNetworkEvent: !isClient);
}
inputContainer.Inventory.TryPutItem(matchingItem, user: null, createNetworkEvent: !isClient);
@@ -31,20 +31,6 @@ namespace Barotrauma.Items.Components
private float pumpSpeedLockTimer, isActiveLockTimer;
private bool infected;
[Serialize(false, true, description: "Whether or not the pump is infected with ballast flora spores.")]
public bool Infected
{
get => infected;
set
{
infected = value;
}
}
public string InfectIdentifier;
[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
{
@@ -116,35 +102,38 @@ namespace Barotrauma.Items.Components
if (item.CurrentHull == null) { return; }
float powerFactor = Math.Min(currPowerConsumption <= 0.0f ? 1.0f : Voltage, 1.0f);
float powerFactor = Math.Min(currPowerConsumption <= 0.0f || MinVoltage <= 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);
if (currFlow < 0 && Infected)
{
InfectBallast(InfectIdentifier);
}
Infected = false;
item.CurrentHull.WaterVolume += currFlow;
if (item.CurrentHull.WaterVolume > item.CurrentHull.Volume) { item.CurrentHull.Pressure += 0.5f; }
}
public void InfectBallast(string identifier)
public void InfectBallast(string identifier, bool allowMultiplePerShip = false)
{
Hull hull = item.CurrentHull;
if (hull == null) { return; }
// if the ship is already infected then do nothing
if (Hull.hullList.Where(h => h.Submarine == hull.Submarine).Any(h => h.BallastFlora != null)) { return; }
if (!allowMultiplePerShip)
{
// if the ship is already infected then do nothing
if (Hull.hullList.Where(h => h.Submarine == hull.Submarine).Any(h => h.BallastFlora != null)) { return; }
}
if (hull.BallastFlora != null) { return; }
var ballastFloraPrefab = BallastFloraPrefab.Find(identifier);
if (ballastFloraPrefab == null)
{
DebugConsole.ThrowError($"Failed to infect a ballast pump (could not find a ballast flora prefab with the identifier \"{identifier}\").\n" + Environment.StackTrace);
return;
}
Vector2 offset = item.WorldPosition - hull.WorldPosition;
hull.BallastFlora = new BallastFloraBehavior(hull, BallastFloraPrefab.Find(identifier), offset, firstGrowth: true);
hull.BallastFlora = new BallastFloraBehavior(hull, ballastFloraPrefab, offset, firstGrowth: true);
#if SERVER
hull.BallastFlora.SendNetworkMessage(hull.BallastFlora, BallastFloraBehavior.NetworkHeader.Spawn);
@@ -180,7 +169,7 @@ namespace Barotrauma.Items.Components
{
if (float.TryParse(signal, NumberStyles.Any, CultureInfo.InvariantCulture, out float tempTarget))
{
TargetLevel = MathHelper.Clamp(tempTarget + 50.0f, 0.0f, 100.0f);
TargetLevel = MathUtils.InverseLerp(-100.0f, 100.0f, tempTarget) * 100.0f;
pumpSpeedLockTimer = 0.1f;
}
}
@@ -137,7 +137,7 @@ namespace Barotrauma.Items.Components
}
}
[Serialize(0.2f, true, description: "How fast the condition of the contained fuel rods deteriorates per second."), Editable(0.0f, 1000.0f)]
[Serialize(0.2f, true, description: "How fast the condition of the contained fuel rods deteriorates per second."), Editable(0.0f, 1000.0f, decimals: 3)]
public float FuelConsumptionRate
{
get { return fuelConsumptionRate; }
@@ -216,7 +216,7 @@ namespace Barotrauma.Items.Components
if (LastAIUser.SelectedConstruction != item && LastAIUser.CanInteractWith(item))
{
AutoTemp = true;
unsentChanges = true;
if (GameMain.NetworkMember?.IsServer ?? false) { unsentChanges = true; }
LastAIUser = null;
}
}
@@ -313,12 +313,11 @@ namespace Barotrauma.Items.Components
if (fissionRate > 0.0f)
{
var containedItems = item.OwnInventory?.Items;
var containedItems = item.OwnInventory?.AllItems;
if (containedItems != null)
{
foreach (Item item in containedItems)
{
if (item == null) { continue; }
if (!item.HasTag("reactorfuel")) { continue; }
item.Condition -= fissionRate / 100.0f * fuelConsumptionRate * deltaTime;
}
@@ -399,6 +398,8 @@ namespace Barotrauma.Items.Components
//fission rate is clamped to the amount of available fuel
float maxFissionRate = Math.Min(prevAvailableFuel, 100.0f);
if (maxFissionRate >= 100.0f) { return false; }
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
@@ -412,8 +413,8 @@ namespace Barotrauma.Items.Components
private bool TooMuchFuel()
{
var containedItems = item.OwnInventory?.Items;
if (containedItems != null && containedItems.Count(i => i != null) <= 1) { return false; }
var containedItems = item.OwnInventory?.AllItems;
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);
@@ -530,12 +531,11 @@ namespace Barotrauma.Items.Components
fireTimer = 0.0f;
meltDownTimer = 0.0f;
var containedItems = item.OwnInventory?.Items;
var containedItems = item.OwnInventory?.AllItems;
if (containedItems != null)
{
foreach (Item containedItem in containedItems)
{
if (containedItem == null) { continue; }
containedItem.Condition = 0.0f;
}
}
@@ -557,58 +557,68 @@ namespace Barotrauma.Items.Components
public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return false; }
bool shutDown = objective.Option.Equals("shutdown", StringComparison.OrdinalIgnoreCase);
IsActive = true;
float degreeOfSuccess = DegreeOfSuccess(character);
float refuelLimit = 0.3f;
//characters with insufficient skill levels don't refuel the reactor
if (degreeOfSuccess > refuelLimit)
if (!shutDown)
{
if (objective.SubObjectives.None())
float degreeOfSuccess = DegreeOfSuccess(character);
float refuelLimit = 0.3f;
//characters with insufficient skill levels don't refuel the reactor
if (degreeOfSuccess > refuelLimit)
{
if (!AIDecontainEmptyItems(character, objective, equip: false))
if (aiUpdateTimer > 0.0f)
{
aiUpdateTimer -= deltaTime;
return false;
}
}
aiUpdateTimer = AIUpdateInterval;
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())
// 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))
{
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, spawnItemIfNotFound: character.TeamID == Character.TeamType.FriendlyNPC);
character.Speak(TextManager.Get("DialogReactorFuel"), null, 0.0f, "reactorfuel", 30.0f);
}
return false;
}
else if (TooMuchFuel())
{
var container = item.GetComponent<ItemContainer>();
var containedItems = item.OwnInventory?.Items;
if (containedItems != null)
{
foreach (Item item in containedItems)
bool outOfFuel = false;
var container = item.GetComponent<ItemContainer>();
if (objective.SubObjectives.None())
{
if (item != null && container.ContainableItems.Any(ri => ri.MatchesItem(item)))
int itemCount = item.ContainedItems.Count(i => i != null && container.ContainableItems.Any(ri => ri.MatchesItem(i))) + 1;
var containObjective = AIContainItems<Reactor>(container, character, objective, itemCount, equip: false, removeEmpty: true, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC, dropItemOnDeselected: true);
containObjective.Completed += ReportFuelRodCount;
containObjective.Abandoned += ReportFuelRodCount;
character.Speak(TextManager.Get("DialogReactorFuel"), null, 0.0f, "reactorfuel", 30.0f);
void ReportFuelRodCount()
{
if (!character.Inventory.TryPutItem(item, character, allowedSlots: item.AllowedSlots))
if (!character.IsOnPlayerTeam) { return; }
int remainingFuelRods = Submarine.MainSub.GetItems(false).Count(i => i.HasTag("reactorfuel") && i.Condition > 1);
if (remainingFuelRods == 0)
{
character.Speak(TextManager.Get("DialogOutOfFuelRods"), null, 0.0f, "outoffuelrods", 30.0f);
outOfFuel = true;
}
else if (remainingFuelRods < 3)
{
character.Speak(TextManager.Get("DialogLowOnFuelRods"), null, 0.0f, "lowonfuelrods", 30.0f);
}
}
}
return outOfFuel;
}
else if (TooMuchFuel())
{
if (item.OwnInventory?.AllItems != null)
{
var container = item.GetComponent<ItemContainer>();
foreach (Item item in item.OwnInventory.AllItemsMod)
{
if (container.ContainableItems.Any(ri => ri.MatchesItem(item)))
{
item.Drop(character);
break;
}
break;
}
}
}
@@ -619,13 +629,13 @@ namespace Barotrauma.Items.Components
{
if (lastUser != null && lastUser != character && lastUser != LastAIUser)
{
if (lastUser.SelectedConstruction == item)
if (lastUser.SelectedConstruction == item && character.IsOnPlayerTeam)
{
character.Speak(TextManager.Get("DialogReactorTaken"), null, 0.0f, "reactortaken", 10.0f);
}
}
}
else if (LastUserWasPlayer)
else if (LastUserWasPlayer && lastUser != null && lastUser.TeamID == character.TeamID)
{
return true;
}
@@ -637,48 +647,45 @@ namespace Barotrauma.Items.Components
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;
unsentChanges = true;
return true;
}
if (autoTemp != prevAutoTemp ||
prevPowerOn != _powerOn ||
Math.Abs(prevFissionRate - targetFissionRate) > 1.0f ||
Math.Abs(prevTurbineOutput - targetTurbineOutput) > 1.0f)
if (shutDown)
{
PowerOn = false;
AutoTemp = false;
targetFissionRate = 0.0f;
targetTurbineOutput = 0.0f;
unsentChanges = true;
return true;
}
else
{
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
if (autoTemp != prevAutoTemp ||
prevPowerOn != _powerOn ||
Math.Abs(prevFissionRate - targetFissionRate) > 1.0f ||
Math.Abs(prevTurbineOutput - targetTurbineOutput) > 1.0f)
{
unsentChanges = true;
}
aiUpdateTimer = AIUpdateInterval;
return false;
}
aiUpdateTimer = AIUpdateInterval;
return false;
}
public override void OnMapLoaded()
@@ -697,14 +704,14 @@ namespace Barotrauma.Items.Components
AutoTemp = false;
targetFissionRate = 0.0f;
targetTurbineOutput = 0.0f;
unsentChanges = true;
if (GameMain.NetworkMember?.IsServer ?? false) { unsentChanges = true; }
}
break;
case "set_fissionrate":
if (PowerOn && float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float newFissionRate))
{
targetFissionRate = newFissionRate;
unsentChanges = true;
if (GameMain.NetworkMember?.IsServer ?? false) { unsentChanges = true; }
#if CLIENT
FissionRateScrollBar.BarScroll = targetFissionRate / 100.0f;
#endif
@@ -714,7 +721,7 @@ namespace Barotrauma.Items.Components
if (PowerOn && float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float newTurbineOutput))
{
targetTurbineOutput = newTurbineOutput;
unsentChanges = true;
if (GameMain.NetworkMember?.IsServer ?? false) { unsentChanges = true; }
#if CLIENT
TurbineOutputScrollBar.BarScroll = targetTurbineOutput / 100.0f;
#endif
@@ -104,7 +104,8 @@ namespace Barotrauma.Items.Components
set;
}
[Editable, Serialize(false, false, description: "Does the sonar have mineral scanning mode?")]
[Editable, Serialize(false, false, description: "Does the sonar have mineral scanning mode. " +
"Only available in-game when the Item has no Steering component.")]
public bool HasMineralScanner { get; set; }
public float Zoom
@@ -251,9 +252,10 @@ namespace Barotrauma.Items.Components
}
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;
if (c.IsDead || c.Removed || !c.Enabled) { continue; }
if (c.AnimController.CurrentHull != null || c.Params.HideInSonar) { 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))
@@ -276,9 +278,12 @@ namespace Barotrauma.Items.Components
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);
if (character.IsOnPlayerTeam)
{
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++)
@@ -22,7 +22,6 @@ namespace Barotrauma.Items.Components
private const float AutoPilotMaxSpeed = 0.5f;
private const float AIPilotMaxSpeed = 1.0f;
private Vector2 currVelocity;
private Vector2 targetVelocity;
private Vector2 steeringInput;
@@ -52,7 +51,13 @@ namespace Barotrauma.Items.Components
private Sonar sonar;
private Submarine controlledSub;
private bool showIceSpireWarning;
private List<Submarine> connectedSubs = new List<Submarine>();
private const float ConnectedSubUpdateInterval = 1.0f;
float connectedSubUpdateTimer;
public bool AutoPilot
{
get { return autoPilot; }
@@ -67,7 +72,10 @@ namespace Barotrauma.Items.Components
{
if (pathFinder == null)
{
pathFinder = new PathFinder(WayPoint.WayPointList, false);
pathFinder = new PathFinder(WayPoint.WayPointList, false)
{
GetNodePenalty = GetNodePenalty
};
}
MaintainPos = true;
if (posToMaintain == null)
@@ -87,7 +95,7 @@ namespace Barotrauma.Items.Components
}
}
[Editable(0.0f, 1.0f, decimals: 3),
[Editable(0.0f, 1.0f, decimals: 4),
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
@@ -299,6 +307,7 @@ namespace Barotrauma.Items.Components
}
else
{
showIceSpireWarning = false;
if (user != null && user.Info != null &&
user.SelectedConstruction == item &&
controlledSub != null && controlledSub.Velocity.LengthSquared() > 0.01f)
@@ -323,12 +332,13 @@ namespace Barotrauma.Items.Components
}
}
}
item.SendSignal(0, targetVelocity.X.ToString(CultureInfo.InvariantCulture), "velocity_x_out", user);
float targetLevel = -targetVelocity.Y;
float targetLevel = targetVelocity.X;
if (controlledSub != null && controlledSub.FlippedX) { targetLevel *= -1; }
item.SendSignal(0, targetLevel.ToString(CultureInfo.InvariantCulture), "velocity_x_out", user);
targetLevel = -targetVelocity.Y;
targetLevel += (neutralBallastLevel - 0.5f) * 100.0f;
item.SendSignal(0, targetLevel.ToString(CultureInfo.InvariantCulture), "velocity_y_out", user);
}
@@ -342,7 +352,7 @@ namespace Barotrauma.Items.Components
user.Info.IncreaseSkillLevel(
"helm",
SkillSettings.Current.SkillIncreasePerSecondWhenSteering / userSkill * deltaTime,
user.WorldPosition + Vector2.UnitY * 150.0f);
user.Position + Vector2.UnitY * 150.0f);
}
private void UpdateAutoPilot(float deltaTime)
@@ -351,7 +361,8 @@ namespace Barotrauma.Items.Components
if (posToMaintain != null)
{
Vector2 steeringVel = GetSteeringVelocity((Vector2)posToMaintain, 10.0f);
TargetVelocity = Vector2.Lerp(TargetVelocity, steeringVel, AutoPilotSteeringLerp);
TargetVelocity = Vector2.Lerp(TargetVelocity, steeringVel, AutoPilotSteeringLerp);
showIceSpireWarning = false;
return;
}
@@ -365,9 +376,21 @@ namespace Barotrauma.Items.Components
autopilotRecalculatePathTimer = RecalculatePathInterval;
}
if (steeringPath == null) { return; }
if (steeringPath == null)
{
showIceSpireWarning = false;
return;
}
steeringPath.CheckProgress(ConvertUnits.ToSimUnits(controlledSub.WorldPosition), 10.0f);
connectedSubUpdateTimer -= deltaTime;
if (connectedSubUpdateTimer <= 0.0f)
{
connectedSubs.Clear();
connectedSubs = controlledSub?.GetConnectedSubs();
connectedSubUpdateTimer = ConnectedSubUpdateInterval;
}
if (autopilotRayCastTimer <= 0.0f && steeringPath.NextNode != null)
{
Vector2 diff = ConvertUnits.ToSimUnits(steeringPath.NextNode.Position - controlledSub.WorldPosition);
@@ -417,27 +440,38 @@ namespace Barotrauma.Items.Components
Math.Max(1000.0f * Math.Abs(controlledSub.Velocity.Y), controlledSub.Borders.Height * 0.75f));
float avoidRadius = avoidDist.Length();
float damagingWallAvoidRadius = avoidRadius * 1.5f;
float damagingWallAvoidRadius = MathHelper.Clamp(avoidRadius * 1.5f, 5000.0f, 10000.0f);
Vector2 newAvoidStrength = Vector2.Zero;
debugDrawObstacles.Clear();
//steer away from nearby walls
showIceSpireWarning = false;
var closeCells = Level.Loaded.GetCells(controlledSub.WorldPosition, 4);
foreach (VoronoiCell cell in closeCells)
{
if (Level.Loaded?.ExtraWalls.Any(w => w.WallDamageOnTouch > 0.0f && w.Cells.Contains(cell)) ?? false)
if (cell.DoesDamage)
{
foreach (GraphEdge edge in cell.Edges)
{
Vector2 closestPoint = MathUtils.GetClosestPointOnLineSegment(edge.Point1 + cell.Translation, edge.Point2 + cell.Translation, controlledSub.WorldPosition);
float dist = Vector2.Distance(closestPoint, controlledSub.WorldPosition);
Vector2 diff = closestPoint - controlledSub.WorldPosition;
float dist = diff.Length() - Math.Max(controlledSub.Borders.Width, controlledSub.Borders.Height) / 2;
if (dist > damagingWallAvoidRadius) { continue; }
Vector2 diff = controlledSub.WorldPosition - cell.Center;
Vector2 avoid = Vector2.Normalize(diff) * (damagingWallAvoidRadius - dist) / damagingWallAvoidRadius;
Vector2 normalizedDiff = Vector2.Normalize(diff);
float dot = Vector2.Dot(normalizedDiff, controlledSub.Velocity);
float avoidStrength = MathHelper.Clamp(MathHelper.Lerp(1.0f, 0.0f, dist / damagingWallAvoidRadius - dot), 0.0f, 1.0f);
Vector2 avoid = -normalizedDiff * avoidStrength;
newAvoidStrength += avoid;
debugDrawObstacles.Add(new ObstacleDebugInfo(edge, edge.Center, 1.0f, avoid, cell.Translation));
if (dot > 0.0f)
{
showIceSpireWarning = true;
}
}
continue;
}
@@ -453,7 +487,7 @@ namespace Barotrauma.Items.Components
debugDrawObstacles.Add(new ObstacleDebugInfo(edge, intersection, 0.0f, Vector2.Zero, Vector2.Zero));
continue;
}
if (diff.LengthSquared() < 1.0f) diff = Vector2.UnitY;
if (diff.LengthSquared() < 1.0f) { diff = Vector2.UnitY; }
Vector2 normalizedDiff = Vector2.Normalize(diff);
float dot = controlledSub.Velocity == Vector2.Zero ?
@@ -480,8 +514,7 @@ namespace Barotrauma.Items.Components
//steer away from other subs
foreach (Submarine sub in Submarine.Loaded)
{
if (sub == controlledSub) { continue; }
if (controlledSub.DockedTo.Contains(sub)) { continue; }
if (sub == controlledSub || connectedSubs.Contains(sub)) { continue; }
Point sizeSum = controlledSub.Borders.Size + sub.Borders.Size;
Vector2 minDist = sizeSum.ToVector2() / 2;
Vector2 diff = controlledSub.WorldPosition - sub.WorldPosition;
@@ -512,6 +545,15 @@ namespace Barotrauma.Items.Components
}
}
private float? GetNodePenalty(PathNode node, PathNode nextNode)
{
if (node.Waypoint?.Tunnel == null || controlledSub == null || node.Waypoint.Tunnel.Type == Level.TunnelType.MainPath) { return 0.0f; }
//never navigate from the main path to another type of path
if (node.Waypoint.Tunnel.Type == Level.TunnelType.MainPath && nextNode.Waypoint?.Tunnel?.Type != Level.TunnelType.MainPath) { return null; }
//higher cost for side paths (= autopilot prefers the main path, but can still navigate side paths if it ends up on one)
return 1000.0f;
}
private void UpdatePath()
{
if (Level.Loaded == null) { return; }
@@ -584,7 +626,7 @@ namespace Barotrauma.Items.Components
{
if (objective.Override)
{
if (user != character && user != null && user.SelectedConstruction == item)
if (user != character && user != null && user.SelectedConstruction == item && character.IsOnPlayerTeam)
{
character.Speak(TextManager.Get("DialogSteeringTaken"), null, 0.0f, "steeringtaken", 10.0f);
}
@@ -647,6 +689,10 @@ namespace Barotrauma.Items.Components
break;
}
sonar?.AIOperate(deltaTime, character, objective);
if (!MaintainPos && showIceSpireWarning && character.IsOnPlayerTeam)
{
character.Speak(TextManager.Get("dialogicespirespottedsonar"), null, 0.0f, "icespirespottedsonar", 60.0f);
}
return false;
}
@@ -654,7 +700,7 @@ namespace Barotrauma.Items.Components
{
if (connection.Name == "velocity_in")
{
currVelocity = XMLExtensions.ParseVector2(signal, false);
TargetVelocity = XMLExtensions.ParseVector2(signal, errorMessages: false);
}
else
{
@@ -4,7 +4,7 @@ namespace Barotrauma.Items.Components
{
class NameTag : ItemComponent
{
[InGameEditable, Serialize("", false, description: "Name written on the tag.", alwaysUseInstanceValues: true)]
[InGameEditable(MaxLength = 32), Serialize("", false, description: "Name written on the tag.", alwaysUseInstanceValues: true)]
public string WrittenName { get; set; }
public NameTag(Item item, XElement element) : base(item, element)
@@ -74,6 +74,8 @@ namespace Barotrauma.Items.Components
[Serialize(100f, true, "How much fertilizer can the planter hold.")]
public float FertilizerCapacity { get; set; }
public string LastAction { get; set; } = "";
public Growable?[] GrowableSeeds = new Growable?[0];
private readonly List<RelatedItem> SuitableFertilizer = new List<RelatedItem>();
@@ -168,6 +170,8 @@ namespace Barotrauma.Items.Components
switch (plantItem.Type)
{
case PlantItemType.Seed:
LastAction = "PlantSeed";
ApplyStatusEffects(ActionType.OnPicked, 1.0f, character);
return container.Inventory.TryPutItem(plantItem.Item, character, new List<InvSlotType> { InvSlotType.Any });
case PlantItemType.Fertilizer when plantItem.Item != null:
float canAdd = FertilizerCapacity - Fertilizer;
@@ -178,6 +182,8 @@ namespace Barotrauma.Items.Components
#if CLIENT
character.UpdateHUDProgressBar(this, Item.DrawPosition, Fertilizer / FertilizerCapacity, Color.SaddleBrown, Color.SaddleBrown, "entityname.fertilizer");
#endif
LastAction = "ApplyFertilizer";
ApplyStatusEffects(ActionType.OnPicked, 1.0f, character);
return false;
}
@@ -203,6 +209,8 @@ namespace Barotrauma.Items.Components
container?.Inventory.RemoveItem(seed.Item);
Entity.Spawner?.AddToRemoveQueue(seed.Item);
GrowableSeeds[i] = null;
LastAction = "Harvest";
ApplyStatusEffects(ActionType.OnPicked, 1.0f, character);
return true;
}
}
@@ -226,12 +234,11 @@ namespace Barotrauma.Items.Components
if (container?.Inventory == null) { return; }
for (var i = 0; i < container.Inventory.Items.Length; i++)
for (var i = 0; i < container.Inventory.Capacity; i++)
{
if (i < 0 || GrowableSeeds.Length <= i) { continue; }
Item containedItem = container.Inventory.Items[i];
Item containedItem = container.Inventory.GetItemAt(i);
Growable? growable = containedItem?.GetComponent<Growable>();
if (growable != null)
@@ -289,11 +296,9 @@ namespace Barotrauma.Items.Components
private SuitablePlantItem GetSuitableItem(Character character)
{
foreach (Item heldItem in character.SelectedItems)
foreach (Item heldItem in character.HeldItems)
{
if (heldItem == null) { continue; }
if (container?.Inventory != null && !container.Inventory.IsFull())
if (container?.Inventory != null && container.Inventory.CanBePut(heldItem))
{
if (heldItem.GetComponent<Growable>() != null && SuitableSeeds.Any(ri => ri.MatchesItem(heldItem)))
{
@@ -228,11 +228,13 @@ namespace Barotrauma.Items.Components
{
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);
#endif
if (character.IsOnPlayerTeam)
{
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
@@ -249,9 +251,12 @@ namespace Barotrauma.Items.Components
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);
if (character.IsOnPlayerTeam)
{
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);
}
}
}
@@ -131,7 +131,7 @@ namespace Barotrauma.Items.Components
{
if (!powerOnSoundPlayed && powerOnSound != null)
{
SoundPlayer.PlaySound(powerOnSound.Sound, item.WorldPosition, powerOnSound.Volume, powerOnSound.Range, hullGuess: item.CurrentHull);
SoundPlayer.PlaySound(powerOnSound.Sound, item.WorldPosition, powerOnSound.Volume, powerOnSound.Range, hullGuess: item.CurrentHull, ignoreMuffling: powerOnSound.IgnoreMuffling);
powerOnSoundPlayed = true;
}
}
@@ -213,12 +213,15 @@ namespace Barotrauma.Items.Components
private void Launch(Character user, Vector2 simPosition, float rotation)
{
//User = user;
Item.body.ResetDynamics();
Item.SetTransform(simPosition, rotation);
Use();
if (Item.Removed) { return; }
// Set user for hitscan projectiles to work properly.
User = user;
// Need to set null for non-characterusable items.
Use(character: null);
// Set user for normal projectiles to work properly.
User = user;
if (Item.Removed) { return; }
launchPos = simPosition;
//set the rotation of the projectile again because dropping the projectile resets the rotation
Item.SetTransform(simPosition, rotation + (Item.body.Dir * LaunchRotationRadians));
@@ -328,21 +331,21 @@ namespace Barotrauma.Items.Components
IsActive = true;
Vector2 rayStart = simPositon;
Vector2 rayEnd = simPositon + dir * 1000.0f;
Vector2 rayEnd = simPositon + dir * 500.0f;
List<HitscanResult> hits = new List<HitscanResult>();
hits.AddRange(DoRayCast(rayStart, rayEnd));
hits.AddRange(DoRayCast(rayStart, rayEnd, submarine: item.Submarine));
if (item.Submarine != null)
{
//shooting indoors, do a hitscan outside as well
hits.AddRange(DoRayCast(rayStart + item.Submarine.SimPosition, rayEnd + item.Submarine.SimPosition));
hits.AddRange(DoRayCast(rayStart + item.Submarine.SimPosition, rayEnd + item.Submarine.SimPosition, submarine: null));
//also in the coordinate space of docked subs
foreach (Submarine dockedSub in item.Submarine.DockedTo)
{
if (dockedSub == item.Submarine) { continue; }
hits.AddRange(DoRayCast(rayStart + item.Submarine.SimPosition - dockedSub.SimPosition, rayEnd + item.Submarine.SimPosition - dockedSub.SimPosition));
hits.AddRange(DoRayCast(rayStart + item.Submarine.SimPosition - dockedSub.SimPosition, rayEnd + item.Submarine.SimPosition - dockedSub.SimPosition, dockedSub));
}
}
else
@@ -350,7 +353,7 @@ namespace Barotrauma.Items.Components
//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);
var inSubHits = DoRayCast(rayStart - submarine.SimPosition, rayEnd - submarine.SimPosition, submarine);
//transform back to world coordinates
for (int i = 0; i < inSubHits.Count; i++)
{
@@ -386,12 +389,20 @@ namespace Barotrauma.Items.Components
}
else
{
Entity.Spawner.AddToRemoveQueue(item);
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
{
//clients aren't allowed to remove items by themselves, so lets hide the projectile until the server tells us to remove it
item.HiddenInGame = Hitscan;
}
else
{
Entity.Spawner.AddToRemoveQueue(item);
}
}
}
}
private List<HitscanResult> DoRayCast(Vector2 rayStart, Vector2 rayEnd)
private List<HitscanResult> DoRayCast(Vector2 rayStart, Vector2 rayEnd, Submarine submarine)
{
List<HitscanResult> hits = new List<HitscanResult>();
@@ -406,14 +417,20 @@ namespace Barotrauma.Items.Components
if (fixture?.Body == null || fixture.IsSensor) { return true; }
if (fixture.Body.UserData is VineTile) { return true; }
if (fixture.Body.UserData is Item item && (item.GetComponent<Door>() == null && !item.Prefab.DamagedByProjectiles || item.Condition <= 0)) { return true; }
if (fixture.Body?.UserData as string == "ruinroom") { return true; }
if (fixture.Body.UserData as string == "ruinroom") { return true; }
//if doing the raycast in a submarine's coordinate space, ignore anything that's not in that sub
if (submarine != null)
{
if (fixture.Body.UserData is Entity entity && entity.Submarine != submarine) { 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; }
if (fixture.Body.UserData is VoronoiCell && this.item.Submarine != null) { return true; }
if (fixture.Body.UserData is VoronoiCell && (this.item.Submarine != null || submarine != null)) { return true; }
fixture.Body.GetTransform(out FarseerPhysics.Common.Transform transform);
if (!fixture.Shape.TestPoint(ref transform, ref rayStart)) { return true; }
@@ -436,7 +453,13 @@ namespace Barotrauma.Items.Components
!fixture.CollisionCategories.HasFlag(Physics.CollisionWall) &&
!fixture.CollisionCategories.HasFlag(Physics.CollisionLevel)) { return -1; }
//ignore level cells if the item the point of impact are inside a sub
//if doing the raycast in a submarine's coordinate space, ignore anything that's not in that sub
if (submarine != null)
{
if (fixture.Body.UserData is Entity entity && entity.Submarine != submarine) { return -1; }
}
//ignore level cells if the item and the point of impact are inside a sub
if (fixture.Body.UserData is VoronoiCell && this.item.Submarine != null)
{
if (Hull.FindHull(ConvertUnits.ToDisplayUnits(point), this.item.CurrentHull) != null)
@@ -536,7 +559,7 @@ namespace Barotrauma.Items.Components
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 &&
if (wallBody?.FixtureList?.First() != null && wallBody.UserData is 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)
{
@@ -623,7 +646,7 @@ namespace Barotrauma.Items.Components
{
if (Attack != null) { attackResult = Attack.DoDamage(User, damageable, item.WorldPosition, 1.0f); }
}
else if (target.Body.UserData is VoronoiCell voronoiCell && Attack != null && Math.Abs(Attack.StructureDamage) > 0.0f)
else if (target.Body.UserData is VoronoiCell voronoiCell && voronoiCell.IsDestructible && Attack != null && Math.Abs(Attack.StructureDamage) > 0.0f)
{
if (Level.Loaded?.ExtraWalls.Find(w => w.Body == target.Body) is DestructibleLevelWall destructibleWall)
{
@@ -633,8 +656,14 @@ namespace Barotrauma.Items.Components
if (character != null) { character.LastDamageSource = item; }
ActionType actionType = ActionType.OnUse;
if (_user != null && Rand.Range(0.0f, 0.5f) > DegreeOfSuccess(_user))
{
actionType = ActionType.OnFailure;
}
#if CLIENT
PlaySound(ActionType.OnUse, user: _user);
PlaySound(actionType, user: _user);
PlaySound(ActionType.OnImpact, user: _user);
#endif
@@ -642,7 +671,7 @@ namespace Barotrauma.Items.Components
{
if (target.Body.UserData is Limb targetLimb)
{
ApplyStatusEffects(ActionType.OnUse, 1.0f, character, targetLimb, user: _user);
ApplyStatusEffects(actionType, 1.0f, character, targetLimb, user: _user);
ApplyStatusEffects(ActionType.OnImpact, 1.0f, character, targetLimb, user: _user);
var attack = targetLimb.attack;
if (attack != null)
@@ -672,19 +701,19 @@ namespace Barotrauma.Items.Components
#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, 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, 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, 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
@@ -741,12 +770,11 @@ namespace Barotrauma.Items.Components
item.body.LinearVelocity *= 0.5f;
}
var containedItems = item.OwnInventory?.Items;
var containedItems = item.OwnInventory?.AllItems;
if (containedItems != null)
{
foreach (Item contained in containedItems)
{
if (contained == null) { continue; }
if (contained.body != null)
{
contained.SetTransform(item.SimPosition, contained.body.Rotation);
@@ -756,7 +784,15 @@ namespace Barotrauma.Items.Components
if (RemoveOnHit)
{
Entity.Spawner?.AddToRemoveQueue(item);
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
{
//clients aren't allowed to remove items by themselves, so lets hide the projectile until the server tells us to remove it
item.HiddenInGame = Hitscan;
}
else
{
Entity.Spawner?.AddToRemoveQueue(item);
}
}
return true;
@@ -786,7 +822,8 @@ namespace Barotrauma.Items.Components
{
MotorEnabled = true,
MaxMotorForce = 30.0f,
LimitEnabled = true
LimitEnabled = true,
Breakpoint = 1000.0f
};
if (StickPermanently)
@@ -351,7 +351,7 @@ namespace Barotrauma.Items.Components
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);
CurrentFixer.Position + Vector2.UnitY * 100.0f);
}
SteamAchievementManager.OnItemRepaired(item, CurrentFixer);
}
@@ -381,7 +381,7 @@ namespace Barotrauma.Items.Components
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);
CurrentFixer.Position + Vector2.UnitY * 100.0f);
}
deteriorationTimer = 0.0f;
@@ -52,15 +52,18 @@ namespace Barotrauma.Items.Components
sealed public override void Update(float deltaTime, Camera cam)
{
bool deactivate = true;
bool earlyReturn = false;
for (int i = 0; i < timeSinceReceived.Length; i++)
{
if (timeSinceReceived[i] > timeFrame)
{
IsActive = false;
return;
}
deactivate &= timeSinceReceived[i] > timeFrame;
earlyReturn |= timeSinceReceived[i] > timeFrame;
timeSinceReceived[i] += deltaTime;
}
// only stop Update() if both signals timed-out. if IsActive == false, then the component stops updating.
IsActive = !deactivate;
// early return if either of the signal timed-out
if (earlyReturn) { return; }
float output = Calculate(receivedSignal[0], receivedSignal[1]);
if (MathUtils.IsValid(output))
{
@@ -1,9 +1,23 @@
using System.Xml.Linq;
using System;
namespace Barotrauma.Items.Components
{
class ConcatComponent : StringComponent
{
private int maxOutputLength;
[Editable, Serialize(256, false, description: "The maximum length of the output string. Warning: Large values can lead to large memory usage or networking load.")]
public int MaxOutputLength
{
get { return maxOutputLength; }
set
{
maxOutputLength = Math.Max(value, 0);
}
}
public ConcatComponent(Item item, XElement element)
: base(item, element)
{
@@ -11,7 +25,8 @@ namespace Barotrauma.Items.Components
protected override string Calculate(string signal1, string signal2)
{
return signal1 + signal2;
string output = signal1 + signal2;
return output.Length <= maxOutputLength ? output : output.Substring(0, MaxOutputLength);
}
}
}
@@ -8,13 +8,16 @@ namespace Barotrauma.Items.Components
{
partial class Connection
{
//how many wires can be linked to a single connector
public const int MaxLinked = 5;
//how many wires can be linked to connectors by default
private const int DefaultMaxWires = 5;
//how many wires can be linked to this connection
public readonly int MaxWires = 5;
public readonly string Name;
public readonly string DisplayName;
private Wire[] wires;
private readonly Wire[] wires;
public IEnumerable<Wire> Wires
{
get { return wires; }
@@ -77,7 +80,8 @@ namespace Barotrauma.Items.Components
ConnectionPanel = connectionPanel;
item = connectionPanel.Item;
wires = new Wire[MaxLinked];
MaxWires = element.GetAttributeInt("maxwires", DefaultMaxWires);
wires = new Wire[MaxWires];
IsOutput = element.Name.ToString() == "output";
Name = element.GetAttributeString("name", IsOutput ? "output" : "input");
@@ -135,7 +139,7 @@ namespace Barotrauma.Items.Components
Effects = new List<StatusEffect>();
wireId = new ushort[MaxLinked];
wireId = new ushort[MaxWires];
foreach (XElement subElement in element.Elements())
{
@@ -143,7 +147,7 @@ namespace Barotrauma.Items.Components
{
case "link":
int index = -1;
for (int i = 0; i < MaxLinked; i++)
for (int i = 0; i < MaxWires; i++)
{
if (wireId[i] < 1) index = i;
}
@@ -173,7 +177,7 @@ namespace Barotrauma.Items.Components
private void RefreshRecipients()
{
recipients.Clear();
for (int i = 0; i < MaxLinked; i++)
for (int i = 0; i < MaxWires; i++)
{
if (wires[i] == null) continue;
Connection recipient = wires[i].OtherConnection(this);
@@ -184,7 +188,7 @@ namespace Barotrauma.Items.Components
public int FindEmptyIndex()
{
for (int i = 0; i < MaxLinked; i++)
for (int i = 0; i < MaxWires; i++)
{
if (wires[i] == null) return i;
}
@@ -193,7 +197,7 @@ namespace Barotrauma.Items.Components
public int FindWireIndex(Wire wire)
{
for (int i = 0; i < MaxLinked; i++)
for (int i = 0; i < MaxWires; i++)
{
if (wires[i] == wire) return i;
}
@@ -202,7 +206,7 @@ namespace Barotrauma.Items.Components
public int FindWireIndex(Item wireItem)
{
for (int i = 0; i < MaxLinked; i++)
for (int i = 0; i < MaxWires; i++)
{
if (wires[i] == null && wireItem == null) return i;
if (wires[i] != null && wires[i].Item == wireItem) return i;
@@ -212,7 +216,7 @@ namespace Barotrauma.Items.Components
public bool TryAddLink(Wire wire)
{
for (int i = 0; i < MaxLinked; i++)
for (int i = 0; i < MaxWires; i++)
{
if (wires[i] == null)
{
@@ -250,7 +254,7 @@ namespace Barotrauma.Items.Components
public void SendSignal(int stepsTaken, string signal, Item source, Character sender, float power, float signalStrength = 1.0f)
{
for (int i = 0; i < MaxLinked; i++)
for (int i = 0; i < MaxWires; i++)
{
if (wires[i] == null) { continue; }
@@ -265,16 +269,19 @@ namespace Barotrauma.Items.Components
ic.ReceiveSignal(stepsTaken, signal, recipient, source, sender, power, signalStrength);
}
foreach (StatusEffect effect in recipient.Effects)
if (signal != "0")
{
recipient.Item.ApplyStatusEffect(effect, ActionType.OnUse, (float)Timing.Step);
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++)
for (int i = 0; i < MaxWires; i++)
{
if (wires[i] == null) { continue; }
@@ -286,7 +293,7 @@ namespace Barotrauma.Items.Components
}
public void ClearConnections()
{
for (int i = 0; i < MaxLinked; i++)
for (int i = 0; i < MaxWires; i++)
{
if (wires[i] == null) continue;
@@ -300,7 +307,7 @@ namespace Barotrauma.Items.Components
{
if (wireId == null) return;
for (int i = 0; i < MaxLinked; i++)
for (int i = 0; i < MaxWires; i++)
{
if (wireId[i] == 0) { continue; }
@@ -329,7 +336,7 @@ namespace Barotrauma.Items.Components
return wire1.Item.ID.CompareTo(wire2.Item.ID);
});
for (int i = 0; i < MaxLinked; i++)
for (int i = 0; i < MaxWires; i++)
{
if (wires[i] == null) continue;
@@ -1,7 +1,5 @@
using Barotrauma.Networking;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
@@ -21,6 +19,14 @@ namespace Barotrauma.Items.Components
private List<ushort> disconnectedWireIds;
/// <summary>
/// Allows rewiring the connection panel despite rewiring being disabled on a server
/// </summary>
public bool AlwaysAllowRewiring
{
get { return item.Submarine?.Info.Type == SubmarineType.BeaconStation; }
}
[Editable, Serialize(false, true, description: "Locked connection panels cannot be rewired in-game.", alwaysUseInstanceValues: true)]
public bool Locked
{
@@ -103,7 +109,7 @@ namespace Barotrauma.Items.Components
public override void OnItemLoaded()
{
if (item.body != null)
if (item.body != null && item.body.BodyType == FarseerPhysics.BodyType.Dynamic)
{
var holdable = item.GetComponent<Holdable>();
if (holdable == null || !holdable.Attachable)
@@ -122,12 +128,12 @@ namespace Barotrauma.Items.Components
{
foreach (Wire wire in c.Wires)
{
if (wire == null) continue;
if (wire == null) { continue; }
#if CLIENT
if (wire.Item.IsSelected) continue;
if (wire.Item.IsSelected) { continue; }
#endif
var wireNodes = wire.GetNodes();
if (wireNodes.Count == 0) continue;
if (wireNodes.Count == 0) { continue; }
if (Submarine.RectContains(item.Rect, wireNodes[0] + wireNodeOffset))
{
@@ -176,7 +182,7 @@ namespace Barotrauma.Items.Components
{
//attaching wires to items with a body is not allowed
//(signal items remove their bodies when attached to a wall)
if (item.body != null)
if (item.body != null && item.body.BodyType == FarseerPhysics.BodyType.Dynamic)
{
return false;
}
@@ -239,10 +245,32 @@ namespace Barotrauma.Items.Components
for (int i = 0; i < loadedConnections.Count && i < Connections.Count; i++)
{
loadedConnections[i].wireId.CopyTo(Connections[i].wireId, 0);
if (loadedConnections[i].wireId.Length == Connections[i].wireId.Length)
{
loadedConnections[i].wireId.CopyTo(Connections[i].wireId, 0);
}
else
{
//backwards compatibility when maximum number of wires has changed
foreach (ushort id in loadedConnections[i].wireId)
{
for (int j = 0; j < Connections[i].wireId.Length; j++)
{
if (Connections[i].wireId[j] == 0)
{
Connections[i].wireId[j] = id;
break;
}
}
}
}
}
disconnectedWireIds = element.GetAttributeUshortArray("disconnectedwires", new ushort[0]).ToList();
for (int i = 0; i < disconnectedWireIds.Count; i++)
{
disconnectedWireIds[i] = idRemap.GetOffsetId(disconnectedWireIds[i]);
}
}
public override XElement Save(XElement parentElement)
@@ -12,25 +12,72 @@ namespace Barotrauma.Items.Components
public bool ContinuousSignal;
public bool State;
public string ConnectionName;
public string PropertyName;
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 PropertyName { get; }
public bool TargetOnlyParentProperty { get; }
public int NumberInputMin { get; }
public int NumberInputMax { get; }
public const int DefaultNumberInputMin = 0, DefaultNumberInputMax = 99;
public bool IsIntegerInput { get; }
public bool HasPropertyName { get; }
public bool ShouldSetProperty { get; set; }
public string Name => "CustomInterfaceElement";
public Dictionary<string, SerializableProperty> SerializableProperties { get; set; }
public List<StatusEffect> StatusEffects = new List<StatusEffect>();
public CustomInterfaceElement(XElement element)
/// <summary>
/// Pass the parent component to the constructor to access the serializable properties
/// for elements which change property values.
/// </summary>
public CustomInterfaceElement(XElement element, CustomInterface parent)
{
Label = element.GetAttributeString("text", "");
ConnectionName = element.GetAttributeString("connection", "");
PropertyName = element.GetAttributeString("propertyname", "").ToLowerInvariant();
Signal = element.GetAttributeString("signal", "1");
TargetOnlyParentProperty = element.GetAttributeBool("targetonlyparentproperty", false);
NumberInputMin = element.GetAttributeInt("min", DefaultNumberInputMin);
NumberInputMax = element.GetAttributeInt("max", DefaultNumberInputMax);
HasPropertyName = !string.IsNullOrEmpty(PropertyName);
IsIntegerInput = HasPropertyName && element.Name.ToString().ToLowerInvariant() == "integerinput";
if (element.Attribute("signal") is XAttribute attribute)
{
Signal = attribute.Value;
ShouldSetProperty = HasPropertyName;
}
else if (HasPropertyName && parent != null)
{
if (TargetOnlyParentProperty)
{
if (parent.SerializableProperties.ContainsKey(PropertyName))
{
Signal = parent.SerializableProperties[PropertyName].GetValue(parent) as string;
}
}
else
{
foreach (ISerializableEntity e in parent.item.AllPropertyObjects)
{
if (!e.SerializableProperties.ContainsKey(PropertyName)) { continue; }
Signal = e.SerializableProperties[PropertyName].GetValue(e) as string;
break;
}
}
}
else
{
Signal = "1";
}
foreach (XElement subElement in element.Elements())
{
@@ -50,13 +97,14 @@ namespace Barotrauma.Items.Components
set
{
if (value == null) { return; }
string[] splitValues = value == "" ? new string[0] : value.Split(',');
if (customInterfaceElementList.Count > 0)
{
string[] splitValues = value == "" ? new string[0] : value.Split(',');
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
@@ -67,34 +115,29 @@ namespace Barotrauma.Items.Components
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];
}
string[] splitValues = value == "" ? new string[0] : value.Split(';');
UpdateSignals(splitValues);
}
}
}
public override bool RecreateGUIOnResolutionChange => true;
private List<CustomInterfaceElement> customInterfaceElementList = new List<CustomInterfaceElement>();
private readonly 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":
case "textbox":
var button = new CustomInterfaceElement(subElement)
case "integerinput":
var button = new CustomInterfaceElement(subElement, this)
{
ContinuousSignal = false
};
@@ -105,7 +148,7 @@ namespace Barotrauma.Items.Components
customInterfaceElementList.Add(button);
break;
case "tickbox":
var tickBox = new CustomInterfaceElement(subElement)
var tickBox = new CustomInterfaceElement(subElement, this)
{
ContinuousSignal = true
};
@@ -116,10 +159,9 @@ namespace Barotrauma.Items.Components
customInterfaceElementList.Add(tickBox);
break;
}
i++;
}
IsActive = true;
InitProjSpecific(element);
InitProjSpecific();
Labels = element.GetAttributeString("labels", "");
Signals = element.GetAttributeString("signals", "");
}
@@ -142,6 +184,47 @@ namespace Barotrauma.Items.Components
UpdateLabelsProjSpecific();
}
private void UpdateSignals(string[] newSignals)
{
signals = new string[customInterfaceElementList.Count];
for (int i = 0; i < customInterfaceElementList.Count; i++)
{
var element = customInterfaceElementList[i];
if (i < newSignals.Length)
{
var newSignal = newSignals[i];
signals[i] = newSignal;
element.ShouldSetProperty = element.Signal != newSignal;
element.Signal = newSignal;
}
else
{
signals[i] = element.Signal;
}
if (element.HasPropertyName && element.ShouldSetProperty)
{
if (element.TargetOnlyParentProperty)
{
if (SerializableProperties.ContainsKey(element.PropertyName))
{
SerializableProperties[element.PropertyName].TrySetValue(this, element.Signal);
}
}
else
{
foreach (var po in item.AllPropertyObjects)
{
if (!po.SerializableProperties.ContainsKey(element.PropertyName)) { continue; }
po.SerializableProperties[element.PropertyName].TrySetValue(po, element.Signal);
}
}
customInterfaceElementList[i].ShouldSetProperty = false;
}
}
UpdateSignalsProjSpecific();
}
public override void OnItemLoaded()
{
foreach (CustomInterfaceElement ciElement in customInterfaceElementList)
@@ -152,7 +235,9 @@ namespace Barotrauma.Items.Components
partial void UpdateLabelsProjSpecific();
partial void InitProjSpecific(XElement element);
partial void UpdateSignalsProjSpecific();
partial void InitProjSpecific();
private void ButtonClicked(CustomInterfaceElement btnElement)
{
@@ -175,14 +260,38 @@ namespace Barotrauma.Items.Components
private void TextChanged(CustomInterfaceElement textElement, string text)
{
if (textElement == null) { return; }
textElement.Signal = text;
foreach (ISerializableEntity e in item.AllPropertyObjects)
if (!textElement.TargetOnlyParentProperty)
{
if (e.SerializableProperties.ContainsKey(textElement.PropertyName))
foreach (ISerializableEntity e in item.AllPropertyObjects)
{
if (!e.SerializableProperties.ContainsKey(textElement.PropertyName)) { continue; }
e.SerializableProperties[textElement.PropertyName].TrySetValue(e, text);
}
}
}
else if (SerializableProperties.ContainsKey(textElement.PropertyName))
{
SerializableProperties[textElement.PropertyName].TrySetValue(this, text);
}
}
private void ValueChanged(CustomInterfaceElement numberInputElement, int value)
{
if (numberInputElement == null) { return; }
numberInputElement.Signal = value.ToString();
if (!numberInputElement.TargetOnlyParentProperty)
{
foreach (ISerializableEntity e in item.AllPropertyObjects)
{
if (!e.SerializableProperties.ContainsKey(numberInputElement.PropertyName)) { continue; }
e.SerializableProperties[numberInputElement.PropertyName].TrySetValue(e, value);
}
}
else if (SerializableProperties.ContainsKey(numberInputElement.PropertyName))
{
SerializableProperties[numberInputElement.PropertyName].TrySetValue(this, value);
}
}
public override void Update(float deltaTime, Camera cam)
@@ -35,7 +35,7 @@ namespace Barotrauma.Items.Components
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);
item.SendSignal(stepsTaken, MathUtils.Pow(value, Exponent).ToString("G", CultureInfo.InvariantCulture), "signal_out", sender, source: source);
break;
}
}
@@ -31,17 +31,17 @@ namespace Barotrauma.Items.Components
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;
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);
item.SendSignal(stepsTaken, Math.Round(value).ToString("G", CultureInfo.InvariantCulture), "signal_out", sender, source: source);
break;
case FunctionType.Ceil:
item.SendSignal(0, Math.Ceiling(value).ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
item.SendSignal(stepsTaken, Math.Ceiling(value).ToString("G", CultureInfo.InvariantCulture), "signal_out", sender, source: source);
break;
case FunctionType.Floor:
item.SendSignal(0, Math.Floor(value).ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
item.SendSignal(stepsTaken, Math.Floor(value).ToString("G", CultureInfo.InvariantCulture), "signal_out", sender, source: source);
break;
case FunctionType.Factorial:
int intVal = (int)Math.Min(value, 20);
@@ -50,15 +50,15 @@ namespace Barotrauma.Items.Components
{
factorial *= (ulong)i;
}
item.SendSignal(0, factorial.ToString(), "signal_out", null);
item.SendSignal(stepsTaken, factorial.ToString(), "signal_out", sender, source: source);
break;
case FunctionType.AbsoluteValue:
item.SendSignal(0, Math.Abs(value).ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
item.SendSignal(stepsTaken, Math.Abs(value).ToString("G", CultureInfo.InvariantCulture), "signal_out", sender, source: source);
break;
case FunctionType.SquareRoot:
if (value > 0)
{
item.SendSignal(0, Math.Sqrt(value).ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
item.SendSignal(stepsTaken, Math.Sqrt(value).ToString("G", CultureInfo.InvariantCulture), "signal_out", sender, source: source);
}
break;
default:
@@ -25,6 +25,8 @@ namespace Barotrauma.Items.Components
public PhysicsBody ParentBody;
private Turret turret;
[Serialize(100.0f, true, description: "The range of the emitted light. Higher values are more performance-intensive.", alwaysUseInstanceValues: true),
Editable(MinValueFloat = 0.0f, MaxValueFloat = 2048.0f)]
public float Range
@@ -214,7 +216,14 @@ namespace Barotrauma.Items.Components
IsActive = IsOn;
item.AddTag("light");
}
public override void OnItemLoaded()
{
base.OnItemLoaded();
SetLightSourceState(IsActive, lightBrightness);
turret = item.GetComponent<Turret>();
}
public override void Update(float deltaTime, Camera cam)
{
if (item.AiTarget != null)
@@ -232,9 +241,19 @@ namespace Barotrauma.Items.Components
return;
}
#if CLIENT
light.Position = ParentBody != null ? ParentBody.Position : item.Position;
if (ParentBody != null)
{
light.Position = ParentBody.Position;
}
else if (turret != null)
{
light.Position = new Vector2(item.Rect.X + turret.TransformedBarrelPos.X, item.Rect.Y - turret.TransformedBarrelPos.Y);
}
else
{
light.Position = item.Position;
}
#endif
PhysicsBody body = ParentBody ?? item.body;
if (body != null)
{
@@ -49,6 +49,7 @@ namespace Barotrauma.Items.Components
}
break;
case "signal_store":
case "lock_state":
writeable = signal == "1";
break;
}
@@ -32,7 +32,7 @@ namespace Barotrauma.Items.Components
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);
item.SendSignal(stepsTaken, (value % modulus).ToString("G", CultureInfo.InvariantCulture), "signal_out", sender, source: source);
break;
}
@@ -15,17 +15,24 @@ namespace Barotrauma.Items.Components
private float updateTimer;
public enum TargetType
{
Any,
Human,
Monster
}
[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?", alwaysUseInstanceValues: true)]
public bool OnlyHumans
[InGameEditable, Serialize(TargetType.Any, true, description: "Which kind of targets can trigger the sensor?", alwaysUseInstanceValues: true)]
public TargetType Target
{
get;
set;
}
[Editable, Serialize(false, true, description: "Should the sensor ignore the bodies of dead characters?", alwaysUseInstanceValues: true)]
[InGameEditable, Serialize(false, true, description: "Should the sensor ignore the bodies of dead characters?", alwaysUseInstanceValues: true)]
public bool IgnoreDead
{
get;
@@ -55,7 +62,7 @@ namespace Barotrauma.Items.Components
}
}
[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.")]
[InGameEditable, 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; }
@@ -80,7 +87,6 @@ namespace Barotrauma.Items.Components
set;
}
public MotionSensor(Item item, XElement element)
: base(item, element)
{
@@ -93,6 +99,16 @@ namespace Barotrauma.Items.Components
}
}
public override void Load(XElement componentElement, bool usePrefabValues, IdRemap idRemap)
{
base.Load(componentElement, usePrefabValues, idRemap);
//backwards compatibility
if (componentElement.GetAttributeBool("onlyhumans", false))
{
Target = TargetType.Human;
}
}
public override void Update(float deltaTime, Camera cam)
{
string signalOut = MotionDetected ? Output : FalseOutput;
@@ -121,7 +137,20 @@ namespace Barotrauma.Items.Components
foreach (Character c in Character.CharacterList)
{
if (IgnoreDead && c.IsDead) { continue; }
if (OnlyHumans && !c.IsHuman) { continue; }
//ignore characters that have spawned a second or less ago
//makes it possible to detect when a spawned character moves without triggering the detector immediately as the ragdoll spawns and drops to the ground
if (c.SpawnTime > Timing.TotalTime - 1.0) { continue; }
switch (Target)
{
case TargetType.Human:
if (!c.IsHuman) { continue; }
break;
case TargetType.Monster:
if (c.IsHuman || c.IsPet) { continue; }
break;
}
//do a rough check based on the position of the character's collider first
//before the more accurate limb-based check
@@ -4,16 +4,36 @@ namespace Barotrauma.Items.Components
{
class NotComponent : ItemComponent
{
private bool signalReceived;
private bool continuousOutput;
[Editable, Serialize(false, true, description: "When enabled, the component continuously outputs \"1\" when it's not receiving a signal.", alwaysUseInstanceValues: true)]
public bool ContinuousOutput
{
get { return continuousOutput; }
set { continuousOutput = IsActive = value; }
}
public NotComponent(Item item, XElement element)
: base (item, element)
{
}
public override void Update(float deltaTime, Camera cam)
{
base.Update(deltaTime, cam);
if (!signalReceived)
{
item.SendSignal(0, "1", "signal_out", null, 0.0f);
}
signalReceived = 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 != "signal_in") return;
item.SendSignal(stepsTaken, signal == "0" ? "1" : "0", "signal_out", sender, 0.0f, source, signalStrength);
if (connection.Name != "signal_in") { return; }
item.SendSignal(stepsTaken, signal == "0" || signal == string.Empty ? "1" : "0", "signal_out", sender, 0.0f, source, signalStrength);
signalReceived = true;
}
}
}
@@ -23,7 +23,7 @@ namespace Barotrauma.Items.Components
[InGameEditable, Serialize(false, true, description: "Should the component output a value of a capture group instead of a constant signal.", alwaysUseInstanceValues: true)]
public bool UseCaptureGroup { get; set; }
[Serialize("0", true, description: "The signal this item outputs when the received signal does not match the regular expression.", alwaysUseInstanceValues: true)]
[InGameEditable, Serialize("0", true, description: "The signal this item outputs when the received signal does not match the regular expression.", alwaysUseInstanceValues: true)]
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.", alwaysUseInstanceValues: true)]
@@ -180,6 +180,7 @@ namespace Barotrauma.Items.Components
}
else if (connection.Name == "toggle")
{
if (signal == "0") { return; }
SetState(!IsOn, false);
}
else if (connection.Name == "set_state")
@@ -25,7 +25,7 @@ namespace Barotrauma.Items.Components
string signalOut = (signal == TargetSignal) ? Output : FalseOutput;
if (string.IsNullOrWhiteSpace(signalOut)) return;
item.SendSignal(stepsTaken, signalOut, "signal_out", sender, signalStrength);
item.SendSignal(stepsTaken, signalOut, "signal_out", sender, signalStrength, source);
break;
case "set_output":
@@ -68,18 +68,18 @@ namespace Barotrauma.Items.Components
{
case FunctionType.Sin:
if (!UseRadians) { value = MathHelper.ToRadians(value); }
item.SendSignal(0, ((float)Math.Sin(value)).ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
item.SendSignal(stepsTaken, ((float)Math.Sin(value)).ToString("G", CultureInfo.InvariantCulture), "signal_out", sender, source: source);
break;
case FunctionType.Cos:
if (!UseRadians) { value = MathHelper.ToRadians(value); }
item.SendSignal(0, ((float)Math.Cos(value)).ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
item.SendSignal(stepsTaken, ((float)Math.Cos(value)).ToString("G", CultureInfo.InvariantCulture), "signal_out", sender, source: source);
break;
case FunctionType.Tan:
if (!UseRadians) { value = MathHelper.ToRadians(value); }
//tan is undefined if the value is (π / 2) + πk, where k is any integer
if (!MathUtils.NearlyEqual(value % MathHelper.Pi, MathHelper.PiOver2))
{
item.SendSignal(0, ((float)Math.Tan(value)).ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
item.SendSignal(stepsTaken, ((float)Math.Tan(value)).ToString("G", CultureInfo.InvariantCulture), "signal_out", sender, source: source);
}
break;
case FunctionType.Asin:
@@ -88,7 +88,7 @@ namespace Barotrauma.Items.Components
{
float angle = (float)Math.Asin(value);
if (!UseRadians) { angle = MathHelper.ToDegrees(angle); }
item.SendSignal(0, angle.ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
item.SendSignal(stepsTaken, angle.ToString("G", CultureInfo.InvariantCulture), "signal_out", sender, source: source);
}
break;
case FunctionType.Acos:
@@ -97,7 +97,7 @@ namespace Barotrauma.Items.Components
{
float angle = (float)Math.Acos(value);
if (!UseRadians) { angle = MathHelper.ToDegrees(angle); }
item.SendSignal(0, angle.ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
item.SendSignal(stepsTaken, angle.ToString("G", CultureInfo.InvariantCulture), "signal_out", sender, source: source);
}
break;
case FunctionType.Atan:
@@ -115,7 +115,7 @@ namespace Barotrauma.Items.Components
{
float angle = (float)Math.Atan(value);
if (!UseRadians) { angle = MathHelper.ToDegrees(angle); }
item.SendSignal(0, angle.ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
item.SendSignal(stepsTaken, angle.ToString("G", CultureInfo.InvariantCulture), "signal_out", sender, source: source);
}
break;
default:
@@ -24,8 +24,8 @@ namespace Barotrauma.Items.Components
private int[] channelMemory = new int[ChannelMemorySize];
[Serialize(Character.TeamType.None, true, description: "WiFi components can only communicate with components that have the same Team ID.", alwaysUseInstanceValues: true)]
public Character.TeamType TeamID { get; set; }
[Serialize(CharacterTeamType.None, true, description: "WiFi components can only communicate with components that have the same Team ID.", alwaysUseInstanceValues: true)]
public CharacterTeamType TeamID { get; set; }
[Editable, Serialize(20000.0f, false, description: "How close the recipient has to be to receive a signal from this WiFi component.", alwaysUseInstanceValues: true)]
public float Range
@@ -152,7 +152,7 @@ namespace Barotrauma.Items.Components
channelMemory[index] = MathHelper.Clamp(value, 0, 10000);
}
public void TransmitSignal(int stepsTaken, string signal, Item source, Character sender, bool sendToChat, float signalStrength = 1.0f)
public void TransmitSignal(int stepsTaken, string signal, Item source, Character sender, bool sentFromChat, float signalStrength = 1.0f)
{
var senderComponent = source?.GetComponent<WifiComponent>();
if (senderComponent != null && !CanReceive(senderComponent)) { return; }
@@ -162,6 +162,8 @@ namespace Barotrauma.Items.Components
var receivers = GetReceiversInRange();
foreach (WifiComponent wifiComp in receivers)
{
if (sentFromChat && !wifiComp.LinkToChat) { continue; }
//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);
@@ -176,11 +178,12 @@ namespace Barotrauma.Items.Components
source.LastSentSignalRecipients.Add(receiverItem);
}
}
}
}
if (DiscardDuplicateChatMessages && signal == prevSignal) continue;
if (DiscardDuplicateChatMessages && signal == prevSignal) { continue; }
if (LinkToChat && wifiComp.LinkToChat && chatMsgCooldown <= 0.0f && sendToChat)
//create a chat message
if (LinkToChat && wifiComp.LinkToChat && chatMsgCooldown <= 0.0f && !sentFromChat)
{
if (wifiComp.item.ParentInventory != null &&
wifiComp.item.ParentInventory.Owner != null)
@@ -232,7 +235,7 @@ namespace Barotrauma.Items.Components
switch (connection.Name)
{
case "signal_in":
TransmitSignal(stepsTaken, signal, source, sender, true, signalStrength);
TransmitSignal(stepsTaken, signal, source, sender, false, signalStrength);
break;
case "set_channel":
if (int.TryParse(signal, out int newChannel))
@@ -37,6 +37,11 @@ namespace Barotrauma.Items.Components
angle = MathUtils.VectorToAngle(end - start);
length = Vector2.Distance(start, end);
if (length > 5000.0f)
{
int akjsdnfkjsadf = 1;
}
}
}
@@ -183,8 +188,12 @@ namespace Barotrauma.Items.Components
if (refSub == null)
{
Structure attachTarget = Structure.GetAttachTarget(newConnection.Item.WorldPosition);
if (attachTarget == null) { continue; }
refSub = attachTarget.Submarine;
if (attachTarget == null && !(newConnection.Item.GetComponent<Holdable>()?.Attached ?? false))
{
connections[i] = null;
continue;
}
refSub = attachTarget?.Submarine;
}
Vector2 nodePos = refSub == null ?
@@ -238,18 +247,18 @@ namespace Barotrauma.Items.Components
{
foreach (ItemComponent ic in item.Components)
{
if (ic == this) continue;
if (ic == this) { continue; }
ic.Drop(null);
}
if (item.Container != null) item.Container.RemoveContained(this.item);
if (item.body != null) item.body.Enabled = false;
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 (item.body != null) { item.Submarine = newConnection.Item.Submarine; }
if (sendNetworkEvent)
{
@@ -735,6 +744,11 @@ namespace Barotrauma.Items.Components
public override void FlipX(bool relativeToSub)
{
if (item.ParentInventory != null) { return; }
#if CLIENT
if (!relativeToSub && Screen.Selected != GameMain.SubEditorScreen) { return; }
#else
if (!relativeToSub) { return; }
#endif
Vector2 refPos = item.Submarine == null ?
Vector2.Zero :
@@ -76,7 +76,7 @@ namespace Barotrauma.Items.Components
set { launchImpulse = value; }
}
[Editable(0.0f, 1000.0f), Serialize(5.0f, false, description: "The period of time the user has to wait between shots.")]
[Editable(0.0f, 1000.0f, decimals: 3), Serialize(5.0f, false, description: "The period of time the user has to wait between shots.")]
public float Reload
{
get { return reloadTime; }
@@ -198,6 +198,7 @@ namespace Barotrauma.Items.Components
private set;
}
private float prevScale;
float prevBaseRotation;
[Serialize(0.0f, true, description: "The angle of the turret's base in degrees.", alwaysUseInstanceValues: true)]
public float BaseRotation
@@ -250,33 +251,42 @@ namespace Barotrauma.Items.Components
private void UpdateTransformedBarrelPos()
{
float flippedRotation = item.Rotation;
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);
transformedBarrelPos = MathUtils.RotatePointAroundTarget(barrelPos * item.Scale, new Vector2(item.Rect.Width / 2, item.Rect.Height / 2), MathHelper.ToRadians(item.Rotation));
#if CLIENT
item.ResetCachedVisibleSize();
#endif
item.Rotation = flippedRotation;
prevBaseRotation = item.Rotation;
prevScale = item.Scale;
}
public override void OnItemLoaded()
public override void OnMapLoaded()
{
base.OnItemLoaded();
var lightComponents = item.GetComponents<LightComponent>();
if (lightComponents != null && lightComponents.Count() > 0)
base.OnMapLoaded();
FindLightComponent();
if (loadedRotationLimits.HasValue) { RotationLimits = loadedRotationLimits.Value; }
if (loadedBaseRotation.HasValue) { BaseRotation = loadedBaseRotation.Value; }
UpdateTransformedBarrelPos();
}
private void FindLightComponent()
{
foreach (LightComponent lc in item.GetComponents<LightComponent>())
{
lightComponent = lightComponents.FirstOrDefault(lc => lc.Parent == this);
#if CLIENT
if (lightComponent != null)
if (lc?.Parent == this)
{
lightComponent.Parent = null;
lightComponent.Rotation = Rotation - MathHelper.ToRadians(item.Rotation);
lightComponent.Light.Rotation = -rotation;
lightComponent = lc;
break;
}
#endif
}
#if CLIENT
if (lightComponent != null)
{
lightComponent.Parent = null;
lightComponent.Rotation = Rotation - MathHelper.ToRadians(item.Rotation);
lightComponent.Light.Rotation = -rotation;
}
#endif
}
public override void Update(float deltaTime, Camera cam)
@@ -284,7 +294,7 @@ namespace Barotrauma.Items.Components
this.cam = cam;
if (reload > 0.0f) { reload -= deltaTime; }
if (!MathUtils.NearlyEqual(item.Rotation, prevBaseRotation))
if (!MathUtils.NearlyEqual(item.Rotation, prevBaseRotation) || !MathUtils.NearlyEqual(item.Scale, prevScale))
{
UpdateTransformedBarrelPos();
}
@@ -303,7 +313,11 @@ namespace Barotrauma.Items.Components
UpdateProjSpecific(deltaTime);
if (minRotation == maxRotation) { return; }
if (MathUtils.NearlyEqual(minRotation, maxRotation))
{
UpdateLightComponent();
return;
}
float targetMidDiff = MathHelper.WrapAngle(targetRotation - (minRotation + maxRotation) / 2.0f);
@@ -325,28 +339,56 @@ namespace Barotrauma.Items.Components
{
user.Info.IncreaseSkillLevel("weapons",
SkillSettings.Current.SkillIncreasePerSecondWhenOperatingTurret * deltaTime / Math.Max(user.GetSkillLevel("weapons"), 1.0f),
user.WorldPosition + Vector2.UnitY * 150.0f);
user.Position + Vector2.UnitY * 150.0f);
}
float rotMidDiff = MathHelper.WrapAngle(rotation - (minRotation + maxRotation) / 2.0f);
float targetRotationDiff = MathHelper.WrapAngle(targetRotation - rotation);
if ((maxRotation - minRotation) < MathHelper.TwoPi)
{
float targetRotationMaxDiff = MathHelper.WrapAngle(targetRotation - maxRotation);
float targetRotationMinDiff = MathHelper.WrapAngle(targetRotation - minRotation);
if (Math.Abs(targetRotationMaxDiff) < Math.Abs(targetRotationMinDiff) &&
rotMidDiff < 0.0f &&
targetRotationDiff < 0.0f)
{
targetRotationDiff += MathHelper.TwoPi;
}
else if (Math.Abs(targetRotationMaxDiff) > Math.Abs(targetRotationMinDiff) &&
rotMidDiff > 0.0f &&
targetRotationDiff > 0.0f)
{
targetRotationDiff -= MathHelper.TwoPi;
}
}
angularVelocity +=
(MathHelper.WrapAngle(targetRotation - rotation) * springStiffness - angularVelocity * springDamping) * deltaTime;
(targetRotationDiff * springStiffness - angularVelocity * springDamping) * deltaTime;
angularVelocity = MathHelper.Clamp(angularVelocity, -rotationSpeed, rotationSpeed);
rotation += angularVelocity * deltaTime;
float rotMidDiff = MathHelper.WrapAngle(rotation - (minRotation + maxRotation) / 2.0f);
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;
}
UpdateLightComponent();
}
private void UpdateLightComponent()
{
if (lightComponent != null)
{
lightComponent.Rotation = Rotation - MathHelper.ToRadians(item.Rotation);
@@ -667,10 +709,7 @@ namespace Barotrauma.Items.Components
}
else
{
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) { return; }
if (!CheckTurretAngle(angle)) { return; }
float enemyAngle = MathUtils.VectorToAngle(target.WorldPosition - item.WorldPosition);
float turretAngle = -rotation;
if (Math.Abs(MathUtils.GetShortestAngle(enemyAngle, turretAngle)) > 0.15f) { return; }
@@ -685,7 +724,11 @@ namespace Barotrauma.Items.Components
}
var collisionCategories = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel;
var pickedBody = Submarine.PickBody(start, end, null, collisionCategories, allowInsideFixture: true,
customPredicate: (Fixture f) => { return !item.StaticFixtures.Contains(f); });
customPredicate: (Fixture f) =>
{
if (f.UserData is Item i && i.GetComponent<Turret>() != null) { return false; }
return !item.StaticFixtures.Contains(f);
});
if (pickedBody == null) { return; }
Character targetCharacter = null;
if (pickedBody.UserData is Character c)
@@ -724,12 +767,13 @@ namespace Barotrauma.Items.Components
TryLaunch(deltaTime, ignorePower: true);
}
private bool outOfAmmo;
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.Speak(TextManager.Get("DialogTurretTargetDead"), null, 0.0f, "killedtarget" + previousTarget.ID, 10.0f);
character.AIController.SelectTarget(null);
}
@@ -741,7 +785,7 @@ namespace Barotrauma.Items.Components
PowerContainer batteryToLoad = null;
foreach (PowerContainer battery in batteries)
{
if (battery.Item.NonInteractable) { continue; }
if (!battery.Item.IsInteractable(character)) { continue; }
if (batteryToLoad == null || battery.Charge < lowestCharge)
{
batteryToLoad = battery;
@@ -762,18 +806,16 @@ namespace Barotrauma.Items.Components
int maxProjectileCount = 0;
foreach (MapEntity e in item.linkedTo)
{
if (item.NonInteractable) { continue; }
if (!item.IsInteractable(character)) { continue; }
if (e is Item projectileContainer)
{
var containedItems = projectileContainer.ContainedItems;
if (containedItems != null)
var container = projectileContainer.GetComponent<ItemContainer>();
if (container != null)
{
var container = projectileContainer.GetComponent<ItemContainer>();
maxProjectileCount += container.Capacity;
int projectiles = containedItems.Count(it => it.Condition > 0.0f);
usableProjectileCount += projectiles;
}
int projectiles = projectileContainer.ContainedItems.Count(it => it.Condition > 0.0f);
usableProjectileCount += projectiles;
}
}
}
@@ -785,39 +827,44 @@ namespace Barotrauma.Items.Components
{
containerItem = e as Item;
if (containerItem == null) { continue; }
if (containerItem.NonInteractable) { continue; }
if (!containerItem.IsInteractable(character)) { continue; }
if (character.AIController is HumanAIController aiController && aiController.IgnoredItems.Contains(containerItem)) { continue; }
container = containerItem.GetComponent<ItemContainer>();
if (container != null) { break; }
}
if (container == null || container.ContainableItems.Count == 0)
{
character.Speak(TextManager.GetWithVariable("DialogCannotLoadTurret", "[itemname]", item.Name, true), null, 0.0f, "cannotloadturret", 30.0f);
if (!outOfAmmo && character.IsOnPlayerTeam)
{
character.Speak(TextManager.GetWithVariable("DialogCannotLoadTurret", "[itemname]", item.Name, true), null, 0.0f, "cannotloadturret", 30.0f);
}
return true;
}
if (objective.SubObjectives.None())
{
if (!AIDecontainEmptyItems(character, objective, equip: true, sourceContainer: container))
var loadItemsObjective = AIContainItems<Turret>(container, character, objective, usableProjectileCount + 1, equip: true, removeEmpty: true, dropItemOnDeselected: true);
loadItemsObjective.ignoredContainerIdentifiers = new string[] { containerItem.prefab.Identifier };
if (character.IsOnPlayerTeam)
{
return false;
}
}
if (objective.SubObjectives.None())
{
var loadItemsObjective = AIContainItems<Turret>(container, character, objective, usableProjectileCount + 1, equip: true, removeEmpty: true);
if (loadItemsObjective == null)
{
if (usableProjectileCount == 0)
{
character.Speak(TextManager.GetWithVariable("DialogCannotLoadTurret", "[itemname]", item.Name, true), null, 0.0f, "cannotloadturret", 30.0f);
return true;
}
}
else
{
loadItemsObjective.ignoredContainerIdentifiers = new string[] { containerItem.prefab.Identifier };
character.Speak(TextManager.GetWithVariable("DialogLoadTurret", "[itemname]", item.Name, true), null, 0.0f, "loadturret", 30.0f);
return false;
}
loadItemsObjective.Abandoned += CheckRemainingAmmo;
loadItemsObjective.Completed += CheckRemainingAmmo;
return outOfAmmo;
void CheckRemainingAmmo()
{
if (!character.IsOnPlayerTeam) { return; }
string ammoType = container.Item.HasTag("railgunammosource") ? "railgunammo" : container.Item.HasTag("coilgunammosource") ? "coilgunammo" : "turretammo";
int remainingAmmo = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(ammoType) && i.Condition > 1);
if (remainingAmmo == 0)
{
character.Speak(TextManager.Get($"DialogOutOf{ammoType}"), null, 0.0f, "outofammo", 30.0f);
}
else if (remainingAmmo < 3)
{
character.Speak(TextManager.Get($"DialogLowOn{ammoType}"), null, 0.0f, "outofammo", 30.0f);
}
}
}
if (objective.SubObjectives.Any())
@@ -828,53 +875,166 @@ namespace Barotrauma.Items.Components
//enough shells and power
Character closestEnemy = null;
float closestDist = AIRange * AIRange;
Vector2? targetPos = null;
float maxDistance = 10000;
float shootDistance = AIRange * item.OffsetOnSelectedMultiplier;
float closestDistance = maxDistance * maxDistance;
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; }
// Don't aim monsters that are inside a submarine.
if (!enemy.IsHuman && enemy.CurrentHull != null) { 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; }
if (dist > closestDistance) { continue; }
if (dist < shootDistance * shootDistance)
{
// Only check the angle to targets that are close enough to be shot at
// We shouldn't check the angle when a long creature is traveling outside of the shooting range, because doing so would not allow us to shoot the limbs that might be close enough to shoot at.
if (!CheckTurretAngle(enemy.WorldPosition)) { continue; }
}
closestEnemy = enemy;
closestDist = dist;
closestDistance = dist;
}
if (closestEnemy == null) { return false; }
character.AIController.SelectTarget(closestEnemy.AiTarget);
if (closestEnemy != null)
{
// Target the closest limb. Doesn't make much difference with smaller creatures, but enables the bots to shoot longer abyss creatures like the endworm. Otherwise they just target the main body = head.
targetPos = closestEnemy.WorldPosition;
float closestDist = closestDistance;
foreach (Limb limb in closestEnemy.AnimController.Limbs)
{
if (limb.IsSevered) { continue; }
if (limb.Hidden) { continue; }
if (!CheckTurretAngle(limb.WorldPosition)) { continue; }
float dist = Vector2.DistanceSquared(limb.WorldPosition, item.WorldPosition);
if (dist < closestDist)
{
closestDist = dist;
targetPos = limb.WorldPosition;
}
}
if (closestDist > shootDistance * shootDistance)
{
// Not close enough to shoot
closestEnemy = null;
targetPos = null;
}
}
else if (item.Submarine != null && Level.Loaded != null)
{
// Check ice spires
closestDistance = shootDistance;
foreach (var wall in Level.Loaded.ExtraWalls)
{
if (!(wall is DestructibleLevelWall destructibleWall) || destructibleWall.Destroyed) { continue; }
foreach (var cell in wall.Cells)
{
if (cell.DoesDamage)
{
foreach (var edge in cell.Edges)
{
Vector2 p1 = edge.Point1 + cell.Translation;
Vector2 p2 = edge.Point2 + cell.Translation;
Vector2 closestPoint = MathUtils.GetClosestPointOnLineSegment(p1, p2, item.WorldPosition);
if (!CheckTurretAngle(closestPoint))
{
// The closest point can't be targeted -> get a point directly in front of the turret
Vector2 barrelDir = new Vector2((float)Math.Cos(rotation), -(float)Math.Sin(rotation));
if (MathUtils.GetLineIntersection(p1, p2, item.WorldPosition, item.WorldPosition + barrelDir * shootDistance, out Vector2 intersection))
{
closestPoint = intersection;
if (!CheckTurretAngle(closestPoint)) { continue; }
}
else
{
continue;
}
}
float dist = Vector2.Distance(closestPoint, item.WorldPosition);
if (dist > AIRange + 1000) { continue; }
float dot = 0;
if (item.Submarine.Velocity != Vector2.Zero)
{
dot = Vector2.Dot(Vector2.Normalize(item.Submarine.Velocity), Vector2.Normalize(closestPoint - item.Submarine.WorldPosition));
}
float minAngle = 0.5f;
if (dot < minAngle && dist > 1000)
{
// The sub is not moving towards the target and it's not very close to the turret either -> ignore
continue;
}
// Allow targeting farther when heading towards the spire (up to 1000 px)
dist -= MathHelper.Lerp(0, 1000, MathUtils.InverseLerp(minAngle, 1, dot)); ;
if (dist > closestDistance) { continue; }
targetPos = closestPoint;
closestDistance = dist;
}
}
}
}
}
character.CursorPosition = closestEnemy.WorldPosition;
if (targetPos == null) { return false; }
if (closestEnemy != null && character.AIController.SelectedAiTarget != closestEnemy.AiTarget)
{
if (character.IsOnPlayerTeam)
{
if (character.AIController.SelectedAiTarget == null)
{
if (GameMain.Config.RecentlyEncounteredCreatures.Contains(closestEnemy.SpeciesName))
{
character.Speak(TextManager.Get("DialogNewTargetSpotted"), null, 0.0f, "newtargetspotted", 30.0f);
}
else if (GameMain.Config.EncounteredCreatures.Any(name => name.Equals(closestEnemy.SpeciesName, StringComparison.OrdinalIgnoreCase)))
{
character.Speak(TextManager.GetWithVariable("DialogIdentifiedTargetSpotted", "[speciesname]", closestEnemy.DisplayName), null, 0.0f, "identifiedtargetspotted", 30.0f);
}
else
{
character.Speak(TextManager.Get("DialogUnidentifiedTargetSpotted"), null, 0.0f, "unidentifiedtargetspotted", 5.0f);
}
}
else if (GameMain.Config.EncounteredCreatures.None(name => name.Equals(closestEnemy.SpeciesName, StringComparison.OrdinalIgnoreCase)))
{
character.Speak(TextManager.Get("DialogUnidentifiedTargetSpotted"), null, 0.0f, "unidentifiedtargetspotted", 5.0f);
}
character.AddEncounter(closestEnemy);
}
character.AIController.SelectTarget(closestEnemy.AiTarget);
}
else if (closestEnemy == null && character.IsOnPlayerTeam)
{
character.Speak(TextManager.Get("DialogIceSpireSpotted"), null, 0.0f, "icespirespotted", 60.0f);
}
character.CursorPosition = targetPos.Value;
if (character.Submarine != null)
{
character.CursorPosition -= character.Submarine.Position;
}
float enemyAngle = MathUtils.VectorToAngle(closestEnemy.WorldPosition - item.WorldPosition);
float enemyAngle = MathUtils.VectorToAngle(targetPos.Value - 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)
Vector2 end = ConvertUnits.ToSimUnits(targetPos.Value);
if (closestEnemy != null && 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, allowInsideFixture: true,
customPredicate: (Fixture f) => { return !item.StaticFixtures.Contains(f); });
customPredicate: (Fixture f) =>
{
if (f.UserData is Item i && i.GetComponent<Turret>() != null) { return false; }
return !item.StaticFixtures.Contains(f);
});
if (pickedBody == null) { return false; }
Character targetCharacter = null;
if (pickedBody.UserData is Character c)
@@ -905,17 +1065,30 @@ namespace Barotrauma.Items.Components
// Don't shoot friendly submarines.
if (sub.TeamID == Item.Submarine.TeamID) { return false; }
}
else
else if (!(pickedBody.UserData is Voronoi2.VoronoiCell cell && cell.IsDestructible))
{
// Hit something else, probably a level wall
return false;
}
}
character?.Speak(TextManager.GetWithVariable("DialogFireTurret", "[itemname]", item.Name, true), null, 0.0f, "fireturret", 5.0f);
if (character.IsOnPlayerTeam)
{
character.Speak(TextManager.Get("DialogFireTurret"), null, 0.0f, "fireturret", 10.0f);
}
character.SetInput(InputType.Shoot, true, true);
return false;
}
private bool CheckTurretAngle(float angle)
{
float midRotation = (minRotation + maxRotation) / 2.0f;
while (midRotation - angle < -MathHelper.Pi) { angle -= MathHelper.TwoPi; }
while (midRotation - angle > MathHelper.Pi) { angle += MathHelper.TwoPi; }
return angle >= minRotation && angle <= maxRotation;
}
private bool CheckTurretAngle(Vector2 target) => CheckTurretAngle(-MathUtils.VectorToAngle(target - item.WorldPosition));
protected override void RemoveComponentSpecific()
{
base.RemoveComponentSpecific();
@@ -960,7 +1133,6 @@ namespace Barotrauma.Items.Components
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>();
@@ -997,7 +1169,22 @@ namespace Barotrauma.Items.Components
public override void FlipY(bool relativeToSub)
{
BaseRotation = MathHelper.ToDegrees(MathUtils.WrapAngleTwoPi(MathHelper.ToRadians(BaseRotation - 180)));
BaseRotation = MathHelper.ToDegrees(MathUtils.WrapAngleTwoPi(MathHelper.ToRadians(180 - BaseRotation)));
minRotation = -minRotation;
maxRotation = -maxRotation;
var temp = minRotation;
minRotation = maxRotation;
maxRotation = temp;
while (minRotation < 0)
{
minRotation += MathHelper.TwoPi;
maxRotation += MathHelper.TwoPi;
}
rotation = (minRotation + maxRotation) / 2;
UpdateTransformedBarrelPos();
}
@@ -1016,6 +1203,8 @@ namespace Barotrauma.Items.Components
resetUserTimer = 10.0f;
break;
case "trigger_in":
if (signal == "0") { return; }
lightComponent.IsOn = !lightComponent.IsOn;
item.Use((float)Timing.Step, sender);
user = sender;
resetUserTimer = 10.0f;
@@ -1041,6 +1230,26 @@ namespace Barotrauma.Items.Components
}
}
private Vector2? loadedRotationLimits;
private float? loadedBaseRotation;
public override void Load(XElement componentElement, bool usePrefabValues, IdRemap idRemap)
{
base.Load(componentElement, usePrefabValues, idRemap);
loadedRotationLimits = componentElement.GetAttributeVector2("rotationlimits", RotationLimits);
loadedBaseRotation = componentElement.GetAttributeFloat("baserotation", componentElement.Parent.GetAttributeFloat("rotation", BaseRotation));
}
public override void OnItemLoaded()
{
base.OnItemLoaded();
FindLightComponent();
if (!loadedBaseRotation.HasValue)
{
if (item.FlippedX) { FlipX(relativeToSub: false); }
if (item.FlippedY) { FlipY(relativeToSub: false); }
}
}
public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
{
if (extraData.Length > 2)