v0.12.0.2

This commit is contained in:
Joonas Rikkonen
2021-02-10 17:08:21 +02:00
parent 5c80a59bdd
commit 694cdfee7b
353 changed files with 12897 additions and 5028 deletions
@@ -13,7 +13,16 @@ namespace Barotrauma.Items.Components
{
partial class DockingPort : ItemComponent, IDrawableComponent, IServerSerializable
{
private static List<DockingPort> list = new List<DockingPort>();
public enum DirectionType
{
None,
Top,
Bottom,
Left,
Right
}
private static readonly List<DockingPort> list = new List<DockingPort>();
public static IEnumerable<DockingPort> List
{
get { return list; }
@@ -37,7 +46,7 @@ namespace Barotrauma.Items.Components
//force the sub to the correct position
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 +72,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 +102,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>
@@ -311,10 +329,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 +364,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 +416,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 +431,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 +534,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 +632,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;
@@ -908,7 +953,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 +962,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 +996,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 +1026,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 +1035,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;
}
}
@@ -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; }
@@ -395,6 +395,9 @@ namespace Barotrauma.Items.Components
[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;
@@ -418,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>();
@@ -489,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)
@@ -630,6 +638,8 @@ namespace Barotrauma.Items.Components
{
base.Update(deltaTime, cam);
UpdateFires(deltaTime);
#if CLIENT
foreach (VineTile vine in Vines)
{
@@ -640,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; }
@@ -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,6 +29,8 @@ namespace Barotrauma.Items.Components
private float swingState;
private Character prevEquipper;
private bool attachable, attached, attachedByDefault;
private Voronoi2.VoronoiCell attachTargetCell;
private readonly PhysicsBody body;
@@ -301,11 +303,9 @@ namespace Barotrauma.Items.Components
{
item.SetTransform(picker.SimPosition, 0.0f);
}
}
}
}
picker.DeselectItem(item);
picker.Inventory.RemoveItem(item);
picker = null;
}
@@ -340,34 +340,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;
@@ -488,13 +487,12 @@ namespace Barotrauma.Items.Components
}
}
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);
}
}
@@ -673,7 +671,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; }
@@ -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,7 +147,7 @@ namespace Barotrauma.Items.Components
public override void Update(float deltaTime, Camera cam)
{
if (!item.body.Enabled) { impactQueue.Clear(); return; }
if (picker == null && !picker.HasSelectedItem(item)) { impactQueue.Clear(); IsActive = false; }
if (picker == null && !picker.HeldItems.Contains(item)) { impactQueue.Clear(); IsActive = false; }
while (impactQueue.Count > 0)
{
@@ -244,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; }
@@ -320,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;
@@ -361,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;
@@ -376,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; }
@@ -398,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;
}
@@ -495,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; }
@@ -526,8 +542,7 @@ namespace Barotrauma.Items.Components
}
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);
}
@@ -579,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 &&
@@ -594,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;
}
@@ -620,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)
@@ -635,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;
@@ -643,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)
{
@@ -734,46 +746,52 @@ 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);
@@ -791,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)
@@ -821,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;
@@ -453,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);
}
@@ -484,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);
}
@@ -654,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;
@@ -672,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;
@@ -679,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; }
}
}
@@ -689,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; }
}
}
}
@@ -957,7 +964,7 @@ namespace Barotrauma.Items.Components
previousUser = character;
itemIndex = 0;
}
if (character.FindItem(ref itemIndex, out Item targetContainer, ignoredItems: aiController.IgnoredItems, customPriorityFunction: priority))
if (character.FindItem(ref itemIndex, out Item targetContainer, ignoredItems: aiController.IgnoredItems, customPriorityFunction: priority, positionalReference: Item))
{
suitableContainer = targetContainer;
return true;
@@ -1016,7 +1023,7 @@ namespace Barotrauma.Items.Components
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;
var containedItems = sourceContainer != null ? sourceContainer.Inventory.AllItems : item.OwnInventory.AllItems;
foreach (Item containedItem in containedItems)
{
if (containedItem != null && containedItem.Condition <= 0.0f)
@@ -1027,20 +1034,20 @@ namespace Barotrauma.Items.Components
if (i.IsThisOrAnyContainerIgnoredByAI()) { return 0; }
var container = i.GetComponent<ItemContainer>();
if (container == null) { return 0; }
if (container.Inventory.IsFull()) { return 0; }
if (!container.Inventory.CanBePut(containedItem)) { 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;
return 10;
}
else
{
if (containedItem.Prefab.IsContainerPreferred(container, out bool isPreferencesDefined, out bool isSecondary))
if (containedItem.IsContainerPreferred(container, out bool isPreferencesDefined, out bool isSecondary))
{
return isPreferencesDefined ? isSecondary ? 2 : 3 : 1;
return isPreferencesDefined ? isSecondary ? 2 : 5 : 1;
}
else
{
@@ -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.")]
@@ -141,8 +163,6 @@ namespace Barotrauma.Items.Components
}
InitProjSpecific(element);
itemsWithStatusEffects = new List<Pair<Item, StatusEffect>>();
}
partial void InitProjSpecific(XElement element);
@@ -154,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)
@@ -196,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);
@@ -240,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;
@@ -264,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;
@@ -277,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;
}
@@ -318,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
@@ -362,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;
}
@@ -380,7 +404,7 @@ namespace Barotrauma.Items.Components
if (SpawnWithId.Length > 0)
{
ItemPrefab prefab = ItemPrefab.Prefabs.Find(m => m.Identifier == SpawnWithId);
if (prefab != null && Inventory != null && Inventory.Items.Any(it => it == null))
if (prefab != null && Inventory != null && Inventory.CanBePut(prefab))
{
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 || ic.RemoveContainedItemsOnDeconstruct) { 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);
}
@@ -142,7 +142,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 +161,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 +190,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 +298,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 +332,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 +392,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 +407,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 +421,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 +467,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);
@@ -102,7 +102,7 @@ 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
@@ -112,13 +112,16 @@ namespace Barotrauma.Items.Components
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; }
@@ -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;
}
@@ -414,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);
@@ -532,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;
}
}
@@ -559,55 +557,58 @@ 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))
{
return false;
}
}
if (aiUpdateTimer > 0.0f)
{
aiUpdateTimer -= deltaTime;
return false;
}
aiUpdateTimer = AIUpdateInterval;
// load more fuel if the current maximum output is only 50% of the current load
// or if the fuel rod is (almost) deplenished
float minCondition = fuelConsumptionRate * MathUtils.Pow((degreeOfSuccess - refuelLimit) * 2, 2);
if (NeedMoreFuel(minimumOutputRatio: 0.5f, minCondition: minCondition))
{
var container = item.GetComponent<ItemContainer>();
if (objective.SubObjectives.None())
{
int itemCount = item.ContainedItems.Count(i => i != null && container.ContainableItems.Any(ri => ri.MatchesItem(i))) + 1;
AIContainItems<Reactor>(container, character, objective, itemCount, equip: false, removeEmpty: true, spawnItemIfNotFound: character.TeamID == Character.TeamType.FriendlyNPC, dropItemOnDeselected: true);
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)
if (!AIDecontainEmptyItems(character, objective, equip: false))
{
if (item != null && container.ContainableItems.Any(ri => ri.MatchesItem(item)))
return false;
}
}
if (aiUpdateTimer > 0.0f)
{
aiUpdateTimer -= deltaTime;
return false;
}
aiUpdateTimer = AIUpdateInterval;
// load more fuel if the current maximum output is only 50% of the current load
// or if the fuel rod is (almost) deplenished
float minCondition = fuelConsumptionRate * MathUtils.Pow((degreeOfSuccess - refuelLimit) * 2, 2);
if (NeedMoreFuel(minimumOutputRatio: 0.5f, minCondition: minCondition))
{
var container = item.GetComponent<ItemContainer>();
if (objective.SubObjectives.None())
{
int itemCount = item.ContainedItems.Count(i => i != null && container.ContainableItems.Any(ri => ri.MatchesItem(i))) + 1;
AIContainItems<Reactor>(container, character, objective, itemCount, equip: false, removeEmpty: true, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC, dropItemOnDeselected: true);
character.Speak(TextManager.Get("DialogReactorFuel"), null, 0.0f, "reactorfuel", 30.0f);
}
return false;
}
else if (TooMuchFuel())
{
if (item.OwnInventory?.AllItems != null)
{
var container = item.GetComponent<ItemContainer>();
foreach (Item item in item.OwnInventory.AllItemsMod)
{
item.Drop(character);
break;
if (container.ContainableItems.Any(ri => ri.MatchesItem(item)))
{
item.Drop(character);
break;
}
}
}
}
@@ -624,7 +625,7 @@ namespace Barotrauma.Items.Components
}
}
}
else if (LastUserWasPlayer)
else if (LastUserWasPlayer && lastUser != null && lastUser.TeamID == character.TeamID)
{
return true;
}
@@ -636,48 +637,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()
@@ -696,14 +694,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
@@ -713,7 +711,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
@@ -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; }
@@ -302,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)
@@ -326,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);
}
@@ -345,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)
@@ -354,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;
}
@@ -368,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);
@@ -420,13 +440,14 @@ 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)
{
@@ -435,12 +456,22 @@ namespace Barotrauma.Items.Components
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;
}
@@ -456,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 ?
@@ -483,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;
@@ -659,6 +689,10 @@ namespace Barotrauma.Items.Components
break;
}
sonar?.AIOperate(deltaTime, character, objective);
if (!MaintainPos && showIceSpireWarning)
{
character.Speak(TextManager.Get("dialogicespirespottedsonar"), null, 0.0f, "icespirespottedsonar", 60.0f);
}
return false;
}
@@ -666,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)))
{
@@ -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;
}
}
@@ -559,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)
{
@@ -656,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
@@ -665,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)
@@ -695,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
@@ -764,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);
@@ -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; }
@@ -274,7 +278,7 @@ namespace Barotrauma.Items.Components
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 +290,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 +304,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 +333,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;
@@ -111,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)
@@ -130,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))
{
@@ -184,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;
}
@@ -247,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)
@@ -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;
}
@@ -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,16 @@ namespace Barotrauma.Items.Components
foreach (Character c in Character.CharacterList)
{
if (IgnoreDead && c.IsDead) { continue; }
if (OnlyHumans && !c.IsHuman) { 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")
@@ -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
@@ -262,24 +262,33 @@ namespace Barotrauma.Items.Components
public override void OnMapLoaded()
{
base.OnMapLoaded();
var lightComponents = item.GetComponents<LightComponent>();
if (lightComponents != null && lightComponents.Count() > 0)
{
lightComponent = lightComponents.FirstOrDefault(lc => lc.Parent == this);
#if CLIENT
if (lightComponent != null)
{
lightComponent.Parent = null;
lightComponent.Rotation = Rotation - MathHelper.ToRadians(item.Rotation);
lightComponent.Light.Rotation = -rotation;
}
#endif
}
FindLightComponent();
if (loadedRotationLimits.HasValue) { RotationLimits = loadedRotationLimits.Value; }
if (loadedBaseRotation.HasValue) { BaseRotation = loadedBaseRotation.Value; }
UpdateTransformedBarrelPos();
}
private void FindLightComponent()
{
foreach (LightComponent lc in item.GetComponents<LightComponent>())
{
if (lc?.Parent == this)
{
lightComponent = lc;
break;
}
}
#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)
{
this.cam = cam;
@@ -304,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);
@@ -326,7 +339,7 @@ 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);
@@ -371,6 +384,11 @@ namespace Barotrauma.Items.Components
angularVelocity *= -0.5f;
}
UpdateLightComponent();
}
private void UpdateLightComponent()
{
if (lightComponent != null)
{
lightComponent.Rotation = Rotation - MathHelper.ToRadians(item.Rotation);
@@ -709,7 +727,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)
@@ -753,7 +775,7 @@ namespace Barotrauma.Items.Components
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);
}
@@ -765,7 +787,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;
@@ -786,18 +808,14 @@ 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>();
maxProjectileCount += container.Capacity;
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;
}
}
@@ -809,7 +827,7 @@ 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; }
@@ -852,53 +870,135 @@ namespace Barotrauma.Items.Components
//enough shells and power
Character closestEnemy = null;
float closestDist = AIRange * AIRange;
Vector2? targetPos = null;
float shootDistance = AIRange * item.OffsetOnSelectedMultiplier;
float closestDistance = shootDistance * shootDistance;
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; }
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 (!CheckTurretAngle(enemy.WorldPosition)) { continue; }
closestEnemy = enemy;
closestDist = dist;
closestDistance = dist;
}
if (closestEnemy == null) { return false; }
character.AIController.SelectTarget(closestEnemy.AiTarget);
if (closestEnemy != null)
{
targetPos = closestEnemy.WorldPosition;
}
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.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.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)
@@ -929,17 +1029,26 @@ 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);
character.Speak(TextManager.Get("DialogFireTurret"), null, 0.0f, "fireturret", 10.0f);
character.SetInput(InputType.Shoot, true, true);
return false;
}
private bool CheckTurretAngle(Vector2 target)
{
float angle = -MathUtils.VectorToAngle(target - item.WorldPosition);
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;
}
protected override void RemoveComponentSpecific()
{
base.RemoveComponentSpecific();
@@ -984,7 +1093,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>();
@@ -1094,6 +1202,7 @@ namespace Barotrauma.Items.Components
public override void OnItemLoaded()
{
base.OnItemLoaded();
FindLightComponent();
if (!loadedBaseRotation.HasValue)
{
if (item.FlippedX) { FlipX(relativeToSub: false); }