Release 1.9.7.0 - Summer Update 2025
This commit is contained in:
@@ -220,6 +220,22 @@ namespace Barotrauma
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Can the item be put in the inventory in a slot of the specified type (i.e. is there a suitable free slot or a stack the item can be put in).
|
||||
/// </summary>
|
||||
public bool CanBePut(Item item, InvSlotType slotType)
|
||||
{
|
||||
for (int i = 0; i < capacity; i++)
|
||||
{
|
||||
if (slotType.HasFlag(SlotTypes[i]))
|
||||
{
|
||||
if (CanBePutInSlot(item, i)) { return true; }
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public override bool CanBePutInSlot(Item item, int i, bool ignoreCondition = false)
|
||||
{
|
||||
return
|
||||
|
||||
@@ -179,7 +179,8 @@ namespace Barotrauma.Items.Components
|
||||
var prevDockingTarget = DockingTarget;
|
||||
Undock(applyEffects: false);
|
||||
Dock(prevDockingTarget);
|
||||
Lock(isNetworkMessage: true, applyEffects: false);
|
||||
//don't move subs at this point, it will mess up the placement logic when flipping multi-part subs
|
||||
Lock(isNetworkMessage: true, applyEffects: false, moveSubs: false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,7 +282,7 @@ namespace Barotrauma.Items.Components
|
||||
OnDocked = null;
|
||||
}
|
||||
|
||||
public void Lock(bool isNetworkMessage, bool applyEffects = true)
|
||||
public void Lock(bool isNetworkMessage, bool applyEffects = true, bool moveSubs = true)
|
||||
{
|
||||
#if CLIENT
|
||||
if (GameMain.Client != null && !isNetworkMessage) { return; }
|
||||
@@ -312,16 +313,19 @@ namespace Barotrauma.Items.Components
|
||||
ApplyStatusEffects(ActionType.OnUse, 1.0f);
|
||||
}
|
||||
|
||||
Vector2 jointDiff = joint.WorldAnchorB - joint.WorldAnchorA;
|
||||
if (item.Submarine.PhysicsBody.Mass < DockingTarget.item.Submarine.PhysicsBody.Mass ||
|
||||
DockingTarget.item.Submarine.Info.IsOutpost)
|
||||
if (moveSubs)
|
||||
{
|
||||
item.Submarine.SubBody.SetPosition(item.Submarine.SubBody.Position + ConvertUnits.ToDisplayUnits(jointDiff));
|
||||
}
|
||||
else if (DockingTarget.item.Submarine.PhysicsBody.Mass < item.Submarine.PhysicsBody.Mass ||
|
||||
item.Submarine.Info.IsOutpost)
|
||||
{
|
||||
DockingTarget.item.Submarine.SubBody.SetPosition(DockingTarget.item.Submarine.SubBody.Position - ConvertUnits.ToDisplayUnits(jointDiff));
|
||||
Vector2 jointDiff = joint.WorldAnchorB - joint.WorldAnchorA;
|
||||
if (item.Submarine.PhysicsBody.Mass < DockingTarget.item.Submarine.PhysicsBody.Mass ||
|
||||
DockingTarget.item.Submarine.Info.IsOutpost)
|
||||
{
|
||||
item.Submarine.SubBody.SetPosition(item.Submarine.SubBody.Position + ConvertUnits.ToDisplayUnits(jointDiff));
|
||||
}
|
||||
else if (DockingTarget.item.Submarine.PhysicsBody.Mass < item.Submarine.PhysicsBody.Mass ||
|
||||
item.Submarine.Info.IsOutpost)
|
||||
{
|
||||
DockingTarget.item.Submarine.SubBody.SetPosition(DockingTarget.item.Submarine.SubBody.Position - ConvertUnits.ToDisplayUnits(jointDiff));
|
||||
}
|
||||
}
|
||||
|
||||
ConnectWireBetweenPorts();
|
||||
|
||||
@@ -349,31 +349,31 @@ namespace Barotrauma.Items.Components
|
||||
// used for debugging where a vine failed to grow
|
||||
public readonly HashSet<Rectangle> FailedRectangles = new HashSet<Rectangle>();
|
||||
|
||||
[Serialize(1f, IsPropertySaveable.Yes, "How fast the plant grows.")]
|
||||
[Serialize(1f, IsPropertySaveable.Yes, "How fast the plant grows. Value of 1 means a vine attempts to grow every 10 seconds while 2 and 0.5 mean every 5 and 20 seconds respectively.")]
|
||||
public float GrowthSpeed { get; set; }
|
||||
|
||||
[Serialize(100f, IsPropertySaveable.Yes, "How long the plant can go without watering.")]
|
||||
public float MaxHealth { get; set; }
|
||||
[Serialize(100f, IsPropertySaveable.Yes, "How much water the plant can hold. Affects how long the plant can survive without water.")]
|
||||
public float MaxWater { get; set; }
|
||||
|
||||
[Serialize(1f, IsPropertySaveable.Yes, "How much damage the plant takes while in water.")]
|
||||
public float FloodTolerance { get; set; }
|
||||
[Serialize(1f, IsPropertySaveable.Yes, "How much extra water the plant uses per second while it is submerged in a flooded hull.")]
|
||||
public float ExtraWaterUsedPerSecondWhileFlooded { get; set; }
|
||||
|
||||
[Serialize(1f, IsPropertySaveable.Yes, "How much damage the plant takes while growing.")]
|
||||
public float Hardiness { get; set; }
|
||||
[Serialize(1f, IsPropertySaveable.Yes, "How much water the plant consumes passively per second.")]
|
||||
public float WaterUsedPerSecond { get; set; }
|
||||
|
||||
[Serialize(0.01f, IsPropertySaveable.Yes, "How often a seed is produced.")]
|
||||
public float SeedRate { get; set; }
|
||||
[Serialize(0.01f, IsPropertySaveable.Yes, "Percentage chance of a seed item being produced on growth ticks (every 10 seconds without a multiplier). 0.01 means 1% chance. Not used in vanilla plants.")]
|
||||
public float SeedSpawnChance { get; set; }
|
||||
|
||||
[Serialize(0.01f, IsPropertySaveable.Yes, "How often a product item is produced.")]
|
||||
public float ProductRate { get; set; }
|
||||
[Serialize(0.01f, IsPropertySaveable.Yes, "How often a product item is produced on growth ticks (every 10 seconds without a multiplier). 0.01 means 1% chance.")]
|
||||
public float ProductSpawnChance { get; set; }
|
||||
|
||||
[Serialize(0.5f, IsPropertySaveable.Yes, "Probability of an attribute being randomly modified in a newly produced seed.")]
|
||||
[Serialize(0.5f, IsPropertySaveable.Yes, "Completely unused property that was added on the first design pass but due to the first pass being too complex was never used and now it is used by mods so it cannot be removed.")]
|
||||
public float MutationProbability { get; set; }
|
||||
|
||||
[Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes, "Color of the flowers.")]
|
||||
public Color FlowerTint { get; set; }
|
||||
|
||||
[Serialize(3, IsPropertySaveable.Yes, "Number of flowers drawn when fully grown")]
|
||||
[Serialize(3, IsPropertySaveable.Yes, "Number of flowers drawn.")]
|
||||
public int FlowerQuantity { get; set; }
|
||||
|
||||
[Serialize(0.25f, IsPropertySaveable.Yes, "Size of the flower sprites.")]
|
||||
@@ -403,9 +403,12 @@ namespace Barotrauma.Items.Components
|
||||
[Serialize("1,1,1,1", IsPropertySaveable.Yes, "Probability for the plant to grow in a direction.")]
|
||||
public Vector4 GrowthWeights { get; set; }
|
||||
|
||||
[Serialize(0.0f, IsPropertySaveable.Yes, "How much damage is taken from fires.")]
|
||||
[Serialize(0.0f, IsPropertySaveable.Yes, "How much water is lost due to fires every 10 seconds.")]
|
||||
public float FireVulnerability { get; set; }
|
||||
|
||||
[Serialize("0.0, 0.0", IsPropertySaveable.Yes, "Modifier to the percentage of product and seed items produced before the plant is fully grown based on how many vines have been grown. 0 would mean no products or seeds are produced while 0.5 would mean half of the normal amount.")]
|
||||
public Vector2 LinearProductAndSeedMultiplierBeforeFullyGrown { get; set; }
|
||||
|
||||
private const float increasedDeathSpeed = 10f;
|
||||
private bool accelerateDeath;
|
||||
private float health;
|
||||
@@ -417,7 +420,7 @@ namespace Barotrauma.Items.Components
|
||||
public float Health
|
||||
{
|
||||
get => health;
|
||||
set => health = Math.Clamp(value, 0, MaxHealth);
|
||||
set => health = Math.Clamp(value, 0, MaxWater);
|
||||
}
|
||||
|
||||
public bool Decayed { get; set; }
|
||||
@@ -441,7 +444,14 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
SerializableProperty.DeserializeProperties(this, element);
|
||||
|
||||
Health = MaxHealth;
|
||||
// backwards compatibility
|
||||
MaxWater = element.GetAttributeFloat("maxhealth", MaxWater);
|
||||
WaterUsedPerSecond = element.GetAttributeFloat("hardiness", WaterUsedPerSecond);
|
||||
ExtraWaterUsedPerSecondWhileFlooded = element.GetAttributeFloat("floodtolerance", ExtraWaterUsedPerSecondWhileFlooded);
|
||||
ProductSpawnChance = element.GetAttributeFloat("productrate", ProductSpawnChance);
|
||||
SeedSpawnChance = element.GetAttributeFloat("seedrate", SeedSpawnChance);
|
||||
|
||||
Health = MaxWater;
|
||||
|
||||
if (element.HasElements)
|
||||
{
|
||||
@@ -492,10 +502,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (Decayed) { return; }
|
||||
|
||||
if (FullyGrown)
|
||||
{
|
||||
TryGenerateProduct(planter, slot);
|
||||
}
|
||||
TryGenerateProduct(planter, slot);
|
||||
|
||||
if (Health > 0)
|
||||
{
|
||||
@@ -504,11 +511,11 @@ namespace Barotrauma.Items.Components
|
||||
// fertilizer makes the plant tick faster, compensate by halving water requirement
|
||||
float multipler = planter.Fertilizer > 0 ? 0.5f : 1f;
|
||||
|
||||
Health -= (accelerateDeath ? Hardiness * increasedDeathSpeed : Hardiness) * multipler;
|
||||
Health -= (accelerateDeath ? WaterUsedPerSecond * increasedDeathSpeed : WaterUsedPerSecond) * multipler;
|
||||
|
||||
if (planter.Item.InWater)
|
||||
{
|
||||
Health -= FloodTolerance * multipler;
|
||||
Health -= ExtraWaterUsedPerSecondWhileFlooded * multipler;
|
||||
}
|
||||
#if SERVER
|
||||
if (FullyGrown)
|
||||
@@ -535,7 +542,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
private void UpdateBranchHealth()
|
||||
{
|
||||
Color healthColor = Color.White * (1.0f - Health / MaxHealth);
|
||||
Color healthColor = Color.White * (1.0f - Health / MaxWater);
|
||||
foreach (VineTile vine in Vines)
|
||||
{
|
||||
vine.HealthColor = healthColor;
|
||||
@@ -546,11 +553,24 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
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;
|
||||
float spawnChanceMultiplier = 1f;
|
||||
|
||||
if (!FullyGrown)
|
||||
{
|
||||
if (LinearProductAndSeedMultiplierBeforeFullyGrown.NearlyEquals(Vector2.Zero)) { return; }
|
||||
|
||||
float growthProgress = Vines.Count / (float)MaximumVines;
|
||||
|
||||
spawnChanceMultiplier = MathHelper.Lerp(LinearProductAndSeedMultiplierBeforeFullyGrown.X, LinearProductAndSeedMultiplierBeforeFullyGrown.Y, growthProgress);
|
||||
|
||||
if (MathUtils.NearlyEqual(spawnChanceMultiplier, 0f)) { return; }
|
||||
}
|
||||
|
||||
|
||||
bool spawnProduct = Rand.Range(0f, 1f) < (ProductSpawnChance * spawnChanceMultiplier),
|
||||
spawnSeed = Rand.Range(0f, 1f) < (SeedSpawnChance * spawnChanceMultiplier);
|
||||
|
||||
Vector2 spawnPos;
|
||||
|
||||
@@ -678,7 +698,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
fireCheckCooldown = 5f;
|
||||
fireCheckCooldown = 10f;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Barotrauma.Abilities;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Networking;
|
||||
using FarseerPhysics;
|
||||
using FarseerPhysics.Dynamics;
|
||||
@@ -44,7 +45,12 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
private bool attachable, attached, attachedByDefault;
|
||||
private Voronoi2.VoronoiCell attachTargetCell;
|
||||
private PhysicsBody body;
|
||||
|
||||
/// <summary>
|
||||
/// The item's original physics body (if one exists). When the item is attached to a wall, it's <see cref="Item.body"/> gets set to null,
|
||||
/// and we use this field to keep track of the original body.
|
||||
/// </summary>
|
||||
private PhysicsBody originalBody;
|
||||
|
||||
public readonly ImmutableDictionary<StatTypes, float> HoldableStatValues;
|
||||
|
||||
@@ -62,7 +68,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public PhysicsBody Body
|
||||
{
|
||||
get { return item.body ?? body; }
|
||||
get { return item.body ?? originalBody; }
|
||||
}
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: "Is the item currently attached to a wall (only valid if Attachable is set to true).")]
|
||||
@@ -84,6 +90,9 @@ namespace Barotrauma.Items.Components
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(0f, IsPropertySaveable.Yes, description: "Camera offset to apply when aiming this item. Only valid if Aimable is set to true.")]
|
||||
public float CameraAimOffset { get; set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No, description: "Should the character adjust its pose when aiming with the item. Most noticeable underwater, where the character will rotate its entire body to face the direction the item is aimed at.")]
|
||||
public bool ControlPose
|
||||
{
|
||||
@@ -119,6 +128,32 @@ namespace Barotrauma.Items.Components
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No, description: "When enabled, the item can only be attached to a position where it touches the floor.")]
|
||||
public bool AttachesToFloor
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(true, IsPropertySaveable.No, description: "Can the item be attached inside doors?")]
|
||||
public bool AllowAttachInsideDoors
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
private HashSet<Identifier> disallowAttachingOverTags = new HashSet<Identifier>();
|
||||
|
||||
[Editable, Serialize("", IsPropertySaveable.Yes)]
|
||||
public string DisallowAttachingOverTags
|
||||
{
|
||||
get => disallowAttachingOverTags.ConvertToString();
|
||||
set
|
||||
{
|
||||
disallowAttachingOverTags = value.ToIdentifiers().ToHashSet();
|
||||
}
|
||||
}
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No, description: "Should the item be attached to a wall by default when it's placed in the submarine editor.")]
|
||||
public bool AttachedByDefault
|
||||
{
|
||||
@@ -275,7 +310,7 @@ namespace Barotrauma.Items.Components
|
||||
public Holdable(Item item, ContentXElement element)
|
||||
: base(item, element)
|
||||
{
|
||||
body = item.body;
|
||||
originalBody = item.body;
|
||||
|
||||
Pusher = null;
|
||||
if (element.GetAttributeBool("blocksplayers", false))
|
||||
@@ -411,9 +446,9 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (attachable)
|
||||
{
|
||||
if (body != null)
|
||||
if (originalBody != null)
|
||||
{
|
||||
item.body = body;
|
||||
item.body = originalBody;
|
||||
}
|
||||
DeattachFromWall();
|
||||
}
|
||||
@@ -514,9 +549,9 @@ namespace Barotrauma.Items.Components
|
||||
if (character != null) { item.Submarine = character.Submarine; }
|
||||
if (item.body == null)
|
||||
{
|
||||
if (body != null)
|
||||
if (originalBody != null)
|
||||
{
|
||||
item.body = body;
|
||||
item.body = originalBody;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -565,13 +600,45 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public bool CanBeAttached(Character user)
|
||||
{
|
||||
return CanBeAttached(user, out _);
|
||||
}
|
||||
|
||||
private static List<Item> tempOverlappingItems = new List<Item>();
|
||||
|
||||
private bool CanBeAttached(Character user, out IEnumerable<Item> overlappingItems)
|
||||
{
|
||||
tempOverlappingItems.Clear();
|
||||
overlappingItems = tempOverlappingItems;
|
||||
if (!attachable || !Reattachable) { return false; }
|
||||
|
||||
//can be attached anywhere in sub editor
|
||||
if (Screen.Selected == GameMain.SubEditorScreen) { return true; }
|
||||
|
||||
if (AttachesToFloor && item.CurrentHull == null) { return false; }
|
||||
|
||||
Vector2 attachPos = user == null ? item.WorldPosition : GetAttachPosition(user, useWorldCoordinates: true);
|
||||
|
||||
if (disallowAttachingOverTags.Any() || !AllowAttachInsideDoors)
|
||||
{
|
||||
var connectedHulls = item.CurrentHull?.GetConnectedHulls(includingThis: true, searchDepth: 5, ignoreClosedGaps: true);
|
||||
Vector2 size = item.Rect.Size.ToVector2() / 2;
|
||||
foreach (Item otherItem in Item.ItemList)
|
||||
{
|
||||
if (otherItem == item || otherItem.body is { BodyType: BodyType.Dynamic, Enabled: true }) { continue; }
|
||||
if (connectedHulls != null && !connectedHulls.Contains(otherItem.CurrentHull)) { continue; }
|
||||
if (disallowAttachingOverTags.None(tag => otherItem.HasTag(tag)) &&
|
||||
(otherItem.GetComponent<Door>() == null || AllowAttachInsideDoors))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Rectangle worldRect = otherItem.WorldRect;
|
||||
if (attachPos.X + size.X < worldRect.X || attachPos.X - size.X > worldRect.Right) { continue; }
|
||||
if (attachPos.Y - size.Y > worldRect.Y || attachPos.Y + size.Y < worldRect.Y - worldRect.Height) { continue; }
|
||||
tempOverlappingItems.Add(otherItem);
|
||||
}
|
||||
if (tempOverlappingItems.Any()) { return false; }
|
||||
}
|
||||
|
||||
//can be attached anywhere inside hulls
|
||||
if (item.CurrentHull != null && Submarine.RectContains(item.CurrentHull.WorldRect, attachPos)) { return true; }
|
||||
|
||||
@@ -670,14 +737,19 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (!attachable) { return; }
|
||||
|
||||
if (body == null)
|
||||
if (originalBody == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Tried to attach an item with no physics body to a wall ({item.Prefab.Identifier}).");
|
||||
}
|
||||
|
||||
body.Enabled = false;
|
||||
body.SetTransformIgnoreContacts(body.SimPosition, rotation: 0.0f);
|
||||
item.body = null;
|
||||
originalBody.Enabled = false;
|
||||
originalBody.SetTransformIgnoreContacts(originalBody.SimPosition, rotation: 0.0f);
|
||||
if (item.body != null)
|
||||
{
|
||||
item.body.Dir = 1;
|
||||
item.body = null;
|
||||
}
|
||||
item.GetComponents<LightComponent>().ForEach(static light => light.SetLightSourceTransform());
|
||||
|
||||
//outside hulls/subs -> we need to check if the item is being attached on a structure outside the sub
|
||||
if (item.CurrentHull == null && item.Submarine == null)
|
||||
@@ -689,7 +761,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
//set to submarine-relative position
|
||||
item.SetTransform(ConvertUnits.ToSimUnits(item.WorldPosition - attachTarget.Submarine.Position), 0.0f, false);
|
||||
body.SetTransformIgnoreContacts(item.SimPosition, 0.0f);
|
||||
originalBody.SetTransformIgnoreContacts(item.SimPosition, 0.0f);
|
||||
}
|
||||
item.Submarine = attachTarget.Submarine;
|
||||
}
|
||||
@@ -833,6 +905,9 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
item.Drop(character);
|
||||
item.SetTransform(ConvertUnits.ToSimUnits(GetAttachPosition(character)), 0.0f, findNewHull: false);
|
||||
//don't find the new hull in SetTransform, because that'd also potentially change the submarine (teleport the item outside if it's attached outside)
|
||||
//instead just find the hull, so the item is considered to be in the right hull
|
||||
item.CurrentHull = Hull.FindHull(item.WorldPosition, item.CurrentHull);
|
||||
//the light source won't get properly updated if lighting is disabled (even though the light sprite is still drawn when lighting is disabled)
|
||||
//so let's ensure the light source is up-to-date
|
||||
RefreshLightSources(item);
|
||||
@@ -869,34 +944,60 @@ namespace Barotrauma.Items.Components
|
||||
Vector2 mouseDiff = user.CursorWorldPosition - user.WorldPosition;
|
||||
mouseDiff = mouseDiff.ClampLength(MaxAttachDistance);
|
||||
|
||||
Vector2 submarinePos = useWorldCoordinates && user.Submarine != null ? user.Submarine.Position : Vector2.Zero;
|
||||
Vector2 userPos = useWorldCoordinates ? user.WorldPosition : user.Position;
|
||||
Vector2 attachPos = userPos + mouseDiff;
|
||||
|
||||
Vector2 halfSize = new Vector2(item.Rect.Width, item.Rect.Height) / 2;
|
||||
|
||||
//offset the position by half the size of the grid to get the item to adhere to the grid in the same way as in the sub editor
|
||||
//in the sub editor, we align the top-left corner of the item with the grid
|
||||
//but here the origin of the item is placed at the attach position, so we need to offset it
|
||||
Vector2 offset = new Vector2(
|
||||
-(item.Rect.Width / 2) % Submarine.GridSize.X,
|
||||
(item.Rect.Height / 2) % Submarine.GridSize.Y);
|
||||
-halfSize.X % Submarine.GridSize.X,
|
||||
halfSize.Y % Submarine.GridSize.Y);
|
||||
|
||||
if (user.Submarine != null)
|
||||
{
|
||||
//we must add some "padding" to the raycast to ensure it reaches all the way to a wall
|
||||
//otherwise the cursor might be outside a wall, but the grid cell it's in might be partially inside
|
||||
Vector2 padding = Submarine.GridSize * new Vector2(Math.Sign(mouseDiff.X), Math.Sign(mouseDiff.Y));
|
||||
Vector2 padding = halfSize * new Vector2(Math.Sign(mouseDiff.X), Math.Sign(mouseDiff.Y));
|
||||
|
||||
if (Submarine.PickBody(
|
||||
ConvertUnits.ToSimUnits(user.Position),
|
||||
ConvertUnits.ToSimUnits(user.Position + mouseDiff + padding), collisionCategory: Physics.CollisionWall) != null)
|
||||
ConvertUnits.ToSimUnits(user.Position + mouseDiff + padding), collisionCategory: Physics.CollisionWall,
|
||||
/*don't ignore sensors so the raycast can hit open doors or broken walls*/
|
||||
ignoreSensors: AllowAttachInsideDoors, customPredicate: (Fixture fixture) =>
|
||||
{
|
||||
if (fixture.UserData is Door) { return false; }
|
||||
return true;
|
||||
}) != null)
|
||||
{
|
||||
attachPos = userPos + mouseDiff * Submarine.LastPickedFraction + offset;
|
||||
|
||||
Vector2 pickedPos = userPos + mouseDiff * Submarine.LastPickedFraction + offset - submarinePos;
|
||||
//round down if we're placing on the right side and vice versa: ensures we don't round the position inside a wall
|
||||
return
|
||||
attachPos =
|
||||
new Vector2(
|
||||
(mouseDiff.X > 0 ? MathF.Floor(attachPos.X / Submarine.GridSize.X) : MathF.Ceiling(attachPos.X / Submarine.GridSize.X)) * Submarine.GridSize.X,
|
||||
(mouseDiff.Y > 0 ? MathF.Floor(attachPos.Y / Submarine.GridSize.Y) : MathF.Ceiling(attachPos.Y / Submarine.GridSize.Y)) * Submarine.GridSize.Y)
|
||||
- offset;
|
||||
RoundToGrid(pickedPos.X, Submarine.GridSize.X, roundingDir: -Math.Sign(mouseDiff.X)),
|
||||
RoundToGrid(pickedPos.Y, Submarine.GridSize.Y, roundingDir: -Math.Sign(mouseDiff.Y)))
|
||||
- offset + submarinePos;
|
||||
}
|
||||
|
||||
if (AttachesToFloor)
|
||||
{
|
||||
//if attaching to floor, do a raycast down and move the attach pos where it hits
|
||||
float size = item.Rect.Height / 2.0f;
|
||||
Vector2 rayStart = attachPos - submarinePos;
|
||||
Vector2 rayEnd = rayStart - Vector2.UnitY * MaxAttachDistance * 2;
|
||||
if (Submarine.PickBody(
|
||||
ConvertUnits.ToSimUnits(rayStart),
|
||||
ConvertUnits.ToSimUnits(rayEnd), collisionCategory: Physics.CollisionWall | Physics.CollisionPlatform) != null)
|
||||
{
|
||||
attachPos = ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition) + Vector2.UnitY * size + submarinePos;
|
||||
}
|
||||
else
|
||||
{
|
||||
return Vector2.Zero;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (Level.Loaded != null)
|
||||
@@ -919,9 +1020,30 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
return new Vector2(
|
||||
MathUtils.RoundTowardsClosest(attachPos.X + offset.X, Submarine.GridSize.X),
|
||||
MathUtils.RoundTowardsClosest(attachPos.Y + offset.Y, Submarine.GridSize.Y)) - offset;
|
||||
//subtract the submarine position so we're doing the rounding in the sub's
|
||||
//internal/local coordinate space regardless if we're using world coordinates
|
||||
//(otherwise the rounding would behave differently depending on the value of useWorldCoordinates)
|
||||
Vector2 offsetAttachPos = attachPos + offset - submarinePos;
|
||||
return
|
||||
new Vector2(
|
||||
RoundToGrid(offsetAttachPos.X, Submarine.GridSize.X),
|
||||
//don't round the vertical position if we're attaching to floor - we want the item to align with the floor, not the grid
|
||||
AttachesToFloor ? offsetAttachPos.Y : RoundToGrid(offsetAttachPos.Y, Submarine.GridSize.Y))
|
||||
- offset + submarinePos;
|
||||
|
||||
///<param name="roundingDir">If < 0, the method rounds down. If > 0, rounds up. If 0, rounds to the closest integer.</param>
|
||||
static float RoundToGrid(float position, float gridSize, int roundingDir = 0)
|
||||
{
|
||||
if (roundingDir < 0)
|
||||
{
|
||||
return MathF.Floor(position / gridSize) * gridSize;
|
||||
}
|
||||
else if (roundingDir > 0)
|
||||
{
|
||||
return MathF.Ceiling(position / gridSize) * gridSize;
|
||||
}
|
||||
return MathUtils.RoundTowardsClosest(position, gridSize);
|
||||
}
|
||||
}
|
||||
|
||||
private Voronoi2.VoronoiCell GetAttachTargetCell(float maxDist)
|
||||
@@ -977,7 +1099,7 @@ namespace Barotrauma.Items.Components
|
||||
return;
|
||||
}
|
||||
|
||||
if (picker == Character.Controlled && picker.IsKeyDown(InputType.Aim) && CanBeAttached(picker))
|
||||
if (picker == Character.Controlled && picker.IsKeyDown(InputType.Aim) && attachable && Reattachable)
|
||||
{
|
||||
Drawable = true;
|
||||
}
|
||||
@@ -1115,11 +1237,11 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
else
|
||||
{
|
||||
if (body != null)
|
||||
if (originalBody != null)
|
||||
{
|
||||
body.SetTransformIgnoreContacts(item.SimPosition, item.Rotation);
|
||||
item.body = body;
|
||||
body.Enabled = item.ParentInventory == null;
|
||||
originalBody.SetTransformIgnoreContacts(item.SimPosition, item.Rotation);
|
||||
item.body = originalBody;
|
||||
originalBody.Enabled = item.ParentInventory == null;
|
||||
}
|
||||
DeattachFromWall();
|
||||
}
|
||||
@@ -1134,7 +1256,7 @@ namespace Barotrauma.Items.Components
|
||||
Pusher.Remove();
|
||||
Pusher = null;
|
||||
}
|
||||
body = null;
|
||||
originalBody = null;
|
||||
}
|
||||
|
||||
public override XElement Save(XElement parentElement)
|
||||
|
||||
@@ -435,55 +435,65 @@ namespace Barotrauma.Items.Components
|
||||
Structure targetStructure = target.UserData as Structure ?? targetFixture.UserData as Structure;
|
||||
Item targetItem = target.UserData is Holdable h ? h.Item : target.UserData as Item ?? targetFixture.UserData as Item;
|
||||
Entity targetEntity = targetCharacter ?? targetStructure ?? targetItem ?? target.UserData as Entity;
|
||||
|
||||
if (Attack != null)
|
||||
{
|
||||
Attack.SetUser(user);
|
||||
Attack.DamageMultiplier = damageMultiplier;
|
||||
if (targetLimb != null)
|
||||
bool applyAttack = true;
|
||||
if (Attack.Conditionals.Any(c => !c.TargetSelf && !c.Matches(targetEntity as ISerializableEntity)) ||
|
||||
Attack.Conditionals.Any(c => c.TargetSelf && !c.Matches(user)))
|
||||
{
|
||||
if (targetLimb.character.Removed) { return; }
|
||||
targetLimb.character.LastDamageSource = item;
|
||||
Attack.DoDamageToLimb(user, targetLimb, item.WorldPosition, 1.0f);
|
||||
applyAttack = false;
|
||||
}
|
||||
else if (targetCharacter != null)
|
||||
if (applyAttack)
|
||||
{
|
||||
if (targetCharacter.Removed) { return; }
|
||||
targetCharacter.LastDamageSource = item;
|
||||
Attack.DoDamage(user, targetCharacter, item.WorldPosition, 1.0f);
|
||||
}
|
||||
else if (targetStructure != null)
|
||||
{
|
||||
if (targetStructure.Removed) { return; }
|
||||
Attack.DoDamage(user, targetStructure, item.WorldPosition, 1.0f);
|
||||
}
|
||||
else if (targetItem != null && targetItem.Prefab.DamagedByMeleeWeapons && targetItem.Condition > 0)
|
||||
{
|
||||
if (targetItem.Removed) { return; }
|
||||
var attackResult = Attack.DoDamage(user, targetItem, item.WorldPosition, 1.0f);
|
||||
#if CLIENT
|
||||
if (attackResult.Damage > 0.0f && targetItem.Prefab.ShowHealthBar && Character.Controlled != null &&
|
||||
(user == Character.Controlled || Character.Controlled.CanSeeTarget(item)))
|
||||
Attack.DamageMultiplier = damageMultiplier;
|
||||
if (targetLimb != null)
|
||||
{
|
||||
Character.Controlled.UpdateHUDProgressBar(targetItem,
|
||||
targetItem.WorldPosition,
|
||||
targetItem.Condition / targetItem.MaxCondition,
|
||||
emptyColor: GUIStyle.HealthBarColorLow,
|
||||
fullColor: GUIStyle.HealthBarColorHigh,
|
||||
textTag: targetItem.Prefab.ShowNameInHealthBar ? targetItem.Name : string.Empty);
|
||||
if (targetLimb.character.Removed) { return; }
|
||||
targetLimb.character.LastDamageSource = item;
|
||||
Attack.DoDamageToLimb(user, targetLimb, item.WorldPosition, 1.0f);
|
||||
}
|
||||
else if (targetCharacter != null)
|
||||
{
|
||||
if (targetCharacter.Removed) { return; }
|
||||
targetCharacter.LastDamageSource = item;
|
||||
Attack.DoDamage(user, targetCharacter, item.WorldPosition, 1.0f);
|
||||
}
|
||||
else if (targetStructure != null)
|
||||
{
|
||||
if (targetStructure.Removed) { return; }
|
||||
Attack.DoDamage(user, targetStructure, item.WorldPosition, 1.0f);
|
||||
}
|
||||
else if (targetItem != null && targetItem.Prefab.DamagedByMeleeWeapons && targetItem.Condition > 0)
|
||||
{
|
||||
if (targetItem.Removed) { return; }
|
||||
var attackResult = Attack.DoDamage(user, targetItem, item.WorldPosition, 1.0f);
|
||||
#if CLIENT
|
||||
if (attackResult.Damage > 0.0f && targetItem.Prefab.ShowHealthBar && Character.Controlled != null &&
|
||||
(user == Character.Controlled || Character.Controlled.CanSeeTarget(item)))
|
||||
{
|
||||
Character.Controlled.UpdateHUDProgressBar(targetItem,
|
||||
targetItem.WorldPosition,
|
||||
targetItem.Condition / targetItem.MaxCondition,
|
||||
emptyColor: GUIStyle.HealthBarColorLow,
|
||||
fullColor: GUIStyle.HealthBarColorHigh,
|
||||
textTag: targetItem.Prefab.ShowNameInHealthBar ? targetItem.Name : string.Empty);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
else if (target.UserData is Holdable { CanPush: true } holdable)
|
||||
{
|
||||
if (holdable.Item.Removed) { return; }
|
||||
RestoreCollision();
|
||||
hitting = false;
|
||||
User = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (target.UserData is Holdable { CanPush: true } holdable)
|
||||
{
|
||||
if (holdable.Item.Removed) { return; }
|
||||
RestoreCollision();
|
||||
hitting = false;
|
||||
User = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
|
||||
|
||||
@@ -522,7 +522,7 @@ namespace Barotrauma.Items.Components
|
||||
#if CLIENT
|
||||
float barOffset = 10f * GUI.Scale;
|
||||
Vector2 offset = planter.PlantSlots.ContainsKey(i) ? planter.PlantSlots[i].Offset : Vector2.Zero;
|
||||
user?.UpdateHUDProgressBar(planter, planter.Item.DrawPosition + new Vector2(barOffset, 0) + offset, seed.Health / seed.MaxHealth, GUIStyle.Blue, GUIStyle.Blue, "progressbar.watering");
|
||||
user?.UpdateHUDProgressBar(planter, planter.Item.DrawPosition + new Vector2(barOffset, 0) + offset, seed.Health / seed.MaxWater, GUIStyle.Blue, GUIStyle.Blue, "progressbar.watering");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ namespace Barotrauma.Items.Components
|
||||
public override bool Use(float deltaTime, Character character = null)
|
||||
{
|
||||
//actual throwing logic is handled in Update
|
||||
return characterUsable || character == null;
|
||||
return (characterUsable && !UsageDisabledByRangedWeapon(character)) || character == null;
|
||||
}
|
||||
|
||||
public override bool SecondaryUse(float deltaTime, Character character = null)
|
||||
@@ -111,24 +111,28 @@ namespace Barotrauma.Items.Components
|
||||
return;
|
||||
}
|
||||
|
||||
if (throwState != ThrowState.Throwing)
|
||||
bool aim = false;
|
||||
if (!UsageDisabledByRangedWeapon(picker))
|
||||
{
|
||||
if (picker.IsKeyDown(InputType.Aim))
|
||||
if (throwState != ThrowState.Throwing)
|
||||
{
|
||||
if (picker.IsKeyDown(InputType.Shoot)) { throwState = ThrowState.Initiated; }
|
||||
if (picker.IsKeyDown(InputType.Aim))
|
||||
{
|
||||
if (picker.IsKeyDown(InputType.Shoot)) { throwState = ThrowState.Initiated; }
|
||||
}
|
||||
else if (throwState != ThrowState.Initiated)
|
||||
{
|
||||
throwAngle = ThrowAngleStart;
|
||||
}
|
||||
}
|
||||
else if (throwState != ThrowState.Initiated)
|
||||
{
|
||||
throwAngle = ThrowAngleStart;
|
||||
}
|
||||
}
|
||||
|
||||
bool aim = picker.IsKeyDown(InputType.Aim) && picker.CanAim;
|
||||
aim = picker.IsKeyDown(InputType.Aim) && picker.CanAim;
|
||||
}
|
||||
if (picker.IsDead || !picker.AllowInput)
|
||||
{
|
||||
throwState = ThrowState.None;
|
||||
aim = false;
|
||||
}
|
||||
}
|
||||
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime, picker);
|
||||
//return if the status effect got rid of the picker somehow
|
||||
|
||||
@@ -998,6 +998,13 @@ namespace Barotrauma.Items.Components
|
||||
/// </summary>
|
||||
public virtual void OnItemLoaded() { }
|
||||
|
||||
/// <summary>
|
||||
/// Implement in a base class if the instances of the component contain some sort of data that isn't serialized using the normal serializable properties
|
||||
/// (i.e. some data that changes per-item and isn't loaded from the prefab, but that isn't a property marked with [Serialize] either),
|
||||
/// but that must be copied when cloning the item.
|
||||
/// </summary>
|
||||
public virtual void Clone(ItemComponent original) { }
|
||||
|
||||
public virtual void OnScaleChanged() { }
|
||||
|
||||
/// <summary>
|
||||
@@ -1093,7 +1100,6 @@ namespace Barotrauma.Items.Components
|
||||
componentElement.Add(newElement);
|
||||
}
|
||||
|
||||
|
||||
SerializableProperty.SerializeProperties(this, componentElement);
|
||||
|
||||
parentElement.Add(componentElement);
|
||||
|
||||
@@ -438,9 +438,14 @@ namespace Barotrauma.Items.Components
|
||||
ActiveContainedItem activeContainedItem = new(containedItem, effect, containableItem.ExcludeBroken, containableItem.ExcludeFullCondition, containableItem.BlameEquipperForDeath);
|
||||
activeContainedItems.Add(activeContainedItem);
|
||||
|
||||
if (!ShouldApplyEffects(activeContainedItem)) { continue; }
|
||||
if (!ShouldApplyEffects(activeContainedItem) || item.Submarine is { Loading: true} || initializingLoadedItems ||
|
||||
containedItem.OnInsertedEffectsApplied)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
activeContainedItem.StatusEffect.Apply(ActionType.OnInserted, deltaTime: 1, item, targets);
|
||||
}
|
||||
containedItem.OnInsertedEffectsApplied = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -511,6 +516,8 @@ namespace Barotrauma.Items.Components
|
||||
activeContainedItem.StatusEffect.Apply(ActionType.OnRemoved, deltaTime: 1, item, targets);
|
||||
}
|
||||
|
||||
containedItem.OnInsertedEffectsApplied = false;
|
||||
|
||||
activeContainedItems.RemoveAll(i => i.Item == containedItem);
|
||||
containedItems.RemoveAll(i => i.Item == containedItem);
|
||||
item.SetContainedItemPositions();
|
||||
@@ -680,7 +687,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
foreach (ActiveContainedItem activeContainedItem in activeContainedItems)
|
||||
{
|
||||
if (!ShouldApplyEffects(activeContainedItem)) continue;
|
||||
if (!ShouldApplyEffects(activeContainedItem)) { continue; }
|
||||
|
||||
StatusEffect effect = activeContainedItem.StatusEffect;
|
||||
effect.Apply(ActionType.OnActive, deltaTime, item, targets);
|
||||
@@ -993,6 +1000,8 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
else
|
||||
{
|
||||
//flip if flipped on one axis but not both (flipping on both axes is basically "double negative" and makes the rotation normal again)
|
||||
if (flippedX ^ flippedY) { rotation = -rotation; }
|
||||
rotation += -item.RotationRad;
|
||||
}
|
||||
contained.Item.body.FarseerBody.SetTransformIgnoreContacts(ref simPos, rotation);
|
||||
@@ -1050,8 +1059,17 @@ namespace Barotrauma.Items.Components
|
||||
transformedItemIntervalHorizontal = new Vector2(transformedItemInterval.X, 0.0f);
|
||||
transformedItemIntervalVertical = new Vector2(0.0f, transformedItemInterval.Y);
|
||||
|
||||
flippedX = item.RootContainer?.FlippedX ?? (item.FlippedX && item.Prefab.CanSpriteFlipX);
|
||||
flippedY = item.RootContainer?.FlippedY ?? (item.FlippedY && item.Prefab.CanSpriteFlipY);
|
||||
if (item.RootContainer != null)
|
||||
{
|
||||
flippedX = item.RootContainer.FlippedX && item.RootContainer.Prefab.CanSpriteFlipX;
|
||||
flippedY = item.RootContainer.FlippedY && item.RootContainer.Prefab.CanSpriteFlipY;
|
||||
}
|
||||
else
|
||||
{
|
||||
flippedX = item.FlippedX && item.Prefab.CanSpriteFlipX;
|
||||
flippedY = item.FlippedY && item.Prefab.CanSpriteFlipY;
|
||||
}
|
||||
|
||||
var rootBody = item.RootContainer?.body ?? item.body;
|
||||
bool bodyFlipped = rootBody is { Dir: -1 };
|
||||
|
||||
@@ -1123,10 +1141,13 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
private bool initializingLoadedItems;
|
||||
|
||||
public override void OnMapLoaded()
|
||||
{
|
||||
if (itemIds != null)
|
||||
{
|
||||
initializingLoadedItems = true;
|
||||
for (ushort i = 0; i < itemIds.Length; i++)
|
||||
{
|
||||
if (i >= Inventory.Capacity)
|
||||
@@ -1138,10 +1159,11 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
foreach (ushort id in itemIds[i])
|
||||
{
|
||||
if (!(Entity.FindEntityByID(id) is Item item)) { continue; }
|
||||
if (Entity.FindEntityByID(id) is not Item item) { continue; }
|
||||
Inventory.TryPutItem(item, i, false, false, null, createNetworkEvent: false, ignoreCondition: true);
|
||||
}
|
||||
}
|
||||
initializingLoadedItems = false;
|
||||
itemIds = null;
|
||||
}
|
||||
|
||||
|
||||
@@ -349,7 +349,7 @@ namespace Barotrauma.Items.Components
|
||||
// Don't move lower body limbs if there's another selected secondary item that should control them
|
||||
if (limb.IsLowerBody && user.HasSelectedAnotherSecondaryItem(Item)) { continue; }
|
||||
// Don't move hands if there's a selected primary item that should control them
|
||||
if (!limb.IsLowerBody && Item == user.SelectedSecondaryItem && user.SelectedItem != null) { continue; }
|
||||
if (limb.IsArm && Item == user.SelectedSecondaryItem && user.SelectedItem != null) { continue; }
|
||||
if (lb.AllowUsingLimb)
|
||||
{
|
||||
switch (lb.LimbType)
|
||||
|
||||
@@ -564,7 +564,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
fireTimer += MathHelper.Lerp(deltaTime * 2.0f, deltaTime, item.Condition / item.MaxCondition);
|
||||
#if SERVER
|
||||
if (fireTimer > Math.Min(5.0f, FireDelay / 2) && blameOnBroken?.Character?.SelectedItem == item)
|
||||
if (fireTimer > Math.Min(5.0f, FireDelay / 2) && blameOnBroken?.Character != null)
|
||||
{
|
||||
GameMain.Server.KarmaManager.OnReactorOverHeating(item, blameOnBroken.Character, deltaTime);
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
private double lastReceivedSteeringSignalTime;
|
||||
|
||||
[Serialize(defaultValue: false, isSaveable: IsPropertySaveable.Yes, AlwaysUseInstanceValues = true)]
|
||||
[Serialize(defaultValue: false, isSaveable: IsPropertySaveable.Yes, description: "Is autopilot currently on or not?", AlwaysUseInstanceValues = true)]
|
||||
public bool AutoPilot
|
||||
{
|
||||
get { return autoPilot; }
|
||||
|
||||
@@ -110,17 +110,15 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
base.OnItemLoaded();
|
||||
IsActive = true;
|
||||
#if CLIENT
|
||||
var lights = item.GetComponents<LightComponent>();
|
||||
if (lights.Any())
|
||||
{
|
||||
lightComponents = lights.ToList();
|
||||
foreach (var light in lightComponents)
|
||||
foreach (var light in item.GetComponents<LightComponent>())
|
||||
{
|
||||
light.Light.Enabled = false;
|
||||
light.IsOn = false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
container = item.GetComponent<ItemContainer>();
|
||||
GrowableSeeds = new Growable[container.Capacity];
|
||||
}
|
||||
@@ -233,7 +231,6 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
base.Update(deltaTime, cam);
|
||||
|
||||
#if CLIENT
|
||||
if (lightComponents != null && lightComponents.Count > 0)
|
||||
{
|
||||
bool hasSeed = false;
|
||||
@@ -243,10 +240,9 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
foreach (var light in lightComponents)
|
||||
{
|
||||
light.Light.Enabled = hasSeed;
|
||||
light.IsOn = hasSeed;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (container?.Inventory == null) { return; }
|
||||
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
#nullable enable
|
||||
using Barotrauma.Networking;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
internal partial class PowerDistributor : PowerTransfer
|
||||
{
|
||||
private const int MaxNameLength = 32;
|
||||
|
||||
private const int SupplyRatioSteps = 20;
|
||||
private const float SupplyRatioStep = 1f / SupplyRatioSteps;
|
||||
|
||||
private partial class PowerGroup
|
||||
{
|
||||
private readonly PowerDistributor distributor;
|
||||
public readonly Connection PowerOut;
|
||||
public readonly Connection? RatioInput, RatioOutput;
|
||||
|
||||
private string name;
|
||||
public string Name
|
||||
{
|
||||
get => name;
|
||||
set
|
||||
{
|
||||
name = value;
|
||||
DisplayName = TextManager.Get(name).Fallback(name);
|
||||
#if CLIENT
|
||||
UpdateNameBox();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
public LocalizedString? DisplayName { get; private set; }
|
||||
|
||||
private float supplyRatio = 1f;
|
||||
public float SupplyRatio
|
||||
{
|
||||
get => supplyRatio;
|
||||
set
|
||||
{
|
||||
if (!MathUtils.IsValid(value)) { return; }
|
||||
supplyRatio = MathUtils.RoundTowardsClosest(MathHelper.Clamp(value, 0f, 1f), SupplyRatioStep);
|
||||
#if CLIENT
|
||||
UpdateSlider();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
public float DisplayRatio
|
||||
{
|
||||
get => MathUtils.RoundToInt(supplyRatio * 100);
|
||||
set => SupplyRatio = value / 100f;
|
||||
}
|
||||
|
||||
public float Load;
|
||||
public float ModifiedLoad => Load * SupplyRatio;
|
||||
|
||||
public PowerGroup(PowerDistributor distributor, Connection power, XElement? element = null, Connection? ratioInput = null, Connection? ratioOutput = null)
|
||||
{
|
||||
this.distributor = distributor;
|
||||
PowerOut = power;
|
||||
|
||||
RatioInput = ratioInput;
|
||||
RatioOutput = ratioOutput;
|
||||
|
||||
distributor.powerGroups.Add(this);
|
||||
|
||||
name = TextManager.GetWithVariable("groupx", "[num]", distributor.powerGroups.Count.ToString()).Value;
|
||||
SupplyRatio = 1f;
|
||||
|
||||
if (element != null)
|
||||
{
|
||||
name = element.GetAttributeString("name", name);
|
||||
SupplyRatio = element.GetAttributeFloat("ratio", SupplyRatio);
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
CreateGUI();
|
||||
#endif
|
||||
}
|
||||
|
||||
#region Signals
|
||||
public void ReceiveRatioSignal(Signal signal)
|
||||
{
|
||||
if (!float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out float receivedSignal) || !MathUtils.IsValid(receivedSignal)) { return; }
|
||||
DisplayRatio = receivedSignal;
|
||||
}
|
||||
|
||||
public void SendRatioSignal() => distributor.item.SendSignal(new Signal(DisplayRatio.ToString()), RatioOutput);
|
||||
#endregion
|
||||
}
|
||||
|
||||
private readonly List<PowerGroup> powerGroups = new List<PowerGroup>();
|
||||
|
||||
protected override PowerPriority Priority => PowerPriority.Relay;
|
||||
|
||||
public PowerDistributor(Item item, ContentXElement element) : base(item, element) { }
|
||||
|
||||
public override void OnItemLoaded()
|
||||
{
|
||||
base.OnItemLoaded();
|
||||
|
||||
IEnumerable<Connection> ratioInputs = Item.Connections.Where(static conn => !conn.IsOutput && conn.Name.StartsWith("set_supply_ratio"));
|
||||
IEnumerable<Connection> ratioOutputs = Item.Connections.Where(static conn => conn.IsOutput && conn.Name.StartsWith("supply_ratio_out"));
|
||||
|
||||
for (int i = 0; i < powerOuts.Count; i++)
|
||||
{
|
||||
new PowerGroup(this, powerOuts[i], cachedGroupData.ElementAtOrDefault(i), ratioInputs.ElementAtOrDefault(i), ratioOutputs.ElementAtOrDefault(i));
|
||||
}
|
||||
|
||||
cachedGroupData.Clear();
|
||||
}
|
||||
|
||||
public override void Clone(ItemComponent original)
|
||||
{
|
||||
if (original is not PowerDistributor originalPowerDistributor) { return; }
|
||||
for (int i = 0; i < powerOuts.Count; i++)
|
||||
{
|
||||
powerGroups[i].SupplyRatio = originalPowerDistributor.powerGroups[i].SupplyRatio;
|
||||
powerGroups[i].Name = originalPowerDistributor.powerGroups[i].Name;
|
||||
}
|
||||
}
|
||||
|
||||
#region Signals
|
||||
protected override void SendSignals()
|
||||
{
|
||||
item.SendSignal(MathUtils.RoundToInt(powerIn.Grid?.Power ?? 0f).ToString(), "power_value_out");
|
||||
item.SendSignal(MathUtils.RoundToInt(GetCurrentPowerConsumption(powerIn)).ToString(), "load_value_out");
|
||||
powerGroups.ForEach(static group => group.SendRatioSignal());
|
||||
}
|
||||
|
||||
public override void ReceiveSignal(Signal signal, Connection connection)
|
||||
{
|
||||
if (item.Condition <= 0f || connection.IsPower) { return; }
|
||||
if (connection.IsOutput) { return; }
|
||||
|
||||
powerGroups.FirstOrDefault(group => group.RatioInput == connection)?.ReceiveRatioSignal(signal);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Power Calculation
|
||||
private bool IsShortCircuited(Connection conn) => powerIn.Grid == conn.Grid;
|
||||
|
||||
public override float GetCurrentPowerConsumption(Connection? connection = null)
|
||||
{
|
||||
if (connection != powerIn) { return -1f; }
|
||||
if (isBroken) { return 0f; }
|
||||
return powerGroups.Sum(group => IsShortCircuited(group.PowerOut) ? 0f : group.ModifiedLoad) + ExtraLoad;
|
||||
}
|
||||
|
||||
private float CalculatePowerOut(PowerGroup group)
|
||||
{
|
||||
if (isBroken || powerIn.Grid == null || IsShortCircuited(group.PowerOut)) { return 0f; }
|
||||
return Math.Max(group.ModifiedLoad * Voltage, 0f);
|
||||
}
|
||||
|
||||
public override float GetConnectionPowerOut(Connection connection, float power, PowerRange minMaxPower, float load)
|
||||
{
|
||||
if (connection == powerIn) { return 0f; }
|
||||
PowerGroup group = powerGroups.First(group => group.PowerOut == connection);
|
||||
group.Load = load;
|
||||
return CalculatePowerOut(group);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Serialization
|
||||
private readonly List<XElement> cachedGroupData = new List<XElement>();
|
||||
|
||||
public override XElement Save(XElement parentElement)
|
||||
{
|
||||
XElement componentElement = base.Save(parentElement);
|
||||
foreach (PowerGroup powerGroup in powerGroups)
|
||||
{
|
||||
componentElement.Add(new XElement("PowerGroup",
|
||||
new XAttribute("name", powerGroup.Name),
|
||||
new XAttribute("ratio", powerGroup.SupplyRatio)));
|
||||
}
|
||||
return componentElement;
|
||||
}
|
||||
|
||||
public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap)
|
||||
{
|
||||
base.Load(componentElement, usePrefabValues, idRemap, isItemSwap);
|
||||
if (usePrefabValues) { return; }
|
||||
|
||||
foreach (XElement element in componentElement.Elements())
|
||||
{
|
||||
cachedGroupData.Add(element);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Networking
|
||||
private enum EventType { NameChange, RatioChange }
|
||||
|
||||
private void SharedEventWrite(IWriteMessage msg, NetEntityEvent.IData? extraData = null)
|
||||
{
|
||||
EventData data = ExtractEventData<EventData>(extraData);
|
||||
msg.WriteRangedInteger((int)data.EventType, 0, 1);
|
||||
msg.WriteRangedInteger(powerGroups.IndexOf(data.PowerGroup), 0, powerGroups.Count - 1);
|
||||
switch (data.EventType)
|
||||
{
|
||||
case EventType.NameChange:
|
||||
msg.WriteString(data.PowerGroup.Name);
|
||||
break;
|
||||
case EventType.RatioChange:
|
||||
msg.WriteRangedInteger(MathUtils.RoundToInt(data.PowerGroup.SupplyRatio / SupplyRatioStep), 0, SupplyRatioSteps);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void SharedEventRead(IReadMessage msg, out EventType eventType, out PowerGroup powerGroup, out string newName, out float newRatio)
|
||||
{
|
||||
eventType = (EventType)msg.ReadRangedInteger(0, 1);
|
||||
powerGroup = powerGroups[msg.ReadRangedInteger(0, powerGroups.Count - 1)];
|
||||
|
||||
newName = eventType == EventType.NameChange ? string.Concat(msg.ReadString().Take(MaxNameLength)) : powerGroup.Name;
|
||||
newRatio = eventType == EventType.RatioChange ? msg.ReadRangedInteger(0, SupplyRatioSteps) * SupplyRatioStep : powerGroup.SupplyRatio;
|
||||
}
|
||||
|
||||
private readonly record struct EventData(PowerGroup PowerGroup, EventType EventType) : IEventData;
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -177,18 +177,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
RefreshConnections();
|
||||
|
||||
if (Timing.TotalTime > extraLoadSetTime + 1.0)
|
||||
{
|
||||
//Decay the extra load to 0 from either positive or negative
|
||||
if (extraLoad > 0)
|
||||
{
|
||||
extraLoad = Math.Max(extraLoad - 1000.0f * deltaTime, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
extraLoad = Math.Min(extraLoad + 1000.0f * deltaTime, 0);
|
||||
}
|
||||
}
|
||||
UpdateExtraLoad(deltaTime);
|
||||
|
||||
if (!CanTransfer) { return; }
|
||||
|
||||
@@ -200,6 +189,28 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime);
|
||||
|
||||
SendSignals();
|
||||
|
||||
UpdateOvervoltage(deltaTime);
|
||||
}
|
||||
|
||||
protected virtual void UpdateExtraLoad(float deltaTime)
|
||||
{
|
||||
if (Timing.TotalTime <= extraLoadSetTime + 1.0) { return; }
|
||||
|
||||
//Decay the extra load to 0 from either positive or negative
|
||||
if (extraLoad > 0)
|
||||
{
|
||||
extraLoad = Math.Max(extraLoad - 1000.0f * deltaTime, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
extraLoad = Math.Min(extraLoad + 1000.0f * deltaTime, 0);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void SendSignals()
|
||||
{
|
||||
float powerReadingOut = 0;
|
||||
float loadReadingOut = ExtraLoad;
|
||||
if (powerLoad < 0)
|
||||
@@ -226,7 +237,10 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
item.SendSignal(powerSignal, "power_value_out");
|
||||
item.SendSignal(loadSignal, "load_value_out");
|
||||
}
|
||||
|
||||
protected virtual void UpdateOvervoltage(float deltaTime)
|
||||
{
|
||||
//if the item can't be fixed, don't allow it to break
|
||||
if (!item.Repairables.Any() || !CanBeOverloaded) { return; }
|
||||
|
||||
@@ -234,46 +248,45 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
Overload = Voltage > maxOverVoltage && GameMain.GameSession is not { RoundDuration: < 5 };
|
||||
|
||||
if (Overload && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer))
|
||||
if (!Overload || GameMain.NetworkMember is { IsClient: true }) { return; }
|
||||
|
||||
if (overloadCooldownTimer > 0.0f)
|
||||
{
|
||||
if (overloadCooldownTimer > 0.0f)
|
||||
{
|
||||
overloadCooldownTimer -= deltaTime;
|
||||
return;
|
||||
}
|
||||
overloadCooldownTimer -= deltaTime;
|
||||
return;
|
||||
}
|
||||
|
||||
//damage the item if voltage is too high (except if running as a client)
|
||||
float prevCondition = item.Condition;
|
||||
//some randomness to prevent all junction boxes from breaking at the same time
|
||||
if (Rand.Range(0.0f, 1.0f) < 0.01f)
|
||||
{
|
||||
//damaged boxes are more sensitive to overvoltage (also preventing all boxes from breaking at the same time)
|
||||
float conditionFactor = MathHelper.Lerp(5.0f, 1.0f, item.Condition / item.MaxCondition);
|
||||
item.Condition -= deltaTime * Rand.Range(10.0f, 500.0f) * conditionFactor;
|
||||
}
|
||||
if (item.Condition <= 0.0f && prevCondition > 0.0f)
|
||||
{
|
||||
overloadCooldownTimer = OverloadCooldown;
|
||||
//damage the item if voltage is too high (except if running as a client)
|
||||
float prevCondition = item.Condition;
|
||||
//some randomness to prevent all junction boxes from breaking at the same time
|
||||
if (Rand.Range(0.0f, 1.0f) < 0.01f)
|
||||
{
|
||||
//damaged boxes are more sensitive to overvoltage (also preventing all boxes from breaking at the same time)
|
||||
float conditionFactor = MathHelper.Lerp(5.0f, 1.0f, item.Condition / item.MaxCondition);
|
||||
item.Condition -= deltaTime * Rand.Range(10.0f, 500.0f) * conditionFactor;
|
||||
}
|
||||
|
||||
if (item.Condition > 0.0f || prevCondition <= 0.0f) { return; }
|
||||
|
||||
overloadCooldownTimer = OverloadCooldown;
|
||||
#if CLIENT
|
||||
SoundPlayer.PlaySound("zap", item.WorldPosition, hullGuess: item.CurrentHull);
|
||||
Vector2 baseVel = Rand.Vector(300.0f);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var particle = GameMain.ParticleManager.CreateParticle("spark", item.WorldPosition,
|
||||
baseVel + Rand.Vector(100.0f), 0.0f, item.CurrentHull);
|
||||
if (particle != null) particle.Size *= Rand.Range(0.5f, 1.0f);
|
||||
}
|
||||
SoundPlayer.PlaySound("zap", item.WorldPosition, hullGuess: item.CurrentHull);
|
||||
Vector2 baseVel = Rand.Vector(300.0f);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var particle = GameMain.ParticleManager.CreateParticle("spark", item.WorldPosition,
|
||||
baseVel + Rand.Vector(100.0f), 0.0f, item.CurrentHull);
|
||||
if (particle != null) particle.Size *= Rand.Range(0.5f, 1.0f);
|
||||
}
|
||||
#endif
|
||||
float currentIntensity = GameMain.GameSession?.EventManager != null ?
|
||||
GameMain.GameSession.EventManager.CurrentIntensity : 0.5f;
|
||||
float currentIntensity = GameMain.GameSession?.EventManager != null ?
|
||||
GameMain.GameSession.EventManager.CurrentIntensity : 0.5f;
|
||||
|
||||
//higher probability for fires if the current intensity is low
|
||||
if (FireProbability > 0.0f &&
|
||||
Rand.Range(0.0f, 1.0f) < MathHelper.Lerp(FireProbability, FireProbability * 0.1f, currentIntensity))
|
||||
{
|
||||
new FireSource(item.WorldPosition);
|
||||
}
|
||||
}
|
||||
//higher probability for fires if the current intensity is low
|
||||
if (FireProbability > 0.0f &&
|
||||
Rand.Range(0.0f, 1.0f) < MathHelper.Lerp(FireProbability, FireProbability * 0.1f, currentIntensity))
|
||||
{
|
||||
new FireSource(item.WorldPosition);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -424,7 +437,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
if (this is not RelayComponent)
|
||||
if (this is not RelayComponent and not PowerDistributor)
|
||||
{
|
||||
if (PowerConnections.Any(p => !p.IsOutput) && PowerConnections.Any(p => p.IsOutput))
|
||||
{
|
||||
@@ -452,7 +465,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
//other junction boxes don't need to receive the signal in the pass-through signal connections
|
||||
//because we relay it straight to the connected items without going through the whole chain of junction boxes
|
||||
if (ic is PowerTransfer && ic is not RelayComponent) { continue; }
|
||||
if (ic is PowerTransfer and not RelayComponent and not PowerDistributor) { continue; }
|
||||
ic.ReceiveSignal(signal, recipient);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
#if CLIENT
|
||||
using Barotrauma.Sounds;
|
||||
#endif
|
||||
@@ -93,7 +94,22 @@ namespace Barotrauma.Items.Components
|
||||
/// </summary>
|
||||
protected float powerConsumption;
|
||||
|
||||
protected Connection powerIn, powerOut;
|
||||
protected Connection powerIn;
|
||||
protected List<Connection> powerOuts = new List<Connection>();
|
||||
/// <summary>
|
||||
/// Throws an error if there is more than one power out connection.<br/>
|
||||
/// Use <see cref="powerOuts"/> if a component should handle multiple outputs.
|
||||
/// </summary>
|
||||
protected Connection powerOut
|
||||
{
|
||||
get
|
||||
{
|
||||
if (powerOuts.Count > 1) { DebugConsole.ThrowErrorOnce($"{item.ID}.multiplePowerOut", $"Item {item.Name} ({item.Prefab.Identifier}) has multiple power outputs, but only supports one!"); }
|
||||
return powerOuts.FirstOrDefault();
|
||||
}
|
||||
}
|
||||
|
||||
protected bool powerInIsPowerOut => powerOuts.Contains(powerIn);
|
||||
|
||||
/// <summary>
|
||||
/// Maximum voltage factor when the device is being overvolted. I.e. how many times more effectively the device can function when it's being overvolted
|
||||
@@ -153,9 +169,10 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (powerIn?.Grid != null) { return powerIn.Grid.Voltage; }
|
||||
}
|
||||
else if (powerOut != null)
|
||||
else if (powerOuts.Any())
|
||||
{
|
||||
if (powerOut?.Grid != null) { return powerOut.Grid.Voltage; }
|
||||
IEnumerable<Connection> gridConnections = powerOuts.Where(static conn => conn.Grid != null);
|
||||
if (gridConnections.Any()) { return gridConnections.Average(static conn => conn.Grid.Voltage); }
|
||||
}
|
||||
|
||||
if (this is PowerTransfer && item.Condition <= 0.0f)
|
||||
@@ -245,18 +262,19 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
powerIn = c;
|
||||
}
|
||||
else if (c.Name == "power_out")
|
||||
{
|
||||
powerOut = c;
|
||||
// Connection takes the lowest priority
|
||||
if (Priority > powerOut.Priority)
|
||||
{
|
||||
powerOut.Priority = Priority;
|
||||
}
|
||||
}
|
||||
else if (c.Name == "power")
|
||||
{
|
||||
powerIn = powerOut = c;
|
||||
powerIn = c;
|
||||
powerOuts.Add(c);
|
||||
}
|
||||
else if (c.IsOutput)
|
||||
{
|
||||
powerOuts.Add(c);
|
||||
// Connection takes the lowest priority
|
||||
if (Priority > c.Priority)
|
||||
{
|
||||
c.Priority = Priority;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -271,11 +289,11 @@ namespace Barotrauma.Items.Components
|
||||
DebugConsole.NewMessage($"Item \"{item.Name}\" has a power output connection called power_in. If the item is supposed to receive power through the connection, change it to an input connection.", Color.Orange);
|
||||
#endif
|
||||
}
|
||||
powerOut = c;
|
||||
powerOuts.Add(c);
|
||||
// Connection takes the lowest priority
|
||||
if (Priority > powerOut.Priority)
|
||||
if (Priority > c.Priority)
|
||||
{
|
||||
powerOut.Priority = Priority;
|
||||
c.Priority = Priority;
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -335,9 +353,9 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
powered.powerIn.Grid = null;
|
||||
}
|
||||
if (powered.powerOut != null)
|
||||
foreach (Connection powerOut in powered.powerOuts)
|
||||
{
|
||||
powered.powerOut.Grid = null;
|
||||
powerOut.Grid = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,8 +363,10 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
foreach (Powered powered in poweredList)
|
||||
{
|
||||
if (powered.Item.Condition <= 0f) { continue; }
|
||||
|
||||
//Probe through all connections that don't have a gridID
|
||||
if (powered.powerIn != null && powered.powerIn.Grid == null && powered.powerIn != powered.powerOut && powered.Item.Condition > 0.0f)
|
||||
if (powered.powerIn != null && powered.powerIn.Grid == null && !powered.powerInIsPowerOut)
|
||||
{
|
||||
// Only create grids for networks with more than 1 device
|
||||
if (powered.powerIn.Recipients.Count > 0)
|
||||
@@ -356,13 +376,16 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
if (powered.powerOut != null && powered.powerOut.Grid == null && powered.Item.Condition > 0.0f)
|
||||
foreach (Connection powerOut in powered.powerOuts)
|
||||
{
|
||||
//Only create grids for networks with more than 1 device
|
||||
if (powered.powerOut.Recipients.Count > 0)
|
||||
if (powerOut != null && powerOut.Grid == null)
|
||||
{
|
||||
GridInfo grid = PropagateGrid(powered.powerOut);
|
||||
Grids[grid.ID] = grid;
|
||||
//Only create grids for networks with more than 1 device
|
||||
if (powerOut.Recipients.Count > 0)
|
||||
{
|
||||
GridInfo grid = PropagateGrid(powerOut);
|
||||
Grids[grid.ID] = grid;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -479,7 +502,7 @@ namespace Barotrauma.Items.Components
|
||||
powered.Voltage -= deltaTime;
|
||||
|
||||
//Handle the device if it's got a power connection
|
||||
if (powered.powerIn != null && powered.powerOut != powered.powerIn)
|
||||
if (powered.powerIn != null && !powered.powerInIsPowerOut)
|
||||
{
|
||||
//Get the new load for the connection
|
||||
float currLoad = powered.GetCurrentPowerConsumption(powered.powerIn);
|
||||
@@ -507,10 +530,10 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
|
||||
//Handle the device power depending on if its powerout
|
||||
if (powered.powerOut != null)
|
||||
foreach (Connection powerOut in powered.powerOuts)
|
||||
{
|
||||
//Get the connection's load
|
||||
float currLoad = powered.GetCurrentPowerConsumption(powered.powerOut);
|
||||
float currLoad = powered.GetCurrentPowerConsumption(powerOut);
|
||||
|
||||
//Update the device's output load to the correct variable
|
||||
if (powered is PowerTransfer pt)
|
||||
@@ -529,20 +552,20 @@ namespace Barotrauma.Items.Components
|
||||
if (currLoad >= 0)
|
||||
{
|
||||
//Add to the grid load if possible
|
||||
if (powered.powerOut.Grid != null)
|
||||
if (powerOut.Grid != null)
|
||||
{
|
||||
powered.powerOut.Grid.Load += currLoad;
|
||||
powerOut.Grid.Load += currLoad;
|
||||
}
|
||||
}
|
||||
else if (powered.powerOut.Grid != null)
|
||||
else if (powerOut.Grid != null)
|
||||
{
|
||||
//Add connection as a source to be processed
|
||||
powered.powerOut.Grid.AddSrc(powered.powerOut);
|
||||
powerOut.Grid.AddSrc(powerOut);
|
||||
}
|
||||
else
|
||||
{
|
||||
//Perform power calculations for the singular connection
|
||||
float loadOut = -powered.GetConnectionPowerOut(powered.powerOut, 0, powered.MinMaxPowerOut(powered.powerOut, 0), 0);
|
||||
float loadOut = -powered.GetConnectionPowerOut(powerOut, 0, powered.MinMaxPowerOut(powerOut, 0), 0);
|
||||
if (powered is PowerTransfer pt2)
|
||||
{
|
||||
pt2.PowerLoad = loadOut;
|
||||
@@ -557,7 +580,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
|
||||
//Indicate grid is resolved as it was the only device
|
||||
powered.GridResolved(powered.powerOut);
|
||||
powered.GridResolved(powerOut);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -625,7 +648,7 @@ namespace Barotrauma.Items.Components
|
||||
public virtual float GetCurrentPowerConsumption(Connection connection = null)
|
||||
{
|
||||
// If a handheld device there is no consumption
|
||||
if (powerIn == null && powerOut == null)
|
||||
if (powerIn == null && powerOuts.None())
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
@@ -664,7 +687,7 @@ namespace Barotrauma.Items.Components
|
||||
/// <returns>Power pushed to the grid</returns>
|
||||
public virtual float GetConnectionPowerOut(Connection conn, float power, PowerRange minMaxPower, float load)
|
||||
{
|
||||
return conn == powerOut ? MathHelper.Max(-CurrPowerConsumption, 0) : 0;
|
||||
return powerOuts.Contains(conn) ? MathHelper.Max(-CurrPowerConsumption, 0) : 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -233,6 +233,7 @@ namespace Barotrauma.Items.Components
|
||||
var cloneNode = InputOutputNodes[ioIndex];
|
||||
|
||||
cloneNode.Position = origNode.Position;
|
||||
cloneNode.ReplaceAllConnectionLabelOverrides(origNode.ConnectionLabelOverrides);
|
||||
}
|
||||
|
||||
if (!clonedContainedItems.Any()) { return; }
|
||||
|
||||
@@ -18,7 +18,14 @@ namespace Barotrauma.Items.Components
|
||||
public readonly int MaxWires = 5;
|
||||
|
||||
public readonly string Name;
|
||||
public readonly LocalizedString DisplayName;
|
||||
private readonly LocalizedString _displayName;
|
||||
public LocalizedString DisplayName
|
||||
{
|
||||
get => DisplayNameOverride ?? _displayName;
|
||||
private init => _displayName = value;
|
||||
}
|
||||
|
||||
public LocalizedString DisplayNameOverride;
|
||||
|
||||
private readonly HashSet<Wire> wires;
|
||||
public IReadOnlyCollection<Wire> Wires => wires;
|
||||
@@ -160,8 +167,7 @@ namespace Barotrauma.Items.Components
|
||||
DisplayName = Name;
|
||||
}
|
||||
|
||||
IsPower = Name == "power_in" || Name == "power" || Name == "power_out";
|
||||
|
||||
IsPower = element.GetAttributeBool("ispower", Name is "power_in" or "power" or "power_out");
|
||||
|
||||
LoadedWires = new List<(ushort wireId, int? connectionIndex)>();
|
||||
foreach (var subElement in element.Elements())
|
||||
|
||||
@@ -325,23 +325,18 @@ namespace Barotrauma.Items.Components
|
||||
private bool TriggersOn(Character character, bool triggerFromHumans, bool triggerFromPets, bool triggerFromMonsters)
|
||||
{
|
||||
if (IgnoreDead && character.IsDead) { return false; }
|
||||
if (character.IsHuman)
|
||||
{
|
||||
if (!triggerFromHumans) { return false; }
|
||||
}
|
||||
else if (character.IsPet)
|
||||
if (character.IsPet)
|
||||
{
|
||||
if (!triggerFromPets) { return false; }
|
||||
}
|
||||
else if (character.IsHuman || CharacterParams.CompareGroup(character.Group, CharacterPrefab.HumanGroup))
|
||||
{
|
||||
if (!triggerFromHumans) { return false; }
|
||||
}
|
||||
else
|
||||
{
|
||||
// Not a human or a pet -> monster?
|
||||
if (!triggerFromMonsters) { return false; }
|
||||
if (CharacterParams.CompareGroup(character.Group, CharacterPrefab.HumanGroup))
|
||||
{
|
||||
//characters in the "human" group aren't considered monsters (even if they were something like a friendly mudraptor)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Check matching character, if defined.
|
||||
if (targetCharacters.Any())
|
||||
|
||||
@@ -553,9 +553,9 @@ namespace Barotrauma.Items.Components
|
||||
return new List<Vector2>(nodes);
|
||||
}
|
||||
|
||||
public void SetNodes(List<Vector2> nodes)
|
||||
public void SetNodes(IEnumerable<Vector2> nodes)
|
||||
{
|
||||
this.nodes = new List<Vector2>(nodes);
|
||||
this.nodes = nodes.ToList();
|
||||
UpdateSections();
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace Barotrauma.Items.Components
|
||||
public bool ForceFluctuation { get; set; }
|
||||
|
||||
[Serialize(1.0f, IsPropertySaveable.Yes, description: "How much the fluctuation affects the force. 1 is the maximum fluctuation, 0 is no fluctuation.", alwaysUseInstanceValues: true)]
|
||||
private float ForceFluctuationStrength
|
||||
public float ForceFluctuationStrength
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -33,7 +33,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
[Serialize(1.0f, IsPropertySaveable.Yes, description: "How fast (cycles per second) the force fluctuates.", alwaysUseInstanceValues: true)]
|
||||
private float ForceFluctuationFrequency
|
||||
public float ForceFluctuationFrequency
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -45,7 +45,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
[Serialize(0.01f, IsPropertySaveable.Yes, description: "How often (in seconds) the force fluctuation is calculated.", alwaysUseInstanceValues: true)]
|
||||
private float ForceFluctuationInterval
|
||||
public float ForceFluctuationInterval
|
||||
{
|
||||
get
|
||||
{
|
||||
|
||||
@@ -542,6 +542,10 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
|
||||
IsActive = false;
|
||||
#if CLIENT
|
||||
//stop any sounds that may have been looping while wearing the item
|
||||
StopSounds(ActionType.OnWearing);
|
||||
#endif
|
||||
}
|
||||
|
||||
public override void UpdateBroken(float deltaTime, Camera cam)
|
||||
|
||||
@@ -429,8 +429,6 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public float RotationRad { get; private set; }
|
||||
|
||||
[ConditionallyEditable(ConditionallyEditable.ConditionType.AllowRotating, DecimalCount = 3, ForceShowPlusMinusButtons = true, ValueStep = 0.1f), Serialize(0.0f, IsPropertySaveable.Yes)]
|
||||
public float Rotation
|
||||
{
|
||||
@@ -705,6 +703,22 @@ namespace Barotrauma
|
||||
get; set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Have the <see cref="ActionType.OnInserted"/> effects of the item already triggered when it was placed inside it's current container?
|
||||
/// Used to prevent the effects from executing again when e.g. an existing character (who's inventory items' effects already triggered on some earlier round) spawns mid-round.
|
||||
/// </summary>
|
||||
[Serialize(false, IsPropertySaveable.Yes)]
|
||||
public bool OnInsertedEffectsApplied
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Were the <see cref="ActionType.OnInserted"/> effects already been applied when the item first spawned (loaded from a save)?
|
||||
/// Needed for communicating to the clients whether they should trigger when the item spawns.
|
||||
/// </summary>
|
||||
public bool OnInsertedEffectsAppliedOnPreviousRound;
|
||||
|
||||
public Color Color
|
||||
{
|
||||
get { return spriteColor; }
|
||||
@@ -765,7 +779,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes)]
|
||||
private bool HasBeenInstantiatedOnce { get; set; }
|
||||
public bool HasBeenInstantiatedOnce { get; set; }
|
||||
|
||||
//the default value should be Prefab.Health, but because we can't use it in the attribute,
|
||||
//we'll just use NaN (which does nothing) and set the default value in the constructor/load
|
||||
@@ -807,9 +821,37 @@ namespace Barotrauma
|
||||
set;
|
||||
}
|
||||
|
||||
private bool? isDangerous;
|
||||
/// <summary>
|
||||
/// Bots avoid rooms with dangerous items in them. Normally this value is <see cref="ItemPrefab.IsDangerous">defined in the prefab</see>,
|
||||
/// but this property can be used to override the prefab value.
|
||||
/// </summary>
|
||||
public bool IsDangerous
|
||||
{
|
||||
get { return isDangerous ?? Prefab.IsDangerous; }
|
||||
set
|
||||
{
|
||||
isDangerous = value;
|
||||
if (!value)
|
||||
{
|
||||
_dangerousItems.Remove(this);
|
||||
}
|
||||
else
|
||||
{
|
||||
_dangerousItems.Add(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Editable, Serialize(false, isSaveable: IsPropertySaveable.Yes, "When enabled will prevent the item from taking damage from all sources")]
|
||||
public bool InvulnerableToDamage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Should bots automatically unequip the item? Normally always true, but disabled on items that have been configured to be equipped by default in an item set or the character's job items.
|
||||
/// Note that the value is not saved: if the NPC becomes persistent (e.g. Artie Dolittle hired to the crew) they will no longer keep holding the item.
|
||||
/// </summary>
|
||||
public bool UnequipAutomatically = true;
|
||||
|
||||
/// <summary>
|
||||
/// Was the item stolen during the current round. Note that it's possible for the items to be found in the player's inventory even though they weren't actually stolen.
|
||||
/// For example, a guard can place handcuffs there. So use <see cref="Illegitimate"/> for checking if the item is illegitimately held.
|
||||
@@ -1084,6 +1126,9 @@ namespace Barotrauma
|
||||
|
||||
public bool IsLadder { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Secondary items can be selected at the same time with a primary item (e.g. a ladder or a chair can be selected at the same time with some device).
|
||||
/// </summary>
|
||||
public bool IsSecondaryItem { get; }
|
||||
|
||||
private ItemStatManager statManager;
|
||||
@@ -1369,13 +1414,7 @@ namespace Barotrauma
|
||||
conditionMultiplierCampaign *= campaign.Settings.FuelMultiplier;
|
||||
}
|
||||
}
|
||||
if (!HasBeenInstantiatedOnce)
|
||||
{
|
||||
// This only needs to be done on the very first instantiation.
|
||||
// MaxCondition will be multiplied in RecalculateConditionValues(), ensuring
|
||||
// that Condition will stay in line with the multiplier from then on.
|
||||
condition *= conditionMultiplierCampaign;
|
||||
}
|
||||
condition *= conditionMultiplierCampaign;
|
||||
|
||||
RecalculateConditionValues();
|
||||
|
||||
@@ -1478,13 +1517,14 @@ namespace Barotrauma
|
||||
{
|
||||
ItemComponent component = components[i],
|
||||
cloneComp = clone.components[i];
|
||||
|
||||
if (component is not CircuitBox origBox || cloneComp is not CircuitBox cloneBox)
|
||||
if (component.GetType() == cloneComp.GetType())
|
||||
{
|
||||
continue;
|
||||
cloneComp.Clone(component);
|
||||
}
|
||||
if (component is CircuitBox origBox && cloneComp is CircuitBox cloneBox)
|
||||
{
|
||||
cloneBox.CloneFrom(origBox, clonedContainedItems);
|
||||
}
|
||||
|
||||
cloneBox.CloneFrom(origBox, clonedContainedItems);
|
||||
}
|
||||
|
||||
clone.FullyInitialized = true;
|
||||
@@ -2293,6 +2333,7 @@ namespace Barotrauma
|
||||
|
||||
private void SendPendingNetworkUpdatesInternal()
|
||||
{
|
||||
DebugConsole.NewMessage($"Sending status event for item {Name}", Color.Gray);
|
||||
CreateStatusEvent(loadingRound: false);
|
||||
lastSentCondition = condition;
|
||||
sendConditionUpdateTimer = NetConfig.ItemConditionUpdateInterval;
|
||||
@@ -2300,6 +2341,17 @@ namespace Barotrauma
|
||||
|
||||
public void CreateStatusEvent(bool loadingRound)
|
||||
{
|
||||
//A little hacky: clients aren't allowed to apply OnFire effects themselves, which means effects that rely on the "onfire" status tag
|
||||
//won't work properly. But let's notify clients of the item being on fire when it breaks, so they can e.g. make tanks explode.
|
||||
|
||||
//An alternative could be to allow clients to run OnFire effects, but I suspect it could lead to desyncs if/when there's minor
|
||||
//discrepancies in the progress of the fires (which is most likely why running them was disabled on clients).
|
||||
if (GameMain.NetworkMember is { IsServer: true } &&
|
||||
condition <= 0.0f &&
|
||||
StatusEffect.DurationList.Any(d => d.Targets.Contains(this) && d.Parent.HasTag(Barotrauma.Tags.OnFireStatusEffectTag)))
|
||||
{
|
||||
GameMain.NetworkMember.CreateEntityEvent(this, new ApplyStatusEffectEventData(ActionType.OnFire));
|
||||
}
|
||||
GameMain.NetworkMember.CreateEntityEvent(this, new ItemStatusEventData(loadingRound));
|
||||
}
|
||||
|
||||
@@ -2326,10 +2378,11 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
private bool isActive = true;
|
||||
public bool IsInRemoveQueue;
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
if (!isActive || IsLayerHidden) { return; }
|
||||
if (!isActive || IsLayerHidden || IsInRemoveQueue) { return; }
|
||||
|
||||
if (impactQueue != null)
|
||||
{
|
||||
@@ -2685,14 +2738,14 @@ namespace Barotrauma
|
||||
|
||||
partial void OnCollisionProjSpecific(float impact);
|
||||
|
||||
public override void FlipX(bool relativeToSub)
|
||||
public override void FlipX(bool relativeToSub, bool force = false)
|
||||
{
|
||||
//call the base method even if the item can't flip, to handle repositioning when flipping the whole sub
|
||||
base.FlipX(relativeToSub);
|
||||
|
||||
if (!Prefab.CanFlipX)
|
||||
if (!Prefab.CanFlipX && !force)
|
||||
{
|
||||
flippedX = false;
|
||||
FlippedX = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2714,14 +2767,14 @@ namespace Barotrauma
|
||||
SetContainedItemPositions();
|
||||
}
|
||||
|
||||
public override void FlipY(bool relativeToSub)
|
||||
public override void FlipY(bool relativeToSub, bool force = false)
|
||||
{
|
||||
//call the base method even if the item can't flip, to handle repositioning when flipping the whole sub
|
||||
base.FlipY(relativeToSub);
|
||||
|
||||
if (!Prefab.CanFlipY)
|
||||
if (!Prefab.CanFlipY && !force)
|
||||
{
|
||||
flippedY = false;
|
||||
FlippedY = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4059,6 +4112,10 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
//store this at this point so we can tell the clients whether the effects had already been applied when the item was first loaded,
|
||||
//(in which case a client should not execute them when they spawn the item)
|
||||
item.OnInsertedEffectsAppliedOnPreviousRound = item.OnInsertedEffectsApplied;
|
||||
|
||||
item.ParseLinks(element, idRemap);
|
||||
|
||||
bool thisIsOverride = element.GetAttributeBool("isoverride", false);
|
||||
@@ -4124,8 +4181,8 @@ namespace Barotrauma
|
||||
if (element.GetAttributeBool("markedfordeconstruction", false)) { _deconstructItems.Add(item); }
|
||||
|
||||
float prevRotation = item.Rotation;
|
||||
if (element.GetAttributeBool("flippedx", false)) { item.FlipX(false); }
|
||||
if (element.GetAttributeBool("flippedy", false)) { item.FlipY(false); }
|
||||
if (element.GetAttributeBool("flippedx", false)) { item.FlipX(relativeToSub: false, force: true); }
|
||||
if (element.GetAttributeBool("flippedy", false)) { item.FlipY(relativeToSub: false, force: true); }
|
||||
item.Rotation = prevRotation;
|
||||
|
||||
if (appliedSwap != null)
|
||||
|
||||
@@ -5,6 +5,7 @@ using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Xml.Linq;
|
||||
@@ -132,10 +133,30 @@ namespace Barotrauma
|
||||
{
|
||||
public readonly Identifier ItemPrefabIdentifier;
|
||||
|
||||
public ItemPrefab ItemPrefab =>
|
||||
ItemPrefab.Prefabs.TryGet(ItemPrefabIdentifier, out var prefab) ? prefab
|
||||
: MapEntityPrefab.FindByName(ItemPrefabIdentifier.Value) as ItemPrefab;
|
||||
|
||||
[MaybeNull, AllowNull]
|
||||
public ItemPrefab cachedItemPrefab;
|
||||
|
||||
[MaybeNull, AllowNull]
|
||||
private Md5Hash prevContentPackagesHash;
|
||||
|
||||
[MaybeNull]
|
||||
public ItemPrefab ItemPrefab
|
||||
{
|
||||
get
|
||||
{
|
||||
if (prevContentPackagesHash == null ||
|
||||
!prevContentPackagesHash.Equals(ContentPackageManager.EnabledPackages.MergedHash))
|
||||
{
|
||||
cachedItemPrefab = ItemPrefab.Prefabs.TryGet(ItemPrefabIdentifier, out var prefab)
|
||||
? prefab
|
||||
: MapEntityPrefab.FindByName(ItemPrefabIdentifier.Value) as ItemPrefab;
|
||||
prevContentPackagesHash = ContentPackageManager.EnabledPackages.MergedHash;
|
||||
}
|
||||
|
||||
return cachedItemPrefab;
|
||||
}
|
||||
}
|
||||
|
||||
public override UInt32 UintIdentifier { get; }
|
||||
|
||||
public override IEnumerable<ItemPrefab> ItemPrefabs => ItemPrefab == null ? Enumerable.Empty<ItemPrefab>() : ItemPrefab.ToEnumerable();
|
||||
@@ -145,7 +166,7 @@ namespace Barotrauma
|
||||
|
||||
public override bool MatchesItem(Item item)
|
||||
{
|
||||
return item?.Prefab.Identifier == ItemPrefabIdentifier;
|
||||
return item?.Prefab.Identifier == (ItemPrefab?.Identifier ?? ItemPrefabIdentifier);
|
||||
}
|
||||
|
||||
public RequiredItemByIdentifier(Identifier itemPrefab, int amount, float minCondition, float maxCondition, bool useCondition, LocalizedString overrideDescription, LocalizedString overrideHeader) :
|
||||
@@ -711,6 +732,9 @@ namespace Barotrauma
|
||||
[Serialize(false, IsPropertySaveable.No, description: "Hides the condition displayed in the item's tooltip.")]
|
||||
public bool HideConditionInTooltip { get; set; }
|
||||
|
||||
[Serialize("", IsPropertySaveable.No, description: "If set, displays if the given fabrication recipe has been unlocked or not in the tooltip. The actual unlocking of the recipe should be handled in a status effect.")]
|
||||
public Identifier UnlockedRecipeInToolTip { get; set; }
|
||||
|
||||
//if true and the item has trigger areas defined, characters need to be within the trigger to interact with the item
|
||||
//if false, trigger areas define areas that can be used to highlight the item
|
||||
[Serialize(true, IsPropertySaveable.No)]
|
||||
@@ -864,7 +888,7 @@ namespace Barotrauma
|
||||
[Serialize(10.0f, IsPropertySaveable.No)]
|
||||
public float MaxScale { get; private set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No)]
|
||||
[Serialize(false, IsPropertySaveable.No, description: "Bots avoid rooms with dangerous items in them.")]
|
||||
public bool IsDangerous { get; private set; }
|
||||
|
||||
private int maxStackSize;
|
||||
@@ -1285,6 +1309,11 @@ namespace Barotrauma
|
||||
this.LevelCommonness = levelCommonness.ToImmutableDictionary();
|
||||
this.LevelQuantity = levelQuantity.ToImmutableDictionary();
|
||||
|
||||
//flipping holdable items vertically is not properly supported (uses the orientation of the physics body, which depends on which direction the character holding the item is facing)
|
||||
//so let's by default make the item non-flippable, but if there's some use case where the item needs to flip vertically, it can be enabled by explicitly defining it in the XML.
|
||||
bool canFlipYByDefault = ConfigElement.GetChildElement(nameof(Holdable)) == null;
|
||||
CanFlipY = ConfigElement.GetAttributeBool(nameof(CanFlipY), def: canFlipYByDefault);
|
||||
|
||||
// Backwards compatibility
|
||||
if (storePrices.Any())
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user