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
@@ -14,7 +14,7 @@ namespace Barotrauma
partial class CharacterInventory : Inventory
{
private Character character;
private readonly Character character;
public InvSlotType[] SlotTypes
{
@@ -33,8 +33,14 @@ namespace Barotrauma
private set;
}
private static string[] ParseSlotTypes(XElement element)
{
string slotString = element.GetAttributeString("slots", null);
return slotString == null ? new string[0] : slotString.Split(',');
}
public CharacterInventory(XElement element, Character character)
: base(character, element.GetAttributeString("slots", "").Split(',').Count())
: base(character, ParseSlotTypes(element).Length)
{
this.character = character;
IsEquipped = new bool[capacity];
@@ -42,7 +48,7 @@ namespace Barotrauma
AccessibleWhenAlive = element.GetAttributeBool("accessiblewhenalive", true);
string[] slotTypeNames = element.GetAttributeString("slots", "").Split(',');
string[] slotTypeNames = ParseSlotTypes(element);
System.Diagnostics.Debug.Assert(slotTypeNames.Length == capacity);
for (int i = 0; i < capacity; i++)
@@ -56,11 +62,9 @@ namespace Barotrauma
SlotTypes[i] = parsedSlotType;
switch (SlotTypes[i])
{
//case InvSlotType.Head:
//case InvSlotType.OuterClothes:
case InvSlotType.LeftHand:
case InvSlotType.RightHand:
hideEmptySlot[i] = true;
slots[i].HideIfEmpty = true;
break;
}
}
@@ -83,7 +87,7 @@ namespace Barotrauma
continue;
}
Entity.Spawner?.AddToSpawnQueue(itemPrefab, this);
Entity.Spawner?.AddToSpawnQueue(itemPrefab, this, ignoreLimbSlots: subElement.GetAttributeBool("forcetoslot", false));
}
}
@@ -91,25 +95,44 @@ namespace Barotrauma
public int FindLimbSlot(InvSlotType limbSlot)
{
for (int i = 0; i < Items.Length; i++)
for (int i = 0; i < slots.Length; i++)
{
if (SlotTypes[i] == limbSlot) { return i; }
}
return -1;
}
public Item GetItemInLimbSlot(InvSlotType limbSlot)
{
for (int i = 0; i < slots.Length; i++)
{
if (SlotTypes[i] == limbSlot) { return slots[i].FirstOrDefault(); }
}
return null;
}
public bool IsInLimbSlot(Item item, InvSlotType limbSlot)
{
for (int i = 0; i < Items.Length; i++)
for (int i = 0; i < slots.Length; i++)
{
if (Items[i] == item && SlotTypes[i] == limbSlot) { return true; }
if (SlotTypes[i] == limbSlot && slots[i].Contains(item)) { return true; }
}
return false;
}
public override bool CanBePut(Item item, int i)
{
return base.CanBePut(item, i) && item.AllowedSlots.Contains(SlotTypes[i]);
return
base.CanBePut(item, i) && item.AllowedSlots.Contains(SlotTypes[i]) &&
(SlotTypes[i] == InvSlotType.Any || slots[i].ItemCount < 1);
}
public override bool CanBePut(ItemPrefab itemPrefab, int i)
{
return
base.CanBePut(itemPrefab, i) &&
(SlotTypes[i] == InvSlotType.Any || slots[i].ItemCount < 1);
}
public bool CanBeAutoMovedToCorrectSlots(Item item)
@@ -118,9 +141,9 @@ namespace Barotrauma
foreach (var allowedSlot in item.AllowedSlots)
{
InvSlotType slotsFree = InvSlotType.None;
for (int i = 0; i < Items.Length; i++)
for (int i = 0; i < slots.Length; i++)
{
if (allowedSlot.HasFlag(SlotTypes[i]) && Items[i] == null) { slotsFree |= SlotTypes[i]; }
if (allowedSlot.HasFlag(SlotTypes[i]) && slots[i].Empty()) { slotsFree |= SlotTypes[i]; }
}
if (allowedSlot == slotsFree) { return true; }
}
@@ -130,7 +153,7 @@ namespace Barotrauma
/// <summary>
/// If there is no room in the generic inventory (InvSlotType.Any), check if the item can be auto-equipped into its respective limbslot
/// </summary>
public bool TryPutItemWithAutoEquipCheck(Item item, Character user, List<InvSlotType> allowedSlots = null, bool createNetworkEvent = true)
public bool TryPutItemWithAutoEquipCheck(Item item, Character user, IEnumerable<InvSlotType> allowedSlots = null, bool createNetworkEvent = true)
{
// Does not auto-equip the item if specified and no suitable any slot found (for example handcuffs are not auto-equipped)
if (item.AllowedSlots.Contains(InvSlotType.Any))
@@ -148,7 +171,7 @@ namespace Barotrauma
/// <summary>
/// If there is room, puts the item in the inventory and returns true, otherwise returns false
/// </summary>
public override bool TryPutItem(Item item, Character user, List<InvSlotType> allowedSlots = null, bool createNetworkEvent = true)
public override bool TryPutItem(Item item, Character user, IEnumerable<InvSlotType> allowedSlots = null, bool createNetworkEvent = true)
{
if (allowedSlots == null || !allowedSlots.Any()) { return false; }
if (item == null)
@@ -174,7 +197,7 @@ namespace Barotrauma
int currentSlot = -1;
for (int i = 0; i < capacity; i++)
{
if (Items[i] == item)
if (slots[i].Contains(item))
{
currentSlot = i;
if (allowedSlots.Any(a => a.HasFlag(SlotTypes[i])))
@@ -208,18 +231,18 @@ namespace Barotrauma
bool free = true;
for (int i = 0; i < capacity; i++)
{
if (allowedSlot.HasFlag(SlotTypes[i]) && item.AllowedSlots.Any(s => s.HasFlag(SlotTypes[i])) && Items[i] != null && Items[i] != item)
if (allowedSlot.HasFlag(SlotTypes[i]) && item.AllowedSlots.Any(s => s.HasFlag(SlotTypes[i])) && slots[i].Items.Any(it => it != item))
{
#if CLIENT
if (PersonalSlots.HasFlag(SlotTypes[i])) { hidePersonalSlots = false; }
#endif
if (!Items[i].AllowedSlots.Contains(InvSlotType.Any) || !TryPutItem(Items[i], character, new List<InvSlotType> { InvSlotType.Any }, true))
if (!slots[i].First().AllowedSlots.Contains(InvSlotType.Any) || !TryPutItem(slots[i].FirstOrDefault(), character, new List<InvSlotType> { InvSlotType.Any }, true))
{
free = false;
#if CLIENT
for (int j = 0; j < capacity; j++)
{
if (slots != null && Items[j] == Items[i]) slots[j].ShowBorderHighlight(GUI.Style.Red, 0.1f, 0.9f);
if (visualSlots != null && slots[j] == slots[i]) { visualSlots[j].ShowBorderHighlight(GUI.Style.Red, 0.1f, 0.9f); }
}
#endif
}
@@ -230,7 +253,7 @@ namespace Barotrauma
for (int i = 0; i < capacity; i++)
{
if (allowedSlot.HasFlag(SlotTypes[i]) && item.AllowedSlots.Any(s => s.HasFlag(SlotTypes[i])) && Items[i] == null)
if (allowedSlot.HasFlag(SlotTypes[i]) && item.AllowedSlots.Any(s => s.HasFlag(SlotTypes[i])) && slots[i].Empty())
{
#if CLIENT
if (PersonalSlots.HasFlag(SlotTypes[i])) { hidePersonalSlots = false; }
@@ -238,7 +261,7 @@ namespace Barotrauma
bool removeFromOtherSlots = item.ParentInventory != this;
if (placedInSlot == -1 && inWrongSlot)
{
if (!hideEmptySlot[i] || SlotTypes[currentSlot] != InvSlotType.Any) removeFromOtherSlots = true;
if (!slots[i].HideIfEmpty || SlotTypes[currentSlot] != InvSlotType.Any) { removeFromOtherSlots = true; }
}
PutItem(item, i, user, removeFromOtherSlots, createNetworkEvent);
@@ -254,35 +277,51 @@ namespace Barotrauma
public int CheckIfAnySlotAvailable(Item item, bool inWrongSlot)
{
for (int i = 0; i < capacity; i++)
//attempt to stack first
for (int i = 0; i < capacity; i++)
{
if (SlotTypes[i] != InvSlotType.Any) { continue; }
if (!slots[i].Empty() && CanBePut(item, i))
{
if (SlotTypes[i] != InvSlotType.Any) continue;
if (Items[i] == item)
{
return i;
}
}
for (int i = 0; i < capacity; i++)
{
if (SlotTypes[i] != InvSlotType.Any) continue;
if (inWrongSlot)
{
if (Items[i] != item && Items[i] != null) continue;
}
else
{
if (Items[i] != null) continue;
}
return i;
}
}
for (int i = 0; i < capacity; i++)
{
if (SlotTypes[i] != InvSlotType.Any) { continue; }
if (slots[i].Contains(item))
{
return i;
}
}
for (int i = 0; i < capacity; i++)
{
if (SlotTypes[i] != InvSlotType.Any) { continue; }
if (CanBePut(item, i))
{
return i;
}
}
for (int i = 0; i < capacity; i++)
{
if (SlotTypes[i] != InvSlotType.Any) { continue; }
if (inWrongSlot)
{
//another item already in the slot
if (slots[i].Any() && slots[i].Items.Any(it => it != item)) { continue; }
}
else
{
if (!CanBePut(item, i)) { continue; }
}
return i;
}
return -1;
}
public override bool TryPutItem(Item item, int index, bool allowSwapping, bool allowCombine, Character user, bool createNetworkEvent = true)
{
if (index < 0 || index >= Items.Length)
if (index < 0 || index >= slots.Length)
{
string errorMsg = "CharacterInventory.TryPutItem failed: index was out of range(" + index + ").\n" + Environment.StackTrace.CleanupStackTrace();
GameAnalyticsManager.AddErrorEventOnce("CharacterInventory.TryPutItem:IndexOutOfRange", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
@@ -292,18 +331,16 @@ namespace Barotrauma
if (PersonalSlots.HasFlag(SlotTypes[index])) { hidePersonalSlots = false; }
#endif
//there's already an item in the slot
if (Items[index] != null)
if (slots[index].Any())
{
if (Items[index] == item) return false;
if (slots[index].Contains(item)) { return false; }
return base.TryPutItem(item, index, allowSwapping, allowCombine, user, createNetworkEvent);
}
if (SlotTypes[index] == InvSlotType.Any)
{
if (!item.AllowedSlots.Contains(InvSlotType.Any)) return false;
if (Items[index] != null) return Items[index] == item;
if (!item.AllowedSlots.Contains(InvSlotType.Any)) { return false; }
if (slots[index].Any()) { return slots[index].Contains(item); }
PutItem(item, index, user, true, createNetworkEvent);
return true;
}
@@ -311,28 +348,24 @@ namespace Barotrauma
InvSlotType placeToSlots = InvSlotType.None;
bool slotsFree = true;
List<InvSlotType> allowedSlots = item.AllowedSlots;
foreach (InvSlotType allowedSlot in allowedSlots)
foreach (InvSlotType allowedSlot in item.AllowedSlots)
{
if (!allowedSlot.HasFlag(SlotTypes[index])) continue;
if (!allowedSlot.HasFlag(SlotTypes[index])) { continue; }
#if CLIENT
if (PersonalSlots.HasFlag(allowedSlot)) { hidePersonalSlots = false; }
#endif
for (int i = 0; i < capacity; i++)
{
if (allowedSlot.HasFlag(SlotTypes[i]) && Items[i] != null && Items[i] != item)
if (allowedSlot.HasFlag(SlotTypes[i]) && slots[i].Any() && !slots[i].Contains(item))
{
slotsFree = false;
break;
}
placeToSlots = allowedSlot;
}
}
if (!slotsFree) return false;
if (!slotsFree) { return false; }
return TryPutItem(item, user, new List<InvSlotType>() { placeToSlots }, createNetworkEvent);
}
@@ -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)
@@ -1,4 +1,5 @@
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
@@ -9,17 +10,209 @@ namespace Barotrauma
{
partial class Inventory : IServerSerializable, IClientSerializable
{
public const int MaxStackSize = 32;
public class ItemSlot
{
private readonly List<Item> items = new List<Item>(MaxStackSize);
public bool HideIfEmpty;
public IEnumerable<Item> Items
{
get { return items; }
}
public int ItemCount
{
get { return items.Count; }
}
public bool CanBePut(Item item)
{
if (item == null) { return false; }
if (items.Count > 0)
{
if (item.IsFullCondition)
{
if (items.Any(it => !it.IsFullCondition)) { return false; }
}
else if (MathUtils.NearlyEqual(item.Condition, 0.0f))
{
if (items.Any(it => !MathUtils.NearlyEqual(it.Condition, 0.0f))) { return false; }
}
else
{
return false;
}
if (items[0].Prefab.Identifier != item.Prefab.Identifier ||
items.Count + 1 > item.Prefab.MaxStackSize)
{
return false;
}
}
return true;
}
public bool CanBePut(ItemPrefab itemPrefab)
{
if (itemPrefab == null) { return false; }
if (items.Count > 0)
{
if (items.Any(it => !it.IsFullCondition)) { return false; }
if (items[0].Prefab.Identifier != itemPrefab.Identifier ||
items.Count + 1 > itemPrefab.MaxStackSize)
{
return false;
}
}
return true;
}
/// <param name="maxStackSize">Defaults to <see cref="ItemPrefab.MaxStackSize"/> if null</param>
public int HowManyCanBePut(ItemPrefab itemPrefab, int? maxStackSize = null)
{
if (itemPrefab == null) { return 0; }
maxStackSize ??= itemPrefab.MaxStackSize;
if (items.Count > 0)
{
if (items.Any(it => !it.IsFullCondition)) { return 0; }
if (items[0].Prefab.Identifier != itemPrefab.Identifier) { return 0; }
return maxStackSize.Value - items.Count;
}
else
{
return maxStackSize.Value;
}
}
public void Add(Item item)
{
if (item == null)
{
throw new InvalidOperationException("Tried to add a null item to an inventory slot.");
}
if (items.Count > 0)
{
if (items[0].Prefab.Identifier != item.Prefab.Identifier)
{
throw new InvalidOperationException("Tried to stack different types of items.");
}
else if (items.Count + 1 > item.Prefab.MaxStackSize)
{
throw new InvalidOperationException("Tried to add an item to a full inventory slot (stack already full).");
}
}
if (items.Contains(item)) { return; }
items.Add(item);
}
/// <summary>
/// Removes one item from the slot
/// </summary>
public Item RemoveItem()
{
if (items.Count == 0) { return null; }
var item = items[0];
items.RemoveAt(0);
return item;
}
public void RemoveItem(Item item)
{
items.Remove(item);
}
/// <summary>
/// Removes all items from the slot
/// </summary>
public void RemoveAllItems()
{
items.Clear();
}
public bool Any()
{
return items.Count > 0;
}
public bool Empty()
{
return items.Count == 0;
}
public Item First()
{
return items[0];
}
public Item FirstOrDefault()
{
return items.FirstOrDefault();
}
public Item LastOrDefault()
{
return items.LastOrDefault();
}
public bool Contains(Item item)
{
return items.Contains(item);
}
}
public readonly Entity Owner;
protected readonly int capacity;
public Item[] Items;
protected bool[] hideEmptySlot;
protected readonly ItemSlot[] slots;
public bool Locked;
protected float syncItemsDelay;
/// <summary>
/// All items contained in the inventory. Stacked items are returned as individual instances. DO NOT modify the contents of the inventory while enumerating this list.
/// </summary>
public IEnumerable<Item> AllItems
{
get
{
for (int i = 0; i < capacity; i++)
{
foreach (var item in slots[i].Items)
{
bool duplicateFound = false;
for (int j = 0; j < i; j++)
{
if (slots[j].Items.Contains(item))
{
duplicateFound = true;
break;
}
}
if (!duplicateFound) { yield return item; }
}
}
}
}
private readonly List<Item> allItemsList = new List<Item>();
/// <summary>
/// All items contained in the inventory. Allows modifying the contents of the inventory while being enumerated.
/// </summary>
public IEnumerable<Item> AllItemsMod
{
get
{
allItemsList.Clear();
allItemsList.AddRange(AllItems);
return allItemsList;
}
}
public int Capacity
{
get { return capacity; }
@@ -31,8 +224,11 @@ namespace Barotrauma
this.Owner = owner;
Items = new Item[capacity];
hideEmptySlot = new bool[capacity];
slots = new ItemSlot[capacity];
for (int i = 0; i < capacity; i++)
{
slots[i] = new ItemSlot();
}
#if CLIENT
this.slotsPerRow = slotsPerRow;
@@ -54,71 +250,117 @@ namespace Barotrauma
#endif
}
public static Item FindItemRecursive(Item item, Predicate<Item> condition)
/// <summary>
/// Is the item contained in this inventory. Does not recursively check items inside items.
/// </summary>
public bool Contains(Item item)
{
if (condition.Invoke(item))
return slots.Any(i => i.Contains(item));
}
/// <summary>
/// Return the first item in the inventory, or null if the inventory is empty.
/// </summary>
public Item FirstOrDefault()
{
foreach (var itemSlot in slots)
{
return item;
var item = itemSlot.FirstOrDefault();
if (item != null) { return item; }
}
var containers = item.GetComponents<ItemContainer>();
if (containers != null)
{
foreach (var container in containers)
{
foreach (var inventoryItem in container.Inventory.Items)
{
var findItem = FindItemRecursive(inventoryItem, condition);
if (findItem != null)
{
return findItem;
}
}
}
}
return null;
}
/// <summary>
/// Return the last item in the inventory, or null if the inventory is empty.
/// </summary>
public Item LastOrDefault()
{
for (int i = slots.Length - 1; i >= 0; i--)
{
var item = slots[i].LastOrDefault();
if (item != null) { return item; }
}
return null;
}
/// <summary>
/// Get the item stored in the specified inventory slot. If the slot contains a stack of items, returns the first item in the stack.
/// </summary>
public Item GetItemAt(int index)
{
if (index < 0 || index >= slots.Length) { return null; }
return slots[index].FirstOrDefault();
}
/// <summary>
/// Get all the item stored in the specified inventory slot. Can return more than one item if the slot contains a stack of items.
/// </summary>
public IEnumerable<Item> GetItemsAt(int index)
{
if (index < 0 || index >= slots.Length) { return Enumerable.Empty<Item>(); }
return slots[index].Items;
}
/// <summary>
/// Find the index of the first slot the item is contained in.
/// </summary>
public int FindIndex(Item item)
{
for (int i = 0; i < capacity; i++)
{
if (Items[i] == item) return i;
if (slots[i].Contains(item)) { return i; }
}
return -1;
}
/// Returns true if the item owns any of the parent inventories
/// <summary>
/// Find the indices of all the slots the item is contained in (two-hand items for example can be in multiple slots). Note that this method instantiates a new list.
/// </summary>
public List<int> FindIndices(Item item)
{
List<int> indices = new List<int>();
for (int i = 0; i < capacity; i++)
{
if (slots[i].Contains(item)) { indices.Add(i); }
}
return indices;
}
/// <summary>
/// Returns true if the item owns any of the parent inventories.
/// </summary>
public virtual bool ItemOwnsSelf(Item item)
{
if (Owner == null) return false;
if (!(Owner is Item)) return false;
if (Owner == null) { return false; }
if (!(Owner is Item)) { return false; }
Item ownerItem = Owner as Item;
if (ownerItem == item) return true;
if (ownerItem.ParentInventory == null) return false;
if (ownerItem == item) { return true; }
if (ownerItem.ParentInventory == null) { return false; }
return ownerItem.ParentInventory.ItemOwnsSelf(item);
}
public virtual int FindAllowedSlot(Item item)
{
if (ItemOwnsSelf(item)) return -1;
if (ItemOwnsSelf(item)) { return -1; }
for (int i = 0; i < capacity; i++)
{
//item is already in the inventory!
if (Items[i] == item) return -1;
if (slots[i].Contains(item)) { return -1; }
}
for (int i = 0; i < capacity; i++)
{
if (Items[i] == null) return i;
if (slots[i].CanBePut(item)) { return i; }
}
return -1;
}
/// <summary>
/// Can the item be put in the inventory (i.e. is there a suitable free slot or a stack the item can be put in).
/// </summary>
public bool CanBePut(Item item)
{
for (int i = 0; i < capacity; i++)
@@ -128,20 +370,54 @@ namespace Barotrauma
return false;
}
/// <summary>
/// Can the item be put in the specified slot.
/// </summary>
public virtual bool CanBePut(Item item, int i)
{
if (ItemOwnsSelf(item)) return false;
if (i < 0 || i >= Items.Length) return false;
return (Items[i] == null);
if (ItemOwnsSelf(item)) { return false; }
if (i < 0 || i >= slots.Length) { return false; }
return slots[i].CanBePut(item);
}
public bool CanBePut(ItemPrefab itemPrefab)
{
for (int i = 0; i < capacity; i++)
{
if (CanBePut(itemPrefab, i)) { return true; }
}
return false;
}
public virtual bool CanBePut(ItemPrefab itemPrefab, int i)
{
if (i < 0 || i >= slots.Length) { return false; }
return slots[i].CanBePut(itemPrefab);
}
public int HowManyCanBePut(ItemPrefab itemPrefab)
{
int count = 0;
for (int i = 0; i < capacity; i++)
{
count += HowManyCanBePut(itemPrefab, i);
}
return count;
}
public virtual int HowManyCanBePut(ItemPrefab itemPrefab, int i)
{
if (i < 0 || i >= slots.Length) { return 0; }
return slots[i].HowManyCanBePut(itemPrefab);
}
/// <summary>
/// If there is room, puts the item in the inventory and returns true, otherwise returns false
/// </summary>
public virtual bool TryPutItem(Item item, Character user, List<InvSlotType> allowedSlots = null, bool createNetworkEvent = true)
public virtual bool TryPutItem(Item item, Character user, IEnumerable<InvSlotType> allowedSlots = null, bool createNetworkEvent = true)
{
int slot = FindAllowedSlot(item);
if (slot < 0) return false;
if (slot < 0) { return false; }
PutItem(item, slot, user, true, createNetworkEvent);
return true;
@@ -149,7 +425,7 @@ namespace Barotrauma
public virtual bool TryPutItem(Item item, int i, bool allowSwapping, bool allowCombine, Character user, bool createNetworkEvent = true)
{
if (i < 0 || i >= Items.Length)
if (i < 0 || i >= slots.Length)
{
string errorMsg = "Inventory.TryPutItem failed: index was out of range(" + i + ").\n" + Environment.StackTrace.CleanupStackTrace();
GameAnalyticsManager.AddErrorEventOnce("Inventory.TryPutItem:IndexOutOfRange", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
@@ -158,31 +434,41 @@ namespace Barotrauma
if (Owner == null) return false;
//there's already an item in the slot
if (Items[i] != null && allowCombine)
if (slots[i].Any() && allowCombine)
{
if (Items[i].Combine(item, user))
if (slots[i].First().Combine(item, user))
{
//item in the slot removed as a result of combining -> put this item in the now free slot
if (Items[i] == null)
if (!slots[i].Any())
{
return TryPutItem(item, i, allowSwapping, allowCombine, user, createNetworkEvent);
}
return true;
}
}
if (Items[i] != null && item.ParentInventory != null && allowSwapping)
{
return TrySwapping(i, item, user, createNetworkEvent);
}
else if (CanBePut(item, i))
if (CanBePut(item, i))
{
PutItem(item, i, user, true, createNetworkEvent);
return true;
}
else if (slots[i].Any() && item.ParentInventory != null && allowSwapping)
{
var itemInSlot = slots[i].First();
if (itemInSlot.OwnInventory != null &&
!itemInSlot.OwnInventory.Contains(item) &&
(itemInSlot.GetComponent<ItemContainer>()?.MaxStackSize ?? 0) == 1 &&
itemInSlot.OwnInventory.TrySwapping(0, item, user, createNetworkEvent, swapWholeStack: false))
{
return true;
}
return
TrySwapping(i, item, user, createNetworkEvent, swapWholeStack: true) ||
TrySwapping(i, item, user, createNetworkEvent, swapWholeStack: false);
}
else
{
#if CLIENT
if (slots != null && createNetworkEvent) slots[i].ShowBorderHighlight(GUI.Style.Red, 0.1f, 0.9f);
if (visualSlots != null && createNetworkEvent) { visualSlots[i].ShowBorderHighlight(GUI.Style.Red, 0.1f, 0.9f); }
#endif
return false;
}
@@ -190,14 +476,14 @@ namespace Barotrauma
protected virtual void PutItem(Item item, int i, Character user, bool removeItem = true, bool createNetworkEvent = true)
{
if (i < 0 || i >= Items.Length)
if (i < 0 || i >= slots.Length)
{
string errorMsg = "Inventory.PutItem failed: index was out of range(" + i + ").\n" + Environment.StackTrace.CleanupStackTrace();
GameAnalyticsManager.AddErrorEventOnce("Inventory.PutItem:IndexOutOfRange", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
return;
}
if (Owner == null) return;
if (Owner == null) { return; }
Inventory prevInventory = item.ParentInventory;
Inventory prevOwnerInventory = item.FindParentInventory(inv => inv is CharacterInventory);
@@ -206,20 +492,23 @@ namespace Barotrauma
{
CreateNetworkEvent();
//also delay syncing the inventory the item was inside
if (prevInventory != null && prevInventory != this) prevInventory.syncItemsDelay = 1.0f;
if (prevInventory != null && prevInventory != this) { prevInventory.syncItemsDelay = 1.0f; }
}
if (removeItem)
{
item.Drop(user);
if (item.ParentInventory != null) item.ParentInventory.RemoveItem(item);
if (item.ParentInventory != null) { item.ParentInventory.RemoveItem(item); }
}
Items[i] = item;
slots[i].Add(item);
item.ParentInventory = this;
#if CLIENT
if (slots != null) slots[i].ShowBorderHighlight(Color.White, 0.1f, 0.4f);
if (visualSlots != null)
{
visualSlots[i]?.ShowBorderHighlight(Color.White, 0.1f, 0.4f);
}
#endif
if (item.body != null)
@@ -258,33 +547,49 @@ namespace Barotrauma
{
for (int i = 0; i < capacity; i++)
{
if (Items[i] != null) return false;
if (slots[i].Any()) { return false; }
}
return true;
}
public bool IsFull()
/// <summary>
/// Is there room to put more items in the inventory. Doesn't take stacking into account by default.
/// </summary>
/// <param name="takeStacksIntoAccount">If true, the inventory is not considered full if all the stacks are not full.</param>
public virtual bool IsFull(bool takeStacksIntoAccount = false)
{
for (int i = 0; i < capacity; i++)
if (takeStacksIntoAccount)
{
if (Items[i] == null) return false;
for (int i = 0; i < capacity; i++)
{
if (!slots[i].Any()) { return false; }
var item = slots[i].FirstOrDefault();
if (slots[i].ItemCount < item.Prefab.MaxStackSize) { return false; }
}
}
else
{
for (int i = 0; i < capacity; i++)
{
if (!slots[i].Any()) { return false; }
}
}
return true;
}
protected bool TrySwapping(int index, Item item, Character user, bool createNetworkEvent)
protected bool TrySwapping(int index, Item item, Character user, bool createNetworkEvent, bool swapWholeStack)
{
if (item?.ParentInventory == null || Items[index] == null) return false;
if (item?.ParentInventory == null || !slots[index].Any()) { return false; }
//swap to InvSlotType.Any if possible
Inventory otherInventory = item.ParentInventory;
bool otherIsEquipped = false;
int otherIndex = -1;
for (int i = 0; i < otherInventory.Items.Length; i++)
for (int i = 0; i < otherInventory.slots.Length; i++)
{
if (otherInventory.Items[i] != item) continue;
if (!otherInventory.slots[i].Contains(item)) { continue; }
if (otherInventory is CharacterInventory characterInventory)
{
if (characterInventory.SlotTypes[i] == InvSlotType.Any)
@@ -298,89 +603,138 @@ namespace Barotrauma
}
}
}
if (otherIndex == -1) otherIndex = Array.IndexOf(otherInventory.Items, item);
Item existingItem = Items[index];
for (int j = 0; j < otherInventory.capacity; j++)
if (otherIndex == -1)
{
if (otherInventory.Items[j] == item) { otherInventory.Items[j] = null; }
}
for (int j = 0; j < capacity; j++)
{
if (Items[j] == existingItem) { Items[j] = null; }
otherIndex = otherInventory.FindIndex(item);
if (otherIndex == -1)
{
DebugConsole.ThrowError("Something went wrong when trying to swap items between inventory slots: couldn't find the source item from it's inventory.\n" + Environment.StackTrace.CleanupStackTrace());
return false;
}
}
List<Item> existingItems = new List<Item>();
if (swapWholeStack)
{
existingItems.AddRange(slots[index].Items);
for (int j = 0; j < capacity; j++)
{
if (existingItems.Any(existingItem => slots[j].Contains(existingItem))) { slots[j].RemoveAllItems(); }
}
}
else
{
existingItems.Add(slots[index].FirstOrDefault());
slots[index].RemoveItem(existingItems.First());
}
List<Item> stackedItems = new List<Item>();
if (swapWholeStack)
{
for (int j = 0; j < otherInventory.capacity; j++)
{
if (otherInventory.slots[j].Contains(item))
{
stackedItems.AddRange(otherInventory.slots[j].Items);
otherInventory.slots[j].RemoveAllItems();
}
}
}
else
{
stackedItems.Add(item);
otherInventory.slots[otherIndex].RemoveItem(item);
}
(otherInventory.Owner as Character)?.DeselectItem(item);
(otherInventory.Owner as Character)?.DeselectItem(existingItem);
bool swapSuccessful = false;
if (otherIsEquipped)
{
swapSuccessful =
TryPutItem(item, index, false, false, user, createNetworkEvent) &&
otherInventory.TryPutItem(existingItem, otherIndex, false, false, user, createNetworkEvent);
stackedItems.Distinct().All(stackedItem => TryPutItem(stackedItem, index, false, false, user, createNetworkEvent))
&&
(existingItems.All(existingItem => otherInventory.TryPutItem(existingItem, otherIndex, false, false, user, createNetworkEvent)) ||
existingItems.Count == 1 && otherInventory.TryPutItem(existingItems.First(), user, CharacterInventory.anySlot, createNetworkEvent));
}
else
{
swapSuccessful =
otherInventory.TryPutItem(existingItem, otherIndex, false, false, user, createNetworkEvent) &&
TryPutItem(item, index, false, false, user, createNetworkEvent);
swapSuccessful =
(existingItems.All(existingItem => otherInventory.TryPutItem(existingItem, otherIndex, false, false, user, createNetworkEvent)) ||
existingItems.Count == 1 && otherInventory.TryPutItem(existingItems.First(),user, CharacterInventory.anySlot, createNetworkEvent))
&&
stackedItems.Distinct().All(stackedItem => TryPutItem(stackedItem, index, false, false, user, createNetworkEvent));
}
//if the item in the slot can be moved to the slot of the moved item
if (swapSuccessful)
{
System.Diagnostics.Debug.Assert(Items[index] == item, "Something when wrong when swapping items, item is not present in the inventory.");
System.Diagnostics.Debug.Assert(otherInventory.Items[otherIndex] == existingItem, "Something when wrong when swapping items, item is not present in the other inventory.");
System.Diagnostics.Debug.Assert(slots[index].Contains(item), "Something when wrong when swapping items, item is not present in the inventory.");
System.Diagnostics.Debug.Assert(otherInventory.Contains(existingItems.FirstOrDefault()), "Something when wrong when swapping items, item is not present in the other inventory.");
#if CLIENT
if (slots != null)
if (visualSlots != null)
{
for (int j = 0; j < capacity; j++)
{
if (Items[j] == item) slots[j].ShowBorderHighlight(GUI.Style.Green, 0.1f, 0.9f);
if (slots[j].Contains(item)) { visualSlots[j].ShowBorderHighlight(GUI.Style.Green, 0.1f, 0.9f); }
}
for (int j = 0; j < otherInventory.capacity; j++)
{
if (otherInventory.Items[j] == existingItem) otherInventory.slots[j].ShowBorderHighlight(GUI.Style.Green, 0.1f, 0.9f);
if (otherInventory.slots[j].Contains(existingItems.FirstOrDefault())) { otherInventory.visualSlots[j].ShowBorderHighlight(GUI.Style.Green, 0.1f, 0.9f); }
}
}
#endif
return true;
}
else
else //swapping the items failed -> move them back to where they were
{
for (int j = 0; j < capacity; j++)
if (swapWholeStack)
{
if (Items[j] == item) Items[j] = null;
foreach (Item stackedItem in stackedItems)
{
for (int j = 0; j < capacity; j++)
{
if (slots[j].Contains(stackedItem)) { slots[j].RemoveItem(stackedItem); };
}
}
foreach (Item existingItem in existingItems)
{
for (int j = 0; j < otherInventory.capacity; j++)
{
if (otherInventory.slots[j].Contains(existingItem)) { otherInventory.slots[j].RemoveItem(existingItem); }
}
}
}
for (int j = 0; j < otherInventory.capacity; j++)
else
{
if (otherInventory.Items[j] == existingItem) otherInventory.Items[j] = null;
for (int j = 0; j < capacity; j++)
{
if (slots[j].Contains(item)) { slots[j].RemoveAllItems(); };
}
for (int j = 0; j < otherInventory.capacity; j++)
{
if (otherInventory.slots[j].Contains(existingItems.FirstOrDefault())) { otherInventory.slots[j].RemoveAllItems(); }
}
}
if (otherIsEquipped)
{
TryPutItem(existingItem, index, false, false, user, createNetworkEvent);
otherInventory.TryPutItem(item, otherIndex, false, false, user, createNetworkEvent);
existingItems.ForEach(existingItem => TryPutItem(existingItem, index, false, false, user, createNetworkEvent));
stackedItems.ForEach(stackedItem => otherInventory.TryPutItem(stackedItem, otherIndex, false, false, user, createNetworkEvent));
}
else
{
otherInventory.TryPutItem(item, otherIndex, false, false, user, createNetworkEvent);
TryPutItem(existingItem, index, false, false, user, createNetworkEvent);
stackedItems.ForEach(stackedItem => otherInventory.TryPutItem(stackedItem, otherIndex, false, false, user, createNetworkEvent));
existingItems.ForEach(existingItem => TryPutItem(existingItem, index, false, false, user, createNetworkEvent));
}
//swapping the items failed -> move them back to where they were
//otherInventory.TryPutItem(item, otherIndex, false, false, user, createNetworkEvent);
//TryPutItem(existingItem, index, false, false, user, createNetworkEvent);
#if CLIENT
if (slots != null)
if (visualSlots != null)
{
for (int j = 0; j < capacity; j++)
{
if (Items[j] == existingItem)
if (slots[j].Contains(existingItems.FirstOrDefault()))
{
slots[j].ShowBorderHighlight(GUI.Style.Red, 0.1f, 0.9f);
visualSlots[j].ShowBorderHighlight(GUI.Style.Red, 0.1f, 0.9f);
}
}
}
@@ -400,10 +754,10 @@ namespace Barotrauma
public Item FindItem(Func<Item, bool> predicate, bool recursive)
{
Item match = Items.FirstOrDefault(i => i != null && predicate(i));
Item match = AllItems.FirstOrDefault(i => predicate(i));
if (match == null && recursive)
{
foreach (var item in Items)
foreach (var item in AllItems)
{
if (item == null) { continue; }
if (item.OwnInventory != null)
@@ -419,13 +773,12 @@ namespace Barotrauma
return match;
}
public List<Item> FindAllItems(Func<Item, bool> predicate, bool recursive = false, List<Item> list = null)
public List<Item> FindAllItems(Func<Item, bool> predicate = null, bool recursive = false, List<Item> list = null)
{
list ??= new List<Item>();
foreach (var item in Items)
foreach (var item in AllItems)
{
if (item == null) { continue; }
if (predicate(item))
if (predicate == null || predicate(item))
{
list.Add(item);
}
@@ -448,30 +801,57 @@ namespace Barotrauma
public Item FindItemByIdentifier(string identifier, bool recursive = false)
{
if (identifier == null) return null;
if (identifier == null) { return null; }
return FindItem(i => i.Prefab.Identifier == identifier, recursive);
}
public virtual void RemoveItem(Item item)
{
if (item == null) return;
if (item == null) { return; }
//go through the inventory and remove the item from all slots
for (int n = 0; n < capacity; n++)
{
if (Items[n] != item) continue;
Items[n] = null;
if (!slots[n].Contains(item)) { continue; }
slots[n].RemoveItem(item);
item.ParentInventory = null;
}
}
/// <summary>
/// Forces an item to a specific slot. Doesn't remove the item from existing slots/inventories or do any other sanity checks, use with caution!
/// </summary>
public void ForceToSlot(Item item, int index)
{
slots[index].Add(item);
item.ParentInventory = this;
if (item.body != null)
{
item.body.Enabled = false;
item.body.BodyType = FarseerPhysics.BodyType.Dynamic;
}
}
/// <summary>
/// Removes an item from a specific slot. Doesn't do any sanity checks, use with caution!
/// </summary>
public void ForceRemoveFromSlot(Item item, int index)
{
slots[index].RemoveItem(item);
}
public void SharedWrite(IWriteMessage msg, object[] extraData = null)
{
msg.Write((byte)capacity);
for (int i = 0; i < capacity; i++)
{
msg.Write((ushort)(Items[i] == null ? 0 : Items[i].ID));
msg.WriteRangedInteger(slots[i].ItemCount, 0, MaxStackSize);
foreach (Item item in slots[i].Items)
{
msg.Write((ushort)(item == null ? 0 : item.ID));
}
}
}
@@ -482,29 +862,17 @@ namespace Barotrauma
{
for (int i = 0; i < capacity; i++)
{
if (Items[i] == null) continue;
foreach (ItemContainer itemContainer in Items[i].GetComponents<ItemContainer>())
if (!slots[i].Any()) { continue; }
foreach (Item item in slots[i].Items)
{
itemContainer.Inventory.DeleteAllItems();
foreach (ItemContainer itemContainer in item.GetComponents<ItemContainer>())
{
itemContainer.Inventory.DeleteAllItems();
}
}
Items[i].Remove();
slots[i].Items.ForEachMod(it => it.Remove());
slots[i].RemoveAllItems();
}
}
public List<Item> GetAllItems()
{
List<Item> deletedItems = new List<Item>();
for (int i = 0; i < capacity; i++)
{
if (Items[i] == null) continue;
foreach (ItemContainer itemContainer in Items[i].GetComponents<ItemContainer>())
{
deletedItems.AddRange(itemContainer.Inventory.GetAllItems());
}
deletedItems.Add(Items[i]);
}
return deletedItems;
}
}
}
@@ -20,7 +20,7 @@ using Microsoft.Xna.Framework.Graphics;
namespace Barotrauma
{
partial class Item : MapEntity, IDamageable, ISerializableEntity, IServerSerializable, IClientSerializable
partial class Item : MapEntity, IDamageable, IIgnorable, ISerializableEntity, IServerSerializable, IClientSerializable
{
public static List<Item> ItemList = new List<Item>();
public ItemPrefab Prefab => prefab as ItemPrefab;
@@ -76,7 +76,7 @@ namespace Barotrauma
private readonly bool hasWaterStatusEffects;
private Inventory parentInventory;
private readonly Inventory ownInventory;
private readonly ItemInventory ownInventory;
private Rectangle defaultRect;
@@ -171,6 +171,43 @@ namespace Barotrauma
set;
}
/// <summary>
/// Use <see cref="IsPlayerInteractable"/> to also check <see cref="NonInteractable"/>
/// </summary>
[Editable, Serialize(false, true, description: "When enabled, item is interactable only for characters on non-player teams.", alwaysUseInstanceValues: true)]
public bool NonPlayerTeamInteractable
{
get;
set;
}
/// <summary>
/// Checks both <see cref="NonInteractable"/> and <see cref="NonPlayerTeamInteractable"/>
/// </summary>
public bool IsPlayerTeamInteractable
{
get
{
return !NonInteractable && !NonPlayerTeamInteractable;
}
}
/// <summary>
/// Returns interactibility based on whether the character is on a player team
/// </summary>
public bool IsInteractable(Character character)
{
if (character != null && character.IsOnPlayerTeam)
{
return IsPlayerTeamInteractable;
}
else
{
return !NonInteractable;
}
}
private float rotationRad;
[Editable(0.0f, 360.0f, DecimalCount = 1, ValueStep = 1f), Serialize(0.0f, true)]
@@ -411,11 +448,10 @@ namespace Barotrauma
get { return condition; }
set
{
#if CLIENT
if (GameMain.Client != null) return;
#endif
if (!MathUtils.IsValid(value)) return;
if (Indestructible) return;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
if (!MathUtils.IsValid(value)) { return; }
if (Indestructible) { return; }
if (InvulnerableToDamage && value <= condition) { return;}
float prev = condition;
bool wasInFullCondition = IsFullCondition;
@@ -428,7 +464,7 @@ namespace Barotrauma
{
ic.PlaySound(ActionType.OnBroken);
}
if (Screen.Selected == GameMain.SubEditorScreen) return;
if (Screen.Selected == GameMain.SubEditorScreen) { return; }
#endif
ApplyStatusEffects(ActionType.OnBroken, 1.0f, null);
}
@@ -468,10 +504,13 @@ namespace Barotrauma
/// </summary>
public bool Indestructible
{
get { return indestructible ?? Prefab.Indestructible; }
set { indestructible = value; }
get => indestructible ?? Prefab.Indestructible;
set => indestructible = value;
}
[Editable, Serialize(false, isSaveable: true, "When enabled will prevent the item from taking damage from all sources")]
public bool InvulnerableToDamage { get; set; }
public bool StolenDuringRound;
private bool spawnedInOutpost;
@@ -568,12 +607,12 @@ namespace Barotrauma
}
//which type of inventory slots (head, torso, any, etc) the item can be placed in
public List<InvSlotType> AllowedSlots
public IEnumerable<InvSlotType> AllowedSlots
{
get
{
Pickable p = GetComponent<Pickable>();
return (p == null) ? new List<InvSlotType>() { InvSlotType.Any } : p.AllowedSlots;
return (p == null) ? InvSlotType.Any.ToEnumerable() : p.AllowedSlots;
}
}
@@ -591,19 +630,11 @@ namespace Barotrauma
{
get
{
// It's not a good practice to return null if the method tells that it returns a collection, because:
// a) the user has to handle this -> more code and more null reference exceptions
// b) it makes it more difficult to make use of chained function calls (which are quite powerful), although '?' makes it possible
// c) it's against the functional paradigm that e.g. Linq follows (for good reasons)
// In general, it's better to return an empty collection instead,
// but changing it here might cause unwanted implications.
// Also it can be a minor optimization to return null instead of creating an empty collection,
// but if that's the case I'd prefer caching an empty collection and using that instead. Just something to consider in the future.
return ownInventory?.Items.Where(i => i != null);
return ownInventory?.AllItems ?? Enumerable.Empty<Item>();
}
}
public Inventory OwnInventory
public ItemInventory OwnInventory
{
get { return ownInventory; }
}
@@ -645,6 +676,8 @@ namespace Barotrauma
get { return allPropertyObjects; }
}
public bool IgnoreByAI => OrderedToBeIgnored || HasTag("ignorebyai");
public bool OrderedToBeIgnored { get; set; }
public Item(ItemPrefab itemPrefab, Vector2 position, Submarine submarine, ushort id = Entity.NullEntityID)
: this(new Rectangle(
@@ -841,12 +874,14 @@ namespace Barotrauma
DebugConsole.Log("Created " + Name + " (" + ID + ")");
if (Components.Any() && Components.All(ic => ic is Wire || ic is Holdable)) { isWire = true; }
if (Components.Any(ic => ic is Wire) && Components.All(ic => ic is Wire || ic is Holdable)) { isWire = true; }
if (HasTag("logic")) { isLogic = true; }
}
partial void InitProjSpecific();
public bool IsContainerPreferred(ItemContainer container, out bool isPreferencesDefined, out bool isSecondary) => Prefab.IsContainerPreferred(this, container, out isPreferencesDefined, out isSecondary);
public override MapEntity Clone()
{
Item clone = new Item(rect, Prefab, Submarine, callOnItemLoaded: false)
@@ -901,14 +936,12 @@ namespace Barotrauma
component.OnItemLoaded();
}
if (ContainedItems != null)
foreach (Item containedItem in ContainedItems)
{
foreach (Item containedItem in ContainedItems)
{
var containedClone = containedItem.Clone();
clone.ownInventory.TryPutItem(containedClone as Item, null);
}
}
var containedClone = containedItem.Clone();
clone.ownInventory.TryPutItem(containedClone as Item, null);
}
return clone;
}
@@ -1143,13 +1176,13 @@ namespace Barotrauma
{
if (parentInventory != null && parentInventory.Owner != null)
{
if (parentInventory.Owner is Character)
if (parentInventory.Owner is Character character)
{
CurrentHull = ((Character)parentInventory.Owner).AnimController.CurrentHull;
CurrentHull = character.AnimController.CurrentHull;
}
else if (parentInventory.Owner is Item)
else if (parentInventory.Owner is Item item)
{
CurrentHull = ((Item)parentInventory.Owner).CurrentHull;
CurrentHull = item.CurrentHull;
}
Submarine = parentInventory.Owner.Submarine;
@@ -1159,7 +1192,7 @@ namespace Barotrauma
}
CurrentHull = Hull.FindHull(WorldPosition, CurrentHull);
if (body != null && body.Enabled)
if (body != null && body.Enabled && (body.BodyType == BodyType.Dynamic || Submarine == null))
{
Submarine = CurrentHull?.Submarine;
body.Submarine = Submarine;
@@ -1182,7 +1215,7 @@ namespace Barotrauma
}
/// <summary>
/// Is the item or any of its containers of the item set to be ignored?
/// Should this item or any of its containers be ignored by the AI?
/// </summary>
public bool IsThisOrAnyContainerIgnoredByAI()
{
@@ -1310,22 +1343,17 @@ namespace Barotrauma
if (effect.HasTargetType(StatusEffect.TargetType.Contained))
{
var containedItems = ownInventory?.Items;
if (containedItems != null)
foreach (Item containedItem in ContainedItems)
{
foreach (Item containedItem in containedItems)
if (effect.TargetIdentifiers != null &&
!effect.TargetIdentifiers.Contains(containedItem.prefab.Identifier) &&
!effect.TargetIdentifiers.Any(id => containedItem.HasTag(id)))
{
if (containedItem == null) { continue; }
if (effect.TargetIdentifiers != null &&
!effect.TargetIdentifiers.Contains(containedItem.prefab.Identifier) &&
!effect.TargetIdentifiers.Any(id => containedItem.HasTag(id)))
{
continue;
}
hasTargets = true;
targets.Add(containedItem);
continue;
}
hasTargets = true;
targets.Add(containedItem);
}
}
@@ -1387,7 +1415,7 @@ namespace Barotrauma
public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = true)
{
if (Indestructible) { return new AttackResult(); }
if (Indestructible || InvulnerableToDamage) { return new AttackResult(); }
float damageAmount = attack.GetItemDamage(deltaTime);
Condition -= damageAmount;
@@ -1577,8 +1605,7 @@ namespace Barotrauma
body.SetTransform(body.SimPosition + prevSub.SimPosition - Submarine.SimPosition, body.Rotation);
}
var containedItems = ownInventory?.Items;
if (Submarine != prevSub && containedItems != null)
if (Submarine != prevSub)
{
foreach (Item containedItem in ContainedItems)
{
@@ -1666,15 +1693,10 @@ namespace Barotrauma
#endif
}
var containedItems = ownInventory?.Items;
if (containedItems != null)
foreach (Item contained in ContainedItems)
{
foreach (Item contained in containedItems)
{
if (contained == null) { continue; }
if (contained.body != null) { contained.HandleCollision(impact); }
}
}
if (contained.body != null) { contained.HandleCollision(impact); }
}
}
}
@@ -1690,6 +1712,11 @@ namespace Barotrauma
flippedX = false;
return;
}
if (Prefab.AllowRotatingInEditor)
{
rotationRad = MathUtils.WrapAngleTwoPi(-rotationRad);
}
#if CLIENT
if (Prefab.CanSpriteFlipX)
{
@@ -1872,7 +1899,7 @@ namespace Barotrauma
{
if (connections == null) { return; }
if (!connections.TryGetValue(connectionName, out Connection c)) { return; }
SendSignal(stepsTaken, signal, c, sender, power, source, signalStrength);
SendSignal(stepsTaken, signal, c, sender, power, source ?? this, signalStrength);
}
public void SendSignal(int stepsTaken, string signal, Connection connection, Character sender, float power = 0.0f, Item source = null, float signalStrength = 1.0f)
@@ -1884,6 +1911,14 @@ namespace Barotrauma
if (stepsTaken > 10)
{
//if the signal has been passed through this item multiple times already, interrupt it to prevent infinite loops
if (source != null)
{
if (source.LastSentSignalRecipients.Count(recipient => recipient == this) > 2)
{
return;
}
}
//use a coroutine to prevent infinite loops by creating a one
//frame delay if the "signal chain" gets too long
CoroutineManager.StartCoroutine(SendSignal(signal, connection, sender, power, signalStrength));
@@ -1908,11 +1943,6 @@ namespace Barotrauma
yield return CoroutineStatus.Success;
}
public float GetDrawDepth()
{
return SpriteDepth + ((ID % 255) * 0.000001f);
}
public bool IsInsideTrigger(Vector2 worldPosition)
{
return IsInsideTrigger(worldPosition, out _);
@@ -1943,7 +1973,7 @@ namespace Barotrauma
Skill requiredSkill = null;
float skillMultiplier = 1;
#endif
if (NonInteractable) { return false; }
if (!IsInteractable(picker)) { return false; }
foreach (ItemComponent ic in components)
{
bool pickHit = false, selectHit = false;
@@ -2047,24 +2077,19 @@ namespace Barotrauma
public float GetContainedItemConditionPercentage()
{
var containedItems = ContainedItems;
if (ownInventory == null) { return -1; }
if (containedItems != null)
float condition = 0f;
float maxCondition = 0f;
foreach (Item item in ContainedItems)
{
float condition = 0f;
float maxCondition = 0f;
foreach (Item item in containedItems)
{
condition += item.condition;
maxCondition += item.MaxCondition;
}
if (maxCondition > 0.0f)
{
return condition / maxCondition;
}
condition += item.condition;
maxCondition += item.MaxCondition;
}
if (maxCondition > 0.0f)
{
return condition / maxCondition;
}
return -1;
}
@@ -2259,7 +2284,6 @@ namespace Barotrauma
public void Unequip(Character character)
{
character.DeselectItem(this);
foreach (ItemComponent ic in components) { ic.Unequip(character); }
}
@@ -2696,11 +2720,18 @@ namespace Barotrauma
public virtual void Reset()
{
var holdable = GetComponent<Holdable>();
bool wasAttached = holdable?.Attached ?? false;
SerializableProperties = SerializableProperty.DeserializeProperties(this, Prefab.ConfigElement);
Sprite.ReloadXML();
SpriteDepth = Sprite.Depth;
condition = MaxCondition;
components.ForEach(c => c.Reset());
if (wasAttached)
{
holdable.AttachToWall();
}
}
public override void OnMapLoaded()
@@ -2747,11 +2778,7 @@ namespace Barotrauma
foreach (Character character in Character.CharacterList)
{
if (character.SelectedConstruction == this) character.SelectedConstruction = null;
for (int i = 0; i < character.SelectedItems.Length; i++)
{
if (character.SelectedItems[i] == this) character.SelectedItems[i] = null;
}
if (character.SelectedConstruction == this) { character.SelectedConstruction = null; }
}
Door door = GetComponent<Door>();
@@ -3,6 +3,7 @@ using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
@@ -22,19 +23,21 @@ namespace Barotrauma
public override int FindAllowedSlot(Item item)
{
if (ItemOwnsSelf(item)) return -1;
if (ItemOwnsSelf(item)) { return -1; }
//item is already in the inventory!
if (Contains(item)) { return -1; }
if (!container.CanBeContained(item)) { return -1; }
//try to stack first
for (int i = 0; i < capacity; i++)
{
//item is already in the inventory!
if (Items[i] == item) return -1;
if (slots[i].Any() && CanBePut(item, i)) { return i; }
}
if (!container.CanBeContained(item)) return -1;
for (int i = 0; i < capacity; i++)
{
if (Items[i] == null) return i;
if (CanBePut(item, i)) { return i; }
}
return -1;
@@ -42,12 +45,50 @@ namespace Barotrauma
public override bool CanBePut(Item item, int i)
{
if (ItemOwnsSelf(item)) return false;
if (i < 0 || i >= Items.Length) return false;
return (item != null && Items[i] == null && container.CanBeContained(item));
if (ItemOwnsSelf(item)) { return false; }
if (i < 0 || i >= slots.Length) { return false; }
if (!container.CanBeContained(item)) { return false; }
return item != null && slots[i].CanBePut(item) && slots[i].ItemCount < container.MaxStackSize;
}
public override bool TryPutItem(Item item, Character user, List<InvSlotType> allowedSlots = null, bool createNetworkEvent = true)
public override bool CanBePut(ItemPrefab itemPrefab, int i)
{
if (i < 0 || i >= slots.Length) { return false; }
if (!container.CanBeContained(itemPrefab)) { return false; }
return itemPrefab != null && slots[i].CanBePut(itemPrefab) && slots[i].ItemCount < container.MaxStackSize;
}
public override int HowManyCanBePut(ItemPrefab itemPrefab, int i)
{
if (itemPrefab == null) { return 0; }
if (i < 0 || i >= slots.Length) { return 0; }
if (!container.CanBeContained(itemPrefab)) { return 0; }
return slots[i].HowManyCanBePut(itemPrefab, maxStackSize: Math.Min(itemPrefab.MaxStackSize, container.MaxStackSize));
}
public override bool IsFull(bool takeStacksIntoAccount = false)
{
if (takeStacksIntoAccount)
{
for (int i = 0; i < capacity; i++)
{
if (!slots[i].Any()) { return false; }
var item = slots[i].FirstOrDefault();
if (slots[i].ItemCount < Math.Min(item.Prefab.MaxStackSize, container.MaxStackSize)) { return false; }
}
}
else
{
for (int i = 0; i < capacity; i++)
{
if (!slots[i].Any()) { return false; }
}
}
return true;
}
public override bool TryPutItem(Item item, Character user, IEnumerable<InvSlotType> allowedSlots = null, bool createNetworkEvent = true)
{
bool wasPut = base.TryPutItem(item, user, allowedSlots, createNetworkEvent);
@@ -55,8 +96,7 @@ namespace Barotrauma
{
foreach (Character c in Character.CharacterList)
{
if (!c.HasSelectedItem(item)) continue;
if (!c.HeldItems.Contains(item)) { continue; }
item.Unequip(c);
break;
}
@@ -71,13 +111,11 @@ namespace Barotrauma
public override bool TryPutItem(Item item, int i, bool allowSwapping, bool allowCombine, Character user, bool createNetworkEvent = true)
{
bool wasPut = base.TryPutItem(item, i, allowSwapping, allowCombine, user, createNetworkEvent);
if (wasPut)
{
foreach (Character c in Character.CharacterList)
{
if (!c.HasSelectedItem(item)) continue;
if (!c.HeldItems.Contains(item)) { continue; }
item.Unequip(c);
break;
}
@@ -7,6 +7,7 @@ using System.Xml.Linq;
using System.Linq;
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using Voronoi2;
namespace Barotrauma
{
@@ -21,6 +22,7 @@ namespace Barotrauma
public readonly float OutCondition;
//should the condition of the deconstructed item be copied to the output items
public readonly bool CopyCondition;
public float Commonness { get; }
public DeconstructItem(XElement element)
{
@@ -29,6 +31,7 @@ namespace Barotrauma
MaxCondition = element.GetAttributeFloat("maxcondition", 1.0f);
OutCondition = element.GetAttributeFloat("outcondition", 1.0f);
CopyCondition = element.GetAttributeBool("copycondition", false);
Commonness = element.GetAttributeFloat("commonness", 1.0f);
}
}
@@ -65,6 +68,7 @@ namespace Barotrauma
public readonly float RequiredTime;
public readonly float OutCondition; //Percentage-based from 0 to 1
public readonly List<Skill> RequiredSkills;
public int Amount { get; }
public FabricationRecipe(XElement element, ItemPrefab itemPrefab)
{
@@ -78,6 +82,7 @@ namespace Barotrauma
RequiredTime = element.GetAttributeFloat("requiredtime", 1.0f);
OutCondition = element.GetAttributeFloat("outcondition", 1.0f);
RequiredItems = new List<RequiredItem>();
Amount = element.GetAttributeInt("amount", 1);
foreach (XElement subElement in element.Elements())
{
@@ -117,7 +122,7 @@ namespace Barotrauma
continue;
}
var existing = RequiredItems.Find(r => r.ItemPrefabs.Count == 1 && r.ItemPrefabs[0] == requiredItem);
var existing = RequiredItems.Find(r => r.ItemPrefabs.Count == 1 && r.ItemPrefabs[0] == requiredItem && MathUtils.NearlyEqual(r.MinCondition, minCondition));
if (existing == null)
{
RequiredItems.Add(new RequiredItem(requiredItem, count, minCondition, useCondition));
@@ -136,7 +141,7 @@ namespace Barotrauma
continue;
}
var existing = RequiredItems.Find(r => r.ItemPrefabs.SequenceEqual(matchingItems));
var existing = RequiredItems.Find(r => r.ItemPrefabs.SequenceEqual(matchingItems) && MathUtils.NearlyEqual(r.MinCondition, minCondition));
if (existing == null)
{
RequiredItems.Add(new RequiredItem(matchingItems, count, minCondition, useCondition));
@@ -156,7 +161,10 @@ namespace Barotrauma
{
public readonly HashSet<string> Primary = new HashSet<string>();
public readonly HashSet<string> Secondary = new HashSet<string>();
public float SpawnProbability { get; private set; }
public float MaxCondition { get; private set; }
public float MinCondition { get; private set; }
public int MinAmount { get; private set; }
public int MaxAmount { get; private set; }
@@ -167,6 +175,8 @@ namespace Barotrauma
SpawnProbability = element.GetAttributeFloat("spawnprobability", 0.0f);
MinAmount = element.GetAttributeInt("minamount", 0);
MaxAmount = Math.Max(MinAmount, element.GetAttributeInt("maxamount", 0));
MaxCondition = element.GetAttributeFloat("maxcondition", 100f);
MinCondition = element.GetAttributeFloat("mincondition", 0f);
if (element.Attribute("spawnprobability") == null)
{
@@ -185,7 +195,7 @@ namespace Barotrauma
}
}
partial class ItemPrefab : MapEntityPrefab
partial class ItemPrefab : MapEntityPrefab, IHasUintIdentifier
{
private readonly string name;
public override string Name => name;
@@ -205,7 +215,7 @@ namespace Barotrauma
protected Vector2 size;
private float impactTolerance;
private readonly PriceInfo defaultPrice;
public readonly PriceInfo DefaultPrice;
private readonly Dictionary<string, PriceInfo> locationPrices;
/// <summary>
@@ -465,6 +475,24 @@ namespace Barotrauma
private set;
} = new Dictionary<string, float>();
public Dictionary<string, FixedQuantityResourceInfo> LevelQuantity
{
get;
} = new Dictionary<string, FixedQuantityResourceInfo>();
public struct FixedQuantityResourceInfo
{
public int ClusterQuantity { get; }
public int ClusterSize { get; }
public bool IsIslandSpecifc { get; }
public FixedQuantityResourceInfo(int clusterQuantity, int clusterSize, bool isIslandSpecific)
{
ClusterQuantity = clusterQuantity;
ClusterSize = clusterSize;
IsIslandSpecifc = isIslandSpecific;
}
}
[Serialize(true, false)]
public bool CanFlipX { get; private set; }
@@ -479,9 +507,28 @@ namespace Barotrauma
public bool CanSpriteFlipY { get; private set; }
private int maxStackSize;
[Serialize(1, false)]
public int MaxStackSize
{
get { return maxStackSize; }
set { maxStackSize = MathHelper.Clamp(value, 1, Inventory.MaxStackSize); }
}
public Vector2 Size => size;
public bool CanBeBought => (defaultPrice != null && defaultPrice.CanBeBought) || (locationPrices != null && locationPrices.Any(p => p.Value.CanBeBought));
public bool CanBeBought => (DefaultPrice != null && DefaultPrice.CanBeBought) || (locationPrices != null && locationPrices.Any(p => p.Value.CanBeBought));
/// <summary>
/// Any item with a Price element in the definition can be sold everywhere.
/// </summary>
public bool CanBeSold => DefaultPrice != null;
public bool RandomDeconstructionOutput { get; }
public int RandomDeconstructionOutputAmount { get; }
public uint UIntIdentifier { get; set; }
public static void RemoveByFile(string filePath) => Prefabs.RemoveByFile(filePath);
@@ -597,11 +644,13 @@ namespace Barotrauma
originalName = element.GetAttributeString("name", "");
name = originalName;
identifier = element.GetAttributeString("identifier", "");
if (!Enum.TryParse(element.GetAttributeString("category", "Misc"), true, out MapEntityCategory category))
string categoryStr = element.GetAttributeString("category", "Misc");
if (!Enum.TryParse(categoryStr, true, out MapEntityCategory category))
{
category = MapEntityCategory.Misc;
}
Category = category;
Category = category;
var parentType = element.Parent?.GetAttributeString("itemtype", "") ?? string.Empty;
@@ -725,7 +774,7 @@ namespace Barotrauma
if (locationPrices == null) { locationPrices = new Dictionary<string, PriceInfo>(); }
if (subElement.Attribute("baseprice") != null)
{
foreach (Tuple<string, PriceInfo> priceInfo in PriceInfo.CreatePriceInfos(subElement, out defaultPrice))
foreach (Tuple<string, PriceInfo> priceInfo in PriceInfo.CreatePriceInfos(subElement, out DefaultPrice))
{
if (priceInfo == null) { continue; }
locationPrices.Add(priceInfo.Item1, priceInfo.Item2);
@@ -857,6 +906,8 @@ namespace Barotrauma
case "deconstruct":
DeconstructTime = subElement.GetAttributeFloat("time", 1.0f);
AllowDeconstruct = true;
RandomDeconstructionOutput = subElement.GetAttributeBool("chooserandom", false);
RandomDeconstructionOutputAmount = subElement.GetAttributeInt("amount", 1);
foreach (XElement deconstructItem in subElement.Elements())
{
if (deconstructItem.Attribute("name") != null)
@@ -864,10 +915,9 @@ namespace Barotrauma
DebugConsole.ThrowError("Error in item config \"" + Name + "\" - use item identifiers instead of names to configure the deconstruct items.");
continue;
}
DeconstructItems.Add(new DeconstructItem(deconstructItem));
}
RandomDeconstructionOutputAmount = Math.Min(RandomDeconstructionOutputAmount, DeconstructItems.Count);
break;
case "fabricate":
case "fabricable":
@@ -898,12 +948,25 @@ namespace Barotrauma
break;
case "levelresource":
foreach (XElement levelCommonnessElement in subElement.Elements())
foreach (XElement levelCommonnessElement in subElement.GetChildElements("commonness"))
{
string levelName = levelCommonnessElement.GetAttributeString("leveltype", "").ToLowerInvariant();
if (!LevelCommonness.ContainsKey(levelName))
if (!levelCommonnessElement.GetAttributeBool("fixedquantity", false))
{
LevelCommonness.Add(levelName, levelCommonnessElement.GetAttributeFloat("commonness", 0.0f));
if (!LevelCommonness.ContainsKey(levelName))
{
LevelCommonness.Add(levelName, levelCommonnessElement.GetAttributeFloat("commonness", 0.0f));
}
}
else
{
if (!LevelQuantity.ContainsKey(levelName))
{
LevelQuantity.Add(levelName, new FixedQuantityResourceInfo(
levelCommonnessElement.GetAttributeInt("clusterquantity", 0),
levelCommonnessElement.GetAttributeInt("clustersize", 0),
levelCommonnessElement.GetAttributeBool("isislandspecific", false)));
}
}
}
break;
@@ -926,7 +989,14 @@ namespace Barotrauma
// with separate Price elements and there is no default price explicitly set
if (locationPrices != null && locationPrices.Any())
{
defaultPrice ??= new PriceInfo(GetMinPrice() ?? 0, false);
DefaultPrice ??= new PriceInfo(GetMinPrice() ?? 0, false);
}
//backwards compatibility
if (categoryStr.Equals("Thalamus", StringComparison.OrdinalIgnoreCase))
{
Category = MapEntityCategory.Wrecked;
Subcategory = "Thalamus";
}
if (sprite == null)
@@ -964,6 +1034,7 @@ namespace Barotrauma
AllowedLinks = element.GetAttributeStringArray("allowedlinks", new string[0], convertToLowerInvariant: true).ToList();
Prefabs.Add(this, allowOverriding);
this.CalculatePrefabUIntIdentifier(Prefabs);
}
public float GetTreatmentSuitability(string treatmentIdentifier)
@@ -981,7 +1052,7 @@ namespace Barotrauma
}
else
{
return defaultPrice;
return DefaultPrice;
}
}
@@ -1026,9 +1097,9 @@ namespace Barotrauma
int? minPrice = locationPrices != null && locationPrices.Values.Any() ? locationPrices?.Values.Min(p => p.Price) : null;
if (minPrice.HasValue)
{
if (defaultPrice != null)
if (DefaultPrice != null)
{
return minPrice < defaultPrice.Price ? minPrice : defaultPrice.Price;
return minPrice < DefaultPrice.Price ? minPrice : DefaultPrice.Price;
}
else
{
@@ -1037,36 +1108,38 @@ namespace Barotrauma
}
else
{
return defaultPrice?.Price;
return DefaultPrice?.Price;
}
}
public bool IsContainerPreferred(ItemContainer itemContainer, out bool isPreferencesDefined, out bool isSecondary)
public bool IsContainerPreferred(Item item, ItemContainer targetContainer, out bool isPreferencesDefined, out bool isSecondary)
{
isPreferencesDefined = PreferredContainers.Any();
isSecondary = false;
if (!isPreferencesDefined) { return true; }
if (PreferredContainers.Any(pc => IsContainerPreferred(pc.Primary, itemContainer)))
if (PreferredContainers.Any(pc => IsItemConditionAcceptable(item, pc) && IsContainerPreferred(pc.Primary, targetContainer)))
{
return true;
}
isSecondary = true;
return PreferredContainers.Any(pc => IsContainerPreferred(pc.Secondary, itemContainer));
return PreferredContainers.Any(pc => IsItemConditionAcceptable(item, pc) && IsContainerPreferred(pc.Secondary, targetContainer));
}
public bool IsContainerPreferred(string[] identifiersOrTags, out bool isPreferencesDefined, out bool isSecondary)
public bool IsContainerPreferred(Item item, string[] identifiersOrTags, out bool isPreferencesDefined, out bool isSecondary)
{
isPreferencesDefined = PreferredContainers.Any();
isSecondary = false;
if (!isPreferencesDefined) { return true; }
if (PreferredContainers.Any(pc => IsContainerPreferred(pc.Primary, identifiersOrTags)))
if (PreferredContainers.Any(pc => IsItemConditionAcceptable(item, pc) && IsContainerPreferred(pc.Primary, identifiersOrTags)))
{
return true;
}
isSecondary = true;
return PreferredContainers.Any(pc => IsContainerPreferred(pc.Secondary, identifiersOrTags));
return PreferredContainers.Any(pc => IsItemConditionAcceptable(item, pc) && IsContainerPreferred(pc.Secondary, identifiersOrTags));
}
private bool IsItemConditionAcceptable(Item item, PreferredContainer pc) => item.ConditionPercentage >= pc.MinCondition && item.ConditionPercentage <= pc.MaxCondition;
public static bool IsContainerPreferred(IEnumerable<string> preferences, ItemContainer c) => preferences.Any(id => c.Item.Prefab.Identifier == id || c.Item.HasTag(id));
public static bool IsContainerPreferred(IEnumerable<string> preferences, IEnumerable<string> ids) => ids.Any(id => preferences.Contains(id));
}
@@ -17,6 +17,7 @@ namespace Barotrauma
}
public bool IsOptional { get; set; }
public bool MatchOnEmpty { get; set; }
public bool IgnoreInEditor { get; set; }
@@ -30,6 +31,10 @@ namespace Barotrauma
public string Msg;
public string MsgTag;
/// <summary>
/// Should broken (0 condition) items be excluded
/// </summary>
public bool ExcludeBroken { get; private set; }
public RelationType Type
{
@@ -107,21 +112,20 @@ namespace Barotrauma
return CheckContained(parentItem);
case RelationType.Container:
if (parentItem == null || parentItem.Container == null) { return MatchOnEmpty; }
return parentItem.Container.Condition > 0.0f && MatchesItem(parentItem.Container);
return (!ExcludeBroken || parentItem.Container.Condition > 0.0f) && MatchesItem(parentItem.Container);
case RelationType.Equipped:
if (character == null) { return false; }
if (MatchOnEmpty && character.SelectedItems.All(it => it == null)) { return true; }
foreach (Item equippedItem in character.SelectedItems)
if (MatchOnEmpty && !character.HeldItems.Any()) { return true; }
foreach (Item equippedItem in character.HeldItems)
{
if (equippedItem == null) { continue; }
if (equippedItem.Condition > 0.0f && MatchesItem(equippedItem)) { return true; }
if ((!ExcludeBroken || equippedItem.Condition > 0.0f) && MatchesItem(equippedItem)) { return true; }
}
break;
case RelationType.Picked:
if (character == null || character.Inventory == null) { return false; }
foreach (Item pickedItem in character.Inventory.Items)
foreach (Item pickedItem in character.Inventory.AllItems)
{
if (pickedItem == null) { continue; }
if (MatchesItem(pickedItem)) { return true; }
}
break;
@@ -134,18 +138,16 @@ namespace Barotrauma
private bool CheckContained(Item parentItem)
{
var containedItems = parentItem.OwnInventory?.Items;
if (containedItems == null) { return false; }
if (parentItem.OwnInventory == null) { return false; }
if (MatchOnEmpty && !containedItems.Any(ci => ci != null))
if (MatchOnEmpty && parentItem.OwnInventory.IsEmpty())
{
return true;
}
foreach (Item contained in containedItems)
foreach (Item contained in parentItem.ContainedItems)
{
if (contained == null) { continue; }
if (contained.Condition > 0.0f && MatchesItem(contained)) { return true; }
if ((!ExcludeBroken || contained.Condition > 0.0f) && MatchesItem(contained)) { return true; }
if (CheckContained(contained)) { return true; }
}
return false;
@@ -157,7 +159,8 @@ namespace Barotrauma
new XAttribute("items", JoinedIdentifiers),
new XAttribute("type", type.ToString()),
new XAttribute("optional", IsOptional),
new XAttribute("ignoreineditor", IgnoreInEditor));
new XAttribute("ignoreineditor", IgnoreInEditor),
new XAttribute("excludebroken", ExcludeBroken));
if (excludedIdentifiers.Length > 0)
{
@@ -215,9 +218,13 @@ namespace Barotrauma
}
}
if (identifiers.Length == 0 && excludedIdentifiers.Length == 0 && !returnEmpty) { return null; }
RelatedItem ri = new RelatedItem(identifiers, excludedIdentifiers);
RelatedItem ri = new RelatedItem(identifiers, excludedIdentifiers)
{
ExcludeBroken = element.GetAttributeBool("excludebroken", true)
};
string typeStr = element.GetAttributeString("type", "");
if (string.IsNullOrEmpty(typeStr))
{