Release 1.9.7.0 - Summer Update 2025

This commit is contained in:
Regalis11
2025-06-17 16:38:11 +03:00
parent 22227f13e5
commit ea5a2bc693
297 changed files with 7344 additions and 2421 deletions
@@ -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)