Merge remote-tracking branch 'upstream/dev' into develop

This commit is contained in:
EvilFactory
2022-12-09 17:33:44 -03:00
416 changed files with 12674 additions and 5862 deletions
@@ -1,4 +1,5 @@
using Barotrauma.Networking;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
@@ -63,6 +64,9 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(0.0f, IsPropertySaveable.No)]
public float RaycastRange { get; set; }
[Serialize(0.25f, IsPropertySaveable.Yes, description: "The duration of an individual discharge (in seconds)."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 60.0f, ValueStep = 0.1f, DecimalCount = 2)]
public float Duration
{
@@ -70,6 +74,13 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(0.25f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 60.0f, ValueStep = 0.1f, DecimalCount = 2)]
public float Reload
{
get;
set;
}
[Serialize(false, IsPropertySaveable.Yes, "If set to true, the discharge cannot travel inside the submarine nor shock anyone inside."), Editable]
public bool OutdoorsOnly
{
@@ -77,6 +88,13 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(false, IsPropertySaveable.Yes)]
public bool IgnoreUser
{
get;
set;
}
private readonly List<Node> nodes = new List<Node>();
public IEnumerable<Node> Nodes
{
@@ -91,6 +109,10 @@ namespace Barotrauma.Items.Components
private readonly Attack attack;
private Character user;
private float reloadTimer;
public ElectricalDischarger(Item item, ContentXElement element) :
base(item, element)
{
@@ -125,6 +147,7 @@ namespace Barotrauma.Items.Components
charging = true;
timer = Duration;
IsActive = true;
user = character;
#if SERVER
if (GameMain.Server != null) { item.CreateServerEvent(this); }
#endif
@@ -144,6 +167,11 @@ namespace Barotrauma.Items.Components
if (timer <= 0.0f)
{
if (reloadTimer > 0.0f)
{
reloadTimer -= deltaTime;
return;
}
IsActive = false;
return;
}
@@ -196,6 +224,7 @@ namespace Barotrauma.Items.Components
private void Discharge()
{
reloadTimer = Reload;
ApplyStatusEffects(ActionType.OnUse, 1.0f);
FindNodes(item.WorldPosition, Range);
if (attack != null)
@@ -203,7 +232,7 @@ namespace Barotrauma.Items.Components
foreach ((Character character, Node node) in charactersInRange)
{
if (character == null || character.Removed) { continue; }
character.ApplyAttack(null, node.WorldPosition, attack, MathHelper.Clamp(Voltage, 1.0f, MaxOverVoltageFactor));
character.ApplyAttack(user, node.WorldPosition, attack, MathHelper.Clamp(Voltage, 1.0f, MaxOverVoltageFactor));
}
}
DischargeProjSpecific();
@@ -214,6 +243,18 @@ namespace Barotrauma.Items.Components
public void FindNodes(Vector2 worldPosition, float range)
{
if (RaycastRange > 0.0f)
{
float angle = 0.0f;
float dir = 1;
if (item.body != null)
{
angle += item.body.Rotation;
dir = item.body.Dir;
}
worldPosition += new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle)) * RaycastRange * dir;
}
//see which submarines are within range so we can skip structures that are in far-away subs
List<Submarine> submarinesInRange = new List<Submarine>();
foreach (Submarine sub in Submarine.Loaded)
@@ -222,7 +263,7 @@ namespace Barotrauma.Items.Components
{
submarinesInRange.Add(sub);
}
else
else if (sub != null)
{
Rectangle subBorders = new Rectangle(
sub.Borders.X - (int)range, sub.Borders.Y + (int)range,
@@ -235,7 +276,7 @@ namespace Barotrauma.Items.Components
}
}
//get all walls within range
//get all walls within range the arc could potentially hit
List<Entity> entitiesInRange = new List<Entity>(100);
foreach (Structure structure in Structure.WallList)
{
@@ -243,10 +284,10 @@ namespace Barotrauma.Items.Components
if (structure.Submarine != null&& !submarinesInRange.Contains(structure.Submarine)) { continue; }
var structureWorldRect = structure.WorldRect;
if (worldPosition.X < structureWorldRect.X - range) continue;
if (worldPosition.X > structureWorldRect.Right + range) continue;
if (worldPosition.Y > structureWorldRect.Y + range) continue;
if (worldPosition.Y < structureWorldRect.Y -structureWorldRect.Height - range) continue;
if (worldPosition.X < structureWorldRect.X - range) { continue; }
if (worldPosition.X > structureWorldRect.Right + range) { continue; }
if (worldPosition.Y > structureWorldRect.Y + range) { continue; }
if (worldPosition.Y < structureWorldRect.Y - structureWorldRect.Height - range) { continue; }
if (structure.Submarine != null)
{
@@ -263,26 +304,51 @@ namespace Barotrauma.Items.Components
entitiesInRange.Add(structure);
}
nodes.Clear();
if (RaycastRange > 0.0f)
{
nodes.Add(new Node(item.WorldPosition, -1));
int parentNodeIndex = 0;
AddNodesBetweenPoints(item.WorldPosition, worldPosition, 0.5f, ref parentNodeIndex);
}
else
{
nodes.Add(new Node(worldPosition, -1));
}
//get all characters within range the arc could potentially hit
float totalRange = RaycastRange + range;
foreach (Character character in Character.CharacterList)
{
if (!character.Enabled) continue;
if (OutdoorsOnly && character.Submarine != null) continue;
if (character.Submarine != null && !submarinesInRange.Contains(character.Submarine)) continue;
if (!character.Enabled) { continue; }
if (IgnoreUser && character == user) { continue; }
if (OutdoorsOnly && character.Submarine != null) { continue; }
if (character.Submarine != null && !submarinesInRange.Contains(character.Submarine)) { continue; }
if (Vector2.DistanceSquared(character.WorldPosition, worldPosition) < range * range * RangeMultiplierInWalls)
if (Vector2.DistanceSquared(character.WorldPosition, worldPosition) < totalRange * totalRange * RangeMultiplierInWalls)
{
entitiesInRange.Add(character);
}
//if the weapon does a raycast, check distance to the ray too (not just the end of the ray)
if (RaycastRange > 0)
{
float distSqr = MathUtils.LineSegmentToPointDistanceSquared(worldPosition, item.WorldPosition, character.WorldPosition);
//if the distance from the initial raycast to the character is small (e.g. goes through the character), we know it must hit
if (distSqr < range * range * RangeMultiplierInWalls)
{
if (!entitiesInRange.Contains(character)) { entitiesInRange.Add(character); }
charactersInRange.Add((character, nodes.First()));
}
}
}
nodes.Clear();
nodes.Add(new Node(worldPosition, -1));
FindNodes(entitiesInRange, worldPosition, 0, range);
FindNodes(entitiesInRange, worldPosition, nodes.Count - 1, range);
//construct final nodes (w/ lengths and angles so they don't have to be recalculated when rendering the discharge)
for (int i = 0; i < nodes.Count; i++)
{
if (nodes[i].ParentIndex < 0) continue;
if (nodes[i].ParentIndex < 0) { continue; }
Node parentNode = nodes[nodes[i].ParentIndex];
float length = Vector2.Distance(nodes[i].WorldPosition, parentNode.WorldPosition) * Rand.Range(1.0f, 1.25f);
float angle = MathUtils.VectorToAngle(parentNode.WorldPosition - nodes[i].WorldPosition);
@@ -292,7 +358,7 @@ namespace Barotrauma.Items.Components
private void FindNodes(List<Entity> entitiesInRange, Vector2 currPos, int parentNodeIndex, float currentRange)
{
if (currentRange <= 0.0f || nodes.Count >= MaxNodes) return;
if (currentRange <= 0.0f || nodes.Count >= MaxNodes) { return; }
//find the closest structure
int closestIndex = -1;
@@ -322,7 +388,7 @@ namespace Barotrauma.Items.Components
}
else if (entitiesInRange[i] is Character character)
{
dist = Vector2.Distance(character.WorldPosition, currPos);
dist = MathUtils.LineSegmentToPointDistanceSquared(currPos, nodes[parentNodeIndex].WorldPosition, character.WorldPosition);
}
if (dist < closestDist)
@@ -434,20 +500,35 @@ namespace Barotrauma.Items.Components
for (int j = 0; j < entitiesInRange.Count; j++)
{
var otherEntity = entitiesInRange[j];
if (!(otherEntity is Character character)) continue;
if (OutdoorsOnly && character.Submarine != null) continue;
if (otherEntity is not Character character) { continue; }
if (IgnoreUser && character == user) { continue; }
if (OutdoorsOnly && character.Submarine != null) { continue; }
Vector2 characterMin = new Vector2(character.AnimController.Limbs.Min(l => l.WorldPosition.X), character.AnimController.Limbs.Min(l => l.WorldPosition.Y));
Vector2 characterMax = new Vector2(character.AnimController.Limbs.Max(l => l.WorldPosition.X), character.AnimController.Limbs.Max(l => l.WorldPosition.Y));
if (targetStructure.IsHorizontal)
{
if (otherEntity.WorldPosition.X < targetStructure.WorldRect.X) continue;
if (otherEntity.WorldPosition.X > targetStructure.WorldRect.Right) continue;
if (Math.Abs(otherEntity.WorldPosition.Y - targetStructure.WorldPosition.Y) > currentRange) continue;
if (characterMax.X < targetStructure.WorldRect.X) { continue; }
if (characterMin.X > targetStructure.WorldRect.Right) { continue; }
if (Math.Abs(characterMin.Y - targetStructure.WorldPosition.Y) > currentRange &&
Math.Abs(characterMax.Y - targetStructure.WorldPosition.Y) > currentRange)
{
continue;
}
}
else
{
if (otherEntity.WorldPosition.Y < targetStructure.WorldRect.Y - targetStructure.Rect.Height) continue;
if (otherEntity.WorldPosition.Y > targetStructure.WorldRect.Y) continue;
if (Math.Abs(otherEntity.WorldPosition.X - targetStructure.WorldPosition.X) > currentRange) continue;
if (characterMax.Y < targetStructure.WorldRect.Y - targetStructure.Rect.Height) { continue; }
if (characterMin.Y > targetStructure.WorldRect.Y) { continue; }
if (Math.Abs(characterMin.X - targetStructure.WorldPosition.X) > currentRange &&
Math.Abs(characterMax.X - targetStructure.WorldPosition.X) > currentRange)
{
continue;
}
}
if (!charactersInRange.Any(c => c.character == character))
{
charactersInRange.Add((character, nodes[parentNodeIndex]));
}
float closestNodeDistSqr = float.MaxValue;
int closestNodeIndex = -1;
@@ -473,7 +554,10 @@ namespace Barotrauma.Items.Components
AddNodesBetweenPoints(currPos, targetPos, 0.25f, ref parentNodeIndex);
nodes.Add(new Node(targetPos, parentNodeIndex));
entitiesInRange.RemoveAt(closestIndex);
charactersInRange.Add((character, nodes[parentNodeIndex]));
if (!charactersInRange.Any(c => c.character == character))
{
charactersInRange.Add((character, nodes[parentNodeIndex]));
}
FindNodes(entitiesInRange, targetPos, nodes.Count - 1, currentRange);
}
}
@@ -483,7 +567,7 @@ namespace Barotrauma.Items.Components
Vector2 diff = targetPos - currPos;
float dist = diff.Length();
Vector2 normal = new Vector2(-diff.Y, diff.X) / dist;
for (float x = MaxNodeDistance; x < dist - MaxNodeDistance; x += MaxNodeDistance * Rand.Range(0.5f, 1.5f))
for (float x = MaxNodeDistance; x < dist - MaxNodeDistance; x += MaxNodeDistance * Rand.Range(0.5f, 1.0f))
{
//0 at the edges, 1 at the center
float normalOffset = (0.5f - Math.Abs(x / dist - 0.5f)) * 2.0f;
@@ -127,7 +127,7 @@ namespace Barotrauma.Items.Components
set { attachedByDefault = value; }
}
[Editable, Serialize("0.0,0.0", IsPropertySaveable.No, description: "The position the character holds the item at (in pixels, as an offset from the character's shoulder)."+
[Serialize("0.0,0.0", IsPropertySaveable.No, description: "The position the character holds the item at (in pixels, as an offset from the character's shoulder)."+
" For example, a value of 10,-100 would make the character hold the item 100 pixels below the shoulder and 10 pixels forwards.")]
public Vector2 HoldPos
{
@@ -143,7 +143,11 @@ namespace Barotrauma.Items.Components
set { aimPos = ConvertUnits.ToSimUnits(value); }
}
#if DEBUG
[Editable, Serialize(0.0f, IsPropertySaveable.No, description: "The rotation at which the character holds the item (in degrees, relative to the rotation of the character's hand).")]
#else
[Serialize(0.0f, IsPropertySaveable.No)]
#endif
public float HoldAngle
{
get { return MathHelper.ToDegrees(holdAngle); }
@@ -151,23 +155,50 @@ namespace Barotrauma.Items.Components
}
private Vector2 swingAmount;
#if DEBUG
[Editable, Serialize("0.0,0.0", IsPropertySaveable.No, description: "How much the item swings around when aiming/holding it (in pixels, as an offset from AimPos/HoldPos).")]
#else
[Serialize("0.0,0.0", IsPropertySaveable.No)]
#endif
public Vector2 SwingAmount
{
get { return ConvertUnits.ToDisplayUnits(swingAmount); }
set { swingAmount = ConvertUnits.ToSimUnits(value); }
}
#if DEBUG
[Editable, Serialize(0.0f, IsPropertySaveable.No, description: "How fast the item swings around when aiming/holding it (only valid if SwingAmount is set).")]
#else
[Serialize(0.0f, IsPropertySaveable.No)]
#endif
public float SwingSpeed { get; set; }
#if DEBUG
[Editable, Serialize(false, IsPropertySaveable.No, description: "Should the item swing around when it's being held.")]
#else
[Serialize(false, IsPropertySaveable.No)]
#endif
public bool SwingWhenHolding { get; set; }
#if DEBUG
[Editable, Serialize(false, IsPropertySaveable.No, description: "Should the item swing around when it's being aimed.")]
#else
[Serialize(false, IsPropertySaveable.No)]
#endif
public bool SwingWhenAiming { get; set; }
#if DEBUG
[Editable, Serialize(false, IsPropertySaveable.No, description: "Should the item swing around when it's being used (for example, when firing a weapon or a welding tool).")]
#else
[Serialize(false, IsPropertySaveable.No)]
#endif
public bool SwingWhenUsing { get; set; }
#if DEBUG
[Editable, Serialize(false, IsPropertySaveable.No)]
#else
[Serialize(false, IsPropertySaveable.No)]
#endif
public bool DisableHeadRotation { get; set; }
[ConditionallyEditable(ConditionallyEditable.ConditionType.Attachable, MinValueFloat = 0.0f, MaxValueFloat = 0.999f, DecimalCount = 3), Serialize(0.55f, IsPropertySaveable.No, description: "Sprite depth that's used when the item is NOT attached to a wall.")]
@@ -650,11 +681,10 @@ namespace Barotrauma.Items.Components
return false;
}
Vector2 attachPos = GetAttachPosition(character, useWorldCoordinates: true);
Structure attachTarget = Structure.GetAttachTarget(attachPos);
Submarine attachSubmarine = Structure.GetAttachTarget(attachPos)?.Submarine ?? item.Submarine;
int maxAttachableCount = (int)character.Info.GetSavedStatValue(StatTypes.MaxAttachableCount, item.Prefab.Identifier);
int currentlyAttachedCount = Item.ItemList.Count(
i => i.Submarine == attachTarget?.Submarine && i.GetComponent<Holdable>() is Holdable holdable && holdable.Attached && i.Prefab.Identifier == item.Prefab.Identifier);
i => i.Submarine == attachSubmarine && i.GetComponent<Holdable>() is Holdable holdable && holdable.Attached && i.Prefab.Identifier == item.Prefab.Identifier);
if (maxAttachableCount == 0)
{
#if CLIENT
@@ -812,10 +842,18 @@ namespace Barotrauma.Items.Components
public override void Update(float deltaTime, Camera cam)
{
if (item.body == null || !item.body.Enabled) { return; }
Character owner = picker ?? item.GetRootInventoryOwner() as Character;
if (owner != null)
{
ApplyStatusEffects(ActionType.OnActive, deltaTime, owner);
}
if (picker == null || !picker.HasEquippedItem(item))
{
if (Pusher != null) { Pusher.Enabled = false; }
if (attachTargetCell == null) { IsActive = false; }
if (attachTargetCell == null && owner == null) { IsActive = false; }
return;
}
@@ -824,23 +862,7 @@ namespace Barotrauma.Items.Components
Drawable = true;
}
Vector2 swing = Vector2.Zero;
if (swingAmount != Vector2.Zero && !picker.IsUnconscious && picker.Stun <= 0.0f)
{
swingState += deltaTime;
swingState %= 1.0f;
if (SwingWhenHolding ||
(SwingWhenAiming && picker.IsKeyDown(InputType.Aim)) ||
(SwingWhenUsing && picker.IsKeyDown(InputType.Aim) && picker.IsKeyDown(InputType.Shoot)))
{
swing = swingAmount * new Vector2(
PerlinNoise.GetPerlin(swingState * SwingSpeed * 0.1f, swingState * SwingSpeed * 0.1f) - 0.5f,
PerlinNoise.GetPerlin(swingState * SwingSpeed * 0.1f + 0.5f, swingState * SwingSpeed * 0.1f + 0.5f) - 0.5f);
}
}
ApplyStatusEffects(ActionType.OnActive, deltaTime, picker);
UpdateSwingPos(deltaTime, out Vector2 swingPos);
if (item.body.Dir != picker.AnimController.Dir)
{
item.FlipX(relativeToSub: false);
@@ -853,7 +875,7 @@ namespace Barotrauma.Items.Components
scaledHandlePos[0] = handlePos[0] * item.Scale;
scaledHandlePos[1] = handlePos[1] * item.Scale;
bool aim = picker.IsKeyDown(InputType.Aim) && aimPos != Vector2.Zero && picker.CanAim;
picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, holdPos + swing, aimPos + swing, aim, holdAngle);
picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, holdPos + swingPos, aimPos + swingPos, aim, holdAngle);
if (!aim)
{
var rope = GetRope();
@@ -890,6 +912,24 @@ namespace Barotrauma.Items.Components
}
}
public void UpdateSwingPos(float deltaTime, out Vector2 swingPos)
{
swingPos = Vector2.Zero;
if (swingAmount != Vector2.Zero && !picker.IsUnconscious && picker.Stun <= 0.0f)
{
swingState += deltaTime;
swingState %= 1.0f;
if (SwingWhenHolding ||
(SwingWhenAiming && picker.IsKeyDown(InputType.Aim)) ||
(SwingWhenUsing && picker.IsKeyDown(InputType.Aim) && picker.IsKeyDown(InputType.Shoot)))
{
swingPos = swingAmount * new Vector2(
PerlinNoise.GetPerlin(swingState * SwingSpeed * 0.1f, swingState * SwingSpeed * 0.1f) - 0.5f,
PerlinNoise.GetPerlin(swingState * SwingSpeed * 0.1f + 0.5f, swingState * SwingSpeed * 0.1f + 0.5f) - 0.5f);
}
}
}
public override void ReceiveSignal(Signal signal, Connection connection)
{
//do nothing
@@ -214,8 +214,9 @@ namespace Barotrauma.Items.Components
bool aim = item.RequireAimToUse && picker.AllowInput && picker.IsKeyDown(InputType.Aim) && reloadTimer <= 0 && picker.CanAim;
if (aim)
{
UpdateSwingPos(deltaTime, out Vector2 swingPos);
hitPos = MathUtils.WrapAnglePi(Math.Min(hitPos + deltaTime * 3f, MathHelper.PiOver4));
ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, hitPos, holdAngle + hitPos, aimMelee: true);
ac.HoldItem(deltaTime, item, handlePos, aimPos + swingPos, Vector2.Zero, aim: false, hitPos, holdAngle + hitPos, aimMelee: true);
if (ac.InWater)
{
ac.LockFlippingUntil = (float)Timing.TotalTime + Reload;
@@ -314,6 +315,7 @@ namespace Barotrauma.Items.Components
if (f2.Body.UserData is Limb targetLimb)
{
if (targetLimb.IsSevered || targetLimb.character == null || targetLimb.character == User) { return false; }
if (targetLimb.character.IgnoreMeleeWeapons) { return false; }
var targetCharacter = targetLimb.character;
if (targetCharacter == picker) { return false; }
if (AllowHitMultiple)
@@ -329,6 +331,7 @@ namespace Barotrauma.Items.Components
else if (f2.Body.UserData is Character targetCharacter)
{
if (targetCharacter == picker || targetCharacter == User) { return false; }
if (targetCharacter.IgnoreMeleeWeapons) { return false; }
targetLimb = targetCharacter.AnimController.GetLimb(LimbType.Torso); //Otherwise armor can be bypassed in strange ways
if (AllowHitMultiple)
{
@@ -392,37 +395,38 @@ namespace Barotrauma.Items.Components
float damageMultiplier = 1 + User.GetStatValue(StatTypes.MeleeAttackMultiplier);
damageMultiplier *= 1.0f + item.GetQualityModifier(Quality.StatType.StrikingPowerMultiplier);
Character user = User;
Limb targetLimb = target.UserData as Limb;
Character targetCharacter = targetLimb?.character ?? target.UserData as Character;
GameMain.LuaCs.Hook.Call("meleeWeapon.handleImpact", this, target);
if (Attack != null)
{
Attack.SetUser(User);
Attack.SetUser(user);
Attack.DamageMultiplier = damageMultiplier;
if (targetLimb != null)
{
if (targetLimb.character.Removed) { return; }
targetLimb.character.LastDamageSource = item;
Attack.DoDamageToLimb(User, targetLimb, item.WorldPosition, 1.0f);
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);
Attack.DoDamage(user, targetCharacter, item.WorldPosition, 1.0f);
}
else if ((target.UserData as Structure ?? targetFixture.UserData as Structure) is Structure targetStructure)
{
if (targetStructure.Removed) { return; }
Attack.DoDamage(User, targetStructure, item.WorldPosition, 1.0f);
Attack.DoDamage(user, targetStructure, item.WorldPosition, 1.0f);
}
else if (target.UserData is Item targetItem && targetItem.Prefab.DamagedByMeleeWeapons && targetItem.Condition > 0)
{
if (targetItem.Removed) { return; }
var attackResult = Attack.DoDamage(User, targetItem, item.WorldPosition, 1.0f);
var attackResult = Attack.DoDamage(user, targetItem, item.WorldPosition, 1.0f);
#if CLIENT
if (attackResult.Damage > 0.0f)
if (attackResult.Damage > 0.0f && targetItem.Prefab.ShowHealthBar)
{
Character.Controlled?.UpdateHUDProgressBar(targetItem,
targetItem.WorldPosition,
@@ -436,7 +440,7 @@ namespace Barotrauma.Items.Components
else if (target.UserData is Holdable holdable && holdable.CanPush)
{
if (holdable.Item.Removed) { return; }
Attack.DoDamage(User, holdable.Item, item.WorldPosition, 1.0f);
Attack.DoDamage(user, holdable.Item, item.WorldPosition, 1.0f);
RestoreCollision();
hitting = false;
User = null;
@@ -449,29 +453,32 @@ namespace Barotrauma.Items.Components
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
bool success = Rand.Range(0.0f, 0.5f) < DegreeOfSuccess(User);
#if SERVER
if (GameMain.Server != null && targetCharacter != null) //TODO: Log structure hits
ActionType conditionalActionType = ActionType.OnSuccess;
if (user != null && Rand.Range(0.0f, 0.5f) > DegreeOfSuccess(user))
{
GameMain.Server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(
success ? ActionType.OnUse : ActionType.OnFailure,
targetItemComponent: null,
targetCharacter, targetLimb));
string logStr = picker?.LogName + " used " + item.Name;
if (item.ContainedItems != null && item.ContainedItems.Any())
{
logStr += " (" + string.Join(", ", item.ContainedItems.Select(i => i?.Name)) + ")";
}
logStr += " on " + targetCharacter.LogName + ".";
Networking.GameServer.Log(logStr, Networking.ServerLog.MessageType.Attack);
conditionalActionType = ActionType.OnFailure;
}
if (GameMain.NetworkMember is { IsServer: true } server && targetCharacter != null)
{
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, targetItemComponent: null, targetCharacter, targetLimb));
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnUse, targetItemComponent: null, targetCharacter, targetLimb));
#if SERVER
if (GameMain.Server != null) //TODO: Log structure hits
{
string logStr = picker?.LogName + " used " + item.Name;
if (item.ContainedItems != null && item.ContainedItems.Any())
{
logStr += " (" + string.Join(", ", item.ContainedItems.Select(i => i?.Name)) + ")";
}
logStr += " on " + targetCharacter.LogName + ".";
Networking.GameServer.Log(logStr, Networking.ServerLog.MessageType.Attack);
}
#endif
}
#endif
if (targetCharacter != null) //TODO: Allow OnUse to happen on structures too maybe??
{
ApplyStatusEffects(success ? ActionType.OnUse : ActionType.OnFailure, 1.0f, targetCharacter, targetLimb, user: User, afflictionMultiplier: damageMultiplier);
ApplyStatusEffects(conditionalActionType, 1.0f, targetCharacter, targetLimb, user: user, afflictionMultiplier: damageMultiplier);
ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb, user: user, afflictionMultiplier: damageMultiplier);
}
if (DeleteOnUse)
@@ -12,7 +12,7 @@ namespace Barotrauma.Items.Components
{
public enum UseEnvironment
{
Air, Water, Both
Air, Water, Both, None
};
private float useState;
@@ -23,6 +23,8 @@ namespace Barotrauma.Items.Components
[Serialize(0.0f, IsPropertySaveable.No, description: "The force to apply to the user's body."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)]
public float Force { get; set; }
[Serialize(true, IsPropertySaveable.No, description: "If the item is held in RightHand or LeftHand, apply extra force there")]
public bool ApplyToHands { get; set; }
#if CLIENT
private string particles;
[Serialize("", IsPropertySaveable.No, description: "The name of the particle prefab the item emits when used.")]
@@ -42,6 +44,7 @@ namespace Barotrauma.Items.Components
{
if (character == null || character.Removed) { return false; }
if (!character.IsKeyDown(InputType.Aim) || character.Stun > 0.0f) { return false; }
if (UsableIn == UseEnvironment.None) { return false; }
IsActive = true;
useState = 0.1f;
@@ -70,13 +73,16 @@ namespace Barotrauma.Items.Components
character.AnimController.Collider.ApplyForce(propulsion);
if (character.Inventory.IsInLimbSlot(item, InvSlotType.RightHand))
{
character.AnimController.GetLimb(LimbType.RightHand)?.body.ApplyForce(propulsion);
}
if (character.Inventory.IsInLimbSlot(item, InvSlotType.LeftHand))
{
character.AnimController.GetLimb(LimbType.LeftHand)?.body.ApplyForce(propulsion);
if (ApplyToHands)
{
if (character.Inventory.IsInLimbSlot(item, InvSlotType.RightHand))
{
character.AnimController.GetLimb(LimbType.RightHand)?.body.ApplyForce(propulsion);
}
if (character.Inventory.IsInLimbSlot(item, InvSlotType.LeftHand))
{
character.AnimController.GetLimb(LimbType.LeftHand)?.body.ApplyForce(propulsion);
}
}
#if CLIENT
@@ -32,6 +32,20 @@ namespace Barotrauma.Items.Components
set { reload = Math.Max(value, 0.0f); }
}
[Serialize(0f, IsPropertySaveable.No, description: "Weapons skill requirement to reload at normal speed.")]
public float ReloadSkillRequirement
{
get;
set;
}
[Serialize(1.0f, IsPropertySaveable.No, description: "Reload time at 0 skill level. Reload time scales with skill level up to the Weapons skill requirement.")]
public float ReloadNoSkill
{
get;
set;
}
[Serialize(false, IsPropertySaveable.No, description: "Tells the AI to hold the trigger down when it uses this weapon")]
public bool HoldTrigger
{
@@ -39,7 +53,7 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(1, IsPropertySaveable.No, description: "How projectiles the weapon launches when fired once.")]
[Serialize(1, IsPropertySaveable.No, description: "How many projectiles the weapon launches when fired once.")]
public int ProjectileCount
{
get;
@@ -60,6 +74,23 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(0.0f, IsPropertySaveable.No, description: "The impulse applied to the physics body of the projectile (the higher the impulse, the faster the projectiles are launched). Sum of weapon + projectile.")]
public float LaunchImpulse
{
get;
set;
}
[Serialize(0.0f, IsPropertySaveable.Yes, description: "Percentage of damage mitigation ignored when hitting armored body parts (deflecting limbs). Sum of weapon + projectile."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1f)]
public float Penetration { get; private set; }
[Serialize(1f, IsPropertySaveable.Yes, description: "Weapon's damage modifier")]
public float WeaponDamageModifier
{
get;
private set;
}
[Serialize(0f, IsPropertySaveable.Yes, description: "The time required for a charge-type turret to charge up before able to fire.")]
public float MaxChargeTime
{
@@ -99,6 +130,12 @@ namespace Barotrauma.Items.Components
// TODO: should define this in xml if we have ranged weapons that don't require aim to use
item.RequireAimToUse = true;
characterUsable = true;
if (ReloadSkillRequirement > 0 && ReloadNoSkill <= reload)
{
DebugConsole.AddWarning($"Invalid XML at {item.Name}: ReloadNoSkill is lower or equal than it's reload skill, despite having ReloadSkillRequirement.");
}
InitProjSpecific(element);
}
@@ -167,7 +204,15 @@ namespace Barotrauma.Items.Components
if (currentChargeTime < MaxChargeTime) { return false; }
IsActive = true;
ReloadTimer = reload / (1 + character?.GetStatValue(StatTypes.RangedAttackSpeed) ?? 0f);
float baseReloadTime = reload;
float weaponSkill = character.GetSkillLevel("weapons");
if (ReloadSkillRequirement > 0 && ReloadNoSkill > reload && weaponSkill < ReloadSkillRequirement)
{
//Examples, assuming 40 weapon skill required: 1 - 40/40 = 0 ... 1 - 0/40 = 1 ... 1 - 20 / 40 = 0.5
float reloadFailure = MathHelper.Clamp(1 - (weaponSkill / ReloadSkillRequirement), 0, 1);
baseReloadTime = MathHelper.Lerp(reload, ReloadNoSkill, reloadFailure);
}
ReloadTimer = baseReloadTime / (1 + character?.GetStatValue(StatTypes.RangedAttackSpeed) ?? 0f);
currentChargeTime = 0f;
if (character != null)
@@ -218,15 +263,18 @@ namespace Barotrauma.Items.Components
{
lastProjectile?.Item.GetComponent<Rope>()?.Snap();
}
float damageMultiplier = 1f + item.GetQualityModifier(Quality.StatType.FirepowerMultiplier);
float damageMultiplier = (1f + item.GetQualityModifier(Quality.StatType.FirepowerMultiplier)) * WeaponDamageModifier;
projectile.Launcher = item;
projectile.Shoot(character, character.AnimController.AimSourceSimPos, barrelPos, rotation + spread, ignoredBodies: ignoredBodies.ToList(), createNetworkEvent: false, damageMultiplier);
projectile.Shoot(character, character.AnimController.AimSourceSimPos, barrelPos, rotation + spread, ignoredBodies: ignoredBodies.ToList(), createNetworkEvent: false, damageMultiplier, LaunchImpulse);
projectile.Item.GetComponent<Rope>()?.Attach(Item, projectile.Item);
if (i == 0)
if (projectile.Item.body != null)
{
Item.body.ApplyLinearImpulse(new Vector2((float)Math.Cos(projectile.Item.body.Rotation), (float)Math.Sin(projectile.Item.body.Rotation)) * Item.body.Mass * -50.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
if (i == 0)
{
Item.body.ApplyLinearImpulse(new Vector2((float)Math.Cos(projectile.Item.body.Rotation), (float)Math.Sin(projectile.Item.body.Rotation)) * Item.body.Mass * -50.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
}
projectile.Item.body.ApplyTorque(projectile.Item.body.Mass * degreeOfFailure * Rand.Range(-10.0f, 10.0f));
}
projectile.Item.body.ApplyTorque(projectile.Item.body.Mass * degreeOfFailure * Rand.Range(-10.0f, 10.0f));
Item.RemoveContained(projectile.Item);
}
LastProjectile = projectile;
@@ -636,11 +636,14 @@ namespace Barotrauma.Items.Components
float addedDetachTime = deltaTime * (1f + user.GetStatValue(StatTypes.RepairToolDeattachTimeMultiplier)) * (1f + item.GetQualityModifier(Quality.StatType.RepairToolDeattachTimeMultiplier));
levelResource.DeattachTimer += addedDetachTime;
#if CLIENT
Character.Controlled?.UpdateHUDProgressBar(
this,
targetItem.WorldPosition,
levelResource.DeattachTimer / levelResource.DeattachDuration,
GUIStyle.Red, GUIStyle.Green, "progressbar.deattaching");
if (targetItem.Prefab.ShowHealthBar)
{
Character.Controlled?.UpdateHUDProgressBar(
this,
targetItem.WorldPosition,
levelResource.DeattachTimer / levelResource.DeattachDuration,
GUIStyle.Red, GUIStyle.Green, "progressbar.deattaching");
}
#endif
FixItemProjSpecific(user, deltaTime, targetItem, showProgressBar: false);
return true;
@@ -1,17 +1,26 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
class Throwable : Holdable
{
private float throwPos;
private bool throwing, throwDone;
enum ThrowState
{
None,
Initiated,
Throwing
}
private const float ThrowAngleStart = -MathHelper.PiOver2, ThrowAngleEnd = MathHelper.PiOver2;
private float throwAngle = ThrowAngleStart;
private bool midAir;
private ThrowState throwState;
//continuous collision detection is used while the item is moving faster than this
const float ContinuousCollisionThreshold = 5.0f;
@@ -27,7 +36,6 @@ namespace Barotrauma.Items.Components
public Throwable(Item item, ContentXElement element)
: base(item, element)
{
//throwForce = ToolBox.GetAttributeFloat(element, "throwforce", 1.0f);
if (aimPos == Vector2.Zero)
{
aimPos = new Vector2(0.6f, 0.1f);
@@ -36,22 +44,21 @@ namespace Barotrauma.Items.Components
public override bool Use(float deltaTime, Character character = null)
{
return characterUsable || character == null; //We do the actual throwing in Aim because Use might be used by chems
//actual throwing logic is handled in Update
return characterUsable || character == null;
}
public override bool SecondaryUse(float deltaTime, Character character = null)
{
if (!throwDone) return false; //This should only be triggered in update
throwDone = false;
return true;
//actual throwing logic is handled in Update - SecondaryUse only triggers when the item is thrown
return false;
}
public override void Drop(Character dropper)
{
base.Drop(dropper);
throwing = false;
throwPos = 0.0f;
throwState = ThrowState.None;
throwAngle = ThrowAngleStart;
}
public override void UpdateBroken(float deltaTime, Camera cam)
@@ -100,13 +107,22 @@ namespace Barotrauma.Items.Components
return;
}
if (picker.IsKeyDown(InputType.Aim) && picker.IsKeyHit(InputType.Shoot)) { throwing = true; }
if (!picker.IsKeyDown(InputType.Aim) && !throwing) { throwPos = 0.0f; }
bool aim = picker.IsKeyDown(InputType.Aim) && picker.CanAim;
if (throwState != ThrowState.Throwing)
{
if (picker.IsKeyDown(InputType.Aim))
{
if (picker.IsKeyDown(InputType.Shoot)) { throwState = ThrowState.Initiated; }
}
else if (throwState != ThrowState.Initiated)
{
throwAngle = ThrowAngleStart;
}
}
bool aim = picker.IsKeyDown(InputType.Aim) && picker.CanAim;
if (picker.IsDead || !picker.AllowInput)
{
throwing = false;
throwState = ThrowState.None;
aim = false;
}
@@ -124,25 +140,29 @@ namespace Barotrauma.Items.Components
item.Submarine = picker.Submarine;
if (!throwing)
if (throwState != ThrowState.Throwing)
{
if (aim)
if (aim || throwState == ThrowState.Initiated)
{
throwPos = MathUtils.WrapAnglePi(System.Math.Min(throwPos + deltaTime * 5.0f, MathHelper.PiOver2));
ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, throwPos);
throwAngle = System.Math.Min(throwAngle + deltaTime * 8.0f, ThrowAngleEnd);
ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, throwAngle);
if (throwAngle >= ThrowAngleEnd && throwState == ThrowState.Initiated)
{
throwState = ThrowState.Throwing;
}
}
else
{
throwPos = 0;
throwAngle = ThrowAngleStart;
ac.HoldItem(deltaTime, item, handlePos, holdPos, Vector2.Zero, aim: false, holdAngle);
}
}
else
{
throwPos = MathUtils.WrapAnglePi(throwPos - deltaTime * 15.0f);
ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, throwPos);
throwAngle = MathUtils.WrapAnglePi(throwAngle - deltaTime * 15.0f);
ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, throwAngle);
if (throwPos < 0)
if (throwAngle < 0)
{
Vector2 throwVector = Vector2.Normalize(picker.CursorWorldPosition - picker.WorldPosition);
//throw upwards if cursor is at the position of the character
@@ -180,8 +200,7 @@ namespace Barotrauma.Items.Components
Limb rightHand = ac.GetLimb(LimbType.RightHand);
item.body.AngularVelocity = rightHand.body.AngularVelocity;
throwPos = 0;
throwDone = true;
throwAngle = ThrowAngleStart;
IsActive = true;
if (GameMain.NetworkMember is { IsServer: true })
@@ -193,7 +212,7 @@ namespace Barotrauma.Items.Components
//Stun grenades, flares, etc. all have their throw-related things handled in "onSecondaryUse"
ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, CurrentThrower, user: CurrentThrower);
}
throwing = false;
throwState = ThrowState.None;
}
}
}
@@ -113,6 +113,13 @@ namespace Barotrauma.Items.Components
private bool drawable = true;
[Serialize(PropertyConditional.Comparison.And, IsPropertySaveable.No)]
public PropertyConditional.Comparison IsActiveConditionalComparison
{
get;
set;
}
public List<PropertyConditional> IsActiveConditionals;
public bool Drawable
@@ -243,6 +250,20 @@ namespace Barotrauma.Items.Components
[Serialize(0, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
public int ManuallySelectedSound { get; private set; }
/// <summary>
/// Can be used by status effects or conditionals to the speed of the item
/// </summary>
public float Speed
{
get
{
return item.Speed;
}
}
public readonly bool InheritStatusEffects;
public ItemComponent(Item item, ContentXElement element)
{
this.item = item;
@@ -303,6 +324,7 @@ namespace Barotrauma.Items.Components
string inheritStatusEffectsFrom = element.GetAttributeString("inheritstatuseffectsfrom", "");
if (!string.IsNullOrEmpty(inheritStatusEffectsFrom))
{
InheritStatusEffects = true;
var component = item.Components.Find(ic => ic.Name.Equals(inheritStatusEffectsFrom, StringComparison.OrdinalIgnoreCase));
if (component == null)
{
@@ -799,7 +821,14 @@ namespace Barotrauma.Items.Components
}
else
{
hasRequiredItems = itemList.Any(Predicate);
if (itemList.Any(Predicate))
{
hasRequiredItems = !relatedItem.RequireEmpty;
}
else
{
hasRequiredItems = relatedItem.MatchOnEmpty || relatedItem.RequireEmpty;
}
if (!hasRequiredItems)
{
shouldBreak = true;
@@ -816,7 +845,7 @@ namespace Barotrauma.Items.Components
}
}
public void ApplyStatusEffects(ActionType type, float deltaTime, Character character = null, Limb targetLimb = null, Entity useTarget = null, Character user = null, Vector2? worldPosition = null, float afflictionMultiplier = 1.0f, float applyOnUserFraction = 0.0f)
public void ApplyStatusEffects(ActionType type, float deltaTime, Character character = null, Limb targetLimb = null, Entity useTarget = null, Character user = null, Vector2? worldPosition = null, float afflictionMultiplier = 1.0f)
{
if (statusEffectLists == null) { return; }
@@ -830,11 +859,6 @@ namespace Barotrauma.Items.Components
if (user != null) { effect.SetUser(user); }
effect.AfflictionMultiplier = afflictionMultiplier;
item.ApplyStatusEffect(effect, type, deltaTime, character, targetLimb, useTarget, isNetworkEvent: false, checkCondition: false, worldPosition);
if (user != null && applyOnUserFraction > 0.0f && effect.HasTargetType(StatusEffect.TargetType.Character))
{
effect.AfflictionMultiplier = applyOnUserFraction;
item.ApplyStatusEffect(effect, type, deltaTime, user, targetLimb == null ? null : user.AnimController.GetLimb(targetLimb.type), useTarget, false, false, worldPosition);
}
effect.AfflictionMultiplier = 1.0f;
reducesCondition |= effect.ReducesItemCondition();
}
@@ -47,6 +47,13 @@ namespace Barotrauma.Items.Components
{
return ContainableItems == null || ContainableItems.Count == 0 || ContainableItems.Any(c => c.MatchesItem(itemPrefab));
}
public bool MatchesItem(Identifier identifierOrTag)
{
return
ContainableItems == null || ContainableItems.Count == 0 ||
ContainableItems.Any(c => c.Identifiers.Contains(identifierOrTag) && !c.ExcludedIdentifiers.Contains(identifierOrTag));
}
}
public readonly NamedEvent<ItemContainer> OnContainedItemsChanged = new NamedEvent<ItemContainer>();
@@ -65,8 +72,16 @@ namespace Barotrauma.Items.Components
public int Capacity
{
get { return capacity; }
set { capacity = Math.Max(value, 0); }
private set
{
capacity = Math.Max(value, 0);
MainContainerCapacity = value;
}
}
/// <summary>
/// The capacity of the main container without taking the sub containers into account. Only differs when there's a sub container defined for the component.
/// </summary>
public int MainContainerCapacity { get; private set; }
//how many items can be contained
private int maxStackSize;
@@ -99,7 +114,7 @@ namespace Barotrauma.Items.Components
[Serialize(100, IsPropertySaveable.No, description: "How many items are placed in a row before starting a new row.")]
public int ItemsPerRow { get; set; }
[Serialize(true, IsPropertySaveable.No, description: "Should the inventory of this item be visible when the item is selected.")]
[Serialize(true, IsPropertySaveable.No, description: "Should the contents in the item's inventory be visible? Disabled on items like magazines that spawn the contents as needed.")]
public bool DrawInventory
{
get;
@@ -127,9 +142,6 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(true, IsPropertySaveable.No)]
public bool AllowAccess { get; set; }
[Serialize(false, IsPropertySaveable.No)]
public bool AccessOnlyWhenBroken { get; set; }
@@ -229,6 +241,9 @@ namespace Barotrauma.Items.Components
public ImmutableHashSet<Identifier> ContainableItemIdentifiers => containableItemIdentifiers;
public List<RelatedItem> ContainableItems { get; }
public List<RelatedItem> AllSubContainableItems { get; }
public readonly bool HasSubContainers;
public ItemContainer(Item item, ContentXElement element)
: base(item, element)
@@ -251,6 +266,7 @@ namespace Barotrauma.Items.Components
break;
case "subcontainer":
totalCapacity += subElement.GetAttributeInt("capacity", 1);
HasSubContainers = true;
break;
}
}
@@ -270,7 +286,7 @@ namespace Barotrauma.Items.Components
int subCapacity = subElement.GetAttributeInt("capacity", 1);
int subMaxStackSize = subElement.GetAttributeInt("maxstacksize", maxStackSize);
List<RelatedItem> subContainableItems = null;
var subContainableItems = new List<RelatedItem>();
foreach (var subSubElement in subElement.Elements())
{
if (subSubElement.Name.ToString().ToLowerInvariant() != "containable") { continue; }
@@ -281,8 +297,9 @@ namespace Barotrauma.Items.Components
DebugConsole.ThrowError("Error in item config \"" + item.ConfigFilePath + "\" - containable with no identifiers.");
continue;
}
subContainableItems ??= new List<RelatedItem>();
subContainableItems.Add(containable);
AllSubContainableItems ??= new List<RelatedItem>();
AllSubContainableItems.Add(containable);
}
for (int i = subContainerIndex; i < subContainerIndex + subCapacity; i++)
@@ -357,6 +374,14 @@ namespace Barotrauma.Items.Components
//no need to Update() if this item has no statuseffects and no physics body
IsActive = activeContainedItems.Count > 0 || Inventory.AllItems.Any(it => it.body != null);
if (IsActive && item.GetRootInventoryOwner() is Character owner &&
owner.HasEquippedItem(item, predicate: slot => slot.HasFlag(InvSlotType.LeftHand) || slot.HasFlag(InvSlotType.RightHand)))
{
// Set the contained items active if there's an item inserted inside the container. Enables e.g. the rifle flashlight when it's attached to the rifle (put inside of it).
SetContainedActive(true);
}
CharacterHUD.RecreateHudTextsIfFocused(item, containedItem);
OnContainedItemsChanged.Invoke(this);
}
@@ -368,9 +393,9 @@ namespace Barotrauma.Items.Components
public void OnItemRemoved(Item containedItem)
{
activeContainedItems.RemoveAll(i => i.Item == containedItem);
//deactivate if the inventory is empty
IsActive = activeContainedItems.Count > 0 || Inventory.AllItems.Any(it => it.body != null);
CharacterHUD.RecreateHudTextsIfFocused(item, containedItem);
OnContainedItemsChanged.Invoke(this);
}
@@ -409,6 +434,20 @@ namespace Barotrauma.Items.Components
return false;
}
public override void FlipX(bool relativeToSub)
{
base.FlipX(relativeToSub);
if (HideItems) { return; }
if (item.body == null) { return; }
foreach (Item containedItem in Inventory.AllItems)
{
if (containedItem.body != null && containedItem.body.Enabled && containedItem.body.Dir != item.body.Dir)
{
containedItem.FlipX(relativeToSub);
}
}
}
public override void Update(float deltaTime, Camera cam)
{
if (!string.IsNullOrEmpty(SpawnWithId) && !alwaysContainedItemsSpawned)
@@ -441,6 +480,7 @@ namespace Barotrauma.Items.Components
{
foreach (Item item in Inventory.AllItemsMod)
{
item.ApplyStatusEffects(ActionType.OnSuccess, 1.0f, ownerCharacter);
item.ApplyStatusEffects(ActionType.OnUse, 1.0f, ownerCharacter);
item.GetComponent<GeneticMaterial>()?.Equip(ownerCharacter);
autoInjectCooldown = AutoInjectInterval;
@@ -477,7 +517,7 @@ namespace Barotrauma.Items.Components
effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
targets.Clear();
targets.AddRange(effect.GetNearbyTargets(item.WorldPosition, targets));
effect.AddNearbyTargets(item.WorldPosition, targets);
effect.Apply(ActionType.OnActive, deltaTime, item, targets);
}
}
@@ -494,12 +534,12 @@ namespace Barotrauma.Items.Components
public override bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg = null)
{
return AllowAccess && (!AccessOnlyWhenBroken || Item.Condition <= 0) && base.HasRequiredItems(character, addMessage, msg);
return DrawInventory && (!AccessOnlyWhenBroken || Item.Condition <= 0) && base.HasRequiredItems(character, addMessage, msg);
}
public override bool Select(Character character)
{
if (!AllowAccess) { return false; }
if (!DrawInventory) { return false; }
if (item.Container != null) { return false; }
if (AccessOnlyWhenBroken)
{
@@ -535,7 +575,7 @@ namespace Barotrauma.Items.Components
public override bool Pick(Character picker)
{
if (!AllowAccess) { return false; }
if (!DrawInventory) { return false; }
if (AccessOnlyWhenBroken)
{
if (item.Condition > 0)
@@ -582,11 +622,65 @@ namespace Barotrauma.Items.Components
public override void Drop(Character dropper)
{
IsActive = true;
SetContainedActive(false);
}
public override void Equip(Character character)
{
IsActive = true;
if (character != null && character.HasEquippedItem(item, predicate: slot => slot.HasFlag(InvSlotType.LeftHand) || slot.HasFlag(InvSlotType.RightHand)))
{
SetContainedActive(true);
}
}
private void SetContainedActive(bool active)
{
foreach (Item containedItem in Inventory.AllItems)
{
RelatedItem containableItem = FindContainableItem(containedItem);
if (containableItem != null && containableItem.SetActive)
{
foreach (var ic in containedItem.Components)
{
ic.IsActive = active;
}
if (containedItem.body != null)
{
containedItem.body.Enabled = active;
if (active)
{
containedItem.body.PhysEnabled = false;
}
}
}
}
if (active)
{
FlipX(false);
}
}
private RelatedItem FindContainableItem(Item item)
{
var relatedItem = ContainableItems?.FirstOrDefault(ci => ci.MatchesItem(item));
if (relatedItem == null && AllSubContainableItems != null)
{
relatedItem = AllSubContainableItems.FirstOrDefault(ci => ci.MatchesItem(item));
}
return relatedItem;
}
/// <summary>
/// Returns the index of the first slot whose restrictions match the specified tag or identifier
/// </summary>
public int? FindSuitableSubContainerIndex(Identifier itemTagOrIdentifier)
{
for (int i = 0; i < slotRestrictions.Length; i++)
{
if (slotRestrictions[i].MatchesItem(itemTagOrIdentifier)) { return i; }
}
return null;
}
public override void ReceiveSignal(Signal signal, Connection connection)
@@ -604,6 +698,7 @@ namespace Barotrauma.Items.Components
}
}
#warning There's some code duplication here and in DrawContainedItems() method, but it's not straightforward to get rid of it, because of slightly different logic and the usage of draw positions vs. positions etc. Should probably be splitted into smaller methods.
public void SetContainedItemPositions()
{
Vector2 transformedItemPos = ItemPos * item.Scale;
@@ -657,29 +752,70 @@ namespace Barotrauma.Items.Components
transformedItemIntervalHorizontal = Vector2.Transform(transformedItemIntervalHorizontal, transform);
transformedItemPos += item.Position;
}
}
float currentRotation = itemRotation;
if (item.body != null)
{
currentRotation *= item.body.Dir;
currentRotation += item.body.Rotation;
}
else
{
currentRotation += -item.RotationRad;
}
int i = 0;
Vector2 currentItemPos = transformedItemPos;
foreach (Item contained in Inventory.AllItems)
{
Vector2 itemPos = currentItemPos;
var relatedItem = FindContainableItem(contained);
if (relatedItem != null)
{
if (relatedItem.Hide.HasValue && relatedItem.Hide.Value) { continue; }
if (relatedItem.ItemPos.HasValue)
{
Vector2 pos = relatedItem.ItemPos.Value;
if (item.body != null)
{
Matrix transform = Matrix.CreateRotationZ(item.body.Rotation);
pos.X *= item.body.Dir;
itemPos = Vector2.Transform(pos, transform) + item.body.Position;
}
else
{
itemPos = pos;
// This code is aped based on above. Not tested.
if (item.FlippedX)
{
itemPos.X = -itemPos.X;
itemPos.X += item.Rect.Width;
}
if (item.FlippedY)
{
itemPos.Y = -itemPos.Y;
itemPos.Y -= item.Rect.Height;
}
itemPos += new Vector2(item.Rect.X, item.Rect.Y);
if (Math.Abs(item.RotationRad) > 0.01f)
{
Matrix transform = Matrix.CreateRotationZ(item.RotationRad);
itemPos = Vector2.Transform(itemPos - item.Position, transform) + item.Position;
}
}
}
}
if (contained.body != null)
{
try
{
Vector2 simPos = ConvertUnits.ToSimUnits(currentItemPos);
contained.body.FarseerBody.SetTransformIgnoreContacts(ref simPos, currentRotation);
Vector2 simPos = ConvertUnits.ToSimUnits(itemPos);
float rotation = itemRotation;
if (relatedItem != null && relatedItem.Rotation != 0)
{
rotation = MathHelper.ToRadians(relatedItem.Rotation);
}
if (item.body != null)
{
rotation *= item.body.Dir;
rotation += item.body.Rotation;
}
else
{
rotation += -item.RotationRad;
}
contained.body.FarseerBody.SetTransformIgnoreContacts(ref simPos, rotation);
contained.body.SetPrevTransform(contained.body.SimPosition, contained.body.Rotation);
contained.body.UpdateDrawPosition();
}
@@ -695,8 +831,8 @@ namespace Barotrauma.Items.Components
contained.Rect =
new Rectangle(
(int)(currentItemPos.X - contained.Rect.Width / 2.0f),
(int)(currentItemPos.Y + contained.Rect.Height / 2.0f),
(int)(itemPos.X - contained.Rect.Width / 2.0f),
(int)(itemPos.Y + contained.Rect.Height / 2.0f),
contained.Rect.Width, contained.Rect.Height);
contained.Submarine = item.Submarine;
@@ -450,6 +450,7 @@ namespace Barotrauma.Items.Components
public override bool Select(Character activator)
{
if (activator == null || activator.Removed) { return false; }
if (Item.Condition <= 0.0f && !UpdateWhenInactive) { return false; }
if (UsableIn == UseEnvironment.Water && !activator.AnimController.InWater ||
UsableIn == UseEnvironment.Air && activator.AnimController.InWater)
@@ -104,12 +104,14 @@ namespace Barotrauma.Items.Components
// doesn't quite work properly, remaining time changes if tinkering stops
float deconstructionSpeedModifier = userDeconstructorSpeedMultiplier * (1f + tinkeringStrength * TinkeringSpeedIncrease);
float deconstructionSpeed = item.StatManager.GetAdjustedValue(ItemTalentStats.DeconstructorSpeed, DeconstructionSpeed);
if (DeconstructItemsSimultaneously)
{
float deconstructTime = 0.0f;
foreach (Item targetItem in inputContainer.Inventory.AllItems)
{
deconstructTime += targetItem.Prefab.DeconstructTime / (DeconstructionSpeed * deconstructionSpeedModifier);
deconstructTime += targetItem.Prefab.DeconstructTime / (deconstructionSpeed * deconstructionSpeedModifier);
}
progressState = Math.Min(progressTimer / deconstructTime, 1.0f);
@@ -139,7 +141,7 @@ namespace Barotrauma.Items.Components
if (targetItem == null) { return; }
var validDeconstructItems = targetItem.Prefab.DeconstructItems.Where(it => it.IsValidDeconstructor(item)).ToList();
float deconstructTime = validDeconstructItems.Any() ? targetItem.Prefab.DeconstructTime / (DeconstructionSpeed * deconstructionSpeedModifier) : 1.0f;
float deconstructTime = validDeconstructItems.Any() ? targetItem.Prefab.DeconstructTime / (deconstructionSpeed * deconstructionSpeedModifier) : 1.0f;
progressState = Math.Min(progressTimer / deconstructTime, 1.0f);
if (progressTimer > deconstructTime)
@@ -218,7 +220,7 @@ namespace Barotrauma.Items.Components
if (percentageHealth < deconstructProduct.MinCondition || percentageHealth > deconstructProduct.MaxCondition) { return; }
if (!(MapEntityPrefab.Find(null, deconstructProduct.ItemIdentifier) is ItemPrefab itemPrefab))
if (MapEntityPrefab.FindByIdentifier(deconstructProduct.ItemIdentifier) is not ItemPrefab itemPrefab)
{
DebugConsole.ThrowError("Tried to deconstruct item \"" + targetItem.Name + "\" but couldn't find item prefab \"" + deconstructProduct.ItemIdentifier + "\"!");
return;
@@ -460,6 +462,12 @@ namespace Barotrauma.Items.Components
progressTimer = 0.0f;
progressState = 0.0f;
}
#if CLIENT
else
{
HintManager.OnStartDeconstructing(user, this);
}
#endif
inputContainer.Inventory.Locked = IsActive;
}
@@ -30,11 +30,8 @@ namespace Barotrauma.Items.Components
Serialize(500.0f, IsPropertySaveable.Yes, description: "The amount of force exerted on the submarine when the engine is operating at full power.")]
public float MaxForce
{
get { return maxForce; }
set
{
maxForce = Math.Max(0.0f, value);
}
get => maxForce;
set => maxForce = Math.Max(0.0f, value);
}
[Editable, Serialize("0.0,0.0", IsPropertySaveable.Yes,
@@ -94,7 +91,7 @@ namespace Barotrauma.Items.Components
}
partial void InitProjSpecific(ContentXElement element);
public override void Update(float deltaTime, Camera cam)
{
UpdateOnActiveEffects(deltaTime);
@@ -129,12 +126,14 @@ namespace Barotrauma.Items.Components
{
forceMultiplier *= MathHelper.Lerp(0.5f, 2.0f, (float)Math.Sqrt(User.GetSkillLevel("helm") / 100));
}
currForce *= maxForce * forceMultiplier;
if (item.GetComponent<Repairable>() is Repairable repairable && repairable.IsTinkering)
currForce *= item.StatManager.GetAdjustedValue(ItemTalentStats.EngineMaxSpeed, MaxForce) * forceMultiplier;
if (item.GetComponent<Repairable>() is { IsTinkering: true } repairable)
{
currForce *= 1f + repairable.TinkeringStrength * TinkeringForceIncrease;
}
currForce = item.StatManager.GetAdjustedValue(ItemTalentStats.EngineSpeed, currForce);
//less effective when in a bad condition
currForce *= MathHelper.Lerp(0.5f, 2.0f, condition);
if (item.Submarine.FlippedX) { currForce *= -1; }
@@ -15,6 +15,8 @@ namespace Barotrauma.Items.Components
{
private ImmutableDictionary<uint, FabricationRecipe> fabricationRecipes; //this is not readonly because tutorials fuck this up!!!!
private const int MaxAmountToFabricate = 99;
private FabricationRecipe fabricatedItem;
private float timeUntilReady;
private float requiredTime;
@@ -39,6 +41,16 @@ namespace Barotrauma.Items.Components
[Serialize(1.0f, IsPropertySaveable.Yes)]
public float SkillRequirementMultiplier { get; set; }
private int amountToFabricate;
[Serialize(1, IsPropertySaveable.Yes)]
public int AmountToFabricate
{
get { return amountToFabricate; }
set { amountToFabricate = MathHelper.Clamp(value, 1, MaxAmountToFabricate); }
}
private int amountRemaining;
private const float TinkeringSpeedIncrease = 2.5f;
private enum FabricatorState
@@ -89,7 +101,7 @@ namespace Barotrauma.Items.Components
{
DebugConsole.ThrowError("Error in item " + item.Name + "! Fabrication recipes should be defined in the craftable item's xml, not in the fabricator.");
break;
}
}
}
var fabricationRecipes = new Dictionary<uint, FabricationRecipe>();
@@ -104,6 +116,18 @@ namespace Barotrauma.Items.Components
continue;
}
}
bool recipeInvalid = false;
foreach (var requiredItem in recipe.RequiredItems)
{
if (requiredItem.ItemPrefabs.None())
{
DebugConsole.ThrowError($"Error in the fabrication recipe for \"{itemPrefab.Name}\". Could not find the ingredient \"{requiredItem}\".");
recipeInvalid = true;
}
}
if (recipeInvalid) { continue; }
fabricationRecipes.Add(recipe.RecipeHash, recipe);
if (recipe.FabricationLimitMax >= 0)
{
@@ -171,16 +195,20 @@ namespace Barotrauma.Items.Components
if (selectedItem == null) { return; }
if (!outputContainer.Inventory.CanBePut(selectedItem.TargetItem, selectedItem.OutCondition * selectedItem.TargetItem.Health)) { return; }
#if CLIENT
itemList.Enabled = false;
activateButton.Text = TextManager.Get("FabricatorCancel");
#endif
IsActive = true;
this.user = user;
fabricatedItem = selectedItem;
RefreshAvailableIngredients();
#if CLIENT
itemList.Enabled = false;
if (amountInput != null)
{
amountInput.Enabled = false;
}
RefreshActivateButtonText();
#endif
bool isClient = GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient;
if (!isClient)
{
@@ -237,10 +265,11 @@ namespace Barotrauma.Items.Components
}
#elif CLIENT
itemList.Enabled = true;
if (activateButton != null)
if (amountInput != null)
{
activateButton.Text = TextManager.Get(CreateButtonText);
amountInput.Enabled = true;
}
RefreshActivateButtonText();
#endif
fabricatedItem = null;
}
@@ -356,9 +385,10 @@ namespace Barotrauma.Items.Components
bool ingredientsStolen = false;
bool ingredientsAllowStealing = true;
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
if (GameMain.NetworkMember is null || GameMain.NetworkMember.IsServer)
{
fabricatedItem.RequiredItems.ForEach(requiredItem =>
List<Item> foundAvailableItems = new List<Item>();
foreach (FabricationRecipe.RequiredItem requiredItem in fabricatedItem.RequiredItems)
{
for (int usedPrefabsAmount = 0; usedPrefabsAmount < requiredItem.Amount; usedPrefabsAmount++)
{
@@ -367,10 +397,7 @@ namespace Barotrauma.Items.Components
if (!availableIngredients.ContainsKey(requiredPrefab.Identifier)) { continue; }
var availableItems = availableIngredients[requiredPrefab.Identifier];
var availableItem = availableItems.FirstOrDefault(potentialPrefab =>
{
return requiredItem.IsConditionSuitable(potentialPrefab.ConditionPercentage);
});
var availableItem = availableItems.FirstOrDefault(potentialPrefab => requiredItem.IsConditionSuitable(potentialPrefab.ConditionPercentage));
if (availableItem == null) { continue; }
@@ -401,13 +428,21 @@ namespace Barotrauma.Items.Components
}
}
foundAvailableItems.Add(availableItem);
availableItems.Remove(availableItem);
Entity.Spawner.AddItemToRemoveQueue(availableItem);
inputContainer.Inventory.RemoveItem(availableItem);
break;
}
}
});
}
var fabricationIngredients = new AbilityFabricationItemIngredients(foundAvailableItems);
user?.CheckTalents(AbilityEffectType.OnItemFabricatedIngredients, fabricationIngredients);
foreach (Item availableItem in fabricationIngredients.Items)
{
Entity.Spawner.AddItemToRemoveQueue(availableItem);
inputContainer.Inventory.RemoveItem(availableItem);
}
int amountFittingContainer = outputContainer.Inventory.HowManyCanBePut(fabricatedItem.TargetItem, fabricatedItem.OutCondition * fabricatedItem.TargetItem.Health);
@@ -500,20 +535,16 @@ namespace Barotrauma.Items.Components
}
}
//disabled "continuous fabrication" for now
//before we enable it, there should be some UI controls for fabricating a specific number of items
/*var prevFabricatedItem = fabricatedItem;
var prevFabricatedItem = fabricatedItem;
var prevUser = user;
CancelFabricating();
if (CanBeFabricated(prevFabricatedItem))
amountRemaining--;
if (amountRemaining > 0 && CanBeFabricated(prevFabricatedItem, availableIngredients, prevUser))
{
//keep fabricating if we can fabricate more
StartFabricating(prevFabricatedItem, prevUser, addToServerLog: false);
}*/
CancelFabricating();
}
}
}
@@ -535,12 +566,13 @@ namespace Barotrauma.Items.Components
return currPowerConsumption;
}
private int GetFabricatedItemQuality(FabricationRecipe fabricatedItem, Character user)
private static int GetFabricatedItemQuality(FabricationRecipe fabricatedItem, Character user)
{
if (user == null) { return 0; }
if (user?.Info == null) { return 0; }
if (fabricatedItem.TargetItem.ConfigElement.GetChildElement("Quality") == null) { return 0; }
int quality = 0;
float floatQuality = 0.0f;
floatQuality += user.GetStatValue(StatTypes.IncreaseFabricationQuality, includeSaved: false);
foreach (var tag in fabricatedItem.TargetItem.Tags)
{
floatQuality += user.Info.GetSavedStatValue(StatTypes.IncreaseFabricationQuality, tag);
@@ -571,11 +603,25 @@ namespace Barotrauma.Items.Components
}
partial void UpdateRequiredTimeProjSpecific();
private static bool AnyOneHasRecipeForItem(Character user, ItemPrefab item)
{
return
(user != null && user.HasRecipeForItem(item.Identifier)) ||
GameSession.GetSessionCrewCharacters(CharacterType.Bot).Any(c => c.HasRecipeForItem(item.Identifier));
}
private bool CanBeFabricated(FabricationRecipe fabricableItem, IReadOnlyDictionary<Identifier, List<Item>> availableIngredients, Character character)
{
if (fabricableItem == null) { return false; }
if (fabricableItem.RequiresRecipe && (character == null || !character.HasRecipeForItem(fabricableItem.TargetItem.Identifier))) { return false; }
if (fabricableItem.RequiresRecipe)
{
if (character == null) { return false; }
if (!AnyOneHasRecipeForItem(character, fabricableItem.TargetItem))
{
return false;
}
}
if (fabricableItem.RequiredMoney > 0)
{
@@ -637,9 +683,15 @@ namespace Barotrauma.Items.Components
//fabricating takes 100 times longer if degree of success is close to 0
//characters with a higher skill than required can fabricate up to 100% faster
return fabricableItem.RequiredTime / FabricationSpeed / MathHelper.Clamp(t, 0.01f, 2.0f);
float time = fabricableItem.RequiredTime / item.StatManager.GetAdjustedValue(ItemTalentStats.FabricationSpeed, FabricationSpeed) / MathHelper.Clamp(t, 0.01f, 2.0f);
if (user?.Info is { } info && fabricableItem.TargetItem is { } it)
{
time /= 1f + it.Tags.Sum(tag => info.GetSavedStatValue(StatTypes.FabricationSpeed, tag));
}
return time;
}
public float FabricationDegreeOfSuccess(Character character, ImmutableArray<Skill> skills)
{
if (skills.Length == 0) { return 1.0f; }
@@ -700,7 +752,7 @@ namespace Barotrauma.Items.Components
itemList.AddRange(container.Inventory.AllItems);
}
}
if (user?.Inventory != null)
if (user?.Inventory != null && user.SelectedItem == item)
{
itemList.AddRange(user.Inventory.AllItems);
linkedInventories.Add(user.Inventory);
@@ -713,7 +765,31 @@ namespace Barotrauma.Items.Components
{
availableIngredients[itemIdentifier] = new List<Item>(itemList.Count);
}
availableIngredients[itemIdentifier].Add(item);
//order by condition (prefer using worst-condition items)
int index = 0;
while (index < availableIngredients[itemIdentifier].Count &&
compare(item, availableIngredients[itemIdentifier][index], inputContainer.Inventory) < 0)
{
index++;
}
static int compare(Item item1, Item item2, Inventory inputInventory)
{
bool item1InInputInventory = item1.ParentInventory == inputInventory;
bool item2InInputInventory = item2.ParentInventory == inputInventory;
//prefer items in the input inventory
if (item1InInputInventory != item2InInputInventory)
{
return item1InInputInventory ? 1 : -1;
}
else
{
//prefer items in worse condition
return Math.Sign(item2.Condition - item1.Condition);
}
}
availableIngredients[itemIdentifier].Insert(index, item);
}
}
@@ -827,5 +903,15 @@ namespace Barotrauma.Items.Components
public float Value { get; set; }
public ItemPrefab ItemPrefab { get; set; }
}
internal sealed class AbilityFabricationItemIngredients : AbilityObject
{
public List<Item> Items { get; set; }
public AbilityFabricationItemIngredients(List<Item> items)
{
Items = items;
}
}
}
}
@@ -15,6 +15,8 @@ namespace Barotrauma.Items.Components
public float? ReceivedOxygenAmount,
ReceivedWaterAmount;
public double LastOxygenDataTime, LastWaterDataTime;
public readonly HashSet<IdCard> Cards = new HashSet<IdCard>();
public bool Distort;
@@ -23,12 +25,8 @@ namespace Barotrauma.Items.Components
public List<Hull> LinkedHulls = new List<Hull>();
}
private DateTime resetDataTime;
private bool hasPower;
private readonly Dictionary<Hull, HullData> hullDatas;
[Editable, Serialize(false, IsPropertySaveable.Yes, description: "Does the machine require inputs from water detectors in order to show the water levels inside rooms.")]
public bool RequireWaterDetectors
{
@@ -75,7 +73,6 @@ namespace Barotrauma.Items.Components
: base(item, element)
{
IsActive = true;
hullDatas = new Dictionary<Hull, HullData>();
InitProjSpecific();
}
@@ -83,37 +80,6 @@ namespace Barotrauma.Items.Components
public override void Update(float deltaTime, Camera cam)
{
//periodically reset all hull data
//(so that outdated hull info won't be shown if detectors stop sending signals)
if (DateTime.Now > resetDataTime)
{
foreach (HullData hullData in hullDatas.Values)
{
if (!hullData.Distort)
{
hullData.ReceivedOxygenAmount = null;
hullData.ReceivedWaterAmount = null;
}
}
resetDataTime = DateTime.Now + new TimeSpan(0, 0, 1);
}
#if CLIENT
if (cardRefreshTimer > cardRefreshDelay)
{
if (item.Submarine is { } sub)
{
UpdateIDCards(sub);
}
cardRefreshTimer = 0;
}
else
{
cardRefreshTimer += deltaTime;
}
#endif
hasPower = Voltage > MinVoltage;
if (hasPower)
{
@@ -138,65 +104,5 @@ namespace Barotrauma.Items.Components
{
return picker != null;
}
public override void ReceiveSignal(Signal signal, Connection connection)
{
Item source = signal.source;
if (source == null || source.CurrentHull == null) { return; }
Hull sourceHull = source.CurrentHull;
if (!hullDatas.TryGetValue(sourceHull, out HullData hullData))
{
hullData = new HullData();
hullDatas.Add(sourceHull, hullData);
}
if (hullData.Distort) { return; }
switch (connection.Name)
{
case "water_data_in":
//cheating a bit because water detectors don't actually send the water level
bool fromWaterDetector = source.GetComponent<WaterDetector>() != null;
hullData.ReceivedWaterAmount = null;
if (fromWaterDetector)
{
hullData.ReceivedWaterAmount = WaterDetector.GetWaterPercentage(sourceHull);
}
foreach (var linked in sourceHull.linkedTo)
{
if (!(linked is Hull linkedHull)) { continue; }
if (!hullDatas.TryGetValue(linkedHull, out HullData linkedHullData))
{
linkedHullData = new HullData();
hullDatas.Add(linkedHull, linkedHullData);
}
linkedHullData.ReceivedWaterAmount = null;
if (fromWaterDetector)
{
linkedHullData.ReceivedWaterAmount = WaterDetector.GetWaterPercentage(linkedHull);
}
}
break;
case "oxygen_data_in":
if (!float.TryParse(signal.value, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float oxy))
{
oxy = Rand.Range(0.0f, 100.0f);
}
hullData.ReceivedOxygenAmount = oxy;
foreach (var linked in sourceHull.linkedTo)
{
if (!(linked is Hull linkedHull)) { continue; }
if (!hullDatas.TryGetValue(linkedHull, out HullData linkedHullData))
{
linkedHullData = new HullData();
hullDatas.Add(linkedHull, linkedHullData);
}
linkedHullData.ReceivedOxygenAmount = oxy;
}
break;
}
}
}
}
@@ -57,8 +57,8 @@ namespace Barotrauma.Items.Components
[Editable, Serialize(80.0f, IsPropertySaveable.No, description: "How fast the item pumps water in/out when operating at 100%.", alwaysUseInstanceValues: true)]
public float MaxFlow
{
get { return maxFlow; }
set { maxFlow = value; }
get => maxFlow;
set => maxFlow = value;
}
[Editable, Serialize(true, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
@@ -92,13 +92,16 @@ namespace Barotrauma.Items.Components
}
partial void InitProjSpecific(ContentXElement element);
public override void Update(float deltaTime, Camera cam)
{
pumpSpeedLockTimer -= deltaTime;
isActiveLockTimer -= deltaTime;
if (!IsActive) { return; }
if (!IsActive)
{
return;
}
currFlow = 0.0f;
@@ -122,7 +125,10 @@ namespace Barotrauma.Items.Components
FlowPercentage = ((float)TargetLevel - hullPercentage) * 10.0f;
}
if (!HasPower) { return; }
if (!HasPower)
{
return;
}
UpdateProjSpecific(deltaTime);
@@ -132,13 +138,15 @@ namespace Barotrauma.Items.Components
float powerFactor = Math.Min(currPowerConsumption <= 0.0f || MinVoltage <= 0.0f ? 1.0f : Voltage, MaxOverVoltageFactor);
currFlow = flowPercentage / 100.0f * maxFlow * powerFactor;
currFlow = flowPercentage / 100.0f * item.StatManager.GetAdjustedValue(ItemTalentStats.PumpMaxFlow, MaxFlow) * powerFactor;
if (item.GetComponent<Repairable>() is Repairable repairable && repairable.IsTinkering)
if (item.GetComponent<Repairable>() is { IsTinkering: true } repairable)
{
currFlow *= 1f + repairable.TinkeringStrength * TinkeringSpeedIncrease;
}
currFlow = item.StatManager.GetAdjustedValue(ItemTalentStats.PumpSpeed, currFlow);
//less effective when in a bad condition
currFlow *= MathHelper.Lerp(0.5f, 1.0f, item.Condition / item.MaxCondition);
@@ -11,7 +11,7 @@ namespace Barotrauma.Items.Components
{
const float NetworkUpdateIntervalHigh = 0.5f;
const float TemperatureBoostAmount = 20;
const float TemperatureBoostAmount = 25;
//the rate at which the reactor is being run on (higher rate -> higher temperature)
private float fissionRate;
@@ -26,10 +26,6 @@ namespace Barotrauma.Items.Components
//amount of power generated balanced with the load)
private bool autoTemp;
//automatical adjustment to the power output when
//turbine output and temperature are in the optimal range
private float autoAdjustAmount;
private float fuelConsumptionRate;
private float meltDownTimer, meltDownDelay;
@@ -53,6 +49,8 @@ namespace Barotrauma.Items.Components
private float temperatureBoost;
public bool AllowTemperatureBoost => Math.Abs(temperatureBoost) < TemperatureBoostAmount * 0.9f;
private bool _powerOn;
[Serialize(defaultValue: false, isSaveable: IsPropertySaveable.Yes)]
@@ -95,15 +93,12 @@ namespace Barotrauma.Items.Components
}
}
}
[Editable(0.0f, float.MaxValue), Serialize(10000.0f, IsPropertySaveable.Yes, description: "How much power (kW) the reactor generates when operating at full capacity.", alwaysUseInstanceValues: true)]
public float MaxPowerOutput
{
get { return maxPowerOutput; }
set
{
maxPowerOutput = Math.Max(0.0f, value);
}
get => maxPowerOutput;
set => maxPowerOutput = Math.Max(0.0f, value);
}
[Editable(0.0f, float.MaxValue), Serialize(120.0f, IsPropertySaveable.Yes, description: "How long the temperature has to stay critical until a meltdown occurs.")]
@@ -152,11 +147,11 @@ namespace Barotrauma.Items.Components
turbineOutput = MathHelper.Clamp(value, 0.0f, 100.0f);
}
}
[Serialize(0.2f, IsPropertySaveable.Yes, description: "How fast the condition of the contained fuel rods deteriorates per second."), Editable(0.0f, 1000.0f, decimals: 3)]
public float FuelConsumptionRate
{
get { return fuelConsumptionRate; }
get => fuelConsumptionRate;
set
{
if (!MathUtils.IsValid(value)) return;
@@ -256,6 +251,8 @@ namespace Barotrauma.Items.Components
}
#endif
float maxPowerOut = GetMaxOutput();
if (signalControlledTargetFissionRate.HasValue && lastReceivedFissionRateSignalTime > Timing.TotalTime - 1)
{
TargetFissionRate = adjustValueWithoutOverShooting(TargetFissionRate, signalControlledTargetFissionRate.Value, deltaTime * 5.0f);
@@ -289,9 +286,9 @@ namespace Barotrauma.Items.Components
//use a smoothed "correct output" instead of the actual correct output based on the load
//so the player doesn't have to keep adjusting the rate impossibly fast when the load fluctuates heavily
if (!MathUtils.NearlyEqual(MaxPowerOutput, 0.0f))
if (!MathUtils.NearlyEqual(maxPowerOut, 0.0f))
{
CorrectTurbineOutput += MathHelper.Clamp((Load / MaxPowerOutput * 100.0f) - CorrectTurbineOutput, -20.0f, 20.0f) * deltaTime;
CorrectTurbineOutput += MathHelper.Clamp((Load / maxPowerOut * 100.0f) - CorrectTurbineOutput, -20.0f, 20.0f) * deltaTime;
}
//calculate tolerances of the meters based on the skills of the user
@@ -315,7 +312,7 @@ namespace Barotrauma.Items.Components
Temperature += MathHelper.Clamp(Math.Sign(temperatureDiff) * 10.0f * deltaTime, -Math.Abs(temperatureDiff), Math.Abs(temperatureDiff));
temperatureBoost = adjustValueWithoutOverShooting(temperatureBoost, 0.0f, deltaTime);
#if CLIENT
temperatureBoostUpButton.Enabled = temperatureBoostDownButton.Enabled = Math.Abs(temperatureBoost) < TemperatureBoostAmount * 0.9f;
temperatureBoostUpButton.Enabled = temperatureBoostDownButton.Enabled = AllowTemperatureBoost;
#endif
FissionRate = MathHelper.Lerp(fissionRate, Math.Min(TargetFissionRate, AvailableFuel), deltaTime);
@@ -350,7 +347,7 @@ namespace Barotrauma.Items.Components
if (!isConnectedToFriendlyOutpost)
{
item.Condition -= fissionRate / 100.0f * fuelConsumptionRate * deltaTime;
item.Condition -= fissionRate / 100.0f * GetFuelConsumption() * deltaTime;
}
}
fuelLeft += item.ConditionPercentage;
@@ -359,10 +356,10 @@ namespace Barotrauma.Items.Components
if (fissionRate > 0.0f)
{
if (item.AiTarget != null && MaxPowerOutput > 0)
if (item.AiTarget != null && maxPowerOut > 0)
{
var aiTarget = item.AiTarget;
float range = Math.Abs(currPowerConsumption) / MaxPowerOutput;
float range = Math.Abs(currPowerConsumption) / maxPowerOut;
aiTarget.SoundRange = MathHelper.Lerp(aiTarget.MinSoundRange, aiTarget.MaxSoundRange, range);
if (item.CurrentHull != null)
{
@@ -433,15 +430,17 @@ namespace Barotrauma.Items.Components
tolerance = 3f;
}
float maxPowerOut = GetMaxOutput();
float temperatureFactor = Math.Min(temperature / 50.0f, 1.0f);
float minOutput = MaxPowerOutput * Math.Clamp(Math.Min((turbineOutput - tolerance) / 100.0f, temperatureFactor), 0, 1);
float maxOutput = MaxPowerOutput * Math.Min((turbineOutput + tolerance) / 100.0f, temperatureFactor);
float minOutput = maxPowerOut * Math.Clamp(Math.Min((turbineOutput - tolerance) / 100.0f, temperatureFactor), 0, 1);
float maxOutput = maxPowerOut * Math.Min((turbineOutput + tolerance) / 100.0f, temperatureFactor);
minUpdatePowerOut = minOutput;
maxUpdatePowerOut = maxOutput;
float reactorMax = PowerOn ? MaxPowerOutput : maxUpdatePowerOut;
float reactorMax = PowerOn ? maxPowerOut : maxUpdatePowerOut;
return new PowerRange(minOutput, maxOutput, reactorMax);
}
@@ -464,11 +463,13 @@ namespace Barotrauma.Items.Components
float output = MathHelper.Clamp(ratio * (maxUpdatePowerOut - minUpdatePowerOut) + minUpdatePowerOut, minUpdatePowerOut, maxUpdatePowerOut);
float newLoad = loadLeft;
float maxOutput = GetMaxOutput();
//Adjust behaviour for multi reactor setup
if (MaxPowerOutput != minMaxPower.ReactorMaxOutput)
if (maxOutput != minMaxPower.ReactorMaxOutput)
{
float idealLoad = MaxPowerOutput / minMaxPower.ReactorMaxOutput * loadLeft;
float loadAdjust = MathHelper.Clamp((ratio - 0.5f) * 25 + idealLoad - (turbineOutput / 100 * MaxPowerOutput), -MaxPowerOutput / 100, MaxPowerOutput / 100);
float idealLoad = maxOutput / minMaxPower.ReactorMaxOutput * loadLeft;
float loadAdjust = MathHelper.Clamp((ratio - 0.5f) * 25 + idealLoad - (turbineOutput / 100 * maxOutput), -maxOutput / 100, maxOutput / 100);
newLoad = MathHelper.Clamp(loadLeft - (expectedPower - output) + loadAdjust, 0, loadLeft);
}
@@ -509,7 +510,7 @@ namespace Barotrauma.Items.Components
//calculate the maximum output if the fission rate is cranked as high as it goes and turbine output is at max
float theoreticalMaxHeat = GetGeneratedHeat(fissionRate: maxFissionRate);
float temperatureFactor = Math.Min(theoreticalMaxHeat / 50.0f, 1.0f);
float theoreticalMaxOutput = Math.Min(maxTurbineOutput / 100.0f, temperatureFactor) * MaxPowerOutput;
float theoreticalMaxOutput = Math.Min(maxTurbineOutput / 100.0f, temperatureFactor) * GetMaxOutput();
//maximum output not enough, we need more fuel
return theoreticalMaxOutput < Load * minimumOutputRatio;
@@ -693,7 +694,7 @@ namespace Barotrauma.Items.Components
aiUpdateTimer = AIUpdateInterval;
// load more fuel if the current maximum output is only 50% of the current load
// or if the fuel rod is (almost) deplenished
float minCondition = fuelConsumptionRate * MathUtils.Pow2((degreeOfSuccess - refuelLimit) * 2);
float minCondition = GetFuelConsumption() * MathUtils.Pow2((degreeOfSuccess - refuelLimit) * 2);
if (NeedMoreFuel(minimumOutputRatio: 0.5f, minCondition: minCondition))
{
bool outOfFuel = false;
@@ -871,5 +872,8 @@ namespace Barotrauma.Items.Components
if (GameMain.NetworkMember is { IsServer: true }) { unsentChanges = true; }
}
}
private float GetMaxOutput() => item.StatManager.GetAdjustedValue(ItemTalentStats.ReactorMaxOutput, MaxPowerOutput);
private float GetFuelConsumption() => item.StatManager.GetAdjustedValue(ItemTalentStats.ReactorFuelEfficiency, fuelConsumptionRate);
}
}
@@ -151,15 +151,7 @@ namespace Barotrauma.Items.Components
set
{
bool changed = currentMode != value;
currentMode = value;
if (value == Mode.Passive)
{
if (item.AiTarget != null)
{
item.AiTarget.SectorDegrees = 360.0f;
}
}
#if CLIENT
if (changed) { prevPassivePingRadius = float.MaxValue; }
UpdateGUIElements();
@@ -206,13 +198,6 @@ namespace Barotrauma.Items.Components
var activePing = activePings[currentPingIndex];
if (activePing.State > 1.0f)
{
if (item.AiTarget != null)
{
float range = MathUtils.InverseLerp(item.AiTarget.MinSoundRange, item.AiTarget.MaxSoundRange, Range * activePing.State / zoom);
item.AiTarget.SoundRange = MathHelper.Lerp(item.AiTarget.MinSoundRange, item.AiTarget.MaxSoundRange, range);
item.AiTarget.SectorDegrees = activePing.IsDirectional ? DirectionalPingSector : 360.0f;
item.AiTarget.SectorDir = new Vector2(pingDirection.X, -pingDirection.Y);
}
aiPingCheckPending = true;
currentPingIndex = -1;
}
@@ -228,21 +213,27 @@ namespace Barotrauma.Items.Components
activePings[currentPingIndex].Direction = pingDirection;
activePings[currentPingIndex].State = 0.0f;
activePings[currentPingIndex].PrevPingRadius = 0.0f;
if (item.AiTarget != null)
{
item.AiTarget.SectorDegrees = useDirectionalPing ? DirectionalPingSector : 360.0f;
item.AiTarget.SectorDir = new Vector2(pingDirection.X, -pingDirection.Y);
}
item.Use(deltaTime);
}
}
else
{
if (item.AiTarget != null)
{
item.AiTarget.SectorDegrees = 360.0f;
}
aiPingCheckPending = false;
}
}
for (var pingIndex = 0; pingIndex < activePingsCount;)
{
if (item.AiTarget != null)
{
float range = MathUtils.InverseLerp(item.AiTarget.MinSoundRange, item.AiTarget.MaxSoundRange, Range * activePings[pingIndex].State / zoom);
item.AiTarget.SoundRange = Math.Max(item.AiTarget.SoundRange, MathHelper.Lerp(item.AiTarget.MinSoundRange, item.AiTarget.MaxSoundRange, range));
}
if (activePings[pingIndex].State > 1.0f)
{
var lastIndex = --activePingsCount;
@@ -144,6 +144,11 @@ namespace Barotrauma.Items.Components
}
}
public float TargetVelocityLengthSquared
{
get => TargetVelocity.LengthSquared();
}
public Vector2 SteeringInput
{
get { return steeringInput; }
@@ -248,6 +248,7 @@ namespace Barotrauma.Items.Components
if (container?.Inventory == null) { return; }
bool recreateHudTexts = false;
for (var i = 0; i < container.Inventory.Capacity; i++)
{
if (i < 0 || GrowableSeeds.Length <= i) { continue; }
@@ -257,6 +258,7 @@ namespace Barotrauma.Items.Components
if (growable != null)
{
recreateHudTexts |= GrowableSeeds[i] != growable;
GrowableSeeds[i] = growable;
growable.IsActive = true;
}
@@ -267,11 +269,14 @@ namespace Barotrauma.Items.Components
// Kill the plant if it's somehow removed
oldGrowable.Decayed = true;
oldGrowable.IsActive = false;
recreateHudTexts = true;
}
GrowableSeeds[i] = null;
}
}
#if CLIENT
CharacterHUD.RecreateHudTexts |= recreateHudTexts;
#endif
// server handles this
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
@@ -9,6 +9,7 @@ namespace Barotrauma.Items.Components
{
//[power/min]
private float capacity;
private float adjustedCapacity;
private float charge, prevCharge;
@@ -65,8 +66,12 @@ namespace Barotrauma.Items.Components
[Editable, Serialize(10.0f, IsPropertySaveable.Yes, description: "The maximum capacity of the device (kW * min). For example, a value of 1000 means the device can output 100 kilowatts of power for 10 minutes, or 1000 kilowatts for 1 minute.")]
public float Capacity
{
get { return capacity; }
set { capacity = Math.Max(value, 1.0f); }
get => capacity;
set
{
capacity = Math.Max(value, 1.0f);
adjustedCapacity = GetCapacity();
}
}
[Editable, Serialize(0.0f, IsPropertySaveable.Yes, description: "The current charge of the device.")]
@@ -76,10 +81,10 @@ namespace Barotrauma.Items.Components
set
{
if (!MathUtils.IsValid(value)) return;
charge = MathHelper.Clamp(value, 0.0f, capacity);
charge = MathHelper.Clamp(value, 0.0f, adjustedCapacity);
//send a network event if the charge has changed by more than 5%
if (Math.Abs(charge - lastSentCharge) / capacity > 0.05f)
if (Math.Abs(charge - lastSentCharge) / adjustedCapacity > 0.05f)
{
#if SERVER
if (GameMain.Server != null && (!item.Submarine?.Loading ?? true)) { item.CreateServerEvent(this); }
@@ -89,7 +94,7 @@ namespace Barotrauma.Items.Components
}
}
public float ChargePercentage => MathUtils.Percentage(Charge, Capacity);
public float ChargePercentage => MathUtils.Percentage(Charge, adjustedCapacity);
[Editable, Serialize(10.0f, IsPropertySaveable.Yes, description: "How fast the device can be recharged. For example, a recharge speed of 100 kW and a capacity of 1000 kW*min would mean it takes 10 minutes to fully charge the device.")]
public float MaxRechargeSpeed
@@ -125,10 +130,19 @@ namespace Barotrauma.Items.Components
set { efficiency = MathHelper.Clamp(value, 0.0f, 1.0f); }
}
private bool flipIndicator;
[Editable, Serialize(false, IsPropertySaveable.Yes, description: "Should the progress bar indicating the charge be flipped to fill from the other side.")]
public bool FlipIndicator
{
get { return flipIndicator; }
set { flipIndicator = value; }
}
public float RechargeRatio => RechargeSpeed / MaxRechargeSpeed;
public const float aiRechargeTargetRatio = 0.5f;
private bool isRunning;
public bool HasBeenTuned { get; private set; }
public PowerContainer(Item item, ContentXElement element)
@@ -146,8 +160,9 @@ namespace Barotrauma.Items.Components
return picker != null;
}
public override void Update(float deltaTime, Camera cam)
public override void Update(float deltaTime, Camera cam)
{
adjustedCapacity = GetCapacity();
if (item.Connections == null)
{
IsActive = false;
@@ -155,7 +170,7 @@ namespace Barotrauma.Items.Components
}
isRunning = true;
float chargeRatio = charge / capacity;
float chargeRatio = charge / adjustedCapacity;
if (chargeRatio > 0.0f)
{
@@ -171,7 +186,7 @@ namespace Barotrauma.Items.Components
item.SendSignal(((int)Math.Round(CurrPowerOutput)).ToString(), "power_value_out");
item.SendSignal(((int)Math.Round(loadReading)).ToString(), "load_value_out");
item.SendSignal(((int)Math.Round(Charge)).ToString(), "charge");
item.SendSignal(((int)Math.Round(Charge / capacity * 100)).ToString(), "charge_%");
item.SendSignal(((int)Math.Round(Charge / adjustedCapacity * 100)).ToString(), "charge_%");
item.SendSignal(((int)Math.Round(RechargeSpeed / maxRechargeSpeed * 100)).ToString(), "charge_rate");
}
@@ -184,16 +199,16 @@ namespace Barotrauma.Items.Components
if (connection == powerIn)
{
//Don't draw power if fully charged
if (charge >= capacity)
if (charge >= adjustedCapacity)
{
charge = capacity;
charge = adjustedCapacity;
return 0;
}
else
{
if (item.Condition <= 0.0f) { return 0.0f; }
float missingCharge = capacity - charge;
float missingCharge = adjustedCapacity - charge;
float targetRechargeSpeed = rechargeSpeed;
if (ExponentialRechargeSpeed)
@@ -230,7 +245,7 @@ namespace Barotrauma.Items.Components
if (connection == powerOut)
{
float maxOutput;
float chargeRatio = prevCharge / capacity;
float chargeRatio = prevCharge / adjustedCapacity;
if (chargeRatio < 0.1f)
{
maxOutput = Math.Max(chargeRatio * 10.0f, 0.0f) * MaxOutPut;
@@ -283,7 +298,7 @@ namespace Barotrauma.Items.Components
else
{
//Decrease charge based on how much power is leaving the device
Charge = Math.Clamp(Charge - CurrPowerOutput / 60 * UpdateInterval, 0, Capacity);
Charge = Math.Clamp(Charge - CurrPowerOutput / 60 * UpdateInterval, 0, adjustedCapacity);
prevCharge = Charge;
}
}
@@ -370,5 +385,7 @@ namespace Barotrauma.Items.Components
}
}
}
public float GetCapacity() => item.StatManager.GetAdjustedValue(ItemTalentStats.BatteryCapacity, Capacity);
}
}
@@ -206,7 +206,7 @@ namespace Barotrauma.Items.Components
{
if (!powerOnSoundPlayed && powerOnSound != null)
{
SoundPlayer.PlaySound(powerOnSound.Sound, item.WorldPosition, powerOnSound.Volume, powerOnSound.Range, hullGuess: item.CurrentHull, ignoreMuffling: powerOnSound.IgnoreMuffling);
SoundPlayer.PlaySound(powerOnSound.Sound, item.WorldPosition, powerOnSound.Volume, powerOnSound.Range, hullGuess: item.CurrentHull, ignoreMuffling: powerOnSound.IgnoreMuffling, freqMult: powerOnSound.GetRandomFrequencyMultiplier());
powerOnSoundPlayed = true;
}
}
@@ -266,37 +266,43 @@ namespace Barotrauma.Items.Components
if (!subElement.Name.ToString().Equals("attack", StringComparison.OrdinalIgnoreCase)) { continue; }
Attack = new Attack(subElement, item.Name + ", Projectile", item);
}
if (item.body == null)
{
DebugConsole.ThrowError($"Error in projectile definition ({item.Name}): No body defined!");
return;
}
InitProjSpecific(element);
}
partial void InitProjSpecific(ContentXElement element);
public override void OnItemLoaded()
{
if (item.body != null)
if (item.body == null) { return; }
if (Attack != null && Attack.DamageRange <= 0.0f)
{
if (Attack != null && Attack.DamageRange <= 0.0f)
switch (item.body.BodyShape)
{
switch (item.body.BodyShape)
{
case PhysicsBody.Shape.Circle:
Attack.DamageRange = item.body.radius;
break;
case PhysicsBody.Shape.Capsule:
Attack.DamageRange = item.body.height / 2 + item.body.radius;
break;
case PhysicsBody.Shape.Rectangle:
Attack.DamageRange = new Vector2(item.body.width / 2.0f, item.body.height / 2.0f).Length();
break;
}
Attack.DamageRange = ConvertUnits.ToDisplayUnits(Attack.DamageRange);
case PhysicsBody.Shape.Circle:
Attack.DamageRange = item.body.radius;
break;
case PhysicsBody.Shape.Capsule:
Attack.DamageRange = item.body.height / 2 + item.body.radius;
break;
case PhysicsBody.Shape.Rectangle:
Attack.DamageRange = new Vector2(item.body.width / 2.0f, item.body.height / 2.0f).Length();
break;
}
originalCollisionCategories = item.body.CollisionCategories;
originalCollisionTargets = item.body.CollidesWith;
Attack.DamageRange = ConvertUnits.ToDisplayUnits(Attack.DamageRange);
}
originalCollisionCategories = item.body.CollisionCategories;
originalCollisionTargets = item.body.CollidesWith;
}
private void Launch(Character user, Vector2 simPosition, float rotation, float damageMultiplier = 1f, float launchImpulseModifier = 0f)
{
if (Item.body == null) { return; }
Item.body.ResetDynamics();
Item.SetTransform(simPosition, rotation);
if (Attack != null)
@@ -354,6 +360,7 @@ namespace Barotrauma.Items.Components
public bool Use(Character character = null, float launchImpulseModifier = 0f)
{
if (character != null && !characterUsable) { return false; }
if (item.body == null) { return false; }
for (int i = 0; i < HitScanCount; i++)
{
@@ -392,6 +399,7 @@ namespace Barotrauma.Items.Components
}
}
User = character;
ApplyStatusEffects(ActionType.OnUse, 1.0f, User, user: User);
return true;
}
@@ -416,14 +424,7 @@ namespace Barotrauma.Items.Components
item.body.FarseerBody.OnCollision += OnProjectileCollision;
item.body.FarseerBody.IsBullet = true;
item.body.CollisionCategories = Physics.CollisionProjectile;
item.body.CollidesWith = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking;
if (item.Prefab.DamagedByProjectiles && !IgnoreProjectilesWhileActive)
{
item.body.CollidesWith |= Physics.CollisionProjectile;
}
EnableProjectileCollisions();
IsActive = true;
if (stickJoint == null) { return; }
@@ -548,11 +549,10 @@ namespace Barotrauma.Items.Components
}
else if (fixture?.Body == null || fixture.IsSensor)
{
//ignore sensors and items
//ignore sensors
return true;
}
if (fixture.Body.UserData is VineTile) { return true; }
if (fixture.Body.UserData is Item item && (item.GetComponent<Door>() == null && !item.Prefab.DamagedByProjectiles || item.Condition <= 0)) { return true; }
if (fixture.Body.UserData as string == "ruinroom" || fixture.Body.UserData is Hull || fixture.UserData is Hull) { return true; }
//if doing the raycast in a submarine's coordinate space, ignore anything that's not in that sub
@@ -562,13 +562,28 @@ namespace Barotrauma.Items.Components
if (fixture.Body.UserData is Entity entity && entity.Submarine != submarine) { return true; }
}
//ignore everything else than characters, sub walls and level walls
if (!fixture.CollisionCategories.HasFlag(Physics.CollisionCharacter) &&
!fixture.CollisionCategories.HasFlag(Physics.CollisionWall) &&
!fixture.CollisionCategories.HasFlag(Physics.CollisionLevel)) { return true; }
if (fixture.Body.UserData is VoronoiCell && (this.item.Submarine != null || submarine != null)) { return true; }
if (fixture.Body.UserData is Item item)
{
if (item == Item) { return true; }
if (item.Condition <= 0) { return true; }
if (!item.Prefab.DamagedByProjectiles && item.GetComponent<Door>() == null) { return true; }
}
else if (fixture.Body.UserData is Holdable { CanPush: false })
{
// Ignore holdables that can't push -> shouldn't block
return true;
}
else
{
// TODO: This might make us ignore something we don't want to ignore?
// Not item -> ignore everything else than characters, sub walls and level walls
if (!fixture.CollisionCategories.HasFlag(Physics.CollisionCharacter) &&
!fixture.CollisionCategories.HasFlag(Physics.CollisionWall) &&
!fixture.CollisionCategories.HasFlag(Physics.CollisionLevel)) { return true; }
}
fixture.Body.GetTransform(out FarseerPhysics.Common.Transform transform);
if (!fixture.Shape.TestPoint(ref transform, ref rayStart)) { return true; }
@@ -585,20 +600,16 @@ namespace Barotrauma.Items.Components
}
else if (fixture?.Body == null || fixture.IsSensor)
{
//ignore sensors and items
//ignore sensors
return -1;
}
if (fixture.Body.UserData is VineTile) { return -1; }
if (fixture.Body.UserData is Item item && (item.GetComponent<Door>() == null && !item.Prefab.DamagedByProjectiles || item.Condition <= 0)) { return -1; }
if (fixture.Body.UserData as string == "ruinroom" || fixture.Body?.UserData is Hull || fixture.UserData is Hull) { return -1; }
if (!(fixture.Body.UserData is Holdable holdable && holdable.CanPush))
if (fixture.Body.UserData is Item item)
{
//ignore everything else than characters, sub walls and level walls
if (!fixture.CollisionCategories.HasFlag(Physics.CollisionCharacter) &&
!fixture.CollisionCategories.HasFlag(Physics.CollisionWall) &&
!fixture.CollisionCategories.HasFlag(Physics.CollisionLevel)) { return -1; }
if (item.Condition <= 0) { return -1; }
if (!item.Prefab.DamagedByProjectiles && item.GetComponent<Door>() == null) { return -1; }
}
if (fixture.Body.UserData as string == "ruinroom" || fixture.Body?.UserData is Hull || fixture.UserData is Hull) { return -1; }
//if doing the raycast in a submarine's coordinate space, ignore anything that's not in that sub
if (submarine != null)
@@ -608,6 +619,12 @@ namespace Barotrauma.Items.Components
if (fixture.Body.UserData is Limb limb && limb.character?.Submarine != submarine) { return -1; }
}
// Ignore holdables that can't push -> shouldn't block
if (fixture.Body.UserData is Holdable { CanPush: false })
{
return -1;
}
//ignore level cells if the item and the point of impact are inside a sub
if (fixture.Body.UserData is VoronoiCell)
{
@@ -638,7 +655,7 @@ namespace Barotrauma.Items.Components
hits.Add(new HitscanResult(fixture, point, normal, fraction));
return 1;
}, rayStart, rayEnd, Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking);
}, rayStart, rayEnd, Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking | Physics.CollisionProjectile);
return hits;
}
@@ -736,6 +753,7 @@ namespace Barotrauma.Items.Components
{
return false;
}
if (target.IsSensor) { return false; }
if (hits.Contains(target.Body)) { return false; }
if (target.Body.UserData is Submarine)
{
@@ -757,6 +775,12 @@ namespace Barotrauma.Items.Components
else if (target.Body.UserData is Item item)
{
if (item.Condition <= 0.0f) { return false; }
if (!item.Prefab.DamagedByProjectiles) { return false; }
}
else if (target.Body.UserData is Holdable { CanPush: false })
{
// Ignore holdables that can't push -> shouldn't block
return false;
}
//ignore character colliders (the projectile only hits limbs)
@@ -772,7 +796,7 @@ namespace Barotrauma.Items.Components
{
item.body.FarseerBody.ResetDynamics();
}
if (hits.Count() >= MaxTargetsToHit || target.Body.UserData is VoronoiCell)
if (hits.Count >= MaxTargetsToHit || target.Body.UserData is VoronoiCell)
{
DisableProjectileCollisions();
return true;
@@ -881,7 +905,7 @@ namespace Barotrauma.Items.Components
{
attackResult = Attack.DoDamage(User ?? Attacker, targetItem, item.WorldPosition, 1.0f);
#if CLIENT
if (attackResult.Damage > 0.0f)
if (attackResult.Damage > 0.0f && targetItem.Prefab.ShowHealthBar)
{
Character.Controlled?.UpdateHUDProgressBar(targetItem,
targetItem.WorldPosition,
@@ -915,23 +939,22 @@ namespace Barotrauma.Items.Components
if (character != null) { character.LastDamageSource = item; }
ActionType actionType = ActionType.OnUse;
if (_user != null && Rand.Range(0.0f, 0.5f) > DegreeOfSuccess(_user))
ActionType conditionalActionType = ActionType.OnSuccess;
if (User != null && Rand.Range(0.0f, 0.5f) > DegreeOfSuccess(User))
{
actionType = ActionType.OnFailure;
conditionalActionType = ActionType.OnFailure;
}
#if CLIENT
PlaySound(actionType, user: _user);
PlaySound(ActionType.OnImpact, user: _user);
PlaySound(conditionalActionType, user: User);
PlaySound(ActionType.OnImpact, user: User);
#endif
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
{
if (target.Body.UserData is Limb targetLimb)
{
ApplyStatusEffects(actionType, 1.0f, character, targetLimb, user: _user);
ApplyStatusEffects(ActionType.OnImpact, 1.0f, character, targetLimb, user: _user);
ApplyStatusEffects(conditionalActionType, 1.0f, character, targetLimb, user: User);
ApplyStatusEffects(ActionType.OnImpact, 1.0f, character, targetLimb, user: User);
var attack = targetLimb.attack;
if (attack != null)
{
@@ -940,8 +963,6 @@ namespace Barotrauma.Items.Components
{
if (effect.type == ActionType.OnImpact)
{
//effect.Apply(effect.type, 1.0f, targetLimb.character, targetLimb.character, targetLimb.WorldPosition);
if (effect.HasTargetType(StatusEffect.TargetType.This))
{
effect.Apply(effect.type, 1.0f, targetLimb.character, targetLimb.character, targetLimb.WorldPosition);
@@ -950,32 +971,27 @@ namespace Barotrauma.Items.Components
effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
targets.Clear();
targets.AddRange(effect.GetNearbyTargets(targetLimb.WorldPosition, targets));
effect.AddNearbyTargets(targetLimb.WorldPosition, targets);
effect.Apply(ActionType.OnActive, 1.0f, targetLimb.character, targets);
}
}
}
}
#if SERVER
if (GameMain.NetworkMember.IsServer)
if (GameMain.NetworkMember is { IsServer: true } server)
{
GameMain.Server?.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(actionType, this, targetLimb.character, targetLimb, null, item.WorldPosition));
GameMain.Server?.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnImpact, this, targetLimb.character, targetLimb, null, item.WorldPosition));
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, this, targetLimb.character, targetLimb, null, item.WorldPosition));
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnImpact, this, targetLimb.character, targetLimb, null, item.WorldPosition));
}
#endif
}
else
{
ApplyStatusEffects(actionType, 1.0f, useTarget: target.Body.UserData as Entity, user: _user);
ApplyStatusEffects(ActionType.OnImpact, 1.0f, useTarget: target.Body.UserData as Entity, user: _user);
#if SERVER
if (GameMain.NetworkMember.IsServer)
ApplyStatusEffects(conditionalActionType, 1.0f, useTarget: target.Body.UserData as Entity, user: User);
ApplyStatusEffects(ActionType.OnImpact, 1.0f, useTarget: target.Body.UserData as Entity, user: User);
if (GameMain.NetworkMember is { IsServer: true } server)
{
GameMain.Server?.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(actionType, this, null, null, target.Body.UserData as Entity, item.WorldPosition));
GameMain.Server?.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnImpact, this, null, null, target.Body.UserData as Entity, item.WorldPosition));
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, this, null, null, target.Body.UserData as Entity, item.WorldPosition));
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnImpact, this, null, null, target.Body.UserData as Entity, item.WorldPosition));
}
#endif
}
}
@@ -1053,8 +1069,19 @@ namespace Barotrauma.Items.Components
return true;
}
private void EnableProjectileCollisions()
{
item.body.CollisionCategories = Physics.CollisionProjectile;
item.body.CollidesWith = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking;
if (!IgnoreProjectilesWhileActive)
{
item.body.CollidesWith |= Physics.CollisionProjectile;
}
}
private void DisableProjectileCollisions()
{
if (item?.body?.FarseerBody == null) { return; }
item.body.FarseerBody.OnCollision -= OnProjectileCollision;
if (originalCollisionCategories != Category.None && originalCollisionTargets != Category.None)
{
@@ -1,6 +1,7 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
@@ -9,14 +10,6 @@ namespace Barotrauma.Items.Components
{
public const int MaxQuality = 3;
public static readonly float[] QualityCommonnesses = new float[]
{
0.8f,
0.15f,
0.045f,
0.005f,
};
public enum StatType
{
Condition,
@@ -81,5 +74,29 @@ namespace Barotrauma.Items.Components
if (!statValues.ContainsKey(statType)) { return 0.0f; }
return statValues[statType] * qualityLevel;
}
/// <summary>
/// Get a random quality for an item spawning in some sub, taking into account the type of the submarine and the difficulty of the current level
/// (high-quality items become more common as difficulty increases)
/// </summary>
public static int GetSpawnedItemQuality(Submarine submarine, Level level, Rand.RandSync randSync = Rand.RandSync.ServerAndClient)
{
if (submarine?.Info == null || level == null || submarine.Info.Type == SubmarineType.Player) { return 0; }
float difficultyFactor = MathHelper.Clamp(level.Difficulty, 0.0f, 1.0f);
return ToolBox.SelectWeightedRandom(Enumerable.Range(0, MaxQuality + 1), q => GetCommonness(q, difficultyFactor), randSync);
static float GetCommonness(int quality, float difficultyFactor)
{
return quality switch
{
0 => 1,
1 => MathHelper.Lerp(0.0f, 1f, difficultyFactor),
2 => MathHelper.Lerp(0.0f, 1f, Math.Max(difficultyFactor-0.15f, 0f)), //15 difficulty transition to next biome - unlock Excellent loot
3 => MathHelper.Lerp(0.0f, 1f, Math.Max(difficultyFactor-0.35f, 0f)), //35 difficulty transition to next biome - unlock Masterwork loot
_ => 0.0f,
};
}
}
}
}
@@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Abilities;
namespace Barotrauma.Items.Components
{
@@ -420,7 +421,8 @@ namespace Barotrauma.Items.Components
if (item.ConditionPercentage > MinDeteriorationCondition)
{
item.Condition -= DeteriorationSpeed * deltaTime;
float deteriorationSpeed = item.StatManager.GetAdjustedValue(ItemTalentStats.DetoriationSpeed, DeteriorationSpeed);
item.Condition -= deteriorationSpeed * deltaTime;
}
}
return;
@@ -467,8 +469,14 @@ namespace Barotrauma.Items.Components
wasGoodCondition = true;
}
float talentMultiplier = CurrentFixer.GetStatValue(StatTypes.RepairSpeed);
if (requiredSkills.Any(static skill => skill.Identifier == "mechanical"))
{
talentMultiplier += CurrentFixer.GetStatValue(StatTypes.MechanicalRepairSpeed);
}
float fixDuration = MathHelper.Lerp(FixDurationLowSkill, FixDurationHighSkill, successFactor);
fixDuration /= 1 + CurrentFixer.GetStatValue(StatTypes.RepairSpeed) + currentRepairItem?.Prefab.AddedRepairSpeedMultiplier ?? 0f;
fixDuration /= 1 + talentMultiplier + currentRepairItem?.Prefab.AddedRepairSpeedMultiplier ?? 0f;
fixDuration /= 1 + item.GetQualityModifier(Quality.StatType.RepairSpeed);
item.MaxRepairConditionMultiplier = GetMaxRepairConditionMultiplier(CurrentFixer);
@@ -500,7 +508,7 @@ namespace Barotrauma.Items.Components
SkillSettings.Current.SkillIncreasePerRepair / Math.Max(characterSkillLevel, 1.0f));
}
SteamAchievementManager.OnItemRepaired(item, CurrentFixer);
CurrentFixer.CheckTalents(AbilityEffectType.OnRepairComplete);
CurrentFixer.CheckTalents(AbilityEffectType.OnRepairComplete, new AbilityRepairable(item));
}
if (CurrentFixer?.SelectedItem == item) { CurrentFixer.SelectedItem = null; }
deteriorationTimer = Rand.Range(MinDeteriorationDelay, MaxDeteriorationDelay);
@@ -687,4 +695,14 @@ namespace Barotrauma.Items.Components
//where set_active/set_state signals can disable the component
}
}
internal sealed class AbilityRepairable : AbilityObject, IAbilityItem
{
public Item Item { get; set; }
public AbilityRepairable(Item item)
{
Item = item;
}
}
}
@@ -289,7 +289,7 @@ namespace Barotrauma.Items.Components
#if CLIENT
Light.ParentSub = item.Submarine;
#endif
if (item.Container != null)
if (item.Container != null && !(item.GetRootInventoryOwner() is Character))
{
SetLightSourceState(false, 0.0f);
return;
@@ -301,7 +301,7 @@ namespace Barotrauma.Items.Components
if (body != null && !body.Enabled)
{
SetLightSourceState(false, 0.0f);
return;
return;
}
//currPowerConsumption = powerConsumption;
@@ -65,14 +65,7 @@ namespace Barotrauma.Items.Components
{
get
{
if (GameMain.GameSession != null)
{
return (float)(Timing.TotalTime - GameMain.GameSession.RoundStartTime);
}
else
{
return 0.0f;
}
return GameMain.GameSession?.RoundDuration ?? 0.0f;
}
}
@@ -649,7 +649,7 @@ namespace Barotrauma.Items.Components
var e = item.linkedTo[(j + currentLoaderIndex) % item.linkedTo.Count];
//use linked projectile containers in case they have to react to the turret being launched somehow
//(play a sound, spawn more projectiles)
if (!(e is Item linkedItem)) { continue; }
if (e is not Item linkedItem) { continue; }
if (!item.Prefab.IsLinkAllowed(e.Prefab)) { continue; }
if (linkedItem.Condition <= 0.0f)
{
@@ -692,7 +692,7 @@ namespace Barotrauma.Items.Components
foreach (MapEntity e in item.linkedTo)
{
if (!(e is Item linkedItem)) { continue; }
if (e is not Item linkedItem) { continue; }
if (!((MapEntity)item).Prefab.IsLinkAllowed(e.Prefab)) { continue; }
if (linkedItem.GetComponent<Repairable>() is Repairable repairable && repairable.IsTinkering && linkedItem.HasTag("turretammosource"))
{
@@ -872,7 +872,7 @@ namespace Barotrauma.Items.Components
partial void LaunchProjSpecific();
private void ShiftItemsInProjectileContainer(ItemContainer container)
private static void ShiftItemsInProjectileContainer(ItemContainer container)
{
if (container == null) { return; }
bool moved;
@@ -1063,8 +1063,8 @@ namespace Barotrauma.Items.Components
character.AIController.SelectTarget(null);
}
bool canShoot = true;
if (!HasPowerToShoot())
bool canShoot = HasPowerToShoot();
if (!canShoot)
{
List<PowerContainer> batteries = GetDirectlyConnectedBatteries();
float lowestCharge = 0.0f;
@@ -1089,7 +1089,6 @@ namespace Barotrauma.Items.Components
character.Speak(TextManager.Get("DialogSupercapacitorIsBroken").Value,
identifier: "supercapacitorisbroken".ToIdentifier(),
minDurationBetweenSimilar: 30.0f);
canShoot = false;
}
}
}
@@ -1104,7 +1103,6 @@ namespace Barotrauma.Items.Components
character.Speak(TextManager.Get("DialogTurretHasNoPower").Value,
identifier: "turrethasnopower".ToIdentifier(),
minDurationBetweenSimilar: 30.0f);
canShoot = false;
}
}
@@ -1283,7 +1281,7 @@ namespace Barotrauma.Items.Components
closestDistance = shootDistance;
foreach (var wall in Level.Loaded.ExtraWalls)
{
if (!(wall is DestructibleLevelWall destructibleWall) || destructibleWall.Destroyed) { continue; }
if (wall is not DestructibleLevelWall destructibleWall || destructibleWall.Destroyed) { continue; }
foreach (var cell in wall.Cells)
{
if (cell.DoesDamage)
@@ -1405,19 +1403,18 @@ namespace Barotrauma.Items.Components
Vector2 end = ConvertUnits.ToSimUnits(targetPos.Value);
// Check that there's not other entities that shouldn't be targeted (like a friendly sub) between us and the target.
Body worldTarget = CheckLineOfSight(start, end);
bool shoot;
if (closestEnemy != null && closestEnemy.Submarine != null)
{
start -= closestEnemy.Submarine.SimPosition;
end -= closestEnemy.Submarine.SimPosition;
Body transformedTarget = CheckLineOfSight(start, end);
shoot = CanShoot(transformedTarget, character) && (worldTarget == null || CanShoot(worldTarget, character));
canShoot = CanShoot(transformedTarget, character) && (worldTarget == null || CanShoot(worldTarget, character));
}
else
{
shoot = CanShoot(worldTarget, character);
canShoot = CanShoot(worldTarget, character);
}
if (!shoot) { return false; }
if (!canShoot) { return false; }
if (character.IsOnPlayerTeam)
{
character.Speak(TextManager.Get("DialogFireTurret").Value,
@@ -1471,6 +1468,7 @@ namespace Barotrauma.Items.Components
{
if (targetBody.UserData is ISpatialEntity e)
{
if (e is Structure s && s.Indestructible) { return false; }
Submarine sub = e.Submarine ?? e as Submarine;
if (!targetSubmarines && e is Submarine) { return false; }
if (sub == null) { return false; }
@@ -1559,7 +1557,7 @@ namespace Barotrauma.Items.Components
return projectiles;
}
private void CheckProjectileContainer(Item projectileContainer, List<Projectile> projectiles, out bool stopSearching)
private static void CheckProjectileContainer(Item projectileContainer, List<Projectile> projectiles, out bool stopSearching)
{
stopSearching = false;
if (projectileContainer.Condition <= 0.0f) { return; }
@@ -270,6 +270,9 @@ namespace Barotrauma.Items.Components
public bool AutoEquipWhenFull { get; private set; }
public bool DisplayContainedStatus { get; private set; }
[Serialize(false, IsPropertySaveable.No, description: "Can the item be used (assuming it has components that are usable in some way) when worn."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)]
public bool AllowUseWhenWorn { get; set; }
public readonly int Variants;
private int variant;