Merge remote-tracking branch 'origin/master' into dev

# Conflicts:
#	Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs
#	Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs
#	Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs
#	Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs
#	Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs
#	Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs
#	Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs
#	Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs
#	Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs
#	Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs
#	Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs
#	Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs
#	Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs
#	Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs
#	Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs
#	Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs
#	Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs
#	Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs
#	Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs
#	Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs
#	Barotrauma/BarotraumaClient/LinuxClient.csproj
#	Barotrauma/BarotraumaClient/MacClient.csproj
#	Barotrauma/BarotraumaClient/WindowsClient.csproj
#	Barotrauma/BarotraumaServer/LinuxServer.csproj
#	Barotrauma/BarotraumaServer/MacServer.csproj
#	Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs
#	Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs
#	Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs
#	Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs
#	Barotrauma/BarotraumaServer/ServerSource/Utils/DoSProtection.cs
#	Barotrauma/BarotraumaServer/WindowsServer.csproj
#	Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs
#	Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs
#	Barotrauma/BarotraumaShared/SharedSource/Enums.cs
#	Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs
#	Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs
#	Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs
#	Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs
#	Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs
#	Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs
#	Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs
#	Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs
#	Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs
#	Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs
#	Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs
#	Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs
#	Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs
#	Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs
#	Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs
#	Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs
#	Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs
#	Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs
#	Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs
#	Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs
#	Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs
#	Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs
#	Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs
#	Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs
#	Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs
#	Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs
#	Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs
#	Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs
#	Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs
#	Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs
#	Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs
#	Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs
#	Barotrauma/BarotraumaShared/changelog.txt
This commit is contained in:
Regalis11
2023-06-15 16:55:39 +03:00
238 changed files with 5395 additions and 3045 deletions
@@ -24,7 +24,7 @@ namespace Barotrauma
}
public static readonly List<InvSlotType> anySlot = new List<InvSlotType>() { InvSlotType.Any };
public static readonly List<InvSlotType> AnySlot = new List<InvSlotType>() { InvSlotType.Any };
protected bool[] IsEquipped;
@@ -85,6 +85,22 @@ namespace Barotrauma
InitProjSpecific(element);
var itemElements = element.Elements().Where(e => e.Name.ToString().Equals("item", StringComparison.OrdinalIgnoreCase));
int itemCount = itemElements.Count();
if (itemCount > capacity)
{
DebugConsole.ThrowError($"Character \"{character.SpeciesName}\" is configured to spawn with more items than it has inventory capacity for.");
}
#if DEBUG
else if (itemCount > capacity - 2)
{
DebugConsole.ThrowError(
$"Character \"{character.SpeciesName}\" is configured to spawn with so many items it will have less than 2 free inventory slots. " +
"This can cause issues with talents that spawn extra loot in monsters' inventories."
+ " Consider increasing the inventory size.");
}
#endif
if (!spawnInitialItems) { return; }
#if CLIENT
@@ -92,10 +108,8 @@ namespace Barotrauma
if (GameMain.Client != null) { return; }
#endif
foreach (var subElement in element.Elements())
{
if (!subElement.Name.ToString().Equals("item", StringComparison.OrdinalIgnoreCase)) { continue; }
foreach (var subElement in itemElements)
{
string itemIdentifier = subElement.GetAttributeString("identifier", "");
if (!ItemPrefab.Prefabs.TryGet(itemIdentifier, out var itemPrefab))
{
@@ -226,7 +240,7 @@ namespace Barotrauma
if (item.AllowedSlots.Contains(InvSlotType.Any))
{
var wearable = item.GetComponent<Wearable>();
if (wearable != null && !wearable.AutoEquipWhenFull && CheckIfAnySlotAvailable(item, false) == -1)
if (wearable != null && !wearable.AutoEquipWhenFull && !IsAnySlotAvailable(item))
{
return false;
}
@@ -336,7 +350,7 @@ namespace Barotrauma
//try to place the item in a LimbSlot.Any slot if that's allowed
if (allowedSlots.Contains(InvSlotType.Any) && item.AllowedSlots.Contains(InvSlotType.Any))
{
int freeIndex = CheckIfAnySlotAvailable(item, inWrongSlot);
int freeIndex = GetFreeAnySlot(item, inWrongSlot);
if (freeIndex > -1)
{
PutItem(item, freeIndex, user, true, createNetworkEvent);
@@ -393,7 +407,9 @@ namespace Barotrauma
return placedInSlot > -1;
}
public int CheckIfAnySlotAvailable(Item item, bool inWrongSlot)
public bool IsAnySlotAvailable(Item item) => GetFreeAnySlot(item, inWrongSlot: false) > -1;
private int GetFreeAnySlot(Item item, bool inWrongSlot)
{
//attempt to stack first
for (int i = 0; i < capacity; i++)
@@ -1,13 +1,15 @@
using Barotrauma.Networking;
using Barotrauma.IO;
using Barotrauma.Networking;
using FarseerPhysics;
using FarseerPhysics.Dynamics;
using FarseerPhysics.Dynamics.Joints;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using Barotrauma.IO;
using System.Linq;
using System.Xml.Linq;
#if CLIENT
using Barotrauma.Lights;
#endif
namespace Barotrauma.Items.Components
{
@@ -243,10 +245,12 @@ namespace Barotrauma.Items.Components
if (!target.item.Submarine.DockedTo.Contains(item.Submarine))
{
target.item.Submarine.ConnectedDockingPorts.Add(item.Submarine, target);
target.item.Submarine.RefreshConnectedSubs();
}
if (!item.Submarine.DockedTo.Contains(target.item.Submarine))
{
item.Submarine.ConnectedDockingPorts.Add(target.item.Submarine, this);
item.Submarine.RefreshConnectedSubs();
}
DockingTarget = target;
@@ -558,6 +562,7 @@ namespace Barotrauma.Items.Components
var subs = new Submarine[] { item.Submarine, DockingTarget.item.Submarine };
bodies = new Body[4];
RemoveConvexHulls();
if (DockingTarget.Door != null)
{
@@ -648,8 +653,10 @@ namespace Barotrauma.Items.Components
hullRects[i].X -= expand;
hullRects[i].Width += expand * 2;
hullRects[i].Location -= MathUtils.ToPoint(subs[i].WorldPosition - subs[i].HiddenSubPosition);
hulls[i] = new Hull(hullRects[i], subs[i]);
hulls[i].RoomName = IsHorizontal ? "entityname.dockingport" : "entityname.dockinghatch";
hulls[i] = new Hull(hullRects[i], subs[i])
{
RoomName = IsHorizontal ? "entityname.dockingport" : "entityname.dockinghatch"
};
hulls[i].AddToGrid(subs[i]);
hulls[i].FreeID();
@@ -661,6 +668,15 @@ namespace Barotrauma.Items.Components
BodyType.Static);
}
}
#if CLIENT
for (int i = 0; i < 2; i++)
{
convexHulls[i] =
new ConvexHull(new Rectangle(
new Point((int)item.Position.X, item.Rect.Y - item.Rect.Height * i),
new Point((int)(DockingTarget.item.WorldPosition.X - item.WorldPosition.X), 0)), IsHorizontal, item);
}
#endif
if (rightHullDiff <= 100 && hulls[0].Submarine != null)
{
@@ -764,15 +780,17 @@ namespace Barotrauma.Items.Components
hullRects[1].Height += midHullDiff / 2 + 1;
}
int expand = 5;
for (int i = 0; i < 2; i++)
{
hullRects[i].Y += expand;
hullRects[i].Height += expand * 2;
hullRects[i].Location -= MathUtils.ToPoint(subs[i].WorldPosition - subs[i].HiddenSubPosition);
hulls[i] = new Hull(hullRects[i], subs[i]);
hulls[i].RoomName = IsHorizontal ? "entityname.dockingport" : "entityname.dockinghatch";
hulls[i] = new Hull(hullRects[i], subs[i])
{
RoomName = IsHorizontal ? "entityname.dockingport" : "entityname.dockinghatch",
AvoidStaying = true
};
hulls[i].AddToGrid(subs[i]);
hulls[i].FreeID();
@@ -784,6 +802,15 @@ namespace Barotrauma.Items.Components
BodyType.Static);
}
}
#if CLIENT
for (int i = 0; i < 2; i++)
{
convexHulls[i] =
new ConvexHull(new Rectangle(
new Point(item.Rect.X + item.Rect.Width * i, (int)item.Position.Y),
new Point(0, (int)(DockingTarget.item.WorldPosition.Y - item.WorldPosition.Y))), IsHorizontal, item);
}
#endif
if (midHullDiff <= 100 && hulls[0].Submarine != null)
{
@@ -822,6 +849,8 @@ namespace Barotrauma.Items.Components
}
}
partial void RemoveConvexHulls();
private void LinkHullsToGaps()
{
if (gap == null || hulls == null || hulls[0] == null || hulls[1] == null)
@@ -916,7 +945,9 @@ namespace Barotrauma.Items.Components
}
DockingTarget.item.Submarine.ConnectedDockingPorts.Remove(item.Submarine);
DockingTarget.item.Submarine.RefreshConnectedSubs();
item.Submarine.ConnectedDockingPorts.Remove(DockingTarget.item.Submarine);
item.Submarine.RefreshConnectedSubs();
if (Door != null && DockingTarget.Door != null)
{
@@ -976,6 +1007,8 @@ namespace Barotrauma.Items.Components
hulls[0]?.Remove(); hulls[0] = null;
hulls[1]?.Remove(); hulls[1] = null;
RemoveConvexHulls();
if (gap != null)
{
gap.Remove();
@@ -1091,6 +1124,7 @@ namespace Barotrauma.Items.Components
hulls[0]?.Remove(); hulls[0] = null;
hulls[1]?.Remove(); hulls[1] = null;
gap?.Remove(); gap = null;
RemoveConvexHulls();
overlaySprite?.Remove();
overlaySprite = null;
@@ -69,7 +69,7 @@ namespace Barotrauma.Items.Components
private bool isBroken;
public bool CanBeTraversed => (IsOpen || IsBroken) && !IsJammed && !IsStuck && !Impassable;
public bool CanBeTraversed => !Impassable && (IsBroken || IsOpen);
public bool IsBroken
{
@@ -186,13 +186,19 @@ namespace Barotrauma.Items.Components
{
get { return openState; }
set
{
{
openState = MathHelper.Clamp(value, 0.0f, 1.0f);
#if CLIENT
float size = IsHorizontal ? item.Rect.Width : item.Rect.Height;
if (Math.Abs(lastConvexHullState - openState) * size < 5.0f) { return; }
UpdateConvexHulls();
lastConvexHullState = openState;
//refresh convex hulls if the body of the door has moved by 5 pixels,
//or if it becomes fully closed or fully open
if (Math.Abs(lastConvexHullState - openState) * size > 5.0f ||
(openState <= 0.0f && lastConvexHullState > 0.0f) ||
(openState >= 1.0f && lastConvexHullState < 1.0f))
{
UpdateConvexHulls();
lastConvexHullState = openState;
}
#endif
}
}
@@ -383,6 +389,8 @@ namespace Barotrauma.Items.Components
return;
}
bool isClosing = false;
if ((!IsStuck && !IsJammed) || !isOpen)
{
@@ -521,8 +529,11 @@ namespace Barotrauma.Items.Components
{
RefreshLinkedGap();
#if CLIENT
convexHull = new ConvexHull(Rectangle.Empty, !IsHorizontal, item);
if (Window != Rectangle.Empty) { convexHull2 = new ConvexHull(Rectangle.Empty, !IsHorizontal, item); }
convexHull = new ConvexHull(doorRect, IsHorizontal, item);
if (Window != Rectangle.Empty)
{
convexHull2 = new ConvexHull(doorRect, IsHorizontal, item);
}
UpdateConvexHulls();
#endif
}
@@ -556,6 +567,12 @@ namespace Barotrauma.Items.Components
gap.ConnectedDoor = null;
}
}
if (OutsideSubmarineFixture != null)
{
OutsideSubmarineFixture.Body.Remove(OutsideSubmarineFixture);
OutsideSubmarineFixture = null;
}
//no need to remove the gap if we're unloading the whole submarine
//otherwise the gap will be removed twice and cause console warnings
@@ -1,10 +1,9 @@
using System;
using System.Collections.Generic;
using System.Xml.Linq;
using System.Linq;
using Barotrauma.Extensions;
using Barotrauma.Extensions;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
@@ -96,11 +95,16 @@ namespace Barotrauma.Items.Components
if (selectedEffect != null)
{
targetCharacter = character;
ApplyStatusEffects(ActionType.OnWearing, 1.0f);
ApplyStatusEffects(ActionType.OnWearing, 1.0f, targetCharacter);
float selectedEffectStrength = GetCombinedEffectStrength();
character.CharacterHealth.ApplyAffliction(null, selectedEffect.Instantiate(selectedEffectStrength));
var affliction = character.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedEffect);
if (affliction != null) { affliction.Strength = selectedEffectStrength; }
if (affliction != null)
{
affliction.Strength = selectedEffectStrength;
//force strength to the correct value to bypass any clamping e.g. AfflictionHusk might be doing
affliction.SetStrength(selectedEffectStrength);
}
#if SERVER
item.CreateServerEvent(this);
#endif
@@ -110,7 +114,12 @@ namespace Barotrauma.Items.Components
float selectedTaintedEffectStrength = GetCombinedTaintedEffectStrength();
character.CharacterHealth.ApplyAffliction(null, selectedTaintedEffect.Instantiate(selectedTaintedEffectStrength));
var affliction = character.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedTaintedEffect);
if (affliction != null) { affliction.Strength = selectedTaintedEffectStrength; }
if (affliction != null)
{
affliction.Strength = selectedTaintedEffectStrength;
//force strength to the correct value to bypass any clamping e.g. AfflictionHusk might be doing
affliction.SetStrength(selectedTaintedEffectStrength);
}
targetCharacter = character;
#if SERVER
item.CreateServerEvent(this);
@@ -127,7 +136,7 @@ namespace Barotrauma.Items.Components
base.Update(deltaTime, cam);
if (targetCharacter != null)
{
var rootContainer = item.GetRootContainer();
var rootContainer = item.RootContainer;
if (!targetCharacter.HasEquippedItem(item) &&
(rootContainer == null || !targetCharacter.HasEquippedItem(rootContainer) || !targetCharacter.Inventory.IsInLimbSlot(rootContainer, InvSlotType.HealthInterface)))
{
@@ -220,7 +229,7 @@ namespace Barotrauma.Items.Components
return MathHelper.Clamp(probability, 0.0f, 1.0f);
}
private float GetTaintedProbabilityOnCombine(Character user)
private static float GetTaintedProbabilityOnCombine(Character user)
{
if (user == null) { return 1.0f; }
float probability = 1.0f - user.GetStatValue(StatTypes.GeneticMaterialTaintedProbabilityReductionOnCombine);
@@ -409,6 +409,7 @@ namespace Barotrauma.Items.Components
private int leafVariants;
private int[] flowerTiles;
[Serialize(100.0f, IsPropertySaveable.Yes)]
public float Health
{
get => health;
@@ -321,12 +321,12 @@ namespace Barotrauma.Items.Components
}
}
public override void Drop(Character dropper)
public override void Drop(Character dropper, bool setTransform = true)
{
Drop(true, dropper);
Drop(true, dropper, setTransform);
}
private void Drop(bool dropConnectedWires, Character dropper)
private void Drop(bool dropConnectedWires, Character dropper, bool setTransform = true)
{
GetRope()?.Snap();
if (dropConnectedWires)
@@ -343,8 +343,11 @@ namespace Barotrauma.Items.Components
DeattachFromWall();
}
if (Pusher != null) { Pusher.Enabled = false; }
if (item.body != null) { item.body.Enabled = true; }
if (setTransform)
{
if (Pusher != null) { Pusher.Enabled = false; }
if (item.body != null) { item.body.Enabled = true; }
}
IsActive = false;
attachTargetCell = null;
@@ -357,7 +360,7 @@ namespace Barotrauma.Items.Components
item.Submarine = picker.Submarine;
if (item.body != null)
if (item.body != null && setTransform)
{
if (item.body.Removed)
{
@@ -599,6 +602,10 @@ namespace Barotrauma.Items.Components
throw new InvalidOperationException($"Tried to attach an item with no physics body to a wall ({item.Prefab.Identifier}).");
}
body.Enabled = false;
body.SetTransformIgnoreContacts(body.SimPosition, rotation: 0.0f);
item.body = null;
//outside hulls/subs -> we need to check if the item is being attached on a structure outside the sub
if (item.CurrentHull == null && item.Submarine == null)
{
@@ -638,9 +645,6 @@ namespace Barotrauma.Items.Components
}
}
body.Enabled = false;
item.body = null;
DisplayMsg = prevMsg;
PickKey = prevPickKey;
requiredItems = new Dictionary<RelatedItem.RelationType, List<RelatedItem>>(prevRequiredItems);
@@ -700,7 +704,8 @@ namespace Barotrauma.Items.Components
}
Vector2 attachPos = GetAttachPosition(character, useWorldCoordinates: true);
Submarine attachSubmarine = Structure.GetAttachTarget(attachPos)?.Submarine ?? item.Submarine;
int maxAttachableCount = (int)character.Info.GetSavedStatValue(StatTypes.MaxAttachableCount, item.Prefab.Identifier);
int maxAttachableCount = (int)character.Info.GetSavedStatValueWithBotsInMp(StatTypes.MaxAttachableCount, item.Prefab.Identifier);
int currentlyAttachedCount = Item.ItemList.Count(
i => i.Submarine == attachSubmarine && i.GetComponent<Holdable>() is Holdable holdable && holdable.Attached && i.Prefab.Identifier == item.Prefab.Identifier);
if (maxAttachableCount == 0)
@@ -811,7 +816,7 @@ namespace Barotrauma.Items.Components
foreach (var edge in cell.Edges)
{
if (!edge.IsSolid) { continue; }
if (MathUtils.GetLineIntersection(edge.Point1, edge.Point2, user.WorldPosition, attachPos, out Vector2 intersection))
if (MathUtils.GetLineSegmentIntersection(edge.Point1, edge.Point2, user.WorldPosition, attachPos, out Vector2 intersection))
{
attachPos = intersection;
edgeFound = true;
@@ -97,11 +97,18 @@ namespace Barotrauma.Items.Components
{
if (holdable != null && !holdable.Attached)
{
trigger.Enabled = false;
if (trigger != null)
{
trigger.Enabled = false;
}
IsActive = false;
}
else
{
if (trigger == null)
{
CreateTriggerBody();
}
if (trigger != null && Vector2.DistanceSquared(item.SimPosition, trigger.SimPosition) > 0.01f)
{
trigger.SetTransform(item.SimPosition, 0.0f);
@@ -123,12 +130,15 @@ namespace Barotrauma.Items.Components
{
holdable.PickingTime = float.MaxValue;
}
}
private void CreateTriggerBody()
{
System.Diagnostics.Debug.Assert(trigger == null, "LevelResource trigger already created!");
var body = item.body ?? holdable.Body;
if (body != null)
if (body != null && Attached)
{
trigger = new PhysicsBody(body.Width, body.Height, body.Radius,
trigger = new PhysicsBody(body.Width, body.Height, body.Radius,
body.Density,
BodyType.Static,
Physics.CollisionWall,
@@ -143,7 +153,6 @@ namespace Barotrauma.Items.Components
protected override void RemoveComponentSpecific()
{
base.RemoveComponentSpecific();
if (trigger != null)
{
trigger.Remove();
@@ -170,9 +170,9 @@ namespace Barotrauma.Items.Components
return characterUsable || character == null;
}
public override void Drop(Character dropper)
public override void Drop(Character dropper, bool setTransform = true)
{
base.Drop(dropper);
base.Drop(dropper, setTransform);
hitting = false;
hitPos = 0.0f;
}
@@ -241,12 +241,9 @@ namespace Barotrauma.Items.Components
}
}
public override void Drop(Character dropper)
public override void Drop(Character dropper, bool setTransform = true)
{
if (picker == null)
{
picker = dropper;
}
picker ??= dropper;
Vector2 bodyDropPos = Vector2.Zero;
@@ -255,8 +252,7 @@ namespace Barotrauma.Items.Components
if (item.ParentInventory != null && item.ParentInventory.Owner != null && !item.ParentInventory.Owner.Removed)
{
bodyDropPos = item.ParentInventory.Owner.SimPosition;
if (item.body != null) item.body.ResetDynamics();
item.body?.ResetDynamics();
}
}
else if (!picker.Removed)
@@ -270,7 +266,7 @@ namespace Barotrauma.Items.Components
picker = null;
}
if (item.body != null && !item.body.Enabled)
if (item.body != null && !item.body.Enabled && setTransform)
{
if (item.body.Removed)
{
@@ -260,7 +260,7 @@ namespace Barotrauma.Items.Components
{
Vector2 barrelPos = TransformedBarrelPos + item.body.SimPosition;
float rotation = (Item.body.Dir == 1.0f) ? Item.body.Rotation : Item.body.Rotation - MathHelper.Pi;
float spread = GetSpread(character) * Projectile.GetSpreadFromPool();
float spread = GetSpread(character) * projectile.GetSpreadFromPool();
var lastProjectile = LastProjectile;
if (lastProjectile != projectile)
@@ -277,7 +277,7 @@ namespace Barotrauma.Items.Components
{
Item.body.ApplyLinearImpulse(new Vector2((float)Math.Cos(projectile.Item.body.Rotation), (float)Math.Sin(projectile.Item.body.Rotation)) * Item.body.Mass * -50.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
}
projectile.Item.body.ApplyTorque(projectile.Item.body.Mass * degreeOfFailure * 20.0f * Projectile.GetSpreadFromPool());
projectile.Item.body.ApplyTorque(projectile.Item.body.Mass * degreeOfFailure * 20.0f * projectile.GetSpreadFromPool());
}
Item.RemoveContained(projectile.Item);
}
@@ -84,6 +84,9 @@ namespace Barotrauma.Items.Components
[Serialize(false, IsPropertySaveable.No, description: "Can the item repair multiple things at once, or will it only affect the first thing the ray from the barrel hits.")]
public bool RepairMultiple { get; set; }
[Serialize(true, IsPropertySaveable.No, description: "Can the item repair multiple walls at once? Only relevant if RepairMultiple is true.")]
public bool RepairMultipleWalls { get; set; }
[Serialize(false, IsPropertySaveable.No, description: "Can the item repair things through holes in walls.")]
public bool RepairThroughHoles { get; set; }
@@ -383,6 +386,7 @@ namespace Barotrauma.Items.Components
//stop the ray if it already hit a door/wall and is now about to hit some other type of entity
if (lastHitType == typeof(Item) || lastHitType == typeof(Structure)) { break; }
}
if (!RepairMultipleWalls && (bodyType == typeof(Structure) || (body.UserData as Item)?.GetComponent<Door>() != null)) { break; }
Character hitCharacter = null;
if (body.UserData is Limb limb)
@@ -56,9 +56,9 @@ namespace Barotrauma.Items.Components
return false;
}
public override void Drop(Character dropper)
public override void Drop(Character dropper, bool setTransform = true)
{
base.Drop(dropper);
base.Drop(dropper, setTransform);
throwState = ThrowState.None;
throwAngle = ThrowAngleStart;
Item.ResetWaterDragCoefficient();
@@ -237,7 +237,7 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(0f, IsPropertySaveable.No, description: "How useful the item is in combat? Used by AI to decide which item it should use as a weapon. For the sake of clarity, use a value between 0 and 100 (not enforced). Note that there's also a generic BotPriority for all item prefabs.")]
[Serialize(0f, IsPropertySaveable.No, description: "How useful the item is in combat? Used by AI to decide which item it should use as a weapon. For the sake of clarity, use a value between 0 and 100 (not forced). Note that there's also a generic BotPriority for all item prefabs.")]
public float CombatPriority { get; private set; }
/// <summary>
@@ -435,7 +435,7 @@ namespace Barotrauma.Items.Components
}
/// <summary>a Character has dropped the item</summary>
public virtual void Drop(Character dropper) { }
public virtual void Drop(Character dropper, bool setTransform = true) { }
/// <returns>true if the operation was completed</returns>
public virtual bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
@@ -682,9 +682,10 @@ namespace Barotrauma.Items.Components
public virtual void FlipY(bool relativeToSub) { }
public bool IsNotEmpty(Character user, bool checkContainedItems = true) =>
HasRequiredContainedItems(user, addMessage: false) &&
(!checkContainedItems || Item.OwnInventory == null || Item.OwnInventory.AllItems.Any(i => i.Condition > 0));
/// <summary>
/// Shorthand for !HasRequiredContainedItems()
/// </summary>
public bool IsEmpty(Character user) => !HasRequiredContainedItems(user, addMessage: false);
public bool HasRequiredContainedItems(Character user, bool addMessage, LocalizedString msg = null)
{
@@ -716,20 +717,50 @@ namespace Barotrauma.Items.Components
{
if (character.IsBot && item.IgnoreByAI(character)) { return false; }
if (!item.IsInteractable(character)) { return false; }
if (requiredItems.None()) { return true; }
if (character.Inventory != null)
if (requiredItems.Count == 0) { return true; }
if (character.Inventory != null && requiredItems.TryGetValue(RelatedItem.RelationType.Picked, out List<RelatedItem> relatedItems))
{
foreach (Item item in character.Inventory.AllItems)
foreach (RelatedItem relatedItem in relatedItems)
{
if (requiredItems.Any(ri => ri.Value.Any(r => r.Type == RelatedItem.RelationType.Picked && r.MatchesItem(item))))
foreach (Item otherItem in character.Inventory.AllItems)
{
return true;
}
if (relatedItem.MatchesItem(otherItem))
{
if (otherItem.GetComponent<IdCard>() is IdCard idCard)
{
if (!CheckIdCardAccess(relatedItem, idCard))
{
continue;
}
}
return true;
}
}
}
}
return false;
}
/// <summary>
/// Presumes that matching is already checked.
/// </summary>
private bool CheckIdCardAccess(RelatedItem relatedItem, IdCard idCard)
{
if (item.Submarine != null)
{
//id cards don't work in enemy subs (except on items that only require the default "idcard" tag)
if (idCard.TeamID != CharacterTeamType.None && idCard.TeamID != item.Submarine.TeamID && relatedItem.Identifiers.Any(id => id != "idcard"))
{
return false;
}
else if (idCard.SubmarineSpecificID != 0 && item.Submarine.SubmarineSpecificIDTag != idCard.SubmarineSpecificID)
{
return false;
}
}
return true;
}
public virtual bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg = null)
{
if (requiredItems.None()) { return true; }
@@ -765,23 +796,14 @@ namespace Barotrauma.Items.Components
bool CheckItems(RelatedItem relatedItem, IEnumerable<Item> itemList)
{
bool Predicate(Item it)
bool Predicate(Item it)
{
if (it == null || it.Condition <= 0.0f || !relatedItem.MatchesItem(it)) { return false; }
if (item.Submarine != null)
if (it.GetComponent<IdCard>() is IdCard idCard)
{
var idCard = it.GetComponent<IdCard>();
if (idCard != null)
if (!CheckIdCardAccess(relatedItem, idCard))
{
//id cards don't work in enemy subs (except on items that only require the default "idcard" tag)
if (idCard.TeamID != CharacterTeamType.None && idCard.TeamID != item.Submarine.TeamID && relatedItem.Identifiers.Any(id => id != "idcard"))
{
return false;
}
else if (idCard.SubmarineSpecificID != 0 && item.Submarine.SubmarineSpecificIDTag != idCard.SubmarineSpecificID)
{
return false;
}
return false;
}
}
return true;
@@ -20,11 +20,13 @@ namespace Barotrauma.Items.Components
{
public readonly int MaxStackSize;
public List<RelatedItem> ContainableItems;
public readonly bool AutoInject;
public SlotRestrictions(int maxStackSize, List<RelatedItem> containableItems)
public SlotRestrictions(int maxStackSize, List<RelatedItem> containableItems, bool autoInject)
{
MaxStackSize = maxStackSize;
ContainableItems = containableItems;
AutoInject = autoInject;
}
public bool MatchesItem(Item item)
@@ -269,7 +271,7 @@ namespace Barotrauma.Items.Components
List<SlotRestrictions> newSlotRestrictions = new List<SlotRestrictions>(totalCapacity);
for (int i = 0; i < capacity; i++)
{
newSlotRestrictions.Add(new SlotRestrictions(maxStackSize, ContainableItems));
newSlotRestrictions.Add(new SlotRestrictions(maxStackSize, ContainableItems, autoInject: false));
}
int subContainerIndex = capacity;
@@ -279,6 +281,7 @@ namespace Barotrauma.Items.Components
int subCapacity = subElement.GetAttributeInt("capacity", 1);
int subMaxStackSize = subElement.GetAttributeInt("maxstacksize", maxStackSize);
bool autoInject = subElement.GetAttributeBool("autoinject", false);
var subContainableItems = new List<RelatedItem>();
foreach (var subSubElement in subElement.Elements())
@@ -298,7 +301,7 @@ namespace Barotrauma.Items.Components
for (int i = subContainerIndex; i < subContainerIndex + subCapacity; i++)
{
newSlotRestrictions.Add(new SlotRestrictions(subMaxStackSize, subContainableItems));
newSlotRestrictions.Add(new SlotRestrictions(subMaxStackSize, subContainableItems, autoInject));
}
subContainerIndex += subCapacity;
}
@@ -466,7 +469,7 @@ namespace Barotrauma.Items.Components
prevContainedItemPositions = item.Position;
}
if (AutoInject)
if (AutoInject || slotRestrictions.Any(s => s.AutoInject))
{
//normally autoinjection should delete the (medical) item, so it only gets applied once
//but in multiplayer clients aren't allowed to remove items themselves, so they may be able to trigger this dozens of times
@@ -480,7 +483,21 @@ namespace Barotrauma.Items.Components
ownerCharacter.HealthPercentage / 100f <= AutoInjectThreshold &&
ownerCharacter.HasEquippedItem(item))
{
foreach (Item item in Inventory.AllItemsMod)
if (AutoInject)
{
Inventory.AllItemsMod.ForEach(i => Inject(i));
}
else
{
for (int i = 0; i < slotRestrictions.Length; i++)
{
if (slotRestrictions[i].AutoInject)
{
Inventory.GetItemsAt(i).ForEachMod(i => Inject(i));
}
}
}
void Inject(Item item)
{
item.ApplyStatusEffects(ActionType.OnSuccess, 1.0f, ownerCharacter, useTarget: ownerCharacter);
item.ApplyStatusEffects(ActionType.OnUse, 1.0f, ownerCharacter, useTarget: ownerCharacter);
@@ -511,25 +528,28 @@ namespace Barotrauma.Items.Components
if (activeContainedItem.ExcludeFullCondition && contained.IsFullCondition) { continue; }
StatusEffect effect = activeContainedItem.StatusEffect;
targets.Clear();
bool wearing = item.GetComponent<Wearable>() is Wearable { IsActive: true };
if (effect.HasTargetType(StatusEffect.TargetType.This))
{
effect.Apply(ActionType.OnContaining, deltaTime, item, item.AllPropertyObjects);
targets.AddRange(item.AllPropertyObjects);
}
if (effect.HasTargetType(StatusEffect.TargetType.Contained))
{
effect.Apply(ActionType.OnContaining, deltaTime, item, contained.AllPropertyObjects);
targets.AddRange(contained.AllPropertyObjects);
}
if (effect.HasTargetType(StatusEffect.TargetType.Character) && item.ParentInventory?.Owner is Character character)
{
effect.Apply(ActionType.OnContaining, deltaTime, item, character);
targets.Add(character);
}
if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
targets.Clear();
effect.AddNearbyTargets(item.WorldPosition, targets);
effect.Apply(ActionType.OnActive, deltaTime, item, targets);
}
effect.Apply(ActionType.OnActive, deltaTime, item, targets);
effect.Apply(ActionType.OnContaining, deltaTime, item, targets);
if (wearing) { effect.Apply(ActionType.OnWearing, deltaTime, item, targets); }
}
}
@@ -629,7 +649,7 @@ namespace Barotrauma.Items.Components
return false;
}
public override void Drop(Character dropper)
public override void Drop(Character dropper, bool setTransform = true)
{
IsActive = true;
SetContainedActive(false);
@@ -98,6 +98,20 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(true, IsPropertySaveable.No, description: "Can another character select this controller when another character has already selected it?")]
public bool AllowSelectingWhenSelectedByOther
{
get;
set;
}
[Serialize(true, IsPropertySaveable.No, description: "Can another character select this controller when a bot has already selected it?")]
public bool AllowSelectingWhenSelectedByBot
{
get;
set;
}
public bool ControlCharacterPose
{
get { return limbPositions.Count > 0; }
@@ -466,8 +480,18 @@ namespace Barotrauma.Items.Components
IsActive = false;
CancelUsing(user);
user = null;
return false;
}
else if (user.IsBot && !activator.IsBot)
{
if (AllowSelectingWhenSelectedByBot)
{
CancelUsing(user);
user = activator;
IsActive = true;
return true;
}
}
return AllowSelectingWhenSelectedByOther;
}
else
{
@@ -10,6 +10,18 @@ namespace Barotrauma.Items.Components
{
private float force;
/// <summary>
/// Latest signal the set_force connection received, used to set <see cref="targetForce"/> in the Update method.
/// We use a separate variable, because otherwise specific item update orders and sending multiple signals to set_force would lead to bugs:
/// targetForce could be set to 0, then a power grid might update as if the engine was off and mark the voltage of the grid as 1,
/// then another item could set the targetForce to 100 and make it run without power.
/// </summary>
private float? lastReceivedTargetForce;
/// <summary>
/// The amount of force the engine is aiming for (the actual force may be less than this,
/// depending on the amount of power, the condition of the engine or boosts from talents)
/// </summary>
private float targetForce;
private float maxForce;
@@ -58,7 +70,7 @@ namespace Barotrauma.Items.Components
public float CurrentVolume
{
get { return Math.Abs((force / 100.0f) * (MinVoltage <= 0.0f ? 1.0f : Math.Min(prevVoltage / MinVoltage, 1.0f))); }
get { return Math.Abs((force / 100.0f) * (MinVoltage <= 0.0f ? 1.0f : Math.Min(prevVoltage, 1.0f))); }
}
public float CurrentBrokenVolume
@@ -110,7 +122,10 @@ namespace Barotrauma.Items.Components
hasPower = Voltage > MinVoltage;
}
if (lastReceivedTargetForce.HasValue)
{
targetForce = lastReceivedTargetForce.Value;
}
Force = MathHelper.Lerp(force, (Voltage < MinVoltage) ? 0.0f : targetForce, deltaTime * 10.0f);
if (Math.Abs(Force) > 1.0f)
{
@@ -254,7 +269,7 @@ namespace Barotrauma.Items.Components
if (float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out float tempForce))
{
controlLockTimer = 0.1f;
targetForce = MathHelper.Clamp(tempForce, -100.0f, 100.0f);
lastReceivedTargetForce = MathHelper.Clamp(tempForce, -100.0f, 100.0f);
User = signal.sender;
}
}
@@ -784,8 +784,10 @@ namespace Barotrauma.Items.Components
}
else
{
float condition1 = MathUtils.IsValid(item1.Condition) ? item1.Condition : 0;
float condition2 = MathUtils.IsValid(item2.Condition) ? item2.Condition : 0;
//prefer items in worse condition
return Math.Sign(item2.Condition - item1.Condition);
return Math.Sign(condition2 - condition1);
}
}
@@ -775,7 +775,6 @@ namespace Barotrauma.Items.Components
if (shutDown)
{
PowerOn = false;
AutoTemp = false;
TargetFissionRate = 0.0f;
TargetTurbineOutput = 0.0f;
unsentChanges = true;
@@ -68,7 +68,6 @@ namespace Barotrauma.Items.Components
public bool UseDirectionalPing => useDirectionalPing;
private bool useDirectionalPing = false;
private Vector2 pingDirection = new Vector2(1.0f, 0.0f);
private bool useMineralScanner;
private bool aiPingCheckPending;
@@ -133,6 +132,9 @@ namespace Barotrauma.Items.Components
}
}
[Serialize(true, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
public bool UseMineralScanner { get; set; }
public float Zoom
{
get { return zoom; }
@@ -366,7 +368,7 @@ namespace Barotrauma.Items.Components
bool isActive = msg.ReadBoolean();
bool directionalPing = useDirectionalPing;
float zoomT = zoom, pingDirectionT = 0.0f;
bool mineralScanner = useMineralScanner;
bool mineralScanner = UseMineralScanner;
if (isActive)
{
zoomT = msg.ReadRangedSingle(0.0f, 1.0f, 8);
@@ -391,13 +393,13 @@ namespace Barotrauma.Items.Components
float pingAngle = MathHelper.Lerp(0.0f, MathHelper.TwoPi, pingDirectionT);
pingDirection = new Vector2((float)Math.Cos(pingAngle), (float)Math.Sin(pingAngle));
}
useMineralScanner = mineralScanner;
UseMineralScanner = mineralScanner;
#if CLIENT
zoomSlider.BarScroll = zoomT;
directionalModeSwitch.Selected = useDirectionalPing;
if (mineralScannerSwitch != null)
{
mineralScannerSwitch.Selected = useMineralScanner;
mineralScannerSwitch.Selected = UseMineralScanner;
}
#endif
}
@@ -418,7 +420,7 @@ namespace Barotrauma.Items.Components
float pingAngle = MathUtils.WrapAngleTwoPi(MathUtils.VectorToAngle(pingDirection));
msg.WriteRangedSingle(MathUtils.InverseLerp(0.0f, MathHelper.TwoPi, pingAngle), 0.0f, 1.0f, 8);
}
msg.WriteBoolean(useMineralScanner);
msg.WriteBoolean(UseMineralScanner);
}
}
}
@@ -444,7 +444,7 @@ namespace Barotrauma.Items.Components
if (connectedSubUpdateTimer <= 0.0f)
{
connectedSubs.Clear();
connectedSubs = controlledSub?.GetConnectedSubs();
connectedSubs.AddRange(controlledSub.GetConnectedSubs());
connectedSubUpdateTimer = ConnectedSubUpdateInterval;
}
@@ -535,7 +535,7 @@ namespace Barotrauma.Items.Components
foreach (GraphEdge edge in cell.Edges)
{
if (MathUtils.GetLineIntersection(edge.Point1 + cell.Translation, edge.Point2 + cell.Translation, controlledSub.WorldPosition, cell.Center, out Vector2 intersection))
if (MathUtils.GetLineSegmentIntersection(edge.Point1 + cell.Translation, edge.Point2 + cell.Translation, controlledSub.WorldPosition, cell.Center, out Vector2 intersection))
{
Vector2 diff = controlledSub.WorldPosition - intersection;
//far enough -> ignore
@@ -435,7 +435,7 @@ namespace Barotrauma.Items.Components
{
//other junction boxes don't need to receive the signal in the pass-through signal connections
//because we relay it straight to the connected items without going through the whole chain of junction boxes
if (ic is PowerTransfer && !(ic is RelayComponent)) { continue; }
if (ic is PowerTransfer && ic is not RelayComponent) { continue; }
ic.ReceiveSignal(signal, recipient);
}
@@ -14,18 +14,18 @@ namespace Barotrauma.Items.Components
{
partial class Projectile : ItemComponent, IServerSerializable
{
const int SpreadCounterWrapAround = 256;
private static readonly ImmutableArray<float> spreadPool;
static Projectile()
{
MTRandom random = new MTRandom(0);
spreadPool = Enumerable.Range(0, SpreadCounterWrapAround).Select(f => (float)random.NextDouble() - 0.5f).ToImmutableArray();
spreadPool = Enumerable.Range(0, byte.MaxValue + 1).Select(f => (float)random.NextDouble() - 0.5f).ToImmutableArray();
}
public static float GetSpreadFromPool()
public static byte SpreadCounter { get; private set; }
public static void ResetSpreadCounter()
{
return spreadPool[SpreadCounter];
SpreadCounter = 0;
}
struct HitscanResult
@@ -62,7 +62,7 @@ namespace Barotrauma.Items.Components
private bool removePending;
public static byte SpreadCounter { get; private set; }
private byte spreadIndex;
//continuous collision detection is used while the projectile is moving faster than this
const float ContinuousCollisionThreshold = 5.0f;
@@ -299,7 +299,8 @@ namespace Barotrauma.Items.Components
return;
}
SpreadCounter = (byte)(item.ID % SpreadCounterWrapAround);
spreadIndex = SpreadCounter;
SpreadCounter++;
InitProjSpecific(element);
}
@@ -328,6 +329,12 @@ namespace Barotrauma.Items.Components
originalCollisionTargets = item.body.CollidesWith;
}
public float GetSpreadFromPool()
{
spreadIndex = (byte)MathUtils.PositiveModulo(spreadIndex, spreadPool.Length);
return spreadPool[spreadIndex];
}
private void Launch(Character user, Vector2 simPosition, float rotation, float damageMultiplier = 1f, float launchImpulseModifier = 0f)
{
if (Item.body == null) { return; }
@@ -380,7 +387,7 @@ namespace Barotrauma.Items.Components
{
#if SERVER
launchRot = rotation;
Item.CreateServerEvent(this, new EventData(launch: true, spreadCounter: (byte)(SpreadCounter - 1)));
Item.CreateServerEvent(this, new EventData(launch: true, spreadCounter: (byte)(spreadIndex - 1)));
#endif
}
}
@@ -396,7 +403,6 @@ namespace Barotrauma.Items.Components
for (int i = 0; i < HitScanCount; i++)
{
float launchAngle;
if (StaticSpread)
{
launchAngle = initialRotation + MathHelper.ToRadians(i - ((float)(HitScanCount - 1) / 2)) * Spread;
@@ -405,7 +411,7 @@ namespace Barotrauma.Items.Components
{
launchAngle = initialRotation + MathHelper.ToRadians(Spread * GetSpreadFromPool());
}
SpreadCounter++;
spreadIndex++;
Vector2 launchDir = new Vector2((float)Math.Cos(launchAngle), (float)Math.Sin(launchAngle));
if (Hitscan)
@@ -703,7 +709,7 @@ namespace Barotrauma.Items.Components
return hits;
}
public override void Drop(Character dropper)
public override void Drop(Character dropper, bool setTransform = true)
{
Item.ResetWaterDragCoefficient();
if (dropper != null)
@@ -711,7 +717,7 @@ namespace Barotrauma.Items.Components
DisableProjectileCollisions();
Unstick();
}
base.Drop(dropper);
base.Drop(dropper, setTransform);
}
public override void Update(float deltaTime, Camera cam)
@@ -885,17 +891,8 @@ namespace Barotrauma.Items.Components
return false;
}
Vector2 normalizedVel;
Vector2 dir;
if (item.body.LinearVelocity.LengthSquared() < 0.001f)
{
normalizedVel = Vector2.Zero;
dir = contact.Manifold.LocalNormal;
}
else
{
normalizedVel = dir = Vector2.Normalize(item.body.LinearVelocity);
}
Vector2 dir = item.body.LinearVelocity.LengthSquared() < 0.001f ?
contact.Manifold.LocalNormal : Vector2.Normalize(item.body.LinearVelocity);
//do a raycast in the sub's coordinate space to see if it hit a structure
var wallBody = Submarine.PickBody(
@@ -904,7 +901,7 @@ namespace Barotrauma.Items.Components
collisionCategory: Physics.CollisionWall);
if (wallBody?.FixtureList?.First() != null && (wallBody.UserData is Structure || wallBody.UserData is Item) &&
//ignore the hit if it's behind the position the item was launched from, and the projectile is travelling in the opposite direction
Vector2.Dot((item.body.SimPosition + normalizedVel) - launchPos, dir) > 0)
Vector2.Dot(item.body.SimPosition - launchPos, dir) > 0)
{
target = wallBody.FixtureList.First();
if (hits.Contains(target.Body))
@@ -942,7 +939,7 @@ namespace Barotrauma.Items.Components
Character character = null;
if (target.Body.UserData is Submarine submarine && target.UserData is not Barotrauma.Item)
{
item.Move(-submarine.Position);
item.Move(-submarine.Position, ignoreContacts: false);
item.Submarine = submarine;
item.body.Submarine = submarine;
return !Hitscan;
@@ -339,18 +339,19 @@ namespace Barotrauma.Items.Components
if (Math.Abs(TargetPullForce) > 0.001f)
{
var targetBody = GetBodyToPull(target);
if (user != null && targetCharacter != null && !user.AnimController.InWater)
bool lerpForces = LerpForces;
if (!lerpForces && user != null && targetCharacter != null && !user.AnimController.InWater)
{
// Prevents rubberbanding horizontally when dragging a corpse.
if ((forceDir.X < 0) != (user.AnimController.Dir < 0))
{
forceDir.X = Math.Clamp(forceDir.X, -0.1f, 0.1f);
// Prevents rubberbanding horizontally when dragging a corpse.
lerpForces = true;
}
}
float force = LerpForces ? MathHelper.Lerp(0, TargetPullForce, MathUtils.InverseLerp(0, MaxLength / 3, distance - 50)) : TargetPullForce;
float force = lerpForces ? MathHelper.Lerp(0, TargetPullForce, MathUtils.InverseLerp(0, MaxLength / 3, distance - 50)) : TargetPullForce;
targetBody?.ApplyForce(-forceDir * force);
var targetRagdoll = targetCharacter?.AnimController;
if (targetRagdoll != null && (targetRagdoll.InWater || targetRagdoll.OnGround))
if (targetRagdoll?.Collider != null && (targetRagdoll.InWater || targetRagdoll.OnGround))
{
targetRagdoll.Collider.ApplyForce(-forceDir * force * 3);
}
@@ -29,7 +29,7 @@ namespace Barotrauma.Items.Components
private readonly Item item;
public readonly bool IsOutput;
public readonly List<StatusEffect> Effects;
public readonly List<(ushort wireId, int? connectionIndex)> LoadedWires;
@@ -40,6 +40,9 @@ namespace Barotrauma.Items.Components
//Priority in which power output will be handled - load is unaffected
public PowerPriority Priority = PowerPriority.Default;
public Signal LastSentSignal { get; private set; }
public Signal LastReceivedSignal {get; private set;}
public bool IsPower
{
get;
@@ -292,6 +295,7 @@ namespace Barotrauma.Items.Components
public void SendSignal(Signal signal)
{
LastSentSignal = signal;
enumeratingWires = true;
foreach (var wire in wires)
{
@@ -302,6 +306,10 @@ namespace Barotrauma.Items.Components
signal.source?.LastSentSignalRecipients.Add(recipient);
Connection connection = recipient;
connection.LastReceivedSignal = signal;
#if CLIENT
wire.RegisterSignal(signal, source: this);
#endif
foreach (ItemComponent ic in recipient.item.Components)
{
@@ -82,8 +82,10 @@ namespace Barotrauma.Items.Components
signalOut.SendDuration -= 1;
item.SendSignal(new Signal(signalOut.Signal.value, sender: signalOut.Signal.sender, strength: signalOut.Signal.strength), "signal_out");
if (signalOut.SendDuration <= 0)
{
signalQueue.Dequeue();
{
//check the queue isn't empty again, because sending the signal may empty it
//if this component is set to reset when it receives a signal and the signal is routed back to this component
signalQueue.TryDequeue(out _);
}
else
{
@@ -173,6 +173,8 @@ namespace Barotrauma.Items.Components
set
{
lightColor = value;
//reset previously received signal to force updating the color if we receive a set_color signal after the color has been modified manually
prevColorSignal = string.Empty;
#if CLIENT
if (Light != null)
{
@@ -249,6 +251,11 @@ namespace Barotrauma.Items.Components
base.OnItemLoaded();
SetLightSourceState(IsActive, lightBrightness);
turret = item.GetComponent<Turret>();
if (item.body != null)
{
item.body.FarseerBody.OnEnabled += CheckIfNeedsUpdate;
item.body.FarseerBody.OnDisabled += CheckIfNeedsUpdate;
}
#if CLIENT
Drawable = AlphaBlend && Light.LightSprite != null;
if (Screen.Selected.IsEditor)
@@ -277,15 +284,24 @@ namespace Barotrauma.Items.Components
return;
}
if (item.body == null && powerConsumption <= 0.0f && Parent == null && turret == null &&
if ((item.body == null || !item.body.Enabled) &&
powerConsumption <= 0.0f && Parent == null && turret == null &&
(statusEffectLists == null || !statusEffectLists.ContainsKey(ActionType.OnActive)) &&
(IsActiveConditionals == null || IsActiveConditionals.Count == 0))
{
lightBrightness = 1.0f;
SetLightSourceState(true, lightBrightness);
if (item.body != null && !item.body.Enabled)
{
lightBrightness = 0.0f;
SetLightSourceState(false, 0.0f);
}
else
{
lightBrightness = 1.0f;
SetLightSourceState(true, lightBrightness);
}
isOn = true;
SetLightSourceTransformProjSpecific();
base.IsActive = false;
isOn = true;
#if CLIENT
Light.ParentSub = item.Submarine;
#endif
@@ -222,10 +222,10 @@ namespace Barotrauma.Items.Components
{
Vector2 e1 = edge.Point1 + cell.Translation;
Vector2 e2 = edge.Point2 + cell.Translation;
if (MathUtils.LinesIntersect(e1, e2, new Vector2(detectRect.X, detectRect.Y), new Vector2(detectRect.Right, detectRect.Y)) ||
MathUtils.LinesIntersect(e1, e2, new Vector2(detectRect.X, detectRect.Bottom), new Vector2(detectRect.Right, detectRect.Bottom)) ||
MathUtils.LinesIntersect(e1, e2, new Vector2(detectRect.X, detectRect.Y), new Vector2(detectRect.X, detectRect.Bottom)) ||
MathUtils.LinesIntersect(e1, e2, new Vector2(detectRect.Right, detectRect.Y), new Vector2(detectRect.Right, detectRect.Bottom)))
if (MathUtils.LineSegmentsIntersect(e1, e2, new Vector2(detectRect.X, detectRect.Y), new Vector2(detectRect.Right, detectRect.Y)) ||
MathUtils.LineSegmentsIntersect(e1, e2, new Vector2(detectRect.X, detectRect.Bottom), new Vector2(detectRect.Right, detectRect.Bottom)) ||
MathUtils.LineSegmentsIntersect(e1, e2, new Vector2(detectRect.X, detectRect.Y), new Vector2(detectRect.X, detectRect.Bottom)) ||
MathUtils.LineSegmentsIntersect(e1, e2, new Vector2(detectRect.Right, detectRect.Y), new Vector2(detectRect.Right, detectRect.Bottom)))
{
MotionDetected = true;
return;
@@ -8,6 +8,9 @@ namespace Barotrauma.Items.Components
internal Item source;
internal float power;
internal float strength;
public readonly double CreationTime;
public double TimeSinceCreated => Timing.TotalTimeUnpaused - CreationTime;
internal Signal(string value, int stepsTaken = 0, Character sender = null,
Item source = null, float power = 0.0f, float strength = 1.0f)
@@ -18,6 +21,7 @@ namespace Barotrauma.Items.Components
this.source = source;
this.power = power;
this.strength = strength;
CreationTime = Timing.TotalTimeUnpaused;
}
internal Signal WithStepsTaken(int stepsTaken)
@@ -64,7 +64,7 @@ namespace Barotrauma.Items.Components
set;
}
[ConditionallyEditable(ConditionallyEditable.ConditionType.AllowLinkingWifiToChat)]
[ConditionallyEditable(ConditionallyEditable.ConditionType.AllowLinkingWifiToChat, onlyInEditors: false)]
[Serialize(false, IsPropertySaveable.No, description: "If enabled, any signals received from another chat-linked wifi component are displayed " +
"as chat messages in the chatbox of the player holding the item.", alwaysUseInstanceValues: true)]
public bool LinkToChat
@@ -19,7 +19,7 @@ namespace Barotrauma.Items.Components
private Vector2 end;
private readonly float angle;
private readonly float length;
public readonly float Length;
public Vector2 Start
{
@@ -36,7 +36,7 @@ namespace Barotrauma.Items.Components
this.end = end;
angle = MathUtils.VectorToAngle(end - start);
length = Vector2.Distance(start, end);
Length = Vector2.Distance(start, end);
}
}
@@ -52,7 +52,7 @@ namespace Barotrauma.Items.Components
private List<Vector2> nodes;
private readonly List<WireSection> sections;
private Connection[] connections;
private readonly Connection[] connections;
private bool canPlaceNode;
private Vector2 newNodePos;
@@ -81,6 +81,8 @@ namespace Barotrauma.Items.Components
get { return connections; }
}
public float Length { get; private set; }
[Serialize(5000.0f, IsPropertySaveable.No, description: "The maximum distance the wire can extend (in pixels).")]
public float MaxLength
{
@@ -333,7 +335,7 @@ namespace Barotrauma.Items.Components
IsActive = false;
}
public override void Drop(Character dropper)
public override void Drop(Character dropper, bool setTransform = true)
{
if (shouldClearConnections) { ClearConnections(dropper); }
IsActive = false;
@@ -563,6 +565,7 @@ namespace Barotrauma.Items.Components
sections.Add(new WireSection(nodes[i], nodes[i + 1]));
}
Drawable = IsActive || sections.Count > 0;
Length = sections.Count > 0 ? sections.Sum(s => s.Length) : 0;
CalculateExtents();
}
@@ -56,11 +56,9 @@ namespace Barotrauma.Items.Components
private float resetUserTimer;
private float aiTargetingGraceTimer;
private float aiFindTargetTimer;
private ISpatialEntity currentTarget;
private const float CrewAiFindTargetMaxInterval = 3.0f;
private const float CrewAiFindTargetMaxInterval = 1.0f;
private const float CrewAIFindTargetMinInverval = 0.2f;
private int currentLoaderIndex;
@@ -299,7 +297,7 @@ namespace Barotrauma.Items.Components
}
}
[Serialize(3000.0f, IsPropertySaveable.Yes, description: "How close to a target the turret has to be for an AI character to fire it.")]
[Serialize(3500.0f, IsPropertySaveable.Yes, description: "How close to a target the turret has to be for an AI character to fire it.")]
public float AIRange
{
get;
@@ -422,10 +420,7 @@ namespace Barotrauma.Items.Components
// Only make the Turret control the LightComponents that are it's children. So it'd be possible to for example have some extra lights on the turret that don't rotate with it.
if (lc?.Parent == this)
{
if (lightComponents == null)
{
lightComponents = new List<LightComponent>();
}
lightComponents ??= new List<LightComponent>();
lightComponents.Add(lc);
}
}
@@ -439,6 +434,8 @@ namespace Barotrauma.Items.Components
light.Parent = null;
light.Rotation = Rotation - item.RotationRad;
light.Light.Rotation = -rotation;
//turret lights are high-prio (don't want the lights to disappear when you're fighting something)
light.Light.PriorityMultiplier *= 10.0f;
}
}
#endif
@@ -590,10 +587,6 @@ namespace Barotrauma.Items.Components
angularVelocity *= -0.5f;
}
if (aiTargetingGraceTimer > 0f)
{
aiTargetingGraceTimer -= deltaTime;
}
if (aiFindTargetTimer > 0.0f)
{
aiFindTargetTimer -= deltaTime;
@@ -620,10 +613,18 @@ namespace Barotrauma.Items.Components
partial void UpdateProjSpecific(float deltaTime);
private bool isUseBeingCalled;
public override bool Use(float deltaTime, Character character = null)
{
if (!characterUsable && character != null) { return false; }
return TryLaunch(deltaTime, character);
//prevent an infinite loop if launching triggers a StatusEffect that Uses this item
if (isUseBeingCalled) { return false; }
isUseBeingCalled = true;
bool wasSuccessful = TryLaunch(deltaTime, character);
isUseBeingCalled = false;
return wasSuccessful;
}
public float GetPowerRequiredToShoot()
@@ -844,8 +845,13 @@ namespace Barotrauma.Items.Components
{
public readonly Item Projectile;
public EventData(Item projectile)
public EventData(Item projectile, Turret turret)
{
System.Diagnostics.Debug.Assert(projectile != null, $"Tried to create Turret {nameof(EventData)} with no projectile.");
GameAnalyticsManager.AddErrorEventOnce(
"Turret.EventData:entitynull"+ turret.Item.Prefab.Identifier,
GameAnalyticsManager.ErrorSeverity.Error,
$"Turret \"{turret.Item.Prefab.Identifier}\" tried to create {nameof(EventData)} with no projectile.");
Projectile = projectile;
}
}
@@ -885,21 +891,9 @@ namespace Barotrauma.Items.Components
}
float spread = MathHelper.ToRadians(Spread) * Rand.Range(-0.5f, 0.5f);
Vector2 launchPos = ConvertUnits.ToSimUnits(GetRelativeFiringPosition());
//check if there's some other sub between the turret's origin and the launch pos,
//and if so, launch at the intersection of the turret and the sub to prevent the projectile from spawning inside the other sub
Body pickedBody = Submarine.PickBody(ConvertUnits.ToSimUnits(item.WorldPosition), launchPos, null, Physics.CollisionWall, allowInsideFixture: true,
customPredicate: (Fixture f) =>
{
return f.Body.UserData is not Submarine sub || sub != item.Submarine;
});
if (pickedBody != null)
{
launchPos = Submarine.LastPickedPosition;
}
projectile.SetTransform(launchPos, -(launchRotation ?? rotation) + spread);
projectile.SetTransform(
ConvertUnits.ToSimUnits(GetRelativeFiringPosition()),
-(launchRotation ?? rotation) + spread);
projectile.UpdateTransform();
projectile.Submarine = projectile.body?.Submarine;
@@ -929,7 +923,7 @@ namespace Barotrauma.Items.Components
projectile.Container?.RemoveContained(projectile);
}
#if SERVER
item.CreateServerEvent(this, new EventData(projectile));
item.CreateServerEvent(this, new EventData(projectile, this));
#endif
ApplyStatusEffects(ActionType.OnUse, 1.0f, user: user);
@@ -1027,6 +1021,7 @@ namespace Barotrauma.Items.Components
float dist = Vector2.DistanceSquared(item.WorldPosition, targetItem.WorldPosition);
if (dist > closestDist) { continue; }
if (dist > shootDistance * shootDistance) { continue; }
if (!IsTargetItemCloseEnough(targetItem, dist)) { continue; }
if (!CheckTurretAngle(targetItem.WorldPosition)) { continue; }
target = targetItem;
closestDist = dist / priority;
@@ -1142,9 +1137,12 @@ namespace Barotrauma.Items.Components
{
if (character.AIController.SelectedAiTarget?.Entity is Character previousTarget && previousTarget.IsDead)
{
character.Speak(TextManager.Get("DialogTurretTargetDead").Value,
identifier: $"killedtarget{previousTarget.ID}".ToIdentifier(),
minDurationBetweenSimilar: 10.0f);
if (previousTarget.LastAttacker == null || previousTarget.LastAttacker == character)
{
character.Speak(TextManager.Get("DialogTurretTargetDead").Value,
identifier: $"killedtarget{previousTarget.ID}".ToIdentifier(),
minDurationBetweenSimilar: 5.0f);
}
character.AIController.SelectTarget(null);
}
@@ -1277,18 +1275,27 @@ namespace Barotrauma.Items.Components
Vector2? targetPos = null;
float maxDistance = 10000;
float shootDistance = AIRange * item.OffsetOnSelectedMultiplier;
// use full range only if we're actively firing
if (aiTargetingGraceTimer <= 0f)
{
shootDistance *= 0.75f;
}
float closestDistance = maxDistance * maxDistance;
bool hadCurrentTarget = currentTarget != null;
if (hadCurrentTarget)
{
if (!IsValidTarget(currentTarget))
bool isValidTarget = IsValidTarget(currentTarget);
if (isValidTarget)
{
float dist = Vector2.DistanceSquared(item.WorldPosition, currentTarget.WorldPosition);
if (dist > closestDistance)
{
isValidTarget = false;
}
else if (currentTarget is Item targetItem)
{
if (!IsTargetItemCloseEnough(targetItem, dist))
{
isValidTarget = false;
}
}
}
if (!isValidTarget)
{
currentTarget = null;
aiFindTargetTimer = CrewAIFindTargetMinInverval;
@@ -1304,11 +1311,17 @@ namespace Barotrauma.Items.Components
if (character.Submarine != null)
{
if (enemy.Submarine == character.Submarine) { continue; }
if (enemy.Submarine != null && enemy.Submarine.TeamID == character.Submarine.TeamID) { continue; }
if (enemy.Submarine != null)
{
if (enemy.Submarine.TeamID == character.Submarine.TeamID) { continue; }
if (enemy.Submarine.Info.IsOutpost) { continue; }
}
}
// Don't aim monsters that are inside any submarine.
if (!enemy.IsHuman && enemy.CurrentHull != null) { continue; }
if (HumanAIController.IsFriendly(character, enemy)) { continue; }
if (HumanAIController.IsFriendly(character, enemy)) { continue; }
// Don't shoot at captured enemies.
if (enemy.LockHands) { continue; }
float dist = Vector2.DistanceSquared(enemy.WorldPosition, item.WorldPosition);
if (dist > closestDistance) { continue; }
if (dist < shootDistance * shootDistance)
@@ -1330,6 +1343,7 @@ namespace Barotrauma.Items.Components
float dist = Vector2.DistanceSquared(item.WorldPosition, targetItem.WorldPosition);
if (dist > closestDistance) { continue; }
if (dist > shootDistance * shootDistance) { continue; }
if (!IsTargetItemCloseEnough(targetItem, dist)) { continue; }
if (!CheckTurretAngle(targetItem.WorldPosition)) { continue; }
targetPos = targetItem.WorldPosition;
closestDistance = dist / priority;
@@ -1337,14 +1351,7 @@ namespace Barotrauma.Items.Components
closestEnemy = null;
currentTarget = targetItem;
}
if (currentTarget == null)
{
aiFindTargetTimer = CrewAIFindTargetMinInverval;
}
else
{
aiFindTargetTimer = CrewAiFindTargetMaxInterval;
}
aiFindTargetTimer = currentTarget == null ? CrewAiFindTargetMaxInterval : CrewAIFindTargetMinInverval;
}
else if (currentTarget != null)
{
@@ -1358,6 +1365,10 @@ namespace Barotrauma.Items.Components
if (targetCharacter.Submarine != null && targetCharacter.CurrentHull != null && targetCharacter.Submarine != item.Submarine && !targetCharacter.CanSeeTarget(Item))
{
targetPos = targetCharacter.CurrentHull.WorldPosition;
if (closestDistance > maxDistance * maxDistance)
{
ResetTarget();
}
}
else
{
@@ -1377,12 +1388,17 @@ namespace Barotrauma.Items.Components
}
if (closestDist > shootDistance * shootDistance)
{
// Not close enough to shoot.
currentTarget = null;
closestEnemy = null;
targetPos = null;
aiFindTargetTimer = CrewAIFindTargetMinInverval;
ResetTarget();
}
}
void ResetTarget()
{
// Not close enough to shoot.
currentTarget = null;
closestEnemy = null;
targetPos = null;
}
}
else if (targetPos == null && item.Submarine != null && Level.Loaded != null)
{
@@ -1404,7 +1420,7 @@ namespace Barotrauma.Items.Components
{
// The closest point can't be targeted -> get a point directly in front of the turret
Vector2 barrelDir = new Vector2((float)Math.Cos(rotation), -(float)Math.Sin(rotation));
if (MathUtils.GetLineIntersection(p1, p2, item.WorldPosition, item.WorldPosition + barrelDir * shootDistance, out Vector2 intersection))
if (MathUtils.GetLineSegmentIntersection(p1, p2, item.WorldPosition, item.WorldPosition + barrelDir * shootDistance, out Vector2 intersection))
{
closestPoint = intersection;
if (!CheckTurretAngle(closestPoint)) { continue; }
@@ -1532,10 +1548,11 @@ namespace Barotrauma.Items.Components
}
character.SetInput(InputType.Shoot, true, true);
}
aiTargetingGraceTimer = 5f;
return false;
}
private bool IsTargetItemCloseEnough(Item target, float sqrDist) => float.IsPositiveInfinity(target.Prefab.AITurretTargetingMaxDistance) || sqrDist < MathUtils.Pow2(target.Prefab.AITurretTargetingMaxDistance);
/// <summary>
/// Turret doesn't consume grid power, directly takes from the batteries on its grid instead.
/// </summary>
@@ -1565,6 +1582,10 @@ namespace Barotrauma.Items.Components
{
return false;
}
if (targetItem.ParentInventory != null)
{
return false;
}
}
return true;
}
@@ -1580,6 +1601,7 @@ namespace Barotrauma.Items.Components
{
if (item.Submarine != null)
{
if (item.Submarine.Info.IsOutpost) { return false; }
// Check that the target is not in the friendly team, e.g. pirate or a hostile player sub (PvP).
return !target.IsOnFriendlyTeam(item.Submarine.TeamID) && TargetHumans;
}
@@ -1672,7 +1694,7 @@ namespace Barotrauma.Items.Components
return angle >= minRotation && angle <= maxRotation;
}
private bool CheckTurretAngle(Vector2 target) => CheckTurretAngle(-MathUtils.VectorToAngle(target - item.WorldPosition));
public bool CheckTurretAngle(Vector2 target) => CheckTurretAngle(-MathUtils.VectorToAngle(target - item.WorldPosition));
protected override void RemoveComponentSpecific()
{
@@ -1874,22 +1896,27 @@ namespace Barotrauma.Items.Components
{
if (TryExtractEventData(extraData, out EventData eventData))
{
msg.WriteUInt16(eventData.Projectile.ID);
msg.WriteRangedSingle(MathHelper.Clamp(rotation, minRotation, maxRotation), minRotation, maxRotation, 16);
msg.WriteUInt16(eventData.Projectile?.ID ?? Entity.NullEntityID);
msg.WriteRangedSingle(MathHelper.Clamp(wrapAngle(rotation), minRotation, maxRotation), minRotation, maxRotation, 16);
}
else
{
msg.WriteUInt16((ushort)0);
float wrappedTargetRotation = targetRotation;
while (wrappedTargetRotation < minRotation && MathUtils.IsValid(wrappedTargetRotation))
msg.WriteRangedSingle(MathHelper.Clamp(wrapAngle(targetRotation), minRotation, maxRotation), minRotation, maxRotation, 16);
}
float wrapAngle(float angle)
{
float wrappedAngle = angle;
while (wrappedAngle < minRotation && MathUtils.IsValid(wrappedAngle))
{
wrappedTargetRotation += MathHelper.TwoPi;
wrappedAngle += MathHelper.TwoPi;
}
while (wrappedTargetRotation > maxRotation && MathUtils.IsValid(wrappedTargetRotation))
while (wrappedAngle > maxRotation && MathUtils.IsValid(wrappedAngle))
{
wrappedTargetRotation -= MathHelper.TwoPi;
wrappedAngle -= MathHelper.TwoPi;
}
msg.WriteRangedSingle(MathHelper.Clamp(wrappedTargetRotation, minRotation, maxRotation), minRotation, maxRotation, 16);
return wrappedAngle;
}
}
}
@@ -482,11 +482,11 @@ namespace Barotrauma.Items.Components
character.OnWearablesChanged();
}
public override void Drop(Character dropper)
public override void Drop(Character dropper, bool setTransform = true)
{
Character previousPicker = picker;
Unequip(picker);
base.Drop(dropper);
base.Drop(dropper, setTransform);
previousPicker?.OnWearablesChanged();
picker = null;
IsActive = false;
@@ -575,8 +575,8 @@ namespace Barotrauma
if (removeItem)
{
item.Drop(user);
if (item.ParentInventory != null) { item.ParentInventory.RemoveItem(item); }
item.Drop(user, setTransform: false);
item.ParentInventory?.RemoveItem(item);
}
slots[i].Add(item);
@@ -742,13 +742,13 @@ namespace Barotrauma
stackedItems.Distinct().All(stackedItem => TryPutItem(stackedItem, index, false, false, user, createNetworkEvent))
&&
(existingItems.All(existingItem => otherInventory.TryPutItem(existingItem, otherIndex, false, false, user, createNetworkEvent)) ||
existingItems.Count == 1 && otherInventory.TryPutItem(existingItems.First(), user, CharacterInventory.anySlot, createNetworkEvent));
existingItems.Count == 1 && otherInventory.TryPutItem(existingItems.First(), user, CharacterInventory.AnySlot, createNetworkEvent));
}
else
{
swapSuccessful =
(existingItems.All(existingItem => otherInventory.TryPutItem(existingItem, otherIndex, false, false, user, createNetworkEvent)) ||
existingItems.Count == 1 && otherInventory.TryPutItem(existingItems.First(), user, CharacterInventory.anySlot, createNetworkEvent))
existingItems.Count == 1 && otherInventory.TryPutItem(existingItems.First(), user, CharacterInventory.AnySlot, createNetworkEvent))
&&
stackedItems.Distinct().All(stackedItem => TryPutItem(stackedItem, index, false, false, user, createNetworkEvent));
@@ -835,13 +835,31 @@ namespace Barotrauma
if (otherIsEquipped)
{
existingItems.ForEach(existingItem => TryPutItem(existingItem, index, false, false, user, createNetworkEvent, ignoreCondition: true));
stackedItems.ForEach(stackedItem => otherInventory.TryPutItem(stackedItem, otherIndex, false, false, user, createNetworkEvent, ignoreCondition: true));
TryPutAndForce(existingItems, this, index);
TryPutAndForce(stackedItems, otherInventory, otherIndex);
}
else
{
stackedItems.ForEach(stackedItem => otherInventory.TryPutItem(stackedItem, otherIndex, false, false, user, createNetworkEvent, ignoreCondition: true));
existingItems.ForEach(existingItem => TryPutItem(existingItem, index, false, false, user, createNetworkEvent, ignoreCondition: true));
TryPutAndForce(stackedItems, otherInventory, otherIndex);
TryPutAndForce(existingItems, this, index);
}
void TryPutAndForce(IEnumerable<Item> items, Inventory inventory, int slotIndex)
{
foreach (var item in items)
{
if (!inventory.TryPutItem(item, slotIndex, false, false, user, createNetworkEvent, ignoreCondition: true) &&
!inventory.GetItemsAt(slotIndex).Contains(item))
{
inventory.ForceToSlot(item, slotIndex);
}
}
}
if (createNetworkEvent)
{
CreateNetworkEvent();
otherInventory.CreateNetworkEvent();
}
#if CLIENT
@@ -43,6 +43,13 @@ namespace Barotrauma
/// </summary>
public static IReadOnlyCollection<Item> CleanableItems => cleanableItems;
private static readonly List<Item> sonarVisibleItems = new List<Item>();
/// <summary>
/// Items whose <see cref="ItemPrefab.SonarSize"/> is larger than 0
/// </summary>
public static IReadOnlyCollection<Item> SonarVisibleItems => sonarVisibleItems;
public new ItemPrefab Prefab => base.Prefab as ItemPrefab;
public static bool ShowLinks = true;
@@ -126,7 +133,8 @@ namespace Barotrauma
private float condition;
private bool inWater;
private readonly bool hasWaterStatusEffects;
private readonly bool hasInWaterStatusEffects;
private readonly bool hasNotInWaterStatusEffects;
private Inventory parentInventory;
private readonly ItemInventory ownInventory;
@@ -206,6 +214,10 @@ namespace Barotrauma
}
}
public Item RootContainer { get; private set; }
private bool inWaterProofContainer;
private Item container;
public Item Container
{
@@ -217,6 +229,8 @@ namespace Barotrauma
container = value;
CheckCleanable();
SetActiveSprite();
RefreshRootContainer();
}
}
}
@@ -408,14 +422,15 @@ namespace Barotrauma
set { spriteColor = value; }
}
[Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes), Editable]
[Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes), ConditionallyEditable(ConditionallyEditable.ConditionType.Pickable)]
public Color InventoryIconColor
{
get;
protected set;
}
[Editable, Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes, description: "Changes the color of the item this item is contained inside. Only has an effect if either of the UseContainedSpriteColor or UseContainedInventoryIconColor property of the container is set to true.")]
[Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes, description: "Changes the color of the item this item is contained inside. Only has an effect if either of the UseContainedSpriteColor or UseContainedInventoryIconColor property of the container is set to true."),
ConditionallyEditable(ConditionallyEditable.ConditionType.Pickable)]
public Color ContainerColor
{
get;
@@ -699,14 +714,26 @@ namespace Barotrauma
}
}
[Serialize(false, IsPropertySaveable.No)]
public bool FireProof
{
get { return Prefab.FireProof; }
get; private set;
}
private bool waterProof;
[Serialize(false, IsPropertySaveable.No)]
public bool WaterProof
{
get { return Prefab.WaterProof; }
get { return waterProof; }
private set
{
if (waterProof == value) { return; }
waterProof = value;
foreach (Item containedItem in ContainedItems)
{
containedItem.RefreshInWaterProofContainer();
}
}
}
public bool UseInHealthInterface
@@ -735,7 +762,7 @@ namespace Barotrauma
{
//if the item has an active physics body, inWater is updated in the Update method
if (body != null && body.Enabled) { return inWater; }
if (hasWaterStatusEffects) { return inWater; }
if (hasInWaterStatusEffects) { return inWater; }
//if not, we'll just have to check
return IsInWater();
@@ -1067,7 +1094,8 @@ namespace Barotrauma
}
}
hasWaterStatusEffects = hasStatusEffectsOfType[(int)ActionType.InWater] || hasStatusEffectsOfType[(int)ActionType.NotInWater];
hasInWaterStatusEffects = hasStatusEffectsOfType[(int)ActionType.InWater];
hasNotInWaterStatusEffects = hasStatusEffectsOfType[(int)ActionType.NotInWater];
if (body != null)
{
@@ -1123,6 +1151,7 @@ namespace Barotrauma
ItemList.Add(this);
if (Prefab.IsDangerous) { dangerousItems.Add(this); }
if (Repairables.Any()) { repairableItems.Add(this); }
if (Prefab.SonarSize > 0.0f) { sonarVisibleItems.Add(this); }
CheckCleanable();
DebugConsole.Log("Created " + Name + " (" + ID + ")");
@@ -1416,7 +1445,7 @@ namespace Barotrauma
}
}
public override void Move(Vector2 amount, bool ignoreContacts = false)
public override void Move(Vector2 amount, bool ignoreContacts = true)
{
if (!MathUtils.IsValid(amount))
{
@@ -1424,7 +1453,7 @@ namespace Barotrauma
return;
}
base.Move(amount);
base.Move(amount, ignoreContacts);
if (ItemList != null && body != null)
{
@@ -1508,17 +1537,51 @@ namespace Barotrauma
return CurrentHull;
}
public Item GetRootContainer()
private void RefreshRootContainer()
{
if (Container == null) { return null; }
Item rootContainer = Container;
while (rootContainer.Container != null)
Item newRootContainer = null;
inWaterProofContainer = false;
if (Container != null)
{
rootContainer = rootContainer.Container;
Item rootContainer = Container;
inWaterProofContainer |= Container.WaterProof;
while (rootContainer.Container != null)
{
rootContainer = rootContainer.Container;
inWaterProofContainer |= rootContainer.WaterProof;
}
newRootContainer = rootContainer;
}
if (newRootContainer != RootContainer)
{
RootContainer = newRootContainer;
isActive = true;
foreach (Item containedItem in ContainedItems)
{
containedItem.RefreshRootContainer();
}
}
return rootContainer;
}
private void RefreshInWaterProofContainer()
{
inWaterProofContainer = false;
if (container == null) { return; }
if (container.WaterProof || container.inWaterProofContainer)
{
inWaterProofContainer = true;
}
foreach (Item containedItem in ContainedItems)
{
containedItem.RefreshInWaterProofContainer();
}
}
/// <summary>
/// Used by the AI to check whether they can (in principle) and are allowed (in practice) to interact with an object or not.
/// Unlike CanInteractWith(), this method doesn't check the distance, the triggers, or anything like that.
/// </summary>
public bool HasAccess(Character character)
{
if (character.IsBot && IgnoreByAI(character)) { return false; }
@@ -1526,6 +1589,7 @@ namespace Barotrauma
var itemContainer = GetComponent<ItemContainer>();
if (itemContainer != null && !itemContainer.HasAccess(character)) { return false; }
if (Container != null && !Container.HasAccess(character)) { return false; }
if (GetComponent<Pickable>() is { CanBePicked: false }) { return false; }
return true;
}
@@ -1535,9 +1599,8 @@ namespace Barotrauma
{
if (ParentInventory == null) { return this; }
if (ParentInventory.Owner is Character) { return ParentInventory.Owner; }
var rootContainer = GetRootContainer();
if (rootContainer?.ParentInventory?.Owner is Character) { return rootContainer.ParentInventory.Owner; }
return rootContainer ?? this;
if (RootContainer?.ParentInventory?.Owner is Character) { return RootContainer.ParentInventory.Owner; }
return RootContainer ?? this;
}
public Inventory FindParentInventory(Func<Inventory, bool> predicate)
@@ -1587,6 +1650,17 @@ namespace Barotrauma
return tags.Contains(tag) || base.Prefab.Tags.Contains(tag);
}
public bool HasIdentifierOrTags(IEnumerable<Identifier> identifiersOrTags)
{
if (identifiersOrTags == null) { return false; }
if (identifiersOrTags.Contains(Prefab.Identifier)) { return true; }
foreach (Identifier tag in identifiersOrTags)
{
if (HasTag(tag)) { return true; }
}
return false;
}
public void ReplaceTag(string tag, string newTag)
{
ReplaceTag(tag.ToIdentifier(), newTag.ToIdentifier());
@@ -1616,7 +1690,7 @@ namespace Barotrauma
public bool ConditionalMatches(PropertyConditional conditional)
{
if (string.IsNullOrEmpty(conditional.TargetItemComponent))
if (string.IsNullOrEmpty(conditional.TargetItemComponentName))
{
if (!conditional.Matches(this)) { return false; }
}
@@ -1624,7 +1698,7 @@ namespace Barotrauma
{
foreach (ItemComponent component in components)
{
if (component.Name != conditional.TargetItemComponent) { continue; }
if (component.Name != conditional.TargetItemComponentName) { continue; }
if (!conditional.Matches(component)) { return false; }
}
}
@@ -1770,8 +1844,23 @@ namespace Barotrauma
bool wasInFullCondition = IsFullCondition;
float diff = value - condition;
if (GetComponent<Door>() is Door door && door.IsStuck && diff < 0)
{
float dmg = -diff;
// When the door is fully welded shut, reduce the welded state instead of the condition.
float prevStuck = door.Stuck;
door.Stuck -= dmg;
if (door.IsStuck) { return; }
// Reduce the damage by the amount we just adjusted the welded state by.
float damageReduction = dmg - prevStuck;
if (damageReduction < 0) { return; }
value -= damageReduction;
}
condition = MathHelper.Clamp(value, 0.0f, MaxCondition);
if (MathUtils.NearlyEqual(prevCondition, condition, epsilon: 0.000001f)) { return; }
if (MathUtils.NearlyEqual(prevCondition, value, epsilon: 0.000001f)) { return; }
RecalculateConditionValues();
@@ -1930,7 +2019,7 @@ namespace Barotrauma
if (ic.IsActiveConditionals != null)
{
if (ic.IsActiveConditionalComparison == PropertyConditional.LogicalOperatorType.And)
if (ic.IsActiveConditionalComparison == PropertyConditional.Comparison.And)
{
bool shouldBeActive = true;
foreach (var conditional in ic.IsActiveConditionals)
@@ -1994,7 +2083,7 @@ namespace Barotrauma
if (Removed) { return; }
bool needsWaterCheck = hasWaterStatusEffects;
bool needsWaterCheck = hasInWaterStatusEffects || hasNotInWaterStatusEffects;
if (body != null && body.Enabled)
{
System.Diagnostics.Debug.Assert(body.FarseerBody.FixtureList != null);
@@ -2023,7 +2112,7 @@ namespace Barotrauma
if (needsWaterCheck)
{
bool wasInWater = inWater;
inWater = IsInWater() && !WaterProof;
inWater = !inWaterProofContainer && IsInWater() && !WaterProof;
if (inWater)
{
//the item has gone through the surface of the water
@@ -2036,36 +2125,29 @@ namespace Barotrauma
body.LinearVelocity *= 0.2f;
}
}
Item container = this.Container;
while (container != null)
{
if (container.WaterProof)
{
inWater = false;
break;
}
container = container.Container;
}
}
if (hasWaterStatusEffects && condition > 0.0f)
if ((hasInWaterStatusEffects || hasNotInWaterStatusEffects) && condition > 0.0f)
{
ApplyStatusEffects(inWater ? ActionType.InWater : ActionType.NotInWater, deltaTime);
}
}
else
{
if (updateableComponents.Count == 0 &&
(aiTarget == null || !aiTarget.NeedsUpdate) &&
!hasStatusEffectsOfType[(int)ActionType.Always] &&
(body == null || !body.Enabled))
if (inWaterProofContainer && !hasNotInWaterStatusEffects)
{
#if CLIENT
positionBuffer.Clear();
#endif
isActive = false;
needsWaterCheck = false;
}
}
if (!needsWaterCheck &&
updateableComponents.Count == 0 &&
(aiTarget == null || !aiTarget.NeedsUpdate) &&
!hasStatusEffectsOfType[(int)ActionType.Always] &&
(body == null || !body.Enabled))
{
#if CLIENT
positionBuffer.Clear();
#endif
isActive = false;
}
}
partial void Splash();
@@ -2854,7 +2936,7 @@ namespace Barotrauma
if (user != null)
{
var abilityItem = new AbilityApplyTreatment(user, character, this);
var abilityItem = new AbilityApplyTreatment(user, character, this, targetLimb);
user.CheckTalents(AbilityEffectType.OnApplyTreatment, abilityItem);
}
@@ -2915,7 +2997,7 @@ namespace Barotrauma
}
}
foreach (ItemComponent ic in components) { ic.Drop(dropper); }
foreach (ItemComponent ic in components) { ic.Drop(dropper, setTransform); }
if (Container != null)
{
@@ -3550,7 +3632,7 @@ namespace Barotrauma
element.Add(new XAttribute("healthmultiplier", HealthMultiplier.ToString("G", CultureInfo.InvariantCulture)));
}
Item rootContainer = GetRootContainer() ?? this;
Item rootContainer = RootContainer ?? this;
System.Diagnostics.Debug.Assert(Submarine != null || rootContainer.ParentInventory?.Owner is Character);
Vector2 subPosition = Submarine == null ? Vector2.Zero : Submarine.HiddenSubPosition;
@@ -3727,6 +3809,7 @@ namespace Barotrauma
ItemList.Remove(this);
dangerousItems.Remove(this);
repairableItems.Remove(this);
sonarVisibleItems.Remove(this);
cleanableItems.Remove(this);
}
@@ -3750,12 +3833,14 @@ namespace Barotrauma
public Character Character { get; set; }
public Character User { get; set; }
public Item Item { get; set; }
public Limb TargetLimb { get; set; }
public AbilityApplyTreatment(Character user, Character target, Item item)
public AbilityApplyTreatment(Character user, Character target, Item item, Limb limb)
{
Character = target;
User = user;
Item = item;
TargetLimb = limb;
}
}
}
@@ -71,9 +71,9 @@ namespace Barotrauma
{
public EventType EventType => EventType.ItemStat;
public readonly Dictionary<ItemStatManager.TalentStatIdentifier, float> Stats;
public readonly Dictionary<TalentStatIdentifier, float> Stats;
public SetItemStatEventData(Dictionary<ItemStatManager.TalentStatIdentifier, float> stats)
public SetItemStatEventData(Dictionary<TalentStatIdentifier, float> stats)
{
Stats = stats;
}
@@ -88,12 +88,15 @@ namespace Barotrauma
public abstract ItemPrefab FirstMatchingPrefab { get; }
public RequiredItem(int amount, float minCondition, float maxCondition, bool useCondition)
public LocalizedString OverrideDescription { get; }
public RequiredItem(int amount, float minCondition, float maxCondition, bool useCondition, LocalizedString overrideDescription)
{
Amount = amount;
MinCondition = minCondition;
MaxCondition = maxCondition;
UseCondition = useCondition;
OverrideDescription = overrideDescription;
}
public readonly int Amount;
public readonly float MinCondition;
@@ -129,12 +132,14 @@ namespace Barotrauma
public override ItemPrefab FirstMatchingPrefab => ItemPrefab;
public override bool MatchesItem(Item item)
{
return item?.Prefab.Identifier == ItemPrefabIdentifier;
}
public RequiredItemByIdentifier(Identifier itemPrefab, int amount, float minCondition, float maxCondition, bool useCondition) : base(amount, minCondition, maxCondition, useCondition)
public RequiredItemByIdentifier(Identifier itemPrefab, int amount, float minCondition, float maxCondition, bool useCondition, LocalizedString overrideDescription) :
base(amount, minCondition, maxCondition, useCondition, overrideDescription)
{
ItemPrefabIdentifier = itemPrefab;
using MD5 md5 = MD5.Create();
@@ -163,7 +168,8 @@ namespace Barotrauma
return item.HasTag(Tag);
}
public RequiredItemByTag(Identifier tag, int amount, float minCondition, float maxCondition, bool useCondition) : base(amount, minCondition, maxCondition, useCondition)
public RequiredItemByTag(Identifier tag, int amount, float minCondition, float maxCondition, bool useCondition, LocalizedString overrideDescription)
: base(amount, minCondition, maxCondition, useCondition, overrideDescription)
{
Tag = tag;
using MD5 md5 = MD5.Create();
@@ -260,6 +266,12 @@ namespace Barotrauma
bool useCondition = subElement.GetAttributeBool("usecondition", true);
int amount = subElement.GetAttributeInt("count", subElement.GetAttributeInt("amount", 1));
LocalizedString description = string.Empty;
if (subElement.GetAttributeString("description", string.Empty) is string texTag && !texTag.IsNullOrEmpty())
{
description = TextManager.Get(texTag);
}
if (requiredItemIdentifier != Identifier.Empty)
{
var existing = requiredItems.FindIndex(r =>
@@ -272,7 +284,7 @@ namespace Barotrauma
amount += requiredItems[existing].Amount;
requiredItems.RemoveAt(existing);
}
requiredItems.Add(new RequiredItemByIdentifier(requiredItemIdentifier, amount, minCondition, maxCondition, useCondition));
requiredItems.Add(new RequiredItemByIdentifier(requiredItemIdentifier, amount, minCondition, maxCondition, useCondition, description));
}
else
{
@@ -286,7 +298,7 @@ namespace Barotrauma
amount += requiredItems[existing].Amount;
requiredItems.RemoveAt(existing);
}
requiredItems.Add(new RequiredItemByTag(requiredItemTag, amount, minCondition, maxCondition, useCondition));
requiredItems.Add(new RequiredItemByTag(requiredItemTag, amount, minCondition, maxCondition, useCondition, description));
}
break;
}
@@ -669,8 +681,19 @@ namespace Barotrauma
[Serialize(0.0f, IsPropertySaveable.No)]
public float OffsetOnSelected { get; private set; }
private float health;
[Serialize(100.0f, IsPropertySaveable.No)]
public float Health { get; private set; }
public float Health
{
get { return health; }
private set
{
//don't allow health values higher than this, because they lead to various issues:
//e.g. integer overflows when we're casting to int to display a health value, value being set to float.Infinity if it's high enough
health = Math.Min(value, 1000000.0f);
}
}
[Serialize(false, IsPropertySaveable.No)]
public bool AllowSellingWhenBroken { get; private set; }
@@ -702,12 +725,6 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.No)]
public bool DamagedByMonsters { get; private set; }
[Serialize(false, IsPropertySaveable.No)]
public bool FireProof { get; private set; }
[Serialize(false, IsPropertySaveable.No)]
public bool WaterProof { get; private set; }
private float impactTolerance;
[Serialize(0.0f, IsPropertySaveable.No)]
public float ImpactTolerance
@@ -813,6 +830,12 @@ namespace Barotrauma
[Serialize(1.0f, IsPropertySaveable.No, description: "How much the bots prioritize shooting this item with slow turrets, like railguns? Defaults to 1. Not used if AITurretPriority is 0. Distance to the target affects the decision making.")]
public float AISlowTurretPriority { get; private set; }
[Serialize(float.PositiveInfinity, IsPropertySaveable.No, description: "The max distance at which the bots are allowed to target the items. Defaults to infinity.")]
public float AITurretTargetingMaxDistance { get; private set; }
[Serialize(false, IsPropertySaveable.Yes, description: "If enabled, taking items from this container is never considered stealing.")]
public bool AllowStealingContainedItems { get; private set; }
protected override Identifier DetermineIdentifier(XElement element)
{
@@ -1434,6 +1457,22 @@ namespace Barotrauma
"Specify the amount in the variant to fix this.");
}
}
if (originalElement?.Name.ToIdentifier() == "Deconstruct" &&
variantElement?.Name.ToIdentifier() == "Deconstruct")
{
if (originalElement.Elements().Any(e => e.Name.ToIdentifier() == "Item") &&
variantElement.Elements().Any(e => e.Name.ToIdentifier() == "RequiredItem"))
{
DebugConsole.AddWarning($"Potential error in item variant \"{Identifier}\": " +
$"the item defines deconstruction recipes using 'RequiredItem' instead of 'Item'. Overriding the base recipe may not work correctly.");
}
if (variantElement.Elements().Any(e => e.Name.ToIdentifier() == "Item") &&
originalElement.Elements().Any(e => e.Name.ToIdentifier() == "RequiredItem"))
{
DebugConsole.AddWarning($"Potential error in item \"{parent.Identifier}\": " +
$"the item defines deconstruction recipes using 'RequiredItem' instead of 'Item'. The item variant \"{Identifier}\" may not override the base recipe correctly.");
}
}
}
}
@@ -5,29 +5,48 @@ using System.Collections.Generic;
namespace Barotrauma
{
[NetworkSerialize]
internal readonly record struct TalentStatIdentifier(ItemTalentStats Stat, Identifier TalentIdentifier, Option<UInt32> UniqueCharacterId) : INetSerializableStruct
{
/// <summary>
/// Stackable identifiers feature a unique ID to allow multiple stats applied by the same talent from different characters to coexist.
/// </summary>
public static TalentStatIdentifier CreateStackable(ItemTalentStats stat, Identifier talentIdentifier, UInt32 characterId)
=> new(stat, talentIdentifier, Option<UInt32>.Some(characterId));
/// <summary>
/// Unstackable identifiers do not have a unique ID causing them to be identical to other stats applied by the same talent from different characters and thus only one of them will be applied.
/// <see cref="ItemStatManager.ApplyStat"/> will always use the highest value for unstackable stats.
/// </summary>
public static TalentStatIdentifier CreateUnstackable(ItemTalentStats stat, Identifier talentIdentifier)
=> new(stat, talentIdentifier, Option.None);
}
internal sealed class ItemStatManager
{
private Item item;
public ItemStatManager(Item item)
{
this.item = item;
}
[NetworkSerialize]
public readonly record struct TalentStatIdentifier(ItemTalentStats Stat, Identifier TalentIdentifier, UInt32 CharacterID) : INetSerializableStruct;
private readonly Dictionary<TalentStatIdentifier, float> talentStats = new();
private readonly Item item;
public void ApplyStat(ItemTalentStats stat, float value, CharacterTalent talent)
public ItemStatManager(Item item) => this.item = item;
public void ApplyStat(ItemTalentStats stat, bool stackable, float value, CharacterTalent talent)
{
if (talent.Character?.ID is not { } characterId ||
talent.Prefab?.Identifier is not { } talentIdentifier)
talent.Prefab?.Identifier is not { } talentIdentifier) { return; }
var identifier = stackable
? TalentStatIdentifier.CreateStackable(stat, talentIdentifier, characterId)
: TalentStatIdentifier.CreateUnstackable(stat, talentIdentifier);
if (!stackable)
{
return;
if (talentStats.TryGetValue(identifier, out float existingValue))
{
// Always use the highest value for non-stackable stats
if (existingValue > value) { return; }
}
}
TalentStatIdentifier identifier = new TalentStatIdentifier(stat, talentIdentifier, characterId);
talentStats[identifier] = value;
#if SERVER
@@ -38,21 +57,19 @@ namespace Barotrauma
#endif
}
// Used for getting the value value from network packet
public void ApplyStat(TalentStatIdentifier identifier, float value)
{
talentStats[identifier] = value;
}
/// <summary>
/// Used for setting the value value from network packet; bypassing all validity checks.
/// </summary>
public void ApplyStatDirect(TalentStatIdentifier identifier, float value) => talentStats[identifier] = value;
public float GetAdjustedValue(ItemTalentStats stat, float originalValue)
{
float total = originalValue;
foreach (var (key, value) in talentStats)
{
if (key.Stat == stat)
{
total *= value;
}
if (key.Stat != stat) { continue; }
total *= value;
}
return total;