v0.10.5.1

This commit is contained in:
Juan Pablo Arce
2020-09-22 11:31:56 -03:00
parent 44032d0ae0
commit 0002ad2c50
343 changed files with 12276 additions and 5023 deletions
@@ -209,15 +209,17 @@ namespace Barotrauma.Items.Components
#if SERVER
if (GameMain.Server != null && (!item.Submarine?.Loading ?? true))
{
originalDockingTargetID = DockingTarget.item.ID;
item.CreateServerEvent(this);
}
#endif
}
public void Lock(bool isNetworkMessage, bool forcePosition = false)
{
#if CLIENT
if (GameMain.Client != null && !isNetworkMessage) return;
if (GameMain.Client != null && !isNetworkMessage) { return; }
#endif
if (DockingTarget == null)
@@ -251,6 +253,7 @@ namespace Barotrauma.Items.Components
#if SERVER
if (GameMain.Server != null && (!item.Submarine?.Loading ?? true))
{
originalDockingTargetID = DockingTarget.item.ID;
item.CreateServerEvent(this);
}
#else
@@ -332,20 +335,45 @@ namespace Barotrauma.Items.Components
{
if (DockingDir != 0) { return DockingDir; }
if (Door != null)
if (Door != null && Door.LinkedGap.linkedTo.Count > 0)
{
if (Door.LinkedGap.linkedTo.Count == 1)
Hull refHull = null;
float largestHullSize = 0.0f;
foreach (MapEntity linked in Door.LinkedGap.linkedTo)
{
if (!(linked is Hull hull)) { continue; }
if (hull.Volume > largestHullSize)
{
refHull = hull;
largestHullSize = hull.Volume;
}
}
if (refHull != null)
{
return IsHorizontal ?
Math.Sign(Door.Item.WorldPosition.X - Door.LinkedGap.linkedTo[0].WorldPosition.X) :
Math.Sign(Door.Item.WorldPosition.Y - Door.LinkedGap.linkedTo[0].WorldPosition.Y);
Math.Sign(Door.Item.WorldPosition.X - refHull.WorldPosition.X) :
Math.Sign(Door.Item.WorldPosition.Y - refHull.WorldPosition.Y);
}
else if (dockingTarget?.Door?.LinkedGap != null && dockingTarget.Door.LinkedGap.linkedTo.Count == 1)
}
if (dockingTarget?.Door?.LinkedGap != null && dockingTarget.Door.LinkedGap.linkedTo.Count > 0)
{
Hull refHull = null;
float largestHullSize = 0.0f;
foreach (MapEntity linked in dockingTarget.Door.LinkedGap.linkedTo)
{
if (!(linked is Hull hull)) { continue; }
if (hull.Volume > largestHullSize)
{
refHull = hull;
largestHullSize = hull.Volume;
}
}
if (refHull != null)
{
return IsHorizontal ?
Math.Sign(dockingTarget.Door.LinkedGap.linkedTo[0].WorldPosition.X - dockingTarget.Door.Item.WorldPosition.X) :
Math.Sign(dockingTarget.Door.LinkedGap.linkedTo[0].WorldPosition.Y - dockingTarget.Door.Item.WorldPosition.Y);
}
Math.Sign(refHull.WorldPosition.X - dockingTarget.Door.Item.WorldPosition.X) :
Math.Sign(refHull.WorldPosition.Y - dockingTarget.Door.Item.WorldPosition.Y);
}
}
if (dockingTarget != null)
{
@@ -838,6 +866,7 @@ namespace Barotrauma.Items.Components
#if SERVER
if (GameMain.Server != null && (!item.Submarine?.Loading ?? true))
{
originalDockingTargetID = Entity.NullEntityID;
item.CreateServerEvent(this);
}
#endif
@@ -1010,9 +1039,7 @@ namespace Barotrauma.Items.Components
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f)
{
#if CLIENT
if (GameMain.Client != null) return;
#endif
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
bool wasDocked = docked;
DockingPort prevDockingTarget = DockingTarget;
@@ -1020,7 +1047,10 @@ namespace Barotrauma.Items.Components
switch (connection.Name)
{
case "toggle":
Docked = !docked;
if (signal != "0")
{
Docked = !docked;
}
break;
case "set_active":
case "set_state":
@@ -1044,16 +1074,5 @@ namespace Barotrauma.Items.Components
}
#endif
}
public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
{
msg.Write(docked);
if (docked)
{
msg.Write(DockingTarget.item.ID);
msg.Write(hulls != null && hulls[0] != null && hulls[1] != null && gap != null);
}
}
}
}
@@ -90,8 +90,10 @@ namespace Barotrauma.Items.Components
get { return stuck; }
set
{
if (isOpen || isBroken || !CanBeWelded) return;
if (isOpen || isBroken || !CanBeWelded) { return; }
stuck = MathHelper.Clamp(value, 0.0f, 100.0f);
//don't allow clients to make the door stuck unless the server says so (handled in ClientRead)
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
if (stuck <= 0.0f) { IsStuck = false; }
if (stuck >= 99.0f) { IsStuck = true; }
}
@@ -366,12 +368,24 @@ namespace Barotrauma.Items.Components
}
else
{
Body.Enabled = Impassable || openState < 1.0f;
bool wasEnabled = Body.Enabled;
Body.Enabled = Impassable || openState < 1.0f;
if (wasEnabled && !Body.Enabled && IsHorizontal)
{
//when opening a hatch, force characters above it to refresh the floor position
//(otherwise the character won't fall through the hatch until it moves)
foreach (Character c in Character.CharacterList)
{
if (c.WorldPosition.Y < item.WorldPosition.Y) { continue; }
if (c.WorldPosition.X < item.WorldRect.X || c.WorldPosition.X > item.WorldRect.Right) { continue; }
c.AnimController?.ForceRefreshFloorY();
}
}
}
//don't use the predicted state here, because it might set
//other items to an incorrect state if the prediction is wrong
item.SendSignal(0, (isOpen) ? "1" : "0", "state_out", null);
item.SendSignal(0, isOpen ? "1" : "0", "state_out", null);
}
partial void UpdateProjSpecific(float deltaTime);
@@ -616,12 +630,13 @@ namespace Barotrauma.Items.Components
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f)
{
if (IsStuck) return;
if (IsStuck) { return; }
bool wasOpen = PredictedState == null ? isOpen : PredictedState.Value;
if (connection.Name == "toggle")
{
if (signal == "0") { return; }
if (toggleCooldownTimer > 0.0f && sender != lastUser) { OnFailedToOpen(); return; }
if (IsStuck) { toggleCooldownTimer = 1.0f; OnFailedToOpen(); return; }
toggleCooldownTimer = ToggleCoolDown;
@@ -0,0 +1,832 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Xml.Linq;
using Barotrauma.Extensions;
using Barotrauma.Networking;
using FarseerPhysics;
using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
using Vector2 = Microsoft.Xna.Framework.Vector2;
namespace Barotrauma.Items.Components
{
internal class ProducedItem
{
[Serialize(0f, true)]
public float Probability { get; set; }
public readonly List<StatusEffect> StatusEffects = new List<StatusEffect>();
public readonly ItemPrefab? Prefab;
public ProducedItem(ItemPrefab prefab, float probability)
{
Prefab = prefab;
Probability = probability;
}
public ProducedItem(XElement element)
{
SerializableProperty.DeserializeProperties(this, element);
string itemIdentifier = element.GetAttributeString("identifier", string.Empty);
if (!string.IsNullOrWhiteSpace(itemIdentifier))
{
Prefab = ItemPrefab.Find(null, itemIdentifier);
}
LoadSubElements(element);
}
private void LoadSubElements(XElement element)
{
if (!element.HasElements) { return; }
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "statuseffect":
{
StatusEffect effect = StatusEffect.Load(subElement, Prefab?.Name);
if (effect.type != ActionType.OnProduceSpawned)
{
DebugConsole.ThrowError("Only OnProduceSpawned type can be used in <ProducedItem>.");
continue;
}
StatusEffects.Add(effect);
break;
}
}
}
}
}
// ReSharper disable UnusedMember.Global
internal enum VineTileType
{
Stem = 0b0000,
CrossJunction = 0b1111,
VerticalLane = 0b1010,
HorizontalLane = 0b0101,
TurnTopRight = 0b1001,
TurnTopLeft = 0b0011,
TurnBottomLeft = 0b0110,
TurnBottomRight = 0b1100,
TSectionTop = 0b1011,
TSectionLeft = 0b0111,
TSectionBottom = 0b1110,
TSectionRight = 0b1101,
StumpTop = 0b0001,
StumpLeft = 0b0010,
StumpBottom = 0b0100,
StumpRight = 0b1000
}
[Flags]
internal enum TileSide
{
None = 0,
Top = 1 << 0,
Left = 1 << 1,
Bottom = 1 << 2,
Right = 1 << 3
}
internal struct FoliageConfig
{
public static FoliageConfig EmptyConfig = new FoliageConfig { Variant = -1, Rotation = 0f, Scale = 1.0f };
public static readonly int EmptyConfigValue = EmptyConfig.Serialize();
public int Variant;
public float Rotation;
public float Scale;
public readonly int Serialize()
{
int variant = Math.Min(Variant + 1, 15);
int scale = (int) (Scale * 10f);
int rotation = (int) (Rotation / MathHelper.TwoPi * 10f);
return variant | (scale << 4) | (rotation << 8);
}
public static FoliageConfig Deserialize(int value)
{
int variant = value & 0x00F;
int scale = (value & 0x0F0) >> 4;
int rotation = (value & 0xF00) >> 8;
return new FoliageConfig { Variant = variant - 1, Scale = scale / 10f, Rotation = rotation / 10f * MathHelper.TwoPi };
}
public static FoliageConfig CreateRandomConfig(int maxVariants, float minScale, float maxScale, Random? random = null)
{
int flowerVariant = Growable.RandomInt(0, maxVariants, random);
float flowerScale = (float) Growable.RandomDouble(minScale, maxScale, random);
float flowerRotation = (float) Growable.RandomDouble(0, MathHelper.TwoPi, random);
return new FoliageConfig { Variant = flowerVariant, Scale = flowerScale, Rotation = flowerRotation };
}
}
internal partial class VineTile
{
public TileSide Sides = TileSide.None;
public TileSide BlockedSides = TileSide.None;
public readonly FoliageConfig FlowerConfig;
public readonly FoliageConfig LeafConfig;
public int FailedGrowthAttempts;
public Rectangle Rect;
public Vector2 Position;
public Color HealthColor = Color.Transparent;
public float DecayDelay;
private float VineStep;
private float FlowerStep;
private float growthStep;
public float GrowthStep
{
get => growthStep;
set
{
const float limit = 1.0f;
growthStep = value;
VineStep = Math.Min((float) Math.Pow(value, 2), limit);
if (value > limit)
{
FlowerStep = Math.Min((float) Math.Pow(value - limit, 2), limit);
}
}
}
private readonly float diameter;
private Vector2 offset;
private readonly Growable Parent;
public VineTileType Type;
public readonly Dictionary<TileSide, Vector2> AdjacentPositions;
public static int Size = 32;
public VineTile(Growable parent, Vector2 position, VineTileType type, FoliageConfig? flowerConfig = null, FoliageConfig? leafConfig = null, Rectangle? rect = null)
{
FlowerConfig = flowerConfig ?? FoliageConfig.EmptyConfig;
LeafConfig = leafConfig ?? FoliageConfig.EmptyConfig;
Position = position;
Rect = rect ?? CreatePlantRect(position);
Parent = parent;
Type = type;
diameter = Rect.Width / 2.0f;
AdjacentPositions = new Dictionary<TileSide, Vector2>
{
{ TileSide.Top, new Vector2(Position.X, Position.Y + Rect.Height) },
{ TileSide.Bottom, new Vector2(Position.X, Position.Y - Rect.Height) },
{ TileSide.Left, new Vector2(Position.X - Rect.Width, Position.Y) },
{ TileSide.Right, new Vector2(Position.X + Rect.Width, Position.Y) }
};
}
public void UpdateScale(float deltaTime)
{
if (Parent.Decayed && GrowthStep > 1.0f)
{
if (DecayDelay > 0)
{
DecayDelay -= deltaTime;
}
else
{
GrowthStep -= 0.25f * deltaTime;
}
}
if (GrowthStep >= 2.0f || Parent.Decayed) { return; }
GrowthStep += deltaTime;
if (GrowthStep < 1.0f)
{
// I don't know how or why this works
float offsetAmount = diameter * VineStep - diameter;
switch (Type)
{
case VineTileType.StumpLeft:
offset.X = offsetAmount;
break;
case VineTileType.StumpRight:
offset.X = -offsetAmount;
break;
case VineTileType.StumpTop:
offset.Y = offsetAmount;
break;
case VineTileType.Stem:
case VineTileType.StumpBottom:
offset.Y = -offsetAmount;
break;
default:
offset = Vector2.Zero;
break;
}
}
else
{
offset = Vector2.Zero;
}
}
public Vector2 GetWorldPosition(Planter planter, Vector2 slotOffset)
{
return planter.Item.WorldPosition + slotOffset + Position;
}
public void UpdateType()
{
if (Type == VineTileType.Stem) { return; }
Type = (VineTileType) Sides;
}
/// <summary>
/// Returns a random side that is not occupied.
/// </summary>
/// <remarks>
/// There is probably a much better way of doing this than allocating memory with an array
/// but this felt like the most reliable approach I could come up with.
/// </remarks>
/// <returns></returns>
public TileSide GetRandomFreeSide(Random? random = null)
{
const int maxSides = 4;
TileSide occupiedSides = Sides | BlockedSides;
int setBits = occupiedSides.Count();
if (setBits >= maxSides) { return TileSide.None; }
int possible = maxSides - setBits;
int[] pool = new int[possible];
for (int i = 0, j = 0; i < maxSides; i++)
{
if (!occupiedSides.IsBitSet((TileSide) (1 << i)))
{
pool[j] = i;
j++;
}
}
int value = pool[Growable.RandomInt(0, possible, random)];
return (TileSide) (1 << value);
}
public bool CanGrowMore() => (Sides | BlockedSides).Count() < 4;
public static Rectangle CreatePlantRect(Vector2 pos) => new Rectangle((int) pos.X - Size / 2, (int) pos.Y + Size / 2, Size, Size);
}
internal static class GrowthSideExtension
{
// Enum.HasFlag() sucks
public static bool IsBitSet(this TileSide side, TileSide bit)
{
return ((int) side & (int) bit) != 0;
}
// K&R algorithm for counting how many bits are set in a bit field
public static int Count(this TileSide side)
{
int n = (int) side;
int count = 0;
while (n != 0)
{
count += n & 1;
n >>= 1;
}
return count;
}
}
internal partial class Growable : ItemComponent, IServerSerializable
{
// used for debugging where a vine failed to grow
public readonly HashSet<Rectangle> FailedRectangles = new HashSet<Rectangle>();
[Serialize(1f, true, "How fast the plant grows.")]
public float GrowthSpeed { get; set; }
[Serialize(100f, true, "How long the plant can go without watering.")]
public float MaxHealth { get; set; }
[Serialize(1f, true, "How much damage the plant takes while in water.")]
public float FloodTolerance { get; set; }
[Serialize(1f, true, "How much damage the plant takes while growing.")]
public float Hardiness { get; set; }
[Serialize(0.01f, true, "How often a seed is produced.")]
public float SeedRate { get; set; }
[Serialize(0.01f, true, "How often a product item is produced.")]
public float ProductRate { get; set; }
[Serialize(0.5f, true, "Probability of an attribute being randomly modified in a newly produced seed.")]
public float MutationProbability { get; set; }
[Serialize("1.0,1.0,1.0,1.0", true, "Color of the flowers.")]
public Color FlowerTint { get; set; }
[Serialize(3, true, "Number of flowers drawn when fully grown")]
public int FlowerQuantity { get; set; }
[Serialize(0.25f, true, "Size of the flower sprites.")]
public float BaseFlowerScale { get; set; }
[Serialize(0.5f, true, "Size of the leaf sprites.")]
public float BaseLeafScale { get; set; }
[Serialize("1.0,1.0,1.0,1.0", true, "Color of the leaves.")]
public Color LeafTint { get; set; }
[Serialize(0.33f, true, "Chance of a leaf appearing behind a branch.")]
public float LeafProbability { get; set; }
[Serialize("1.0,1.0,1.0,1.0", true, "Color of the vines.")]
public Color VineTint { get; set; }
[Serialize(32, true, "Maximum number of vine tiles the plant can grow.")]
public int MaximumVines { get; set; }
[Serialize(0.25f, true, "Size of the vine sprites.")]
public float VineScale { get; set; }
[Serialize("0.26,0.27,0.29,1.0", true, "Tint of a dead plant.")]
public Color DeadTint { get; set; }
private const float increasedDeathSpeed = 10f;
private bool accelerateDeath;
private float health;
private int flowerVariants;
private int leafVariants;
private int[] flowerTiles;
public float Health
{
get => health;
set => health = Math.Clamp(value, 0, MaxHealth);
}
public bool Decayed;
public bool FullyGrown;
private const int maxProductDelay = 10,
maxVineGrowthDelay = 10;
private int productDelay;
private int vineDelay;
public readonly List<ProducedItem> ProducedItems = new List<ProducedItem>();
public readonly List<VineTile> Vines = new List<VineTile>();
private readonly ProducedItem ProducedSeed;
private static float MinFlowerScale = 0.5f, MaxFlowerScale = 1.0f, MinLeafScale = 0.5f, MaxLeafScale = 1.0f;
private const int VineChunkSize = 32;
public Growable(Item item, XElement element) : base(item, element)
{
SerializableProperty.DeserializeProperties(this, element);
Health = MaxHealth;
if (element.HasElements)
{
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "produceditem":
ProducedItems.Add(new ProducedItem(subElement));
break;
case "vinesprites":
LoadVines(subElement);
break;
}
}
}
ProducedSeed = new ProducedItem(this.item.Prefab, 1.0f);
flowerTiles = new int[FlowerQuantity];
}
public override void OnItemLoaded()
{
base.OnItemLoaded();
if (flowerTiles.All(i => i == 0))
{
GenerateFlowerTiles();
}
}
private void GenerateFlowerTiles(Random? random = null)
{
flowerTiles = new int[FlowerQuantity];
List<int> pool = new List<int>();
for (int i = 0; i < MaximumVines - 1; i++) { pool.Add(i); }
for (int i = 0; i < flowerTiles.Length; i++)
{
int index = RandomInt(0, pool.Count, random);
flowerTiles[i] = pool[index];
pool.RemoveAt(index);
}
}
partial void LoadVines(XElement element);
public void OnGrowthTick(Planter planter, PlantSlot slot)
{
if (Decayed) { return; }
if (FullyGrown)
{
TryGenerateProduct(planter, slot);
}
if (Health > 0)
{
GrowVines(planter, slot);
Health -= accelerateDeath ? Hardiness * increasedDeathSpeed : Hardiness;
if (planter.Item.InWater)
{
Health -= FloodTolerance;
}
}
CheckPlantState();
#if CLIENT
UpdateBranchHealth();
#endif
}
private void UpdateBranchHealth()
{
Color healthColor = Color.White * (1.0f - Health / MaxHealth);
foreach (VineTile vine in Vines)
{
vine.HealthColor = healthColor;
}
}
private void TryGenerateProduct(Planter planter, PlantSlot slot)
{
productDelay++;
if (productDelay <= maxProductDelay) { return; }
productDelay = 0;
bool spawnProduct = Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < ProductRate,
spawnSeed = Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < SeedRate;
Vector2 spawnPos;
if (spawnProduct || spawnSeed)
{
VineTile vine = Vines.GetRandom();
spawnPos = vine.GetWorldPosition(planter, slot.Offset);
}
else
{
return;
}
if (spawnProduct && ProducedItems.Any())
{
SpawnItem(ProducedItems.RandomElementByWeight(it => it.Probability), spawnPos);
return;
}
if (spawnSeed)
{
SpawnItem(ProducedSeed, spawnPos);
}
static void SpawnItem(ProducedItem producedItem, Vector2 pos)
{
if (producedItem.Prefab == null) { return; }
Entity.Spawner?.AddToSpawnQueue(producedItem.Prefab, pos, onSpawned: it =>
{
foreach (StatusEffect effect in producedItem.StatusEffects)
{
it.ApplyStatusEffect(effect, ActionType.OnProduceSpawned, 1.0f, isNetworkEvent: true);
}
it.ApplyStatusEffects(ActionType.OnProduceSpawned, 1.0f, isNetworkEvent: true);
});
}
}
/// <summary>
/// Updates plant's state to fully grown or dead depending on its conditions.
/// </summary>
/// <returns>True if the plant has finished growing.</returns>
private bool CheckPlantState()
{
if (Decayed) { return true; }
if (0 >= Health)
{
Decayed = true;
#if CLIENT
foreach (VineTile vine in Vines)
{
vine.DecayDelay = (float) RandomDouble(0f, 30f);
}
#endif
#if SERVER
item.CreateServerEvent(this);
#endif
return true;
}
if (Vines.Count >= MaximumVines && !FullyGrown)
{
FullyGrown = true;
#if SERVER
item.CreateServerEvent(this);
#endif
return true;
}
if (!FullyGrown && !accelerateDeath && Vines.Any() && Vines.All(tile => !tile.CanGrowMore()))
{
accelerateDeath = true;
}
// if the player somehow finds a way to extract the seed out of a planter kill the plant
if (item.ParentInventory is CharacterInventory)
{
Decayed = true;
#if SERVER
item.CreateServerEvent(this);
#endif
return true;
}
return false;
}
public override void Update(float deltaTime, Camera cam)
{
base.Update(deltaTime, cam);
#if CLIENT
foreach (VineTile vine in Vines)
{
vine.UpdateScale(deltaTime);
}
#endif
CheckPlantState();
}
private void GrowVines(Planter planter, PlantSlot slot)
{
if (FullyGrown) { return; }
vineDelay++;
if (vineDelay <= maxVineGrowthDelay / GrowthSpeed) { return; }
vineDelay = 0;
if (!Vines.Any())
{
// generate first stem
GenerateStem();
return;
}
int count = Vines.Count;
TryGenerateBranches(planter, slot);
if (Vines.Count > count)
{
#if SERVER
for (int i = 0; i < Vines.Count; i += VineChunkSize)
{
GameMain.Server.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ComponentState, item.GetComponentIndex(this), i });
}
#elif CLIENT
ResetPlanterSize();
#endif
}
}
private void GenerateStem()
{
VineTile stem = new VineTile(this, Vector2.Zero, VineTileType.Stem) { BlockedSides = TileSide.Bottom | TileSide.Left | TileSide.Right };
Vines.Add(stem);
}
private void TryGenerateBranches(Planter planter, PlantSlot slot, Random? random = null, Random? flowerRandom = null)
{
List<VineTile> newList = new List<VineTile>(Vines);
foreach (VineTile oldVines in newList)
{
if (oldVines.FailedGrowthAttempts > 8 || !oldVines.CanGrowMore()) { continue; }
if (RandomInt(0, Vines.Count(tile => tile.CanGrowMore()), random) != 0) { continue; }
TileSide side = oldVines.GetRandomFreeSide(random);
if (side == TileSide.None) { continue; }
Vector2 pos = oldVines.AdjacentPositions[side];
Rectangle rect = VineTile.CreatePlantRect(pos);
if (CollidesWithWorld(rect, planter, slot))
{
oldVines.BlockedSides |= side;
oldVines.FailedGrowthAttempts++;
continue;
}
FoliageConfig flowerConfig = FoliageConfig.EmptyConfig;
FoliageConfig leafConfig = FoliageConfig.EmptyConfig;
if (flowerTiles.Any(i => Vines.Count == i))
{
flowerConfig = FoliageConfig.CreateRandomConfig(flowerVariants, MinFlowerScale, MaxFlowerScale, flowerRandom);
}
if (LeafProbability >= RandomDouble(0d, 1.0d, flowerRandom) && leafVariants > 0)
{
leafConfig = FoliageConfig.CreateRandomConfig(leafVariants, MinLeafScale, MaxLeafScale, flowerRandom);
}
VineTile newVine = new VineTile(this, pos, VineTileType.CrossJunction, flowerConfig, leafConfig, rect);
foreach (VineTile otherVine in Vines)
{
var (distX, distY) = pos - otherVine.Position;
int absDistX = (int) Math.Abs(distX), absDistY = (int) Math.Abs(distY);
// check if the tile is within the with or height distance from us but ignore diagonals
if (absDistX > newVine.Rect.Width || absDistY > newVine.Rect.Height || absDistX > 0 && absDistY > 0) { continue; }
// determines what side the tile is relative to the new tile by comparing the X/Y distance values
// if the X value is bigger than Y it's to the left or right of us and then check if X is negative or positive to determine if it's right or left
TileSide connectingSide = absDistX > absDistY ? distX > 0 ? TileSide.Right : TileSide.Left : distY > 0 ? TileSide.Top : TileSide.Bottom;
// We use log2 to find the index and offset that index by 2 since the opposite side is always 2 offsets away
TileSide oppositeSide = (TileSide) (1 << ((int) Math.Log2((int) connectingSide) + 2) % 4);
if (otherVine.BlockedSides.IsBitSet(connectingSide))
{
newVine.BlockedSides |= oppositeSide;
continue;
}
if (otherVine != oldVines)
{
otherVine.BlockedSides |= connectingSide;
newVine.BlockedSides |= oppositeSide;
}
else
{
otherVine.Sides |= connectingSide;
newVine.Sides |= oppositeSide;
}
}
Vines.Add(newVine);
foreach (VineTile vine in Vines)
{
vine.UpdateType();
}
}
}
private bool CollidesWithWorld(Rectangle rect, Planter planter, PlantSlot slot)
{
if (Vines.Any(g => g.Rect.Contains(rect))) { return true; }
Rectangle worldRect = rect;
worldRect.Location = planter.Item.WorldPosition.ToPoint() + slot.Offset.ToPoint() + worldRect.Location;
worldRect.Y -= worldRect.Height;
Rectangle planterRect = planter.Item.WorldRect;
planterRect.Y -= planterRect.Height;
if (planterRect.Intersects(worldRect))
{
#if DEBUG
if (!FailedRectangles.Contains(worldRect))
{
FailedRectangles.Add(worldRect);
}
#endif
return true;
}
Vector2 topLeft = ConvertUnits.ToSimUnits(new Vector2(worldRect.Left, worldRect.Top)),
topRight = ConvertUnits.ToSimUnits(new Vector2(worldRect.Right, worldRect.Top)),
bottomLeft = ConvertUnits.ToSimUnits(new Vector2(worldRect.Left, worldRect.Bottom)),
bottomRight = ConvertUnits.ToSimUnits(new Vector2(worldRect.Right, worldRect.Bottom));
// ray casting a cross on the corners didn't seem to work so we are ray casting along the perimeter
bool hasCollision = planterRect.Intersects(worldRect) || LineCollides(topLeft, topRight) || LineCollides(topRight, bottomRight) || LineCollides(bottomRight, bottomLeft) || LineCollides(bottomLeft, topLeft);
#if DEBUG
if (hasCollision)
{
if (!FailedRectangles.Contains(worldRect))
{
FailedRectangles.Add(worldRect);
}
}
#endif
return hasCollision;
static bool LineCollides(Vector2 point1, Vector2 point2)
{
const Category category = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel;
return Submarine.PickBody(point1, point2, collisionCategory: category, customPredicate: f => !(f.UserData is Hull) && f.CollidesWith.HasFlag(Physics.CollisionItem)) != null;
}
}
public override XElement Save(XElement parentElement)
{
XElement element = base.Save(parentElement);
element.Add(new XAttribute("flowertiles", string.Join(",", flowerTiles)));
element.Add(new XAttribute("decayed", Decayed));
foreach (VineTile vine in Vines)
{
XElement vineElement = new XElement("Vine");
vineElement.Add(new XAttribute("sides", (int) vine.Sides));
vineElement.Add(new XAttribute("blockedsides", (int) vine.BlockedSides));
vineElement.Add(new XAttribute("pos", XMLExtensions.Vector2ToString(vine.Position)));
vineElement.Add(new XAttribute("tile", (int) vine.Type));
vineElement.Add(new XAttribute("failedattempts", vine.FailedGrowthAttempts));
#if SERVER
vineElement.Add(new XAttribute("growthscale", Decayed ? 1.0f : 2.0f));
#else
vineElement.Add(new XAttribute("growthscale", vine.GrowthStep));
#endif
vineElement.Add(new XAttribute("flowerconfig", vine.FlowerConfig.Serialize()));
vineElement.Add(new XAttribute("leafconfig", vine.LeafConfig.Serialize()));
element.Add(vineElement);
}
return element;
}
public override void Load(XElement componentElement, bool usePrefabValues)
{
base.Load(componentElement, usePrefabValues);
flowerTiles = componentElement.GetAttributeIntArray("flowertiles", new int[0]);
Decayed = componentElement.GetAttributeBool("decayed", false);
Vines.Clear();
foreach (XElement element in componentElement.Elements())
{
if (element.Name.ToString().Equals("vine", StringComparison.OrdinalIgnoreCase))
{
VineTileType type = (VineTileType) element.GetAttributeInt("tile", 0);
Vector2 pos = element.GetAttributeVector2("pos", Vector2.Zero);
TileSide sides = (TileSide) element.GetAttributeInt("sides", 0);
TileSide blockedSides = (TileSide) element.GetAttributeInt("blockedsides", 0);
int failedAttempts = element.GetAttributeInt("failedattempts", 0);
float growthscale = element.GetAttributeFloat("growthscale", 0f);
int flowerConfig = element.GetAttributeInt("flowerconfig", FoliageConfig.EmptyConfigValue);
int leafConfig = element.GetAttributeInt("leafconfig", FoliageConfig.EmptyConfigValue);
VineTile tile = new VineTile(this, pos, type, FoliageConfig.Deserialize(flowerConfig), FoliageConfig.Deserialize(leafConfig))
{
Sides = sides, BlockedSides = blockedSides, FailedGrowthAttempts = failedAttempts, GrowthStep = growthscale
};
Vines.Add(tile);
}
}
}
private bool CanGrowMore() => Vines.Any(tile => tile.CanGrowMore());
public static int RandomInt(int min, int max, Random? random = null) => random?.Next(min, max) ?? Rand.Range(min, max);
public static double RandomDouble(double min, double max, Random? random = null) => random?.NextDouble() * (max - min) + min ?? Rand.Range(min, max);
}
}
@@ -5,6 +5,7 @@ using FarseerPhysics.Dynamics.Contacts;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
@@ -392,6 +393,8 @@ namespace Barotrauma.Items.Components
if (item.GetComponent<LevelResource>() != null) { return true; }
if (item.GetComponent<Planter>() is { } planter && planter.GrowableSeeds.Any(seed => seed != null)) { return false; }
//if the item has a connection panel and rewiring is disabled, don't allow deattaching
var connectionPanel = item.GetComponent<ConnectionPanel>();
if (connectionPanel != null && (connectionPanel.Locked || !(GameMain.NetworkMember?.ServerSettings?.AllowRewiring ?? true)))
@@ -476,12 +479,13 @@ namespace Barotrauma.Items.Components
}
}
var containedItems = item.ContainedItems;
var containedItems = item.OwnInventory?.Items;
if (containedItems != null)
{
foreach (Item contained in containedItems)
{
if (contained.body == null) continue;
if (contained == null) { continue; }
if (contained.body == null) { continue; }
contained.SetTransform(item.SimPosition, contained.body.Rotation);
}
}
@@ -573,7 +577,7 @@ namespace Barotrauma.Items.Components
public override void Update(float deltaTime, Camera cam)
{
if (item.body == null || !item.body.Enabled) return;
if (item.body == null || !item.body.Enabled) { return; }
if (picker == null || !picker.HasEquippedItem(item))
{
if (Pusher != null) { Pusher.Enabled = false; }
@@ -598,7 +602,10 @@ namespace Barotrauma.Items.Components
ApplyStatusEffects(ActionType.OnActive, deltaTime, picker);
if (item.body.Dir != picker.AnimController.Dir) Flip();
if (item.body.Dir != picker.AnimController.Dir)
{
item.FlipX(relativeToSub: false);
}
item.Submarine = picker.Submarine;
@@ -635,11 +642,14 @@ namespace Barotrauma.Items.Components
}
}
public void Flip()
public override void FlipX(bool relativeToSub)
{
handlePos[0].X = -handlePos[0].X;
handlePos[1].X = -handlePos[1].X;
item.body.Dir = -item.body.Dir;
if (item.body != null)
{
item.body.Dir = -item.body.Dir;
}
}
public override void OnItemLoaded()
@@ -63,6 +63,13 @@ namespace Barotrauma.Items.Components
item.RequireAimToUse = true;
}
public override void Equip(Character character)
{
base.Equip(character);
reloadTimer = Math.Min(reload, 1.0f);
IsActive = true;
}
public override bool Use(float deltaTime, Character character = null)
{
if (character == null || reloadTimer > 0.0f) { return false; }
@@ -151,7 +158,7 @@ namespace Barotrauma.Items.Components
ApplyStatusEffects(ActionType.OnActive, deltaTime, picker);
if (item.body.Dir != picker.AnimController.Dir) { Flip(); }
if (item.body.Dir != picker.AnimController.Dir) { item.FlipX(relativeToSub: false); }
AnimController ac = picker.AnimController;
@@ -366,13 +373,15 @@ namespace Barotrauma.Items.Components
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
bool success = Rand.Range(0.0f, 0.5f) < DegreeOfSuccess(User);
#if SERVER
if (GameMain.Server != null && targetCharacter != null) //TODO: Log structure hits
{
GameMain.Server.CreateEntityEvent(item, new object[]
{
Networking.NetEntityEvent.Type.ApplyStatusEffect,
ActionType.OnUse,
success ? ActionType.OnUse : ActionType.OnFailure,
null, //itemcomponent
targetCharacter.ID, targetLimb
});
@@ -389,7 +398,7 @@ namespace Barotrauma.Items.Components
if (targetCharacter != null) //TODO: Allow OnUse to happen on structures too maybe??
{
ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb, user: User);
ApplyStatusEffects(success ? ActionType.OnUse : ActionType.OnFailure, 1.0f, targetCharacter, targetLimb, user: User);
}
if (DeleteOnUse)
@@ -61,7 +61,7 @@ namespace Barotrauma.Items.Components
allowedSlots.Add(allowedSlot);
}
canBePicked = true;
canBePicked = true;
}
public override bool Pick(Character picker)
@@ -142,7 +142,8 @@ namespace Barotrauma.Items.Components
this,
item.WorldPosition,
pickTimer / requiredTime,
GUI.Style.Red, GUI.Style.Green);
GUI.Style.Red, GUI.Style.Green,
!string.IsNullOrWhiteSpace(PickingMsg) ? PickingMsg : this is Door ? "progressbar.opening" : "progressbar.deattaching");
#endif
picker.AnimController.UpdateUseItem(true, item.WorldPosition + new Vector2(0.0f, 100.0f) * ((pickTimer / 10.0f) % 0.1f));
@@ -72,6 +72,12 @@ namespace Barotrauma.Items.Components
partial void InitProjSpecific(XElement element);
public override void Equip(Character character)
{
reloadTimer = Math.Min(reload, 1.0f);
IsActive = true;
}
public override void Update(float deltaTime, Camera cam)
{
reloadTimer -= deltaTime;
@@ -180,22 +186,25 @@ namespace Barotrauma.Items.Components
public Projectile FindProjectile(bool triggerOnUseOnContainers = false)
{
var containedItems = item.ContainedItems;
var containedItems = item.OwnInventory?.Items;
if (containedItems == null) { return null; }
foreach (Item item in containedItems)
{
if (item == null) { continue; }
Projectile projectile = item.GetComponent<Projectile>();
if (projectile != null) { return projectile; }
}
//projectile not found, see if one of the contained items contains projectiles
foreach (Item item in containedItems)
foreach (Item it in containedItems)
{
var containedSubItems = item.ContainedItems;
if (it == null) { continue; }
var containedSubItems = it.OwnInventory?.Items;
if (containedSubItems == null) { continue; }
foreach (Item subItem in containedSubItems)
{
if (subItem == null) { continue; }
Projectile projectile = subItem.GetComponent<Projectile>();
//apply OnUse statuseffects to the container in case it has to react to it somehow
//(play a sound, spawn more projectiles, reduce condition...)
@@ -52,12 +52,16 @@ namespace Barotrauma.Items.Components
{
get; set;
}
[Serialize(0.0f, false, description: "How much the item decreases the size of fires per second.")]
public float ExtinguishAmount
{
get; set;
}
[Serialize(0.0f, false, description: "How much water the item provides to planters per second.")]
public float WaterAmount { get; set; }
[Serialize("0.0,0.0", false, description: "The position of the barrel as an offset from the item's center (in pixels).")]
public Vector2 BarrelPos { get; set; }
@@ -82,13 +86,19 @@ namespace Barotrauma.Items.Components
[Serialize(0.0f, false, description: "Force applied to the entity the ray hits.")]
public float TargetForce { get; set; }
[Serialize(0.0f, false, description: "Rotation of the barrel in degrees."), Editable(MinValueFloat = 0, MaxValueFloat = 360, VectorComponentLabels = new string[] { "editable.minvalue", "editable.maxvalue" })]
public float BarrelRotation
{
get; set;
}
public Vector2 TransformedBarrelPos
{
get
{
Matrix bodyTransform = Matrix.CreateRotationZ(item.body.Rotation);
Matrix bodyTransform = Matrix.CreateRotationZ(item.body.Rotation + MathHelper.ToRadians(BarrelRotation));
Vector2 flippedPos = BarrelPos;
if (item.body.Dir < 0.0f) flippedPos.X = -flippedPos.X;
if (item.body.Dir < 0.0f) { flippedPos.X = -flippedPos.X; }
return (Vector2.Transform(flippedPos, bodyTransform));
}
}
@@ -188,7 +198,7 @@ namespace Barotrauma.Items.Components
}
float spread = MathHelper.ToRadians(MathHelper.Lerp(UnskilledSpread, Spread, degreeOfSuccess));
float angle = item.body.Rotation + spread * Rand.Range(-0.5f, 0.5f);
float angle = item.body.Rotation + MathHelper.ToRadians(BarrelRotation) + spread * Rand.Range(-0.5f, 0.5f);
Vector2 rayEnd = rayStart +
ConvertUnits.ToSimUnits(new Vector2(
(float)Math.Cos(angle),
@@ -276,7 +286,7 @@ namespace Barotrauma.Items.Components
ignoreSensors: false,
customPredicate: (Fixture f) =>
{
if (RepairThroughHoles && f.IsSensor && f.Body?.UserData is Structure) { return false; }
if (RepairThroughHoles && f.IsSensor && f.Body?.UserData is Structure || (f.Body?.UserData is Item it && it.GetComponent<Planter>() != null)) { return false; }
if (f.Body?.UserData as string == "ruinroom") { return false; }
return true;
},
@@ -373,6 +383,42 @@ namespace Barotrauma.Items.Components
}
}
if (WaterAmount > 0.0f && item.CurrentHull?.Submarine != null)
{
Vector2 pos = ConvertUnits.ToDisplayUnits(rayStart + item.Submarine.SimPosition);
// Could probably be done much efficiently here
foreach (Item it in Item.ItemList)
{
if (it.Submarine == item.Submarine && it.GetComponent<Planter>() is { } planter)
{
if (it.GetComponent<Holdable>() is { } holdable && holdable.Attachable && !holdable.Attached) { continue; }
Rectangle collisionRect = it.WorldRect;
collisionRect.Y -= collisionRect.Height;
if (collisionRect.Left < pos.X && collisionRect.Right > pos.X && collisionRect.Bottom < pos.Y)
{
Body collision = Submarine.PickBody(rayStart, it.SimPosition, ignoredBodies, collisionCategories);
if (collision == null)
{
for (var i = 0; i < planter.GrowableSeeds.Length; i++)
{
Growable seed = planter.GrowableSeeds[i];
if (seed == null || seed.Decayed) { continue; }
seed.Health += WaterAmount * deltaTime;
#if CLIENT
float barOffset = 10f * GUI.Scale;
Vector2 offset = planter.PlantSlots.ContainsKey(i) ? planter.PlantSlots[i].Offset : Vector2.Zero;
user.UpdateHUDProgressBar(planter, planter.Item.DrawPosition + new Vector2(barOffset, 0) + offset, seed.Health / seed.MaxHealth, GUI.Style.Blue, GUI.Style.Blue, "progressbar.watering");
#endif
}
}
}
}
}
}
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
{
@@ -464,7 +510,7 @@ namespace Barotrauma.Items.Components
}
else if (targetBody.UserData is Item targetItem)
{
if (!HitItems) { return false; }
if (!HitItems || targetItem.NonInteractable) { return false; }
var levelResource = targetItem.GetComponent<LevelResource>();
if (levelResource != null && levelResource.Attached &&
@@ -477,8 +523,9 @@ namespace Barotrauma.Items.Components
this,
targetItem.WorldPosition,
levelResource.DeattachTimer / levelResource.DeattachDuration,
GUI.Style.Red, GUI.Style.Green);
GUI.Style.Red, GUI.Style.Green, "progressbar.deattaching");
#endif
FixItemProjSpecific(user, deltaTime, targetItem);
return true;
}
@@ -571,34 +618,31 @@ namespace Barotrauma.Items.Components
character.AIController.SteeringManager.SteeringSeek(standPos);
}
}
else
if (dist < reach / 2)
{
if (dist < reach / 2)
// Too close -> steer away
character.AIController.SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(character.SimPosition - leak.SimPosition));
}
else if (dist < reach * 2)
{
// In or almost in range
character.CursorPosition = leak.Position;
character.CursorPosition += VectorExtensions.Forward(Item.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, dist / 2);
if (character.AnimController.InWater)
{
// Too close -> steer away
character.AIController.SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(character.SimPosition - leak.SimPosition));
}
else if (dist <= reach)
{
// In range
character.CursorPosition = leak.Position;
character.CursorPosition += VectorExtensions.Forward(Item.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, dist / 2);
if (character.AnimController.InWater)
{
var torso = character.AnimController.GetLimb(LimbType.Torso);
// Turn facing the target when not moving (handled in the animcontroller if not moving)
Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition);
Vector2 diff = (mousePos - torso.SimPosition) * character.AnimController.Dir;
float newRotation = MathUtils.VectorToAngle(diff);
character.AnimController.Collider.SmoothRotate(newRotation, 5.0f);
var torso = character.AnimController.GetLimb(LimbType.Torso);
// Turn facing the target when not moving (handled in the animcontroller if not moving)
Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition);
Vector2 diff = (mousePos - torso.SimPosition) * character.AnimController.Dir;
float newRotation = MathUtils.VectorToAngle(diff);
character.AnimController.Collider.SmoothRotate(newRotation, 5.0f);
if (VectorExtensions.Angle(VectorExtensions.Forward(torso.body.TransformedRotation), fromCharacterToLeak) < MathHelper.PiOver4)
{
// Swim past
Vector2 moveDir = leak.IsHorizontal ? Vector2.UnitY : Vector2.UnitX;
moveDir *= character.AnimController.Dir;
character.AIController.SteeringManager.SteeringManual(deltaTime, moveDir);
}
if (VectorExtensions.Angle(VectorExtensions.Forward(torso.body.TransformedRotation), fromCharacterToLeak) < MathHelper.PiOver4)
{
// Swim past
Vector2 moveDir = leak.IsHorizontal ? Vector2.UnitY : Vector2.UnitX;
moveDir *= character.AnimController.Dir;
character.AIController.SteeringManager.SteeringManual(deltaTime, moveDir);
}
}
}
@@ -674,9 +718,8 @@ namespace Barotrauma.Items.Components
// A general purpose system could be better, but it would most likely require changes in the way we define the status effects in xml.
foreach (ISerializableEntity target in targets)
{
if (!(target is Door door)) { continue; }
if (!door.CanBeWelded) { continue; }
if (!(target is Door door)) { continue; }
if (!door.CanBeWelded || door.Item.NonInteractable) { continue; }
for (int i = 0; i < effect.propertyNames.Length; i++)
{
string propertyName = effect.propertyNames[i];
@@ -685,7 +728,7 @@ namespace Barotrauma.Items.Components
object value = property.GetValue(target);
if (door.Stuck > 0)
{
var progressBar = user.UpdateHUDProgressBar(door, door.Item.WorldPosition, door.Stuck / 100, Color.DarkGray * 0.5f, Color.White);
var progressBar = user.UpdateHUDProgressBar(door, door.Item.WorldPosition, door.Stuck / 100, Color.DarkGray * 0.5f, Color.White, "progressbar.welding");
if (progressBar != null) { progressBar.Size = new Vector2(60.0f, 20.0f); }
}
}
@@ -0,0 +1,62 @@
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
partial class Sprayer : RangedWeapon
{
[Serialize(0.0f, false, description: "The distance at which the item can spray walls.")]
public float Range { get; set; }
[Serialize(1.0f, false, description: "How fast the item changes the color of the walls.")]
public float SprayStrength { get; set; }
private readonly Dictionary<string, Color> liquidColors;
private ItemContainer liquidContainer;
public Sprayer(Item item, XElement element) : base(item, element)
{
item.IsShootable = true;
item.RequireAimToUse = true;
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "paintcolors":
{
liquidColors = new Dictionary<string, Color>();
foreach (XElement paintElement in subElement.Elements())
{
string paintName = paintElement.GetAttributeString("paintitem", string.Empty);
Color paintColor = paintElement.GetAttributeColor("color", Color.Transparent);
if (paintName != string.Empty)
{
liquidColors.Add(paintName, paintColor);
}
}
}
break;
}
}
InitProjSpecific(element);
}
public override void OnItemLoaded()
{
liquidContainer = item.GetComponent<ItemContainer>();
}
partial void InitProjSpecific(XElement element);
#if SERVER
public override bool Use(float deltaTime, Character character = null)
{
return character != null || character.Removed;
}
#endif
}
}
@@ -84,7 +84,7 @@ namespace Barotrauma.Items.Components
ApplyStatusEffects(ActionType.OnActive, deltaTime, picker);
if (item.body.Dir != picker.AnimController.Dir) { Flip(); }
if (item.body.Dir != picker.AnimController.Dir) { item.FlipX(relativeToSub: false); }
AnimController ac = picker.AnimController;
@@ -76,6 +76,13 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize("", false, description: "What to display on the progress bar when this item is being picked.")]
public string PickingMsg
{
get;
set;
}
public Dictionary<string, SerializableProperty> SerializableProperties { get; protected set; }
public Action<bool> OnActiveStateChanged;
@@ -44,6 +44,13 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(true, false, "Allow dragging and dropping items to deposit items into this inventory.")]
public bool AllowDragAndDrop
{
get;
set;
}
[Serialize(false, false, description: "If set to true, interacting with this item will make the character interact with the contained item(s), automatically picking them up if they can be picked up.")]
public bool AutoInteractWithContained
@@ -166,17 +173,21 @@ namespace Barotrauma.Items.Components
public bool CanBeContained(Item item)
{
if (ContainableItems.Count == 0) { return true; }
return (ContainableItems.Find(c => c.MatchesItem(item)) != null);
return ContainableItems.Find(c => c.MatchesItem(item)) != null;
}
public bool CanBeContained(ItemPrefab itemPrefab)
{
if (ContainableItems.Count == 0) { return true; }
return (ContainableItems.Find(c => c.MatchesItem(itemPrefab)) != null);
return ContainableItems.Find(c => c.MatchesItem(itemPrefab)) != null;
}
public override void Update(float deltaTime, Camera cam)
{
if (item.body != null &&
if (item.ParentInventory is CharacterInventory)
{
item.SetContainedItemPositions();
}
else if (item.body != null &&
item.body.Enabled &&
item.body.FarseerBody.Awake)
{
@@ -209,22 +220,6 @@ namespace Barotrauma.Items.Components
}
}
public override void OnItemLoaded()
{
base.OnItemLoaded();
if (SpawnWithId.Length > 0)
{
ItemPrefab prefab = ItemPrefab.Prefabs.Find(m => m.Identifier == SpawnWithId);
if (prefab != null)
{
if (Inventory != null && Inventory.Items.Any(it => it == null))
{
Entity.Spawner?.AddToSpawnQueue(prefab, Inventory);
}
}
}
}
public override bool HasRequiredItems(Character character, bool addMessage, string msg = null)
{
return (!AccessOnlyWhenBroken || Item.Condition <= 0) && base.HasRequiredItems(character, addMessage, msg);
@@ -284,6 +279,8 @@ namespace Barotrauma.Items.Components
public override bool Combine(Item item, Character user)
{
if (!AllowDragAndDrop && user != null) { return false; }
if (!ContainableItems.Any(x => x.MatchesItem(item))) { return false; }
if (user != null && !user.CanAccessInventory(Inventory)) { return false; }
@@ -354,16 +351,28 @@ namespace Barotrauma.Items.Components
public override void OnMapLoaded()
{
if (itemIds == null) { return; }
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 (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);
}
itemIds = null;
}
itemIds = null;
if (SpawnWithId.Length > 0)
{
ItemPrefab prefab = ItemPrefab.Prefabs.Find(m => m.Identifier == SpawnWithId);
if (prefab != null)
{
if (Inventory != null && Inventory.Items.Any(it => it == null))
{
Entity.Spawner?.AddToSpawnQueue(prefab, Inventory);
}
}
}
}
protected override void ShallowRemoveComponentSpecific()
@@ -1,8 +1,9 @@
using Microsoft.Xna.Framework;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
namespace Barotrauma.Items.Components
{
partial class ItemLabel : ItemComponent, IDrawableComponent
partial class ItemLabel : ItemComponent, IDrawableComponent, IServerSerializable
{
public Vector2 DrawSize
{
@@ -10,12 +11,16 @@ namespace Barotrauma.Items.Components
get { return Vector2.Zero; }
}
partial void OnStateChanged();
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0, float signalStrength = 1)
{
switch (connection.Name)
{
case "set_text":
if (Text == signal) { return; }
Text = signal;
OnStateChanged();
break;
}
}
@@ -151,6 +151,7 @@ namespace Barotrauma.Items.Components
if (user == null
|| user.Removed
|| user.SelectedConstruction != item
|| item.ParentInventory != null
|| !user.CanInteractWith(item)
|| (UsableIn == UseEnvironment.Water && !user.AnimController.InWater)
|| (UsableIn == UseEnvironment.Air && user.AnimController.InWater))
@@ -221,7 +222,7 @@ namespace Barotrauma.Items.Components
user.AnimController.ResetPullJoints();
if (dir != 0) user.AnimController.TargetDir = dir;
if (dir != 0) { user.AnimController.TargetDir = dir; }
foreach (LimbPos lb in limbPositions)
{
@@ -14,7 +14,7 @@ namespace Barotrauma.Items.Components
private float maxForce;
private Attack propellerDamage;
private readonly Attack propellerDamage;
private float damageTimer;
@@ -24,6 +24,8 @@ namespace Barotrauma.Items.Components
private float controlLockTimer;
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.")]
public float MaxForce
@@ -106,6 +108,11 @@ namespace Barotrauma.Items.Components
{
//arbitrary multiplier that was added to changes in submarine mass without having to readjust all engines
float forceMultiplier = 0.1f;
if (User != null)
{
forceMultiplier *= MathHelper.Lerp(0.5f, 2.0f, (float)Math.Sqrt(User.GetSkillLevel("helm") / 100));
}
float voltageFactor = MinVoltage <= 0.0f ? 1.0f : Math.Min(Voltage / MinVoltage, 1.0f);
Vector2 currForce = new Vector2(force * maxForce * forceMultiplier * voltageFactor, 0.0f);
//less effective when in a bad condition
@@ -193,6 +200,7 @@ namespace Barotrauma.Items.Components
{
controlLockTimer = 0.1f;
targetForce = MathHelper.Clamp(tempForce, -100.0f, 100.0f);
User = sender;
}
}
}
@@ -2,6 +2,7 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;
@@ -15,6 +16,9 @@ namespace Barotrauma.Items.Components
private float timeUntilReady;
private float requiredTime;
private string savedFabricatedItem;
private float savedTimeUntilReady, savedRequiredTime;
private bool hasPower;
private Character user;
@@ -46,6 +50,7 @@ namespace Barotrauma.Items.Components
if (state == value) { return; }
state = value;
#if SERVER
serverEventId++;
item.CreateServerEvent(this);
#endif
}
@@ -158,8 +163,8 @@ namespace Barotrauma.Items.Components
private void StartFabricating(FabricationRecipe selectedItem, Character user)
{
if (selectedItem == null) return;
if (!outputContainer.Inventory.IsEmpty()) return;
if (selectedItem == null) { return; }
if (!outputContainer.Inventory.IsEmpty()) { return; }
#if CLIENT
itemList.Enabled = false;
@@ -194,14 +199,23 @@ namespace Barotrauma.Items.Components
private void CancelFabricating(Character user = null)
{
if (fabricatedItem == null) { return; }
IsActive = false;
fabricatedItem = null;
this.user = null;
currPowerConsumption = 0.0f;
progressState = 0.0f;
timeUntilReady = 0.0f;
inputContainer.Inventory.Locked = false;
outputContainer.Inventory.Locked = false;
if (GameMain.NetworkMember?.IsServer ?? true)
{
State = FabricatorState.Stopped;
}
if (fabricatedItem == null) { return; }
fabricatedItem = null;
#if CLIENT
itemList.Enabled = true;
if (activateButton != null)
@@ -209,17 +223,6 @@ namespace Barotrauma.Items.Components
activateButton.Text = TextManager.Get("FabricatorCreate");
}
#endif
progressState = 0.0f;
timeUntilReady = 0.0f;
inputContainer.Inventory.Locked = false;
outputContainer.Inventory.Locked = false;
if (GameMain.NetworkMember?.IsServer ?? true)
{
State = FabricatorState.Stopped;
}
#if SERVER
if (user != null)
{
@@ -279,13 +282,13 @@ namespace Barotrauma.Items.Components
{
for (int i = 0; i < ingredient.Amount; i++)
{
var availableItem = availableIngredients.FirstOrDefault(it => it != null && it.Prefab == ingredient.ItemPrefab && it.Condition >= ingredient.ItemPrefab.Health * ingredient.MinCondition);
var availableItem = availableIngredients.FirstOrDefault(it => it != null && ingredient.ItemPrefabs.Contains(it.Prefab) && it.ConditionPercentage >= ingredient.MinCondition * 100.0f);
if (availableItem == null) { continue; }
//Item4 = use condition bool
if (ingredient.UseCondition && availableItem.Condition - ingredient.ItemPrefab.Health * ingredient.MinCondition > 0.0f) //Leave it behind with reduced condition if it has enough to stay above 0
if (ingredient.UseCondition && availableItem.ConditionPercentage - ingredient.MinCondition * 100 > 0.0f) //Leave it behind with reduced condition if it has enough to stay above 0
{
availableItem.Condition -= ingredient.ItemPrefab.Health * ingredient.MinCondition;
availableItem.Condition -= availableItem.Prefab.Health * ingredient.MinCondition;
continue;
}
availableIngredients.Remove(availableItem);
@@ -366,7 +369,8 @@ namespace Barotrauma.Items.Components
public float FabricationDegreeOfSuccess(Character character, List<Skill> skills)
{
if (skills.Count == 0) return 1.0f;
if (skills.Count == 0) { return 1.0f; }
if (character == null) { return 0.0f; }
float skillSum = (from t in skills let characterLevel = character.GetSkillLevel(t.Identifier) select (characterLevel - (t.Level * SkillRequirementMultiplier))).Sum();
float average = skillSum / skills.Count;
@@ -461,8 +465,53 @@ namespace Barotrauma.Items.Components
{
return
item != null &&
item.prefab == requiredItem.ItemPrefab &&
requiredItem.ItemPrefabs.Contains(item.prefab) &&
item.Condition / item.Prefab.Health >= requiredItem.MinCondition;
}
public override XElement Save(XElement parentElement)
{
var componentElement = base.Save(parentElement);
if (fabricatedItem != null)
{
componentElement.Add(new XAttribute("fabricateditemidentifier", fabricatedItem.TargetItem.Identifier));
componentElement.Add(new XAttribute("savedtimeuntilready", timeUntilReady.ToString("G", CultureInfo.InvariantCulture)));
componentElement.Add(new XAttribute("savedrequiredtime", requiredTime.ToString("G", CultureInfo.InvariantCulture)));
}
return componentElement;
}
public override void Load(XElement componentElement, bool usePrefabValues)
{
base.Load(componentElement, usePrefabValues);
savedFabricatedItem = componentElement.GetAttributeString("fabricateditemidentifier", "");
savedTimeUntilReady = componentElement.GetAttributeFloat("savedtimeuntilready", 0.0f);
savedRequiredTime = componentElement.GetAttributeFloat("savedrequiredtime", 0.0f);
}
public override void OnMapLoaded()
{
if (string.IsNullOrEmpty(savedFabricatedItem)) { return; }
inputContainer?.OnMapLoaded();
outputContainer?.OnMapLoaded();
var recipe = fabricationRecipes.Find(r => r.TargetItem.Identifier == savedFabricatedItem);
if (recipe == null)
{
DebugConsole.ThrowError("Error while loading a fabricator. Can't continue fabricating \"" + savedFabricatedItem + "\" (matching recipe not found).");
}
else
{
#if CLIENT
SelectItem(null, recipe, savedRequiredTime);
#endif
StartFabricating(recipe, user: null);
timeUntilReady = savedTimeUntilReady;
requiredTime = savedRequiredTime;
}
savedFabricatedItem = null;
}
}
}
@@ -11,7 +11,8 @@ namespace Barotrauma.Items.Components
{
partial class Reactor : Powered, IServerSerializable, IClientSerializable
{
const float NetworkUpdateInterval = 0.5f;
const float NetworkUpdateIntervalHigh = 0.5f;
const float NetworkUpdateIntervalLow = 10.0f;
//the rate at which the reactor is being run on (higher rate -> higher temperature)
private float fissionRate;
@@ -64,17 +65,18 @@ namespace Barotrauma.Items.Components
}
}
private Character lastAIUser;
public Character LastAIUser { get; private set; }
private Character lastUser;
private Character LastUser
public Character LastUser
{
get { return lastUser; }
set
private set
{
if (lastUser == value) return;
if (lastUser == value) { return; }
lastUser = value;
degreeOfSuccess = lastUser == null ? 0.0f : DegreeOfSuccess(lastUser);
LastUserWasPlayer = lastUser.IsPlayer;
}
}
@@ -176,6 +178,8 @@ namespace Barotrauma.Items.Components
[Serialize(0.0f, true)]
public float AvailableFuel { get; set; }
public bool LastUserWasPlayer { get; private set; }
public Reactor(Item item, XElement element)
: base(item, element)
{
@@ -207,13 +211,13 @@ namespace Barotrauma.Items.Components
//if an AI character was using the item on the previous frame but not anymore, turn autotemp on
// (= bots turn autotemp back on when leaving the reactor)
if (lastAIUser != null)
if (LastAIUser != null)
{
if (lastAIUser.SelectedConstruction != item && lastAIUser.CanInteractWith(item))
if (LastAIUser.SelectedConstruction != item && LastAIUser.CanInteractWith(item))
{
AutoTemp = true;
unsentChanges = true;
lastAIUser = null;
LastAIUser = null;
}
}
@@ -309,10 +313,15 @@ namespace Barotrauma.Items.Components
if (fissionRate > 0.0f)
{
foreach (Item item in item.ContainedItems)
var containedItems = item.OwnInventory?.Items;
if (containedItems != null)
{
if (!item.HasTag("reactorfuel")) continue;
item.Condition -= fissionRate / 100.0f * fuelConsumptionRate * deltaTime;
foreach (Item item in containedItems)
{
if (item == null) { continue; }
if (!item.HasTag("reactorfuel")) { continue; }
item.Condition -= fissionRate / 100.0f * fuelConsumptionRate * deltaTime;
}
}
if (item.CurrentHull != null)
@@ -337,6 +346,7 @@ namespace Barotrauma.Items.Components
item.SendSignal(0, ((int)(temperature * 100.0f)).ToString(), "temperature_out", null);
item.SendSignal(0, ((int)-CurrPowerConsumption).ToString(), "power_value_out", null);
item.SendSignal(0, ((int)load).ToString(), "load_value_out", null);
item.SendSignal(0, ((int)AvailableFuel).ToString(), "fuel_out", null);
UpdateFailures(deltaTime);
#if CLIENT
@@ -344,9 +354,13 @@ namespace Barotrauma.Items.Components
#endif
AvailableFuel = 0.0f;
sendUpdateTimer = Math.Max(sendUpdateTimer - deltaTime, 0.0f);
sendUpdateTimer -= deltaTime;
#if CLIENT
if (unsentChanges && sendUpdateTimer <= 0.0f)
#else
if (sendUpdateTimer < -NetworkUpdateIntervalLow || (unsentChanges && sendUpdateTimer <= 0.0f))
#endif
{
#if SERVER
if (GameMain.Server != null)
@@ -360,7 +374,7 @@ namespace Barotrauma.Items.Components
item.CreateClientEvent(this);
}
#endif
sendUpdateTimer = NetworkUpdateInterval;
sendUpdateTimer = NetworkUpdateIntervalHigh;
unsentChanges = false;
}
}
@@ -398,8 +412,8 @@ namespace Barotrauma.Items.Components
private bool TooMuchFuel()
{
var containedItems = item.ContainedItems;
if (containedItems != null && containedItems.Count() <= 1) { return false; }
var containedItems = item.OwnInventory?.Items;
if (containedItems != null && containedItems.Count(i => i != null) <= 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);
@@ -516,12 +530,12 @@ namespace Barotrauma.Items.Components
fireTimer = 0.0f;
meltDownTimer = 0.0f;
var containedItems = item.ContainedItems;
var containedItems = item.OwnInventory?.Items;
if (containedItems != null)
{
foreach (Item containedItem in containedItems)
{
if (containedItem == null) continue;
if (containedItem == null) { continue; }
containedItem.Condition = 0.0f;
}
}
@@ -583,15 +597,19 @@ namespace Barotrauma.Items.Components
else if (TooMuchFuel())
{
var container = item.GetComponent<ItemContainer>();
foreach (Item item in item.ContainedItems)
var containedItems = item.OwnInventory?.Items;
if (containedItems != null)
{
if (item != null && container.ContainableItems.Any(ri => ri.MatchesItem(item)))
foreach (Item item in containedItems)
{
if (!character.Inventory.TryPutItem(item, character, allowedSlots: item.AllowedSlots))
if (item != null && container.ContainableItems.Any(ri => ri.MatchesItem(item)))
{
item.Drop(character);
if (!character.Inventory.TryPutItem(item, character, allowedSlots: item.AllowedSlots))
{
item.Drop(character);
}
break;
}
break;
}
}
}
@@ -599,7 +617,7 @@ namespace Barotrauma.Items.Components
if (objective.Override)
{
if (lastUser != null && lastUser != character && lastUser != lastAIUser)
if (lastUser != null && lastUser != character && lastUser != LastAIUser)
{
if (lastUser.SelectedConstruction == item)
{
@@ -607,8 +625,12 @@ namespace Barotrauma.Items.Components
}
}
}
else if (LastUserWasPlayer)
{
return true;
}
LastUser = lastAIUser = character;
LastUser = LastAIUser = character;
bool prevAutoTemp = autoTemp;
bool prevPowerOn = _powerOn;
@@ -286,7 +286,8 @@ namespace Barotrauma.Items.Components
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
float userSkill = 0.0f;
if (user != null && (user.SelectedConstruction == item || item.linkedTo.Contains(user.SelectedConstruction)))
if (user != null && controlledSub != null &&
(user.SelectedConstruction == item || item.linkedTo.Contains(user.SelectedConstruction)))
{
userSkill = user.GetSkillLevel("helm") / 100.0f;
}
@@ -298,7 +299,9 @@ namespace Barotrauma.Items.Components
}
else
{
if (user != null && user.Info != null && user.SelectedConstruction == item)
if (user != null && user.Info != null &&
user.SelectedConstruction == item &&
controlledSub != null && controlledSub.Velocity.LengthSquared() > 0.01f)
{
IncreaseSkillLevel(user, deltaTime);
}
@@ -320,13 +323,13 @@ namespace Barotrauma.Items.Components
}
}
}
item.SendSignal(0, targetVelocity.X.ToString(CultureInfo.InvariantCulture), "velocity_x_out", null);
item.SendSignal(0, targetVelocity.X.ToString(CultureInfo.InvariantCulture), "velocity_x_out", user);
float targetLevel = -targetVelocity.Y;
targetLevel += (neutralBallastLevel - 0.5f) * 100.0f;
item.SendSignal(0, targetLevel.ToString(CultureInfo.InvariantCulture), "velocity_y_out", null);
item.SendSignal(0, targetLevel.ToString(CultureInfo.InvariantCulture), "velocity_y_out", user);
}
private void IncreaseSkillLevel(Character user, float deltaTime)
@@ -335,12 +338,11 @@ namespace Barotrauma.Items.Components
// Do not increase the helm skill when "steering" the sub in an outpost level
if (GameMain.GameSession?.Campaign != null && Level.IsLoadedOutpost) { return; }
float userSkill = user.GetSkillLevel("helm") / 100.0f;
float userSkill = Math.Max(user.GetSkillLevel("helm"), 1.0f) / 100.0f;
user.Info.IncreaseSkillLevel(
"helm",
SkillSettings.Current.SkillIncreasePerSecondWhenSteering / Math.Max(userSkill, 1.0f) * deltaTime,
SkillSettings.Current.SkillIncreasePerSecondWhenSteering / userSkill * deltaTime,
user.WorldPosition + Vector2.UnitY * 150.0f);
}
private void UpdateAutoPilot(float deltaTime)
@@ -599,6 +601,7 @@ namespace Barotrauma.Items.Components
}
break;
case "navigateback":
if (Level.IsLoadedOutpost) { break; }
if (DockingSources.Any(d => d.Docked))
{
item.SendSignal(0, "1", "toggle_docking", sender: null);
@@ -613,6 +616,7 @@ namespace Barotrauma.Items.Components
}
break;
case "navigatetodestination":
if (Level.IsLoadedOutpost) { break; }
if (DockingSources.Any(d => d.Docked))
{
item.SendSignal(0, "1", "toggle_docking", sender: null);
@@ -0,0 +1,314 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Xml.Linq;
using Microsoft.Xna.Framework;
namespace Barotrauma.Items.Components
{
internal enum PlantItemType
{
Seed,
Fertilizer
}
internal readonly struct SuitablePlantItem
{
public readonly Item? Item;
public readonly PlantItemType Type;
public readonly string ProgressBarMessage;
public SuitablePlantItem(Item item, PlantItemType type, string progressBarMessage)
{
Item = item;
Type = type;
ProgressBarMessage = progressBarMessage;
}
public bool IsNull() => Item == null;
}
internal struct PlantSlot
{
public Vector2 Offset;
public float Size;
public PlantSlot(XElement element)
{
Offset = element.GetAttributeVector2("offset", Vector2.Zero);
Size = element.GetAttributeFloat("size", 0.5f);
}
public PlantSlot(Vector2 offset, float size)
{
Offset = offset;
Size = size;
}
}
internal partial class Planter : Pickable, IDrawableComponent
{
public static readonly PlantSlot NullSlot = new PlantSlot();
public readonly Dictionary<int, PlantSlot> PlantSlots = new Dictionary<int, PlantSlot>();
private static readonly SuitablePlantItem NullItem = new SuitablePlantItem();
private const string MsgFertilizer = "ItemMsgAddFertilizer";
private const string MsgSeed = "ItemMsgPlantSeed";
private const string MsgHarvest = "ItemMsgHarvest";
private const string MsgUprooting = "progressbar.uprooting";
private const string MsgFertilizing = "progressbar.fertilizing";
private const string MsgPlanting = "progressbar.planting";
public static float GrowthTickDelay = 1f; // 1 second
private float fertilizer;
[Serialize(0f, true, "How much fertilizer the planter has.")]
public float Fertilizer
{
get => fertilizer;
set => fertilizer = Math.Clamp(value, 0, FertilizerCapacity);
}
[Serialize(100f, true, "How much fertilizer can the planter hold.")]
public float FertilizerCapacity { get; set; }
public Growable?[] GrowableSeeds = new Growable?[0];
private readonly List<RelatedItem> SuitableFertilizer = new List<RelatedItem>();
private readonly List<RelatedItem> SuitableSeeds = new List<RelatedItem>();
private ItemContainer? container;
private float growthTickTimer;
public Planter(Item item, XElement element) : base(item, element)
{
canBePicked = true;
SerializableProperty.DeserializeProperties(this, element);
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "plantslot":
PlantSlots.Add(subElement.GetAttributeInt("slot", 0), new PlantSlot(subElement));
break;
case "suitablefertilizer":
SuitableFertilizer.Add(RelatedItem.Load(subElement, true, item.Name));
break;
case "suitableseed":
SuitableSeeds.Add(RelatedItem.Load(subElement, true, item.Name));
break;
}
}
}
public override void OnItemLoaded()
{
base.OnItemLoaded();
IsActive = true;
#if CLIENT
lightComponent = item.GetComponent<LightComponent>();
if (lightComponent != null)
{
lightComponent.Light.Enabled = false;
}
#endif
container = item.GetComponent<ItemContainer>();
GrowableSeeds = new Growable[container.Capacity];
}
public override bool HasRequiredItems(Character character, bool addMessage, string msg = null)
{
if (container?.Inventory == null) { return false; }
SuitablePlantItem plantItem = GetSuitableItem(character);
if (!plantItem.IsNull())
{
Msg = plantItem.Type switch
{
PlantItemType.Seed => MsgSeed,
PlantItemType.Fertilizer => MsgFertilizer,
_ => throw new ArgumentOutOfRangeException()
};
ParseMsg();
return true;
}
if (HasAnyFinishedGrowing())
{
Msg = MsgHarvest;
ParseMsg();
return true;
}
Msg = string.Empty;
ParseMsg();
return false;
}
public override bool Pick(Character character)
{
SuitablePlantItem plantItem = GetSuitableItem(character);
PickingMsg = plantItem.IsNull() ? MsgUprooting : plantItem.ProgressBarMessage;
return base.Pick(character);
}
public override bool OnPicked(Character character)
{
if (container?.Inventory == null) { return false; }
SuitablePlantItem plantItem = GetSuitableItem(character);
if (plantItem.IsNull())
{
return TryHarvest(character);
}
switch (plantItem.Type)
{
case PlantItemType.Seed:
return container.Inventory.TryPutItem(plantItem.Item, character, new List<InvSlotType> { InvSlotType.Any });
case PlantItemType.Fertilizer when plantItem.Item != null:
float canAdd = FertilizerCapacity - Fertilizer;
float maxAvailable = plantItem.Item.Condition;
float toAdd = Math.Min(canAdd, maxAvailable);
plantItem.Item.Condition -= toAdd;
fertilizer += toAdd;
#if CLIENT
character.UpdateHUDProgressBar(this, Item.DrawPosition, Fertilizer / FertilizerCapacity, Color.SaddleBrown, Color.SaddleBrown, "entityname.fertilizer");
#endif
return false;
}
return false;
}
/// <summary>
/// Attempts to harvest a fully grown plant or removes a decayed plant if any
/// </summary>
/// <param name="character">The character who gets the produce or null if they should drop on the floor.</param>
/// <returns></returns>
private bool TryHarvest(Character? character)
{
Debug.Assert(container != null, "Tried to harvest a planter without an item container.");
for (var i = 0; i < GrowableSeeds.Length; i++)
{
Growable? seed = GrowableSeeds[i];
if (seed == null) { continue; }
if (seed.Decayed || seed.FullyGrown)
{
container?.Inventory.RemoveItem(seed.Item);
GrowableSeeds[i] = null;
return true;
}
}
return false;
}
public override void Update(float deltaTime, Camera cam)
{
base.Update(deltaTime, cam);
#if CLIENT
if (lightComponent != null)
{
bool hasSeed = false;
foreach (Growable? seed in GrowableSeeds) { hasSeed |= seed != null; }
lightComponent.Light.Enabled = hasSeed;
}
#endif
if (container?.Inventory == null) { return; }
for (var i = 0; i < container.Inventory.Items.Length; i++)
{
if (i < 0 || GrowableSeeds.Length <= i) { continue; }
Item containedItem = container.Inventory.Items[i];
Growable? growable = containedItem?.GetComponent<Growable>();
if (growable != null)
{
GrowableSeeds[i] = growable;
growable.IsActive = true;
}
else
{
if (GrowableSeeds[i] is { } oldGrowable)
{
// Kill the plant if it's somehow removed
oldGrowable.Decayed = true;
oldGrowable.IsActive = false;
}
GrowableSeeds[i] = null;
}
}
// server handles this
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
float delay = GrowthTickDelay;
if (Fertilizer > 0)
{
delay /= 2f;
Fertilizer -= deltaTime / 10f;
}
if (growthTickTimer > delay)
{
for (var i = 0; i < GrowableSeeds.Length; i++)
{
PlantSlot slot = PlantSlots.ContainsKey(i) ? PlantSlots[i] : NullSlot;
Growable? seed = GrowableSeeds[i];
seed?.OnGrowthTick(this, slot);
}
growthTickTimer = 0;
}
else if (Item.ParentInventory == null)
{
if (item.GetComponent<Holdable>() is { } holdable)
{
if (holdable.Attachable && !holdable.Attached)
{
return;
}
}
growthTickTimer += deltaTime;
}
}
private SuitablePlantItem GetSuitableItem(Character character)
{
foreach (Item heldItem in character.SelectedItems)
{
if (heldItem == null) { continue; }
if (container?.Inventory != null && !container.Inventory.IsFull())
{
if (heldItem.GetComponent<Growable>() != null && SuitableSeeds.Any(ri => ri.MatchesItem(heldItem)))
{
return new SuitablePlantItem(heldItem, PlantItemType.Seed, MsgPlanting);
}
}
if (SuitableFertilizer.Any(ri => ri.MatchesItem(heldItem)))
{
return new SuitablePlantItem(heldItem, PlantItemType.Fertilizer, MsgFertilizing);
}
}
return NullItem;
}
private bool HasAnyFinishedGrowing() => GrowableSeeds.Any(seed => seed != null && (seed.FullyGrown || seed.Decayed));
}
}
@@ -121,7 +121,6 @@ namespace Barotrauma.Items.Components
: base(item, element)
{
IsActive = true;
InitProjSpecific();
}
@@ -184,23 +183,25 @@ namespace Barotrauma.Items.Components
charge = 0.0f;
return;
}
//output starts dropping when the charge is less than 10%
float maxOutputRatio = 1.0f;
if (chargeRatio < 0.1f)
else
{
maxOutputRatio = Math.Max(chargeRatio * 10.0f, 0.0f);
//output starts dropping when the charge is less than 10%
float maxOutputRatio = 1.0f;
if (chargeRatio < 0.1f)
{
maxOutputRatio = Math.Max(chargeRatio * 10.0f, 0.0f);
}
CurrPowerOutput += (gridLoad - gridPower) * deltaTime;
float maxOutput = Math.Min(MaxOutPut * maxOutputRatio, gridLoad);
CurrPowerOutput = MathHelper.Clamp(CurrPowerOutput, 0.0f, maxOutput);
Charge -= CurrPowerOutput / 3600.0f;
}
CurrPowerOutput += (gridLoad - gridPower) * deltaTime;
float maxOutput = Math.Min(MaxOutPut * maxOutputRatio, gridLoad);
CurrPowerOutput = MathHelper.Clamp(CurrPowerOutput, 0.0f, maxOutput);
Charge -= CurrPowerOutput / 3600.0f;
item.SendSignal(0, ((int)Math.Round(Charge)).ToString(), "charge", null);
item.SendSignal(0, ((int)Math.Round((Charge / capacity) * 100)).ToString(), "charge_%", null);
item.SendSignal(0, ((int)Math.Round((RechargeSpeed / maxRechargeSpeed) * 100)).ToString(), "charge_rate", null);
item.SendSignal(0, ((int)Math.Round(Charge / capacity * 100)).ToString(), "charge_%", null);
item.SendSignal(0, ((int)Math.Round(RechargeSpeed / maxRechargeSpeed * 100)).ToString(), "charge_rate", null);
}
public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
@@ -113,6 +113,19 @@ namespace Barotrauma.Items.Components
powerLoad = 0.0f;
currPowerConsumption = 0.0f;
SetAllConnectionsDirty();
foreach (HashSet<Connection> recipientList in connectedRecipients.Values.ToList())
{
foreach (Connection c in recipientList)
{
if (c.Item == item) { continue; }
var recipientPowerTransfer = c.Item.GetComponent<PowerTransfer>();
if (recipientPowerTransfer != null)
{
recipientPowerTransfer.SetAllConnectionsDirty();
recipientPowerTransfer.RefreshConnections();
}
}
}
RefreshConnections();
isBroken = true;
}
@@ -185,29 +198,32 @@ namespace Barotrauma.Items.Components
else if (!connectionDirty[c])
{
continue;
}
HashSet<Connection> connected = new HashSet<Connection>();
if (!connectedRecipients.ContainsKey(c))
{
connectedRecipients.Add(c, connected);
}
else
{
//mark all previous recipients as dirty
foreach (Connection recipient in connectedRecipients[c])
{
var pt = recipient.Item.GetComponent<PowerTransfer>();
if (pt != null) pt.connectionDirty[recipient] = true;
}
}
}
//find all connections that are connected to this one (directly or via another PowerTransfer)
connected.Add(c);
GetConnected(c, connected);
HashSet<Connection> connected = new HashSet<Connection>();
if (item.Condition > 0.0f)
{
if (!connectedRecipients.ContainsKey(c))
{
connectedRecipients.Add(c, connected);
}
else
{
//mark all previous recipients as dirty
foreach (Connection recipient in connectedRecipients[c])
{
var pt = recipient.Item.GetComponent<PowerTransfer>();
if (pt != null) pt.connectionDirty[recipient] = true;
}
}
connected.Add(c);
GetConnected(c, connected);
}
connectedRecipients[c] = connected;
//go through all the PowerTransfers and we're connected to and set their connections to match the ones we just calculated
//go through all the PowerTransfers that we're connected to and set their connections to match the ones we just calculated
//(no need to go through the recursive GetConnected method again)
foreach (Connection recipient in connected)
{
@@ -232,10 +248,10 @@ namespace Barotrauma.Items.Components
foreach (Connection recipient in recipients)
{
if (recipient == null || connected.Contains(recipient)) continue;
if (recipient == null || connected.Contains(recipient)) { continue; }
Item it = recipient.Item;
if (it == null || it.Condition <= 0.0f) continue;
if (it == null || it.Condition <= 0.0f) { continue; }
connected.Add(recipient);
@@ -131,7 +131,7 @@ namespace Barotrauma.Items.Components
{
if (!powerOnSoundPlayed && powerOnSound != null)
{
SoundPlayer.PlaySound(powerOnSound.Sound, item.WorldPosition, powerOnSound.Volume, powerOnSound.Range, item.CurrentHull);
SoundPlayer.PlaySound(powerOnSound.Sound, item.WorldPosition, powerOnSound.Volume, powerOnSound.Range, hullGuess: item.CurrentHull);
powerOnSoundPlayed = true;
}
}
@@ -250,7 +250,7 @@ namespace Barotrauma.Items.Components
}
if (powered is PowerContainer pc)
{
if (pc.CurrPowerOutput <= 0.0f) { continue; }
if (pc.CurrPowerOutput <= 0.0f || pc.item.Condition <= 0.0f) { continue; }
//providing power
lastPowerProbeRecipients.Clear();
powered.powerOut?.SendPowerProbeSignal(powered.item, pc.CurrPowerOutput);
@@ -282,7 +282,7 @@ namespace Barotrauma.Items.Components
continue;
}
var pc = powerSource.Item.GetComponent<PowerContainer>();
if (pc != null)
if (pc != null && pc.item.Condition > 0.0f)
{
float voltage = pc.CurrPowerOutput / Math.Max(powered.CurrPowerConsumption, 1.0f);
powered.voltage += voltage;
@@ -407,6 +407,16 @@ namespace Barotrauma.Items.Components
return hits;
}
public override void Drop(Character dropper)
{
if (dropper != null)
{
Deactivate();
Unstick();
}
base.Drop(dropper);
}
public override void Update(float deltaTime, Camera cam)
{
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
@@ -422,10 +432,13 @@ namespace Barotrauma.Items.Components
if (item.body.LinearVelocity.LengthSquared() < ContinuousCollisionThreshold * ContinuousCollisionThreshold)
{
item.body.FarseerBody.IsBullet = false;
//projectiles with a stickjoint don't become inactive until the stickjoint is detached
if (stickJoint == null) { IsActive = false; }
}
}
//projectiles with a stickjoint don't become inactive until the stickjoint is detached
if (stickJoint == null && !item.body.FarseerBody.IsBullet)
{
IsActive = false;
}
if (stickJoint == null) { return; }
@@ -511,9 +524,10 @@ namespace Barotrauma.Items.Components
hits.Add(target.Body);
impactQueue.Enqueue(new Impact(target, contact.Manifold.LocalNormal, item.body.LinearVelocity));
if (hits.Count() >= MaxTargetsToHit)
IsActive = true;
if (hits.Count() >= MaxTargetsToHit || target.Body.UserData is VoronoiCell)
{
item.body.FarseerBody.OnCollision -= OnProjectileCollision;
Deactivate();
return true;
}
else
@@ -626,20 +640,9 @@ namespace Barotrauma.Items.Components
target.Body.ApplyLinearImpulse(velocity * item.body.Mass);
if (hits.Count() >= MaxTargetsToHit)
if (hits.Count() >= MaxTargetsToHit || hits.LastOrDefault()?.UserData is VoronoiCell)
{
item.body.FarseerBody.OnCollision -= OnProjectileCollision;
if ((item.Prefab.DamagedByProjectiles || item.Prefab.DamagedByMeleeWeapons) && item.Condition > 0)
{
item.body.CollisionCategories = Physics.CollisionCharacter;
item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform | Physics.CollisionProjectile;
}
else
{
item.body.CollisionCategories = Physics.CollisionItem;
item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel;
}
IgnoredBodies.Clear();
Deactivate();
}
if (attackResult.AppliedDamageModifiers != null &&
@@ -684,11 +687,12 @@ namespace Barotrauma.Items.Components
item.body.LinearVelocity *= 0.5f;
}
var containedItems = item.ContainedItems;
var containedItems = item.OwnInventory?.Items;
if (containedItems != null)
{
foreach (Item contained in containedItems)
{
if (contained == null) { continue; }
if (contained.body != null)
{
contained.SetTransform(item.SimPosition, contained.body.Rotation);
@@ -704,6 +708,22 @@ namespace Barotrauma.Items.Components
return true;
}
private void Deactivate()
{
item.body.FarseerBody.OnCollision -= OnProjectileCollision;
if ((item.Prefab.DamagedByProjectiles || item.Prefab.DamagedByMeleeWeapons) && item.Condition > 0)
{
item.body.CollisionCategories = Physics.CollisionCharacter;
item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform | Physics.CollisionProjectile;
}
else
{
item.body.CollisionCategories = Physics.CollisionItem;
item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel;
}
IgnoredBodies.Clear();
}
private void StickToTarget(Body targetBody, Vector2 axis)
{
if (stickJoint != null) { return; }
@@ -206,9 +206,12 @@ namespace Barotrauma.Items.Components
{
if (!CheckCharacterSuccess(character))
{
GameServer.Log($"{GameServer.CharacterLogName(character)} failed to {(action == FixActions.Sabotage ? "sabotage" : "repair")} {item.Name}", ServerLog.MessageType.ItemInteraction);
GameMain.Server?.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnFailure, this, character.ID });
return false;
}
GameServer.Log($"{GameServer.CharacterLogName(character)} started {(action == FixActions.Sabotage ? "sabotaging" : "repairing")} {item.Name}", ServerLog.MessageType.ItemInteraction);
item.CreateServerEvent(this);
}
#else
@@ -62,7 +62,10 @@ namespace Barotrauma.Items.Components
timeSinceReceived[i] += deltaTime;
}
float output = Calculate(receivedSignal[0], receivedSignal[1]);
item.SendSignal(0, MathHelper.Clamp(output, ClampMin, ClampMax).ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
if (MathUtils.IsValid(output))
{
item.SendSignal(0, MathHelper.Clamp(output, ClampMin, ClampMax).ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
}
}
protected abstract float Calculate(float signal1, float signal2);
@@ -0,0 +1,17 @@
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
class ConcatComponent : StringComponent
{
public ConcatComponent(Item item, XElement element)
: base(item, element)
{
}
protected override string Calculate(string signal1, string signal2)
{
return signal1 + signal2;
}
}
}
@@ -1,6 +1,4 @@
using System;
using System.Globalization;
using System.Xml.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
@@ -13,6 +11,7 @@ namespace Barotrauma.Items.Components
protected override float Calculate(float signal1, float signal2)
{
if (MathUtils.NearlyEqual(signal2, 0)) { return float.NaN; }
return signal1 / signal2;
}
}
@@ -56,8 +56,10 @@ namespace Barotrauma.Items.Components
item.SendSignal(0, Math.Abs(value).ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
break;
case FunctionType.SquareRoot:
double square = value > 0 ? Math.Sqrt(value) : 0;
item.SendSignal(0, square.ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
if (value > 0)
{
item.SendSignal(0, Math.Sqrt(value).ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
}
break;
default:
throw new NotImplementedException($"Function {Function} has not been implemented.");
@@ -208,7 +208,8 @@ namespace Barotrauma.Items.Components
else
{
#if CLIENT
light.Rotation = -Rotation;
light.Rotation = -Rotation - MathHelper.ToRadians(item.Rotation);
light.LightSpriteEffect = item.SpriteEffects;
#endif
}
@@ -265,11 +266,14 @@ namespace Barotrauma.Items.Components
switch (connection.Name)
{
case "toggle":
if (!IgnoreContinuousToggle || lastToggleSignalTime < Timing.TotalTime - 0.1)
if (signal != "0")
{
IsOn = !IsOn;
if (!IgnoreContinuousToggle || lastToggleSignalTime < Timing.TotalTime - 0.1)
{
IsOn = !IsOn;
}
lastToggleSignalTime = Timing.TotalTime;
}
lastToggleSignalTime = Timing.TotalTime;
break;
case "set_state":
IsOn = signal != "0";
@@ -1,14 +1,24 @@
using System.Xml.Linq;
using Barotrauma.Networking;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
class MemoryComponent : ItemComponent
partial class MemoryComponent : ItemComponent, IServerSerializable
{
const int MaxValueLength = 256;
private string value;
[InGameEditable, Serialize("", true, description: "The currently stored signal the item outputs.", alwaysUseInstanceValues: true)]
public string Value
{
get;
set;
get { return value; }
set
{
if (value == null) { return; }
this.value = value.Length <= MaxValueLength ? value : value.Substring(0, MaxValueLength);
}
}
protected bool writeable = true;
@@ -24,15 +34,22 @@ namespace Barotrauma.Items.Components
item.SendSignal(0, Value, "signal_out", null);
}
partial void OnStateChanged();
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f)
{
switch (connection.Name)
{
case "signal_in":
if (writeable) { Value = signal; }
if (writeable)
{
if (Value == signal) { return; }
Value = signal;
OnStateChanged();
}
break;
case "signal_store":
writeable = (signal == "1");
writeable = signal == "1";
break;
}
}
@@ -0,0 +1,72 @@
using Microsoft.Xna.Framework;
using System;
using System.Globalization;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
abstract class StringComponent : ItemComponent
{
//an array to keep track of how long ago a signal was received on both inputs
protected float[] timeSinceReceived;
protected string[] receivedSignal;
//the output is sent if both inputs have received a signal within the timeframe
protected float timeFrame;
[InGameEditable(DecimalCount = 2),
Serialize(0.0f, true, description: "The item must have received signals to both inputs within this timeframe to output the result." +
" If set to 0, the inputs must be received at the same time.", alwaysUseInstanceValues: true)]
public float TimeFrame
{
get { return timeFrame; }
set
{
timeFrame = Math.Max(0.0f, value);
}
}
public StringComponent(Item item, XElement element)
: base(item, element)
{
timeSinceReceived = new float[] { Math.Max(timeFrame * 2.0f, 0.1f), Math.Max(timeFrame * 2.0f, 0.1f) };
receivedSignal = new string[2];
}
sealed public override void Update(float deltaTime, Camera cam)
{
for (int i = 0; i < timeSinceReceived.Length; i++)
{
if (timeSinceReceived[i] > timeFrame)
{
IsActive = false;
return;
}
timeSinceReceived[i] += deltaTime;
}
string output = Calculate(receivedSignal[0], receivedSignal[1]);
item.SendSignal(0, output, "signal_out", null);
}
protected abstract string Calculate(string signal1, string signal2);
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f)
{
switch (connection.Name)
{
case "signal_in1":
receivedSignal[0] = signal;
timeSinceReceived[0] = 0.0f;
IsActive = true;
break;
case "signal_in2":
receivedSignal[1] = signal;
timeSinceReceived[1] = 0.0f;
IsActive = true;
break;
}
}
}
}
@@ -62,9 +62,15 @@ namespace Barotrauma.Items.Components
break;
case FunctionType.Tan:
if (!UseRadians) { value = MathHelper.ToRadians(value); }
item.SendSignal(0, ((float)Math.Tan(value)).ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
//tan is undefined if the value is (π / 2) + πk, where k is any integer
if (!MathUtils.NearlyEqual(value % MathHelper.Pi, MathHelper.PiOver2))
{
item.SendSignal(0, ((float)Math.Tan(value)).ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
}
break;
case FunctionType.Asin:
//asin is only defined in the range [-1,1]
if (value >= -1.0f && value <= 1.0f)
{
float angle = (float)Math.Asin(value);
if (!UseRadians) { angle = MathHelper.ToDegrees(angle); }
@@ -72,6 +78,8 @@ namespace Barotrauma.Items.Components
}
break;
case FunctionType.Acos:
//acos is only defined in the range [-1,1]
if (value >= -1.0f && value <= 1.0f)
{
float angle = (float)Math.Acos(value);
if (!UseRadians) { angle = MathHelper.ToDegrees(angle); }
@@ -12,6 +12,8 @@ namespace Barotrauma.Items.Components
{
private static readonly List<WifiComponent> list = new List<WifiComponent>();
const int ChannelMemorySize = 10;
private float range;
private int channel;
@@ -20,6 +22,8 @@ namespace Barotrauma.Items.Components
private string prevSignal;
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; }
@@ -36,7 +40,7 @@ namespace Barotrauma.Items.Components
}
}
[InGameEditable, Serialize(1, true, description: "WiFi components can only communicate with components that use the same channel.", alwaysUseInstanceValues: true)]
[InGameEditable, Serialize(0, true, description: "WiFi components can only communicate with components that use the same channel.", alwaysUseInstanceValues: true)]
public int Channel
{
get { return channel; }
@@ -83,6 +87,18 @@ namespace Barotrauma.Items.Components
{
list.Add(this);
IsActive = true;
channelMemory = element.GetAttributeIntArray("channelmemory", new int[ChannelMemorySize]);
}
public override void OnItemLoaded()
{
if (channelMemory.All(m => m == 0))
{
for (int i = 0; i < channelMemory.Length; i++)
{
channelMemory[i] = i;
}
}
}
public bool CanTransmit()
@@ -118,6 +134,24 @@ namespace Barotrauma.Items.Components
}
}
public int GetChannelMemory(int index)
{
if (index < 0 || index >= ChannelMemorySize)
{
return 0;
}
return channelMemory[index];
}
public void SetChannelMemory(int index, int value)
{
if (index < 0 || index >= ChannelMemorySize)
{
return;
}
channelMemory[index] = MathHelper.Clamp(value, 0, 10000);
}
public void TransmitSignal(int stepsTaken, string signal, Item source, Character sender, bool sendToChat, float signalStrength = 1.0f)
{
var senderComponent = source?.GetComponent<WifiComponent>();
@@ -220,5 +254,12 @@ namespace Barotrauma.Items.Components
base.RemoveComponentSpecific();
list.Remove(this);
}
public override XElement Save(XElement parentElement)
{
var element = base.Save(parentElement);
element.Add(new XAttribute("channelmemory", string.Join(',', channelMemory)));
return element;
}
}
}
@@ -87,7 +87,14 @@ namespace Barotrauma.Items.Components
get;
set;
}
[Serialize(false, false, description: "If enabled, the wire will not be visible in connection panels outside the submarine editor.")]
public bool HiddenInGame
{
get;
set;
}
public Wire(Item item, XElement element)
: base(item, element)
{
@@ -673,12 +680,13 @@ namespace Barotrauma.Items.Components
closestDist = 0.0f;
int closestIndex = -1;
maxDist *= maxDist;
for (int i = 0; i < nodes.Count-1; i++)
{
if ((Math.Abs(nodes[i].X - nodes[i + 1].X)<5 || Math.Sign(mousePos.X - nodes[i].X) != Math.Sign(mousePos.X - nodes[i + 1].X)) &&
(Math.Abs(nodes[i].Y - nodes[i + 1].Y)<5 || Math.Sign(mousePos.Y - nodes[i].Y) != Math.Sign(mousePos.Y - nodes[i + 1].Y)))
{
float dist = MathUtils.LineToPointDistance(nodes[i], nodes[i + 1], mousePos);
float dist = MathUtils.LineToPointDistanceSquared(nodes[i], nodes[i + 1], mousePos);
if (dist > maxDist) continue;
if (closestIndex == -1 || dist < closestDist)
@@ -688,12 +696,15 @@ namespace Barotrauma.Items.Components
}
}
}
closestDist = (float)Math.Sqrt(closestDist);
return closestIndex;
}
public override void FlipX(bool relativeToSub)
{
if (item.ParentInventory != null) { return; }
Vector2 refPos = item.Submarine == null ?
Vector2.Zero :
item.Position - item.Submarine.HiddenSubPosition;
@@ -198,14 +198,14 @@ namespace Barotrauma.Items.Components
private set;
}
private float baseRotationRad;
[Editable(0.0f, 360.0f), Serialize(0.0f, true, description: "The angle of the turret's base in degrees.", alwaysUseInstanceValues: true)]
float prevBaseRotation;
[Serialize(0.0f, true, description: "The angle of the turret's base in degrees.", alwaysUseInstanceValues: true)]
public float BaseRotation
{
get { return MathHelper.ToDegrees(baseRotationRad); }
get { return item.Rotation; }
set
{
baseRotationRad = MathHelper.ToRadians(value);
item.Rotation = value;
UpdateTransformedBarrelPos();
}
}
@@ -250,14 +250,15 @@ namespace Barotrauma.Items.Components
private void UpdateTransformedBarrelPos()
{
float flippedRotation = BaseRotation;
if (item.FlippedX) flippedRotation = -flippedRotation;
float flippedRotation = item.Rotation;
if (item.FlippedX) { flippedRotation = -flippedRotation; }
//if (item.FlippedY) flippedRotation = 180.0f - flippedRotation;
transformedBarrelPos = MathUtils.RotatePointAroundTarget(barrelPos * item.Scale, new Vector2(item.Rect.Width / 2, item.Rect.Height / 2), flippedRotation);
#if CLIENT
item.ResetCachedVisibleSize();
item.SpriteRotation = MathHelper.ToRadians(flippedRotation);
#endif
item.Rotation = flippedRotation;
prevBaseRotation = item.Rotation;
}
public override void OnItemLoaded()
@@ -271,7 +272,7 @@ namespace Barotrauma.Items.Components
if (lightComponent != null)
{
lightComponent.Parent = null;
lightComponent.Rotation = rotation;
lightComponent.Rotation = Rotation - MathHelper.ToRadians(item.Rotation);
lightComponent.Light.Rotation = -rotation;
}
#endif
@@ -283,6 +284,10 @@ namespace Barotrauma.Items.Components
this.cam = cam;
if (reload > 0.0f) { reload -= deltaTime; }
if (!MathUtils.NearlyEqual(item.Rotation, prevBaseRotation))
{
UpdateTransformedBarrelPos();
}
if (user != null && user.Removed)
{
@@ -344,7 +349,7 @@ namespace Barotrauma.Items.Components
if (lightComponent != null)
{
lightComponent.Rotation = rotation;
lightComponent.Rotation = Rotation - MathHelper.ToRadians(item.Rotation);
}
}
@@ -504,6 +509,15 @@ namespace Barotrauma.Items.Components
projectileComponent.Use((float)Timing.Step);
projectile.GetComponent<Rope>()?.Attach(item, projectile);
projectileComponent.User = user;
if (item.Submarine != null && projectile.body != null)
{
Vector2 velocitySum = item.Submarine.PhysicsBody.LinearVelocity + projectile.body.LinearVelocity;
if (velocitySum.LengthSquared() < NetConfig.MaxPhysicsBodyVelocity * NetConfig.MaxPhysicsBodyVelocity * 0.9f)
{
projectile.body.LinearVelocity = velocitySum;
}
}
}
if (projectile.Container != null) { projectile.Container.RemoveContained(projectile); }
@@ -983,26 +997,8 @@ namespace Barotrauma.Items.Components
public override void FlipY(bool relativeToSub)
{
baseRotationRad = MathUtils.WrapAngleTwoPi(baseRotationRad - MathHelper.Pi);
BaseRotation = MathHelper.ToDegrees(MathUtils.WrapAngleTwoPi(MathHelper.ToRadians(BaseRotation - 180)));
UpdateTransformedBarrelPos();
/*minRotation = -minRotation;
maxRotation = -maxRotation;
var temp = minRotation;
minRotation = maxRotation;
maxRotation = temp;
barrelPos.Y = item.Rect.Height / item.Scale - barrelPos.Y;
while (minRotation < 0)
{
minRotation += MathHelper.TwoPi;
maxRotation += MathHelper.TwoPi;
}
rotation = (minRotation + maxRotation) / 2;
UpdateTransformedBarrelPos();*/
}
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power, float signalStrength = 1.0f)
@@ -1012,6 +1008,7 @@ namespace Barotrauma.Items.Components
case "position_in":
if (float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float newRotation))
{
if (!MathUtils.IsValid(newRotation)) { return; }
targetRotation = MathHelper.ToRadians(newRotation);
IsActive = true;
}
@@ -1030,11 +1027,17 @@ namespace Barotrauma.Items.Components
}
break;
case "toggle_light":
if (lightComponent != null)
if (lightComponent != null && signal != "0")
{
lightComponent.IsOn = !lightComponent.IsOn;
}
break;
case "set_light":
if (lightComponent != null)
{
lightComponent.IsOn = signal != "0";
}
break;
}
}