Faction Test v1.0.1.0

This commit is contained in:
Regalis11
2023-02-16 15:01:28 +02:00
parent caa5a2f762
commit 2c5a7923b0
309 changed files with 7502 additions and 4335 deletions
@@ -289,7 +289,16 @@ namespace Barotrauma.Items.Components
return;
}
if (!(joint is WeldJoint))
if (joint == null)
{
string errorMsg = "Error while locking a docking port (joint between submarines doesn't exist)." +
" Submarine: " + (item.Submarine?.Info.Name ?? "null") +
", target submarine: " + (DockingTarget.item.Submarine?.Info.Name ?? "null");
GameAnalyticsManager.AddErrorEventOnce("DockingPort.Lock:JointNotCreated", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
return;
}
if (joint is not WeldJoint)
{
DockingDir = GetDir(DockingTarget);
DockingTarget.DockingDir = -DockingDir;
@@ -1,10 +1,10 @@
using Barotrauma.Networking;
using FarseerPhysics;
using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using FarseerPhysics.Dynamics;
#if CLIENT
using Barotrauma.Lights;
#endif
@@ -173,6 +173,21 @@ namespace Barotrauma.Items.Components
OpenState = isOpen ? 1.0f : 0.0f;
}
}
public bool IsClosed => !IsOpen;
/// <summary>
/// Is the door opening, but not yet fully opened? Returns false both when it's closed and when it's fully open.
/// </summary>
public bool IsOpening => IsOpen && !IsFullyOpen;
/// <summary>
/// Is the door closing, but not yet fully closed? Returns false both when the door is open and when it's fully closed.
/// </summary>
public bool IsClosing => IsClosed && !IsFullyClosed;
public bool IsFullyOpen => IsOpen && OpenState >= 1.0f;
public bool IsFullyClosed => IsClosed && OpenState <= 0f;
[Serialize(false, IsPropertySaveable.No, description: "If the door has integrated buttons, it can be opened by interacting with it directly (instead of using buttons wired to it).")]
public bool HasIntegratedButtons { get; private set; }
@@ -211,6 +226,8 @@ namespace Barotrauma.Items.Components
IsHorizontal = element.GetAttributeBool("horizontal", false);
canBePicked = element.GetAttributeBool("canbepicked", false);
autoOrientGap = element.GetAttributeBool("autoorientgap", false);
allowedSlots.Clear();
foreach (var subElement in element.Elements())
{
@@ -365,7 +382,10 @@ namespace Barotrauma.Items.Components
{
lastBrokenTime = Timing.TotalTime;
//the door has to be restored to 50% health before collision detection on the body is re-enabled
if (item.ConditionPercentage / Math.Max(item.MaxRepairConditionMultiplier, 1.0f) > 50.0f &&
//multiply by MaxRepairConditionMultiplier so the item gets repaired at 50% of the _default max condition_
//otherwise increasing the max condition is arguably harmful, as the door needs to be repaired further to re-enable the collider
if (item.ConditionPercentage * Math.Max(item.MaxRepairConditionMultiplier, 1.0f) > 50.0f &&
(GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer))
{
IsBroken = false;
@@ -58,9 +58,6 @@ namespace Barotrauma.Items.Components
set;
}
//the angle in which the Character holds the item
protected float holdAngle;
public PhysicsBody Body
{
get { return item.body ?? body; }
@@ -143,6 +140,7 @@ namespace Barotrauma.Items.Components
set { aimPos = ConvertUnits.ToSimUnits(value); }
}
protected float holdAngle;
#if DEBUG
[Editable, Serialize(0.0f, IsPropertySaveable.No, description: "The rotation at which the character holds the item (in degrees, relative to the rotation of the character's hand).")]
#else
@@ -154,6 +152,18 @@ namespace Barotrauma.Items.Components
set { holdAngle = MathHelper.ToRadians(value); }
}
protected float aimAngle;
#if DEBUG
[Editable, Serialize(0.0f, IsPropertySaveable.No, description: "The rotation at which the character holds the item while aiming (in degrees, relative to the rotation of the character's hand).")]
#else
[Serialize(0.0f, IsPropertySaveable.No)]
#endif
public float AimAngle
{
get { return MathHelper.ToDegrees(aimAngle); }
set { aimAngle = MathHelper.ToRadians(value); }
}
private Vector2 swingAmount;
#if DEBUG
[Editable, Serialize("0.0,0.0", IsPropertySaveable.No, description: "How much the item swings around when aiming/holding it (in pixels, as an offset from AimPos/HoldPos).")]
@@ -549,10 +559,17 @@ namespace Barotrauma.Items.Components
public override bool OnPicked(Character picker)
{
#if CLIENT
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
{
if (!picker.Inventory.CanBeAutoMovedToCorrectSlots(item))
{
picker.Inventory.FlashAllowedSlots(item, Color.Red);
}
return false;
}
#endif
bool wasAttached = IsAttached;
if (base.OnPicked(picker))
{
DeattachFromWall();
@@ -561,7 +578,7 @@ namespace Barotrauma.Items.Components
if (GameMain.Server != null && attachable)
{
item.CreateServerEvent(this);
if (picker != null)
if (picker != null && wasAttached)
{
GameServer.Log(GameServer.CharacterLogName(picker) + " detached " + item.Name + " from a wall", ServerLog.MessageType.ItemInteraction);
}
@@ -689,16 +706,22 @@ namespace Barotrauma.Items.Components
if (maxAttachableCount == 0)
{
#if CLIENT
GUI.AddMessage(TextManager.Get("itemmsgrequiretraining"), Color.Red);
if (character == Character.Controlled)
{
GUI.AddMessage(TextManager.Get("itemmsgrequiretraining"), Color.Red);
}
#endif
return false;
}
else if (currentlyAttachedCount >= maxAttachableCount)
{
#if CLIENT
GUI.AddMessage($"{TextManager.Get("itemmsgtotalnumberlimited")} ({currentlyAttachedCount}/{maxAttachableCount})", Color.Red);
if (character == Character.Controlled)
{
GUI.AddMessage($"{TextManager.Get("itemmsgtotalnumberlimited")} ({currentlyAttachedCount}/{maxAttachableCount})", Color.Red);
}
#endif
return false;
return false;
}
}
@@ -876,9 +899,13 @@ namespace Barotrauma.Items.Components
scaledHandlePos[0] = handlePos[0] * item.Scale;
scaledHandlePos[1] = handlePos[1] * item.Scale;
bool aim = picker.IsKeyDown(InputType.Aim) && aimPos != Vector2.Zero && picker.CanAim;
picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, holdPos + swingPos, aimPos + swingPos, aim, holdAngle);
if (!aim)
if (aim)
{
picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, holdPos + swingPos, aimPos + swingPos, aim, holdAngle, aimAngle);
}
else
{
picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, holdPos + swingPos, aimPos + swingPos, aim, holdAngle);
var rope = GetRope();
if (rope != null && rope.SnapWhenNotAimed && rope.Item.ParentInventory == null)
{
@@ -223,7 +223,7 @@ namespace Barotrauma.Items.Components
{
UpdateSwingPos(deltaTime, out Vector2 swingPos);
hitPos = MathUtils.WrapAnglePi(Math.Min(hitPos + deltaTime * 3f, MathHelper.PiOver4));
ac.HoldItem(deltaTime, item, handlePos, aimPos + swingPos, Vector2.Zero, aim: false, hitPos, holdAngle + hitPos, aimMelee: true);
ac.HoldItem(deltaTime, item, handlePos, aimPos + swingPos, Vector2.Zero, aim: false, hitPos, holdAngle + hitPos + aimAngle, aimMelee: true);
if (ac.InWater)
{
ac.LockFlipping();
@@ -445,7 +445,7 @@ namespace Barotrauma.Items.Components
targetItem.Condition / targetItem.MaxCondition,
emptyColor: GUIStyle.HealthBarColorLow,
fullColor: GUIStyle.HealthBarColorHigh,
textTag: targetItem.Name);
textTag: targetItem.Prefab.ShowNameInHealthBar ? targetItem.Name : string.Empty);
}
#endif
}
@@ -472,8 +472,8 @@ namespace Barotrauma.Items.Components
}
if (GameMain.NetworkMember is { IsServer: true } server && targetEntity != null)
{
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, targetItemComponent: null, targetCharacter, targetLimb, targetEntity));
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnUse, targetItemComponent: null, targetCharacter, targetLimb, targetEntity));
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, targetItemComponent: null, targetCharacter, targetLimb, useTarget: targetEntity));
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnUse, targetItemComponent: null, targetCharacter, targetLimb, useTarget: targetEntity));
serverLogger ??= new System.Text.StringBuilder();
serverLogger.Clear();
serverLogger.Append($"{picker?.LogName} used {item.Name}");
@@ -26,6 +26,8 @@ namespace Barotrauma.Items.Components
get { return allowedSlots; }
}
public bool PickingDone => pickTimer >= PickingTime;
public Character Picker
{
get
@@ -5,9 +5,7 @@ using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
@@ -145,7 +143,8 @@ namespace Barotrauma.Items.Components
public override void Equip(Character character)
{
ReloadTimer = Math.Min(reload, 1.0f);
//clamp above 1 to prevent rapid-firing by swapping weapons
ReloadTimer = Math.Max(Math.Min(reload, 1.0f), ReloadTimer);
IsActive = true;
}
@@ -261,7 +260,8 @@ namespace Barotrauma.Items.Components
{
Vector2 barrelPos = TransformedBarrelPos + item.body.SimPosition;
float rotation = (Item.body.Dir == 1.0f) ? Item.body.Rotation : Item.body.Rotation - MathHelper.Pi;
float spread = GetSpread(character) * Rand.Range(-0.5f, 0.5f);
float spread = GetSpread(character) * Projectile.GetSpreadFromPool(projectile.SpreadCounter);
var lastProjectile = LastProjectile;
if (lastProjectile != projectile)
{
@@ -277,7 +277,7 @@ namespace Barotrauma.Items.Components
{
Item.body.ApplyLinearImpulse(new Vector2((float)Math.Cos(projectile.Item.body.Rotation), (float)Math.Sin(projectile.Item.body.Rotation)) * Item.body.Mass * -50.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
}
projectile.Item.body.ApplyTorque(projectile.Item.body.Mass * degreeOfFailure * Rand.Range(-10.0f, 10.0f));
projectile.Item.body.ApplyTorque(projectile.Item.body.Mass * degreeOfFailure * 20.0f * Projectile.GetSpreadFromPool(projectile.SpreadCounter));
}
Item.RemoveContained(projectile.Item);
}
@@ -100,6 +100,9 @@ namespace Barotrauma.Items.Components
[Serialize(false, IsPropertySaveable.No, description: "Can the item hit broken doors.")]
public bool HitBrokenDoors { get; set; }
[Serialize(false, IsPropertySaveable.No, description: "Should the tool ignore characters? Enabled e.g. for fire extinguisher.")]
public bool IgnoreCharacters { get; set; }
[Serialize(0.0f, IsPropertySaveable.No, description: "The probability of starting a fire somewhere along the ray fired from the barrel (for example, 0.1 = 10% chance to start a fire during a second of use).")]
public float FireProbability { get; set; }
@@ -313,7 +316,11 @@ namespace Barotrauma.Items.Components
private readonly List<FireSource> fireSourcesInRange = new List<FireSource>();
private void Repair(Vector2 rayStart, Vector2 rayEnd, float deltaTime, Character user, float degreeOfSuccess, List<Body> ignoredBodies)
{
var collisionCategories = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel | Physics.CollisionRepair;
var collisionCategories = Physics.CollisionWall | Physics.CollisionItem | Physics.CollisionLevel | Physics.CollisionRepair;
if (!IgnoreCharacters)
{
collisionCategories |= Physics.CollisionCharacter;
}
//if the item can cut off limbs, activate nearby bodies to allow the raycast to hit them
if (statusEffectLists != null)
@@ -42,6 +42,8 @@ namespace Barotrauma.Items.Components
}
}
public const float WaterDragCoefficient = 0.5f;
public override bool Use(float deltaTime, Character character = null)
{
//actual throwing logic is handled in Update
@@ -59,6 +61,7 @@ namespace Barotrauma.Items.Components
base.Drop(dropper);
throwState = ThrowState.None;
throwAngle = ThrowAngleStart;
Item.ResetWaterDragCoefficient();
}
public override void UpdateBroken(float deltaTime, Camera cam)
@@ -97,6 +100,7 @@ namespace Barotrauma.Items.Components
}
item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform;
midAir = false;
Item.ResetWaterDragCoefficient();
}
return;
}
@@ -188,6 +192,7 @@ namespace Barotrauma.Items.Components
}
item.Drop(CurrentThrower, createNetworkEvent: GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer);
item.WaterDragCoefficient = WaterDragCoefficient;
item.body.ApplyLinearImpulse(throwVector * ThrowForce * item.body.Mass * 3.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
//disable platform collisions until the item comes back to rest again
@@ -205,12 +210,12 @@ namespace Barotrauma.Items.Components
if (GameMain.NetworkMember is { IsServer: true })
{
GameMain.NetworkMember.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnSecondaryUse, this, CurrentThrower));
GameMain.NetworkMember.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnSecondaryUse, this, targetCharacter: CurrentThrower));
}
if (!(GameMain.NetworkMember is { IsClient: true }))
{
//Stun grenades, flares, etc. all have their throw-related things handled in "onSecondaryUse"
ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, CurrentThrower, useTarget: CurrentThrower, user: CurrentThrower);
ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, character: CurrentThrower, user: CurrentThrower);
}
throwState = ThrowState.None;
}
@@ -125,8 +125,8 @@ namespace Barotrauma.Items.Components
get { return drawable; }
set
{
if (value == drawable) return;
if (!(this is IDrawableComponent))
if (value == drawable) { return; }
if (this is not IDrawableComponent)
{
DebugConsole.ThrowError("Couldn't make \"" + this + "\" drawable (the component doesn't implement the IDrawableComponent interface)");
return;
@@ -236,10 +236,7 @@ namespace Barotrauma.Items.Components
set;
}
/// <summary>
/// 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).
/// </summary>
[Serialize(0f, IsPropertySaveable.No, 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).")]
[Serialize(0f, IsPropertySaveable.No, 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). Note that there's also a generic BotPriority for all item prefabs.")]
public float CombatPriority { get; private set; }
/// <summary>
@@ -690,7 +687,7 @@ namespace Barotrauma.Items.Components
public virtual void FlipY(bool relativeToSub) { }
public bool IsLoaded(Character user, bool checkContainedItems = true) =>
public bool IsNotEmpty(Character user, bool checkContainedItems = true) =>
HasRequiredContainedItems(user, addMessage: false) &&
(!checkContainedItems || Item.OwnInventory == null || Item.OwnInventory.AllItems.Any(i => i.Condition > 0));
@@ -12,20 +12,9 @@ namespace Barotrauma.Items.Components
{
partial class ItemContainer : ItemComponent, IDrawableComponent
{
class ActiveContainedItem
{
public readonly Item Item;
public readonly StatusEffect StatusEffect;
public readonly bool ExcludeBroken;
public readonly bool ExcludeFullCondition;
public ActiveContainedItem(Item item, StatusEffect statusEffect, bool excludeBroken, bool excludeFullCondition)
{
Item = item;
StatusEffect = statusEffect;
ExcludeBroken = excludeBroken;
ExcludeFullCondition = excludeFullCondition;
}
}
readonly record struct ActiveContainedItem(Item Item, StatusEffect StatusEffect, bool ExcludeBroken, bool ExcludeFullCondition);
readonly record struct DrawableContainedItem(Item Item, bool Hide, Vector2? ItemPos, float Rotation);
class SlotRestrictions
{
@@ -63,7 +52,9 @@ namespace Barotrauma.Items.Components
public readonly ItemInventory Inventory;
private readonly List<ActiveContainedItem> activeContainedItems = new List<ActiveContainedItem>();
private readonly List<DrawableContainedItem> drawableContainedItems = new List<DrawableContainedItem>();
private List<ushort>[] itemIds;
//how many items can be contained
@@ -114,7 +105,7 @@ namespace Barotrauma.Items.Components
[Serialize(100, IsPropertySaveable.No, description: "How many items are placed in a row before starting a new row.")]
public int ItemsPerRow { get; set; }
[Serialize(true, IsPropertySaveable.No, description: "Should the contents in the item's inventory be visible? Disabled on items like magazines that spawn the contents as needed.")]
[Serialize(true, IsPropertySaveable.No, description: "Should the inventory of this item be visible when the item is selected.")]
public bool DrawInventory
{
get;
@@ -142,6 +133,9 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(true, IsPropertySaveable.No)]
public bool AllowAccess { get; set; }
[Serialize(false, IsPropertySaveable.No)]
public bool AccessOnlyWhenBroken { get; set; }
@@ -348,8 +342,6 @@ namespace Barotrauma.Items.Components
public void OnItemContained(Item containedItem)
{
item.SetContainedItemPositions();
int index = Inventory.FindIndex(containedItem);
if (index >= 0 && index < slotRestrictions.Length)
{
@@ -367,6 +359,12 @@ namespace Barotrauma.Items.Components
}
}
var relatedItem = FindContainableItem(containedItem);
drawableContainedItems.Add(new DrawableContainedItem(containedItem,
Hide: relatedItem?.Hide ?? false,
ItemPos: relatedItem?.ItemPos,
Rotation: relatedItem?.Rotation ?? 0.0f));
if (item.GetComponent<Planter>() != null)
{
GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":GardeningPlanted:" + containedItem.Prefab.Identifier);
@@ -381,6 +379,7 @@ namespace Barotrauma.Items.Components
// Set the contained items active if there's an item inserted inside the container. Enables e.g. the rifle flashlight when it's attached to the rifle (put inside of it).
SetContainedActive(true);
}
item.SetContainedItemPositions();
CharacterHUD.RecreateHudTextsIfFocused(item, containedItem);
OnContainedItemsChanged.Invoke(this);
}
@@ -393,6 +392,7 @@ namespace Barotrauma.Items.Components
public void OnItemRemoved(Item containedItem)
{
activeContainedItems.RemoveAll(i => i.Item == containedItem);
drawableContainedItems.RemoveAll(i => i.Item == containedItem);
//deactivate if the inventory is empty
IsActive = activeContainedItems.Count > 0 || Inventory.AllItems.Any(it => it.body != null);
CharacterHUD.RecreateHudTextsIfFocused(item, containedItem);
@@ -483,8 +483,8 @@ namespace Barotrauma.Items.Components
item.ApplyStatusEffects(ActionType.OnSuccess, 1.0f, ownerCharacter);
item.ApplyStatusEffects(ActionType.OnUse, 1.0f, ownerCharacter);
item.GetComponent<GeneticMaterial>()?.Equip(ownerCharacter);
autoInjectCooldown = AutoInjectInterval;
}
autoInjectCooldown = AutoInjectInterval;
}
}
@@ -509,10 +509,18 @@ namespace Barotrauma.Items.Components
if (activeContainedItem.ExcludeFullCondition && contained.IsFullCondition) { continue; }
StatusEffect effect = activeContainedItem.StatusEffect;
if (effect.HasTargetType(StatusEffect.TargetType.This))
if (effect.HasTargetType(StatusEffect.TargetType.This))
{
effect.Apply(ActionType.OnContaining, deltaTime, item, item.AllPropertyObjects);
if (effect.HasTargetType(StatusEffect.TargetType.Contained))
}
if (effect.HasTargetType(StatusEffect.TargetType.Contained))
{
effect.Apply(ActionType.OnContaining, deltaTime, item, contained.AllPropertyObjects);
}
if (effect.HasTargetType(StatusEffect.TargetType.Character) && item.ParentInventory?.Owner is Character character)
{
effect.Apply(ActionType.OnContaining, deltaTime, item, character);
}
if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
@@ -534,12 +542,12 @@ namespace Barotrauma.Items.Components
public override bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg = null)
{
return DrawInventory && (!AccessOnlyWhenBroken || Item.Condition <= 0) && base.HasRequiredItems(character, addMessage, msg);
return AllowAccess && (!AccessOnlyWhenBroken || Item.Condition <= 0) && base.HasRequiredItems(character, addMessage, msg);
}
public override bool Select(Character character)
{
if (!DrawInventory) { return false; }
if (!AllowAccess) { return false; }
if (item.Container != null) { return false; }
if (AccessOnlyWhenBroken)
{
@@ -575,7 +583,7 @@ namespace Barotrauma.Items.Components
public override bool Pick(Character picker)
{
if (!DrawInventory) { return false; }
if (!AllowAccess) { return false; }
if (AccessOnlyWhenBroken)
{
if (item.Condition > 0)
@@ -756,54 +764,50 @@ namespace Barotrauma.Items.Components
int i = 0;
Vector2 currentItemPos = transformedItemPos;
foreach (Item contained in Inventory.AllItems)
foreach (DrawableContainedItem contained in drawableContainedItems)
{
Vector2 itemPos = currentItemPos;
var relatedItem = FindContainableItem(contained);
if (relatedItem != null)
if (contained.ItemPos.HasValue)
{
if (relatedItem.ItemPos.HasValue)
Vector2 pos = contained.ItemPos.Value;
if (item.body != null)
{
Vector2 pos = relatedItem.ItemPos.Value;
if (item.body != null)
Matrix transform = Matrix.CreateRotationZ(item.body.Rotation);
pos.X *= item.body.Dir;
itemPos = Vector2.Transform(pos, transform) + item.body.Position;
}
else
{
itemPos = pos;
// This code is aped based on above. Not tested.
if (item.FlippedX)
{
Matrix transform = Matrix.CreateRotationZ(item.body.Rotation);
pos.X *= item.body.Dir;
itemPos = Vector2.Transform(pos, transform) + item.body.Position;
itemPos.X = -itemPos.X;
itemPos.X += item.Rect.Width;
}
else
if (item.FlippedY)
{
itemPos = pos;
// This code is aped based on above. Not tested.
if (item.FlippedX)
{
itemPos.X = -itemPos.X;
itemPos.X += item.Rect.Width;
}
if (item.FlippedY)
{
itemPos.Y = -itemPos.Y;
itemPos.Y -= item.Rect.Height;
}
itemPos += new Vector2(item.Rect.X, item.Rect.Y);
if (Math.Abs(item.RotationRad) > 0.01f)
{
Matrix transform = Matrix.CreateRotationZ(item.RotationRad);
itemPos = Vector2.Transform(itemPos - item.Position, transform) + item.Position;
}
itemPos.Y = -itemPos.Y;
itemPos.Y -= item.Rect.Height;
}
itemPos += new Vector2(item.Rect.X, item.Rect.Y);
if (Math.Abs(item.RotationRad) > 0.01f)
{
Matrix transform = Matrix.CreateRotationZ(item.RotationRad);
itemPos = Vector2.Transform(itemPos - item.Position, transform) + item.Position;
}
}
}
}
if (contained.body != null)
if (contained.Item.body != null)
{
try
{
Vector2 simPos = ConvertUnits.ToSimUnits(itemPos);
float rotation = itemRotation;
if (relatedItem != null && relatedItem.Rotation != 0)
if (contained.Rotation != 0)
{
rotation = MathHelper.ToRadians(relatedItem.Rotation);
rotation = MathHelper.ToRadians(contained.Rotation);
}
if (item.body != null)
{
@@ -814,29 +818,29 @@ namespace Barotrauma.Items.Components
{
rotation += -item.RotationRad;
}
contained.body.FarseerBody.SetTransformIgnoreContacts(ref simPos, rotation);
contained.body.SetPrevTransform(contained.body.SimPosition, contained.body.Rotation);
contained.body.UpdateDrawPosition();
contained.Item.body.FarseerBody.SetTransformIgnoreContacts(ref simPos, rotation);
contained.Item.body.SetPrevTransform(contained.Item.body.SimPosition, contained.Item.body.Rotation);
contained.Item.body.UpdateDrawPosition();
}
catch (Exception e)
{
DebugConsole.Log("SetTransformIgnoreContacts threw an exception in SetContainedItemPositions (" + e.Message + ")\n" + e.StackTrace.CleanupStackTrace());
GameAnalyticsManager.AddErrorEventOnce("ItemContainer.SetContainedItemPositions.InvalidPosition:" + contained.Name,
GameAnalyticsManager.AddErrorEventOnce("ItemContainer.SetContainedItemPositions.InvalidPosition:" + contained.Item.Name,
GameAnalyticsManager.ErrorSeverity.Error,
"SetTransformIgnoreContacts threw an exception in SetContainedItemPositions (" + e.Message + ")\n" + e.StackTrace.CleanupStackTrace());
}
contained.body.Submarine = item.Submarine;
contained.Item.body.Submarine = item.Submarine;
}
contained.Rect =
contained.Item.Rect =
new Rectangle(
(int)(itemPos.X - contained.Rect.Width / 2.0f),
(int)(itemPos.Y + contained.Rect.Height / 2.0f),
contained.Rect.Width, contained.Rect.Height);
(int)(itemPos.X - contained.Item.Rect.Width / 2.0f),
(int)(itemPos.Y + contained.Item.Rect.Height / 2.0f),
contained.Item.Rect.Width, contained.Item.Rect.Height);
contained.Submarine = item.Submarine;
contained.CurrentHull = item.CurrentHull;
contained.SetContainedItemPositions();
contained.Item.Submarine = item.Submarine;
contained.Item.CurrentHull = item.CurrentHull;
contained.Item.SetContainedItemPositions();
i++;
if (Math.Abs(ItemInterval.X) > 0.001f && Math.Abs(ItemInterval.Y) > 0.001f)
@@ -116,7 +116,7 @@ namespace Barotrauma.Items.Components
{
float voltageFactor = MinVoltage <= 0.0f ? 1.0f : Math.Min(Voltage, MaxOverVoltageFactor);
float currForce = force * voltageFactor;
float condition = item.Condition / item.MaxCondition;
float condition = item.MaxCondition <= 0.0f ? 0.0f : item.Condition / item.MaxCondition;
// Broken engine makes more noise.
float noise = Math.Abs(currForce) * MathHelper.Lerp(1.5f, 1f, condition);
UpdateAITargets(noise);
@@ -6,6 +6,7 @@ using FarseerPhysics.Dynamics.Joints;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Voronoi2;
@@ -13,6 +14,21 @@ namespace Barotrauma.Items.Components
{
partial class Projectile : ItemComponent, IServerSerializable
{
const int SpreadCounterWrapAround = 256;
private static readonly ImmutableArray<float> spreadPool;
static Projectile()
{
MTRandom random = new MTRandom(0);
spreadPool = Enumerable.Range(0, SpreadCounterWrapAround).Select(f => (float)random.NextDouble() - 0.5f).ToImmutableArray();
}
public static float GetSpreadFromPool(int seed)
{
if (seed < 0) { seed = -seed; }
return spreadPool[seed % SpreadCounterWrapAround];
}
struct HitscanResult
{
public Fixture Fixture;
@@ -41,10 +57,14 @@ namespace Barotrauma.Items.Components
}
}
public const float WaterDragCoefficient = 0.1f;
private readonly Queue<Impact> impactQueue = new Queue<Impact>();
private bool removePending;
public byte SpreadCounter { get; private set; }
//continuous collision detection is used while the projectile is moving faster than this
const float ContinuousCollisionThreshold = 5.0f;
@@ -112,27 +132,34 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(false, IsPropertySaveable.No, description: "Can the item stick to the character it hits.")]
[Serialize(false, IsPropertySaveable.No, description: "Can the projectile stick to characters.")]
public bool StickToCharacters
{
get;
set;
}
[Serialize(false, IsPropertySaveable.No, description: "Can the item stick to the structure it hits.")]
[Serialize(false, IsPropertySaveable.No, description: "Can the projectile stick to walls.")]
public bool StickToStructures
{
get;
set;
}
[Serialize(false, IsPropertySaveable.No, description: "Can the item stick to the item it hits.")]
[Serialize(false, IsPropertySaveable.No, description: "Can the projectile stick to items.")]
public bool StickToItems
{
get;
set;
}
[Serialize(false, IsPropertySaveable.No, description: "Can the projectile stick to doors. Caution: may cause issues.")]
public bool StickToDoors
{
get;
set;
}
[Serialize(false, IsPropertySaveable.No, description: "Can the item stick even to deflective targets.")]
public bool StickToDeflective
{
@@ -273,6 +300,8 @@ namespace Barotrauma.Items.Components
return;
}
SpreadCounter = (byte)(item.ID % SpreadCounterWrapAround);
InitProjSpecific(element);
}
partial void InitProjSpecific(ContentXElement element);
@@ -352,7 +381,7 @@ namespace Barotrauma.Items.Components
{
#if SERVER
launchRot = rotation;
Item.CreateServerEvent(this, new EventData(launch: true));
Item.CreateServerEvent(this, new EventData(launch: true, spreadCounter: (byte)(SpreadCounter - 1)));
#endif
}
}
@@ -376,8 +405,9 @@ namespace Barotrauma.Items.Components
}
else
{
launchAngle = item.body.Rotation + MathHelper.ToRadians(Spread * Rand.Range(-0.5f, 0.5f));
launchAngle = item.body.Rotation + MathHelper.ToRadians(Spread * GetSpreadFromPool(SpreadCounter));
}
SpreadCounter++;
Vector2 launchDir = new Vector2((float)Math.Cos(launchAngle), (float)Math.Sin(launchAngle));
if (Hitscan)
@@ -395,7 +425,6 @@ namespace Barotrauma.Items.Components
item.body.SetTransform(item.body.SimPosition, launchAngle);
float modifiedLaunchImpulse = (LaunchImpulse + launchImpulseModifier) * (1 + Rand.Range(-ImpulseSpread, ImpulseSpread));
DoLaunch(launchDir * modifiedLaunchImpulse);
System.Diagnostics.Debug.WriteLine("launch: " + modifiedLaunchImpulse + " - " + item.body.LinearVelocity);
}
}
User = character;
@@ -416,6 +445,7 @@ namespace Barotrauma.Items.Components
}
item.Drop(null, createNetworkEvent: false);
Item.WaterDragCoefficient = WaterDragCoefficient;
launchPos = item.SimPosition;
@@ -450,6 +480,7 @@ namespace Barotrauma.Items.Components
Vector2 simPositon = item.SimPosition;
Vector2 rayStartWorld = item.WorldPosition;
item.Drop(null);
Item.WaterDragCoefficient = WaterDragCoefficient;
item.body.Enabled = true;
//set the velocity of the body because the OnProjectileCollision method
@@ -467,36 +498,36 @@ namespace Barotrauma.Items.Components
Vector2 rayEndWorld = rayStartWorld + dir * worldDist;
List<HitscanResult> hits = new List<HitscanResult>();
hits.AddRange(DoRayCast(rayStart, rayEnd, submarine: item.Submarine));
if (item.Submarine != null)
{
//shooting indoors, do a hitscan outside as well
hits.AddRange(DoRayCast(rayStart + item.Submarine.SimPosition, rayEnd + item.Submarine.SimPosition, submarine: null));
//also in the coordinate space of docked subs
foreach (Submarine dockedSub in item.Submarine.DockedTo)
{
if (dockedSub == item.Submarine) { continue; }
hits.AddRange(DoRayCast(rayStart + item.Submarine.SimPosition - dockedSub.SimPosition, rayEnd + item.Submarine.SimPosition - dockedSub.SimPosition, dockedSub));
}
//do a hitscan in other subs' coordinate spaces
RayCastInOtherSubs(rayStart + item.Submarine.SimPosition, rayEnd + item.Submarine.SimPosition);
}
else
{
RayCastInOtherSubs(rayStart, rayEnd);
}
void RayCastInOtherSubs(Vector2 rayStart, Vector2 rayEnd)
{
//shooting outdoors, see if we can hit anything inside a sub
foreach (Submarine submarine in Submarine.Loaded)
{
if (submarine == item.Submarine) { continue; }
var inSubHits = DoRayCast(rayStart - submarine.SimPosition, rayEnd - submarine.SimPosition, submarine);
//transform back to world coordinates
for (int i = 0; i < inSubHits.Count; i++)
{
inSubHits[i] = new HitscanResult(
inSubHits[i].Fixture,
inSubHits[i].Point + submarine.SimPosition,
inSubHits[i].Normal,
inSubHits[i].Fixture,
inSubHits[i].Point + submarine.SimPosition,
inSubHits[i].Normal,
inSubHits[i].Fraction);
}
hits.AddRange(inSubHits);
}
}
@@ -508,6 +539,7 @@ namespace Barotrauma.Items.Components
{
var h = hits[i];
item.SetTransform(h.Point, rotation);
item.UpdateTransform();
if (HandleProjectileCollision(h.Fixture, h.Normal, Vector2.Zero))
{
hitCount++;
@@ -675,6 +707,7 @@ namespace Barotrauma.Items.Components
public override void Drop(Character dropper)
{
Item.ResetWaterDragCoefficient();
if (dropper != null)
{
DisableProjectileCollisions();
@@ -941,7 +974,7 @@ namespace Barotrauma.Items.Components
targetItem.Condition / targetItem.MaxCondition,
emptyColor: GUIStyle.HealthBarColorLow,
fullColor: GUIStyle.HealthBarColorHigh,
textTag: targetItem.Name);
textTag: targetItem.Prefab.ShowNameInHealthBar ? targetItem.Name : string.Empty);
}
#endif
}
@@ -1016,8 +1049,8 @@ namespace Barotrauma.Items.Components
}
if (GameMain.NetworkMember is { IsServer: true } server)
{
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, this, targetLimb.character, targetLimb, null, item.WorldPosition));
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnImpact, this, targetLimb.character, targetLimb, null, item.WorldPosition));
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, this, targetLimb.character, targetLimb, useTarget: targetLimb.character, item.WorldPosition));
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnImpact, this, targetLimb.character, targetLimb, useTarget: targetLimb.character, item.WorldPosition));
}
}
else
@@ -1026,8 +1059,8 @@ namespace Barotrauma.Items.Components
ApplyStatusEffects(ActionType.OnImpact, 1.0f, useTarget: target.Body.UserData as Entity, user: User);
if (GameMain.NetworkMember is { IsServer: true } server)
{
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, this, null, null, target.Body.UserData as Entity, item.WorldPosition));
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnImpact, this, null, null, target.Body.UserData as Entity, item.WorldPosition));
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, this, useTarget: target.Body.UserData as Entity, worldPosition: item.WorldPosition));
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnImpact, this, useTarget: target.Body.UserData as Entity, worldPosition: item.WorldPosition));
}
}
}
@@ -1035,13 +1068,12 @@ namespace Barotrauma.Items.Components
target.Body.ApplyLinearImpulse(velocity * item.body.Mass);
target.Body.LinearVelocity = target.Body.LinearVelocity.ClampLength(NetConfig.MaxPhysicsBodyVelocity * 0.5f);
if (hits.Count() >= MaxTargetsToHit || hits.LastOrDefault()?.UserData is VoronoiCell)
if (hits.Count >= MaxTargetsToHit || hits.LastOrDefault()?.UserData is VoronoiCell)
{
DisableProjectileCollisions();
}
if (attackResult.AppliedDamageModifiers != null &&
(attackResult.AppliedDamageModifiers.Any(dm => dm.DeflectProjectiles) && !StickToDeflective))
if (attackResult.AppliedDamageModifiers != null && attackResult.AppliedDamageModifiers.Any(dm => dm.DeflectProjectiles) && !StickToDeflective)
{
item.body.LinearVelocity *= deflectedSpeedMultiplier;
}
@@ -1051,7 +1083,7 @@ namespace Barotrauma.Items.Components
((StickToLightTargets || target.Body.Mass > item.body.Mass * 0.5f) &&
(DoesStick ||
(StickToCharacters && (target.Body.UserData is Limb || target.Body.UserData is Character)) ||
(StickToItems && target.Body.UserData is Item))))
(target.Body.UserData is Item i && (i.GetComponent<Door>() != null ? StickToDoors : StickToItems)))))
{
Vector2 dir = new Vector2(
(float)Math.Cos(item.body.Rotation),
@@ -1,12 +1,11 @@
using Barotrauma.Extensions;
using Barotrauma.Abilities;
using Barotrauma.Extensions;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Abilities;
namespace Barotrauma.Items.Components
{
@@ -17,6 +16,9 @@ namespace Barotrauma.Items.Components
private float deteriorationTimer;
private float deteriorateAlwaysResetTimer;
private int updateDeteriorationCounter;
private const int UpdateDeteriorationInterval = 10;
private int prevSentConditionValue;
private string conditionSignal;
@@ -404,26 +406,11 @@ namespace Barotrauma.Items.Components
#endif
}
}
if (!ShouldDeteriorate()) { return; }
if (item.Condition > 0.0f)
updateDeteriorationCounter++;
if (updateDeteriorationCounter >= UpdateDeteriorationInterval)
{
if (deteriorationTimer > 0.0f)
{
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
{
deteriorationTimer -= deltaTime * GetDeteriorationDelayMultiplier();
#if SERVER
if (deteriorationTimer <= 0.0f) { item.CreateServerEvent(this); }
#endif
}
return;
}
if (item.ConditionPercentage > MinDeteriorationCondition)
{
float deteriorationSpeed = item.StatManager.GetAdjustedValue(ItemTalentStats.DetoriationSpeed, DeteriorationSpeed);
item.Condition -= deteriorationSpeed * deltaTime;
}
UpdateDeterioration(deltaTime * UpdateDeteriorationInterval);
updateDeteriorationCounter = 0;
}
return;
}
@@ -559,6 +546,30 @@ namespace Barotrauma.Items.Components
}
}
private void UpdateDeterioration(float deltaTime)
{
if (item.Condition <= 0.0f) { return; }
if (!ShouldDeteriorate()) { return; }
if (deteriorationTimer > 0.0f)
{
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
{
deteriorationTimer -= deltaTime * GetDeteriorationDelayMultiplier();
#if SERVER
if (deteriorationTimer <= 0.0f) { item.CreateServerEvent(this); }
#endif
}
return;
}
if (item.ConditionPercentage > MinDeteriorationCondition)
{
float deteriorationSpeed = item.StatManager.GetAdjustedValue(ItemTalentStats.DetoriationSpeed, DeteriorationSpeed);
item.Condition -= deteriorationSpeed * deltaTime;
}
}
private float GetMaxRepairConditionMultiplier(Character character)
{
if (character == null) { return 1.0f; }
@@ -302,33 +302,16 @@ namespace Barotrauma.Items.Components
var sourceBody = GetBodyToPull(source);
if (sourceBody != null)
{
var targetBody = GetBodyToPull(target);
if (targetBody != null && targetBody.UserData is not Character)
if (user != null && user.InWater)
{
sourceBody.ApplyForce(targetBody.LinearVelocity * sourceBody.Mass);
}
float forceMultiplier = 1;
if (user != null)
{
user.AnimController.Hang();
if (user.InWater)
if (user.IsRagdolled)
{
if (user.IsRagdolled)
{
forceMultiplier = 0;
}
}
else
{
forceMultiplier = user.IsRagdolled ? 0.1f : 0.4f;
// Prevents too easy smashing to the walls
forceDir.X /= 4;
// Prevents rubberbanding up and down
if (forceDir.Y < 0)
{
forceDir.Y = 0;
}
// Reel in towards the target.
user.AnimController.Hang();
float force = LerpForces ? MathHelper.Lerp(0, SourcePullForce, MathUtils.InverseLerp(0, MaxLength / 2, distance)) : SourcePullForce;
sourceBody.ApplyForce(forceDir * force);
}
// Take the target velocity into account.
if (targetCharacter != null)
{
var myCollider = user.AnimController.Collider;
@@ -341,9 +324,15 @@ namespace Barotrauma.Items.Components
}
}
}
else
{
var targetBody = GetBodyToPull(target);
if (targetBody != null)
{
sourceBody.ApplyForce(targetBody.LinearVelocity * sourceBody.Mass);
}
}
}
float force = LerpForces ? MathHelper.Lerp(0, SourcePullForce, MathUtils.InverseLerp(0, MaxLength / 2, distance)) * forceMultiplier : SourcePullForce * forceMultiplier;
sourceBody.ApplyForce(forceDir * force);
}
}
}
@@ -304,9 +304,12 @@ namespace Barotrauma.Items.Components
}
#if SERVER
//make sure the clients know about the states of the checkboxes and text fields
if (item.Submarine == null || !item.Submarine.Loading)
if (customInterfaceElementList.Any())
{
item.CreateServerEvent(this);
if (item.Submarine == null || !item.Submarine.Loading)
{
item.CreateServerEvent(this);
}
}
#endif
}
@@ -94,7 +94,7 @@ namespace Barotrauma.Items.Components
if (isOn == value && IsActive == value) { return; }
IsActive = isOn = value;
SetLightSourceState(value);
SetLightSourceState(value, value ? lightBrightness : 0.0f);
OnStateChanged();
}
}
@@ -187,6 +187,15 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(true, IsPropertySaveable.No, description: "Should the light sprite be drawn on the item using alpha blending, in addition to being rendered in the light map? Can be used to make the light sprite stand out more.")]
public bool AlphaBlend
{
get;
set;
}
public float TemporaryFlickerTimer;
public override void Move(Vector2 amount, bool ignoreContacts = false)
{
#if CLIENT
@@ -205,7 +214,7 @@ namespace Barotrauma.Items.Components
{
if (base.IsActive == value) { return; }
base.IsActive = isOn = value;
SetLightSourceState(value);
SetLightSourceState(value, value ? lightBrightness : 0.0f);
}
}
@@ -239,6 +248,7 @@ namespace Barotrauma.Items.Components
SetLightSourceState(IsActive);
turret = item.GetComponent<Turret>();
#if CLIENT
Drawable = AlphaBlend && Light.LightSprite != null;
if (Screen.Selected.IsEditor)
{
OnMapLoaded();
@@ -311,8 +321,10 @@ namespace Barotrauma.Items.Components
return;
}
TemporaryFlickerTimer -= deltaTime;
//currPowerConsumption = powerConsumption;
if (Rand.Range(0.0f, 1.0f) < 0.05f && Voltage < Rand.Range(0.0f, MinVoltage))
if (Rand.Range(0.0f, 1.0f) < 0.05f && (Voltage < Rand.Range(0.0f, MinVoltage) || TemporaryFlickerTimer > 0.0f))
{
#if CLIENT
if (Voltage > 0.1f)
@@ -364,7 +376,7 @@ namespace Barotrauma.Items.Components
{
LightColor = XMLExtensions.ParseColor(signal.value, false);
#if CLIENT
SetLightSourceState(Light.Enabled);
SetLightSourceState(Light.Enabled, lightBrightness);
#endif
prevColorSignal = signal.value;
}
@@ -136,7 +136,7 @@ namespace Barotrauma.Items.Components
}
}
[Editable(DecimalCount = 3), Serialize(0.01f, IsPropertySaveable.Yes, description: "How fast the objects within the detector's range have to be moving (in m/s).", alwaysUseInstanceValues: true)]
[Editable(DecimalCount = 3), Serialize(0.1f, IsPropertySaveable.Yes, description: "How fast the objects within the detector's range have to be moving (in m/s).", alwaysUseInstanceValues: true)]
public float MinimumVelocity
{
get;
@@ -25,7 +25,7 @@ namespace Barotrauma.Items.Components
private string prevSignal;
private readonly int[] channelMemory = new int[ChannelMemorySize];
private int[] channelMemory = new int[ChannelMemorySize];
private Connection signalInConnection;
private Connection signalOutConnection;
@@ -94,7 +94,17 @@ namespace Barotrauma.Items.Components
{
list.Add(this);
IsActive = true;
channelMemory = element.GetAttributeIntArray("channelmemory", new int[ChannelMemorySize]);
}
public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap)
{
base.Load(componentElement, usePrefabValues, idRemap);
channelMemory = componentElement.GetAttributeIntArray("channelmemory", new int[ChannelMemorySize]);
if (channelMemory.Length != ChannelMemorySize)
{
DebugConsole.AddWarning($"Error when loading item {item.Prefab.Identifier}: the size of the channel memory doesn't match the default value of {ChannelMemorySize}. Resizing...");
Array.Resize(ref channelMemory, ChannelMemorySize);
}
}
public override void OnItemLoaded()
@@ -13,7 +13,7 @@ namespace Barotrauma.Items.Components
{
partial class Wire : ItemComponent, IDrawableComponent, IServerSerializable, IClientSerializable
{
partial class WireSection
public partial class WireSection
{
private Vector2 start;
private Vector2 end;
@@ -775,20 +775,25 @@ namespace Barotrauma.Items.Components
UpdateSections();
}
public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap)
public static IEnumerable<Vector2> ExtractNodes(XElement element)
{
base.Load(componentElement, usePrefabValues, idRemap);
string nodeString = componentElement.GetAttributeString("nodes", "");
if (nodeString == "") return;
string nodeString = element.GetAttributeString("nodes", "");
if (nodeString.IsNullOrWhiteSpace()) { yield break; }
string[] nodeCoords = nodeString.Split(';');
for (int i = 0; i < nodeCoords.Length / 2; i++)
{
float.TryParse(nodeCoords[i * 2], NumberStyles.Float, CultureInfo.InvariantCulture, out float x);
float.TryParse(nodeCoords[i * 2 + 1], NumberStyles.Float, CultureInfo.InvariantCulture, out float y);
nodes.Add(new Vector2(x, y));
float.TryParse(nodeCoords[i * 2].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out float x);
float.TryParse(nodeCoords[i * 2 + 1].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out float y);
yield return new Vector2(x, y);
}
}
public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap)
{
base.Load(componentElement, usePrefabValues, idRemap);
nodes.AddRange(ExtractNodes(componentElement));
Drawable = nodes.Any();
}
@@ -59,8 +59,9 @@ namespace Barotrauma.Items.Components
private float aiTargetingGraceTimer;
private float aiFindTargetTimer;
private Character currentTarget;
const float aiFindTargetInterval = 5.0f;
private ISpatialEntity currentTarget;
private const float CrewAiFindTargetMaxInterval = 3.0f;
private const float CrewAIFindTargetMinInverval = 0.2f;
private int currentLoaderIndex;
@@ -73,6 +74,8 @@ namespace Barotrauma.Items.Components
private List<LightComponent> lightComponents;
private readonly bool isSlowTurret;
public float Rotation
{
get { return rotation; }
@@ -320,33 +323,36 @@ namespace Barotrauma.Items.Components
[Serialize(false, IsPropertySaveable.Yes, description:"Should the turret operate automatically using AI targeting? Comes with some optional random movement that can be adjusted below."), Editable]
public bool AutoOperate { get; set; }
[Serialize(0f, IsPropertySaveable.Yes, description: "[Auto Operate] How much the turret should adjust the aim off the target randomly instead of tracking the target perfectly?"), Editable]
public float RandomAimAmount { get; private set; }
[Serialize(0f, IsPropertySaveable.Yes, description: "[Auto Operate] How much the turret should adjust the aim off the target randomly instead of tracking the target perfectly? In Degrees."), Editable]
public float RandomAimAmount { get; set; }
[Serialize(0f, IsPropertySaveable.Yes, description: "[Auto Operate] How often the turret should adjust the aim randomly instead of tracking the target perfectly?"), Editable]
public float RandomAimMinTime { get; private set; }
[Serialize(0f, IsPropertySaveable.Yes, description: "[Auto Operate] How often the turret should adjust the aim randomly instead of tracking the target perfectly? Minimum wait time, in seconds."), Editable]
public float RandomAimMinTime { get; set; }
[Serialize(0f, IsPropertySaveable.Yes, description: "[Auto Operate] How often the turret should adjust the aim randomly instead of tracking the target perfectly?"), Editable]
public float RandomAimMaxTime { get; private set; }
[Serialize(0f, IsPropertySaveable.Yes, description: "[Auto Operate] How often the turret should adjust the aim randomly instead of tracking the target perfectly? Maximum wait time, in seconds."), Editable]
public float RandomAimMaxTime { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret move randomly while idle?"), Editable]
public bool RandomMovement { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret always aim at targets without delay?"), Editable]
public bool IgnoreAimDelay { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret have a delay while targeting targets or always aim prefectly?"), Editable]
public bool AimDelay { get; set; }
[Serialize(true, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret target characters?"), Editable]
[Serialize(true, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret target characters in general?"), Editable]
public bool TargetCharacters { get; set; }
[Serialize(true, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret target monsters?"), Editable]
[Serialize(true, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret target all monsters?"), Editable]
public bool TargetMonsters { get; set; }
[Serialize(true, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret target humans (or pets)"), Editable]
[Serialize(true, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret target all humans (or creatures in the same group, like pets)?"), Editable]
public bool TargetHumans { get; set; }
[Serialize(true, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret target other submarines?"), Editable]
public bool TargetSubmarines { get; set; }
[Serialize(true, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret target items?"), Editable]
public bool TargetItems { get; set; }
[Serialize("", IsPropertySaveable.Yes, description: "[Auto Operate] Group or SpeciesName that the AI ignores when the turret is operated automatically."), Editable]
public Identifier FriendlyTag { get; private set; }
@@ -379,6 +385,7 @@ namespace Barotrauma.Items.Components
}
item.IsShootable = true;
item.RequireAimToUse = false;
isSlowTurret = item.HasTag("slowturret");
InitProjSpecific(element);
}
@@ -940,7 +947,7 @@ namespace Barotrauma.Items.Components
}
private float waitTimer;
private float disorderTimer;
private float randomAimTimer;
private float prevTargetRotation;
private float updateTimer;
@@ -950,10 +957,6 @@ namespace Barotrauma.Items.Components
{
IsActive = true;
bool targetCharacters = TargetCharacters || TargetHumans || TargetMonsters;
bool targetHumans = TargetCharacters && TargetHumans;
bool targetMonsters = TargetCharacters && TargetMonsters;
if (friendlyTag.IsEmpty)
{
friendlyTag = FriendlyTag;
@@ -977,7 +980,7 @@ namespace Barotrauma.Items.Components
updateTimer -= deltaTime;
}
if (!IgnoreAimDelay && waitTimer > 0)
if (AimDelay && waitTimer > 0)
{
waitTimer -= deltaTime;
return;
@@ -987,30 +990,34 @@ namespace Barotrauma.Items.Components
float shootDistance = AIRange;
ISpatialEntity target = null;
float closestDist = shootDistance * shootDistance;
if (targetCharacters)
if (TargetCharacters)
{
foreach (var character in Character.CharacterList)
{
if (character == null || character.Removed || character.IsDead) { continue; }
if (!friendlyTag.IsEmpty && (character.SpeciesName.Equals(friendlyTag) || character.Group.Equals(friendlyTag))) { continue; }
bool isHuman = character.IsHuman || character.Group == CharacterPrefab.HumanSpeciesName;
if (isHuman)
{
if (!targetHumans)
{
// Don't target humans if not defined to.
continue;
}
}
else if (!targetMonsters)
{
// Don't target other creatures if not defined to.
continue;
}
if (!IsValidTarget(character)) { continue; }
float priority = isSlowTurret ? character.Params.AISlowTurretPriority : character.Params.AITurretPriority;
if (priority <= 0) { continue; }
if (!IsValidTargetForAutoOperate(character, friendlyTag)) { continue; }
float dist = Vector2.DistanceSquared(character.WorldPosition, item.WorldPosition);
if (dist > closestDist) { continue; }
if (!CheckTurretAngle(character.WorldPosition)) { continue; }
target = character;
closestDist = dist;
closestDist = dist / priority;
}
}
if (TargetItems)
{
foreach (Item targetItem in Item.ItemList)
{
if (!IsValidTarget(targetItem)) { continue; }
float priority = isSlowTurret ? targetItem.Prefab.AISlowTurretPriority : targetItem.Prefab.AITurretPriority;
if (priority <= 0) { continue; }
float dist = Vector2.DistanceSquared(item.WorldPosition, targetItem.WorldPosition);
if (dist > closestDist) { continue; }
if (dist > shootDistance * shootDistance) { continue; }
if (!CheckTurretAngle(targetItem.WorldPosition)) { continue; }
target = targetItem;
closestDist = dist / priority;
}
}
if (TargetSubmarines)
@@ -1020,8 +1027,11 @@ namespace Barotrauma.Items.Components
closestDist = maxDistance * maxDistance;
foreach (Submarine sub in Submarine.Loaded)
{
if (sub.Info.Type != SubmarineType.Player) { continue; }
if (sub == Item.Submarine) { continue; }
if (item.Submarine != null)
{
if (Character.IsOnFriendlyTeam(item.Submarine.TeamID, sub.TeamID)) { continue; }
}
float dist = Vector2.DistanceSquared(sub.WorldPosition, item.WorldPosition);
if (dist > closestDist) { continue; }
closestSub = sub;
@@ -1035,6 +1045,7 @@ namespace Barotrauma.Items.Components
if (!closestSub.IsEntityFoundOnThisSub(hull, true)) { continue; }
float dist = Vector2.DistanceSquared(hull.WorldPosition, item.WorldPosition);
if (dist > closestDist) { continue; }
// Don't check the angle, because it doesn't work on Thalamus spike. The angle check wouldn't be very important here anyway.
target = hull;
closestDist = dist;
}
@@ -1051,22 +1062,23 @@ namespace Barotrauma.Items.Components
return;
}
if (!IgnoreAimDelay)
if (AimDelay)
{
if (RandomAimAmount > 0)
{
if (disorderTimer < 0)
if (randomAimTimer < 0)
{
// Random disorder
disorderTimer = Rand.Range(RandomAimMinTime, RandomAimMaxTime);
// Random disorder or other flaw in the targeting.
randomAimTimer = Rand.Range(RandomAimMinTime, RandomAimMaxTime);
waitTimer = Rand.Range(0.25f, 1f);
targetRotation = MathUtils.WrapAngleTwoPi(targetRotation += Rand.Range(-RandomAimAmount, RandomAimAmount));
float randomAim = MathHelper.ToRadians(RandomAimAmount);
targetRotation = MathUtils.WrapAngleTwoPi(targetRotation += Rand.Range(-randomAim, randomAim));
updatePending = true;
return;
}
else
{
disorderTimer -= deltaTime;
randomAimTimer -= deltaTime;
}
}
}
@@ -1264,18 +1276,19 @@ namespace Barotrauma.Items.Components
bool hadCurrentTarget = currentTarget != null;
if (hadCurrentTarget)
{
if (currentTarget.Removed || currentTarget.IsDead)
if (!IsValidTarget(currentTarget))
{
currentTarget = null;
aiFindTargetTimer = CrewAIFindTargetMinInverval;
}
}
if (aiFindTargetTimer <= 0.0f || currentTarget == null)
if (aiFindTargetTimer <= 0.0f)
{
foreach (Character enemy in Character.CharacterList)
{
// Ignore dead, friendly, and those that are inside the same sub
if (enemy.IsDead || !enemy.Enabled) { continue; }
if (!IsValidTarget(enemy)) { continue; }
float priority = isSlowTurret ? enemy.Params.AISlowTurretPriority : enemy.Params.AITurretPriority;
if (priority <= 0) { continue; }
if (character.Submarine != null)
{
if (enemy.Submarine == character.Submarine) { continue; }
@@ -1292,30 +1305,53 @@ namespace Barotrauma.Items.Components
// We shouldn't check the angle when a long creature is traveling outside of the shooting range, because doing so would not allow us to shoot the limbs that might be close enough to shoot at.
if (!CheckTurretAngle(enemy.WorldPosition)) { continue; }
}
targetPos = enemy.WorldPosition;
closestEnemy = enemy;
closestDistance = dist;
closestDistance = dist / priority;
currentTarget = closestEnemy;
}
currentTarget = closestEnemy;
aiFindTargetTimer = aiFindTargetInterval;
}
else
{
closestEnemy = currentTarget;
}
if (closestEnemy != null)
{
targetPos = closestEnemy.WorldPosition;
//if the enemy is inside another sub, aim at the room they're in to make it less obvious that the enemy "knows" exactly where the target is
if (closestEnemy.Submarine != null && closestEnemy.CurrentHull != null && closestEnemy.Submarine != item.Submarine && !closestEnemy.CanSeeTarget(Item))
foreach (Item targetItem in Item.ItemList)
{
targetPos = closestEnemy.CurrentHull.WorldPosition;
if (!IsValidTarget(targetItem)) { continue; }
float priority = isSlowTurret ? targetItem.Prefab.AISlowTurretPriority : targetItem.Prefab.AITurretPriority;
if (priority <= 0) { continue; }
float dist = Vector2.DistanceSquared(item.WorldPosition, targetItem.WorldPosition);
if (dist > closestDistance) { continue; }
if (dist > shootDistance * shootDistance) { continue; }
if (!CheckTurretAngle(targetItem.WorldPosition)) { continue; }
targetPos = targetItem.WorldPosition;
closestDistance = dist / priority;
// Override the target character so that we can target the item instead.
closestEnemy = null;
currentTarget = targetItem;
}
if (currentTarget == null)
{
aiFindTargetTimer = CrewAIFindTargetMinInverval;
}
else
{
aiFindTargetTimer = CrewAiFindTargetMaxInterval;
}
}
else if (currentTarget != null)
{
targetPos = currentTarget.WorldPosition;
}
bool iceSpireSpotted = false;
// Adjust the target character position (limb or submarine)
if (currentTarget is Character targetCharacter)
{
//if the enemy is inside another sub, aim at the room they're in to make it less obvious that the enemy "knows" exactly where the target is
if (targetCharacter.Submarine != null && targetCharacter.CurrentHull != null && targetCharacter.Submarine != item.Submarine && !targetCharacter.CanSeeTarget(Item))
{
targetPos = targetCharacter.CurrentHull.WorldPosition;
}
else
{
// Target the closest limb. Doesn't make much difference with smaller creatures, but enables the bots to shoot longer abyss creatures like the endworm. Otherwise they just target the main body = head.
float closestDist = closestDistance;
foreach (Limb limb in closestEnemy.AnimController.Limbs)
foreach (Limb limb in targetCharacter.AnimController.Limbs)
{
if (limb.IsSevered) { continue; }
if (limb.Hidden) { continue; }
@@ -1329,13 +1365,14 @@ namespace Barotrauma.Items.Components
}
if (closestDist > shootDistance * shootDistance)
{
// Not close enough to shoot
// Not close enough to shoot.
currentTarget = null;
closestEnemy = null;
targetPos = null;
}
}
}
else if (item.Submarine != null && Level.Loaded != null)
else if (targetPos == null && item.Submarine != null && Level.Loaded != null)
{
// Check ice spires
shootDistance = AIRange * item.OffsetOnSelectedMultiplier;
@@ -1345,50 +1382,49 @@ namespace Barotrauma.Items.Components
if (wall is not DestructibleLevelWall destructibleWall || destructibleWall.Destroyed) { continue; }
foreach (var cell in wall.Cells)
{
if (cell.DoesDamage)
if (!cell.DoesDamage) { continue; }
foreach (var edge in cell.Edges)
{
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))
{
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))
{
// 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;
}
closestPoint = intersection;
if (!CheckTurretAngle(closestPoint)) { continue; }
}
float dist = Vector2.Distance(closestPoint, item.WorldPosition);
//add one px to make sure the visibility raycast doesn't miss the cell due to the end position being right at the edge of the cell
closestPoint += (closestPoint - item.WorldPosition) / Math.Max(dist, 1);
if (dist > AIRange + 1000) { continue; }
float dot = 0;
if (!MathUtils.NearlyEqual(item.Submarine.Velocity, Vector2.Zero))
else
{
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;
}
float dist = Vector2.Distance(closestPoint, item.WorldPosition);
//add one px to make sure the visibility raycast doesn't miss the cell due to the end position being right at the edge of the cell
closestPoint += (closestPoint - item.WorldPosition) / Math.Max(dist, 1);
if (dist > AIRange + 1000) { continue; }
float dot = 0;
if (!MathUtils.NearlyEqual(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;
iceSpireSpotted = true;
}
}
}
@@ -1404,13 +1440,13 @@ namespace Barotrauma.Items.Components
{
if (character.AIController.SelectedAiTarget == null && !hadCurrentTarget)
{
if (CreatureMetrics.Instance.RecentlyEncountered.Contains(closestEnemy.SpeciesName) || closestEnemy.IsHuman)
if (CreatureMetrics.RecentlyEncountered.Contains(closestEnemy.SpeciesName) || closestEnemy.IsHuman)
{
character.Speak(TextManager.Get("DialogNewTargetSpotted").Value,
identifier: "newtargetspotted".ToIdentifier(),
minDurationBetweenSimilar: 30.0f);
}
else if (CreatureMetrics.Instance.Encountered.Contains(closestEnemy.SpeciesName))
else if (CreatureMetrics.Encountered.Contains(closestEnemy.SpeciesName))
{
character.Speak(TextManager.GetWithVariable("DialogIdentifiedTargetSpotted", "[speciesname]", closestEnemy.DisplayName).Value,
identifier: "identifiedtargetspotted".ToIdentifier(),
@@ -1423,17 +1459,17 @@ namespace Barotrauma.Items.Components
minDurationBetweenSimilar: 5.0f);
}
}
else if (!CreatureMetrics.Instance.Encountered.Contains(closestEnemy.SpeciesName))
else if (!CreatureMetrics.Encountered.Contains(closestEnemy.SpeciesName))
{
character.Speak(TextManager.Get("DialogUnidentifiedTargetSpotted").Value,
identifier: "unidentifiedtargetspotted".ToIdentifier(),
minDurationBetweenSimilar: 5.0f);
}
character.AddEncounter(closestEnemy);
CreatureMetrics.AddEncounter(closestEnemy.SpeciesName);
}
character.AIController.SelectTarget(closestEnemy.AiTarget);
}
else if (closestEnemy == null && character.IsOnPlayerTeam)
else if (iceSpireSpotted && character.IsOnPlayerTeam)
{
character.Speak(TextManager.Get("DialogIceSpireSpotted").Value,
identifier: "icespirespotted".ToIdentifier(),
@@ -1496,6 +1532,54 @@ namespace Barotrauma.Items.Components
return 0;
}
// Not exahustive, but helps to get rid of some code duplication
private static bool IsValidTarget(ISpatialEntity target)
{
if (target == null) { return false; }
if (target is Character targetCharacter)
{
if (!targetCharacter.Enabled || targetCharacter.Removed || targetCharacter.IsDead || targetCharacter.AITurretPriority <= 0)
{
return false;
}
}
else if (target is Item targetItem)
{
if (targetItem.Removed || targetItem.Condition <= 0 || !targetItem.Prefab.IsAITurretTarget || targetItem.Prefab.AITurretPriority <= 0 || targetItem.HiddenInGame)
{
return false;
}
if (targetItem.Submarine != null)
{
return false;
}
}
return true;
}
private bool IsValidTargetForAutoOperate(Character target, Identifier friendlyTag)
{
if (!friendlyTag.IsEmpty)
{
if (target.SpeciesName.Equals(friendlyTag) || target.Group.Equals(friendlyTag)) { return false; }
}
bool isHuman = target.IsHuman || target.Group == CharacterPrefab.HumanSpeciesName;
if (isHuman)
{
if (item.Submarine != null)
{
// Check that the target is not in the friendly team, e.g. pirate or a hostile player sub (PvP).
return !target.IsOnFriendlyTeam(item.Submarine.TeamID) && TargetHumans;
}
return TargetHumans;
}
else
{
// Shouldn't check the team here, because all the enemies are in the same team (None).
return TargetMonsters;
}
}
private bool CanShoot(Body targetBody, Character user = null, Identifier friendlyTag = default, bool targetSubmarines = true)
{
if (targetBody == null) { return false; }
@@ -1508,7 +1592,7 @@ namespace Barotrauma.Items.Components
{
targetCharacter = limb.character;
}
if (targetCharacter != null)
if (targetCharacter != null && !targetCharacter.Removed)
{
if (user != null)
{
@@ -1517,27 +1601,25 @@ namespace Barotrauma.Items.Components
return false;
}
}
if (!friendlyTag.IsEmpty)
else if (!IsValidTargetForAutoOperate(targetCharacter, friendlyTag))
{
if (targetCharacter.SpeciesName.Equals(friendlyTag) || targetCharacter.Group.Equals(friendlyTag))
{
return false;
}
// Note that Thalamus runs this even when AutoOperate is false.
return false;
}
}
else
{
if (targetBody.UserData is ISpatialEntity e)
{
if (e is Structure s && s.Indestructible) { return false; }
Submarine sub = e.Submarine ?? e as Submarine;
if (e is Structure { Indestructible: true }) { return false; }
if (!targetSubmarines && e is Submarine) { return false; }
if (sub == null) { return false; }
Submarine sub = e.Submarine ?? e as Submarine;
if (sub == null) { return true; }
if (sub == Item.Submarine) { return false; }
if (sub.Info.IsOutpost || sub.Info.IsWreck || sub.Info.IsBeacon) { return false; }
if (sub.TeamID == Item.Submarine.TeamID) { return false; }
}
else if (!(targetBody.UserData is Voronoi2.VoronoiCell cell && cell.IsDestructible))
else if (targetBody.UserData is not Voronoi2.VoronoiCell { IsDestructible: true })
{
// Hit something else, probably a level wall
return false;
@@ -1548,7 +1630,7 @@ namespace Barotrauma.Items.Components
private Body CheckLineOfSight(Vector2 start, Vector2 end)
{
var collisionCategories = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel;
var collisionCategories = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel | Physics.CollisionProjectile;
Body pickedBody = Submarine.PickBody(start, end, null, collisionCategories, allowInsideFixture: true,
customPredicate: (Fixture f) =>
{
@@ -44,7 +44,16 @@ namespace Barotrauma
}
public LimbType Limb { get; private set; }
public bool HideLimb { get; private set; }
public bool HideOtherWearables { get; private set; }
public enum ObscuringMode
{
None,
Hide,
AlphaClip
}
public ObscuringMode ObscureOtherWearables { get; private set; }
public bool HideOtherWearables => ObscureOtherWearables == ObscuringMode.Hide;
public bool AlphaClipOtherWearables => ObscureOtherWearables == ObscuringMode.AlphaClip;
public bool CanBeHiddenByOtherWearables { get; private set; }
public List<WearableType> HideWearablesOfType { get; private set; }
public bool InheritLimbDepth { get; private set; }
@@ -130,7 +139,7 @@ namespace Barotrauma
case WearableType.Husk:
case WearableType.Herpes:
Limb = LimbType.Head;
HideOtherWearables = false;
ObscureOtherWearables = ObscuringMode.None;
InheritLimbDepth = true;
InheritScale = true;
InheritOrigin = true;
@@ -202,7 +211,16 @@ namespace Barotrauma
Sprite = new Sprite(SourceElement, file: SpritePath);
Limb = (LimbType)Enum.Parse(typeof(LimbType), SourceElement.GetAttributeString("limb", "Head"), true);
HideLimb = SourceElement.GetAttributeBool("hidelimb", false);
HideOtherWearables = SourceElement.GetAttributeBool("hideotherwearables", false);
foreach (var mode in Enum.GetValues<ObscuringMode>())
{
if (mode == ObscuringMode.None) { continue; }
if (SourceElement.GetAttributeBool($"{mode}OtherWearables", false))
{
ObscureOtherWearables = mode;
}
}
CanBeHiddenByOtherWearables = SourceElement.GetAttributeBool("canbehiddenbyotherwearables", true);
InheritLimbDepth = SourceElement.GetAttributeBool("inheritlimbdepth", true);
var scale = SourceElement.GetAttribute("inheritscale");
@@ -509,7 +527,7 @@ namespace Barotrauma.Items.Components
public override void Update(float deltaTime, Camera cam)
{
if (picker.Removed)
if (picker == null || picker.Removed)
{
IsActive = false;
return;
@@ -519,7 +537,7 @@ namespace Barotrauma.Items.Components
if (item.GetComponent<Holdable>() is not { IsActive: true })
{
item.SetTransform(picker.SimPosition, 0.0f);
}
}
item.ApplyStatusEffects(ActionType.OnWearing, deltaTime, picker);
#if CLIENT