v1.0.13.1 (first post-1.0 patch)

This commit is contained in:
Regalis11
2023-05-10 15:07:17 +03:00
parent 96fb49ba14
commit ee1db852b1
272 changed files with 5738 additions and 2413 deletions
@@ -107,12 +107,26 @@ namespace Barotrauma
}
}
public bool HasValidPath(bool requireNonDirty = false, bool requireUnfinished = true) =>
steeringManager is IndoorsSteeringManager pathSteering &&
pathSteering.CurrentPath != null &&
(!requireUnfinished || !pathSteering.CurrentPath.Finished) &&
!pathSteering.CurrentPath.Unreachable &&
(!requireNonDirty || !pathSteering.IsPathDirty);
/// <summary>
/// Is the current path valid, using the provided parameters.
/// </summary>
/// <param name="requireNonDirty"></param>
/// <param name="requireUnfinished"></param>
/// <param name="nodePredicate"></param>
/// <returns>When <paramref name="nodePredicate"/> is defined, returns false if any of the nodes fails to match the predicate.</returns>
public bool HasValidPath(bool requireNonDirty = true, bool requireUnfinished = true, Func<WayPoint, bool> nodePredicate = null)
{
if (SteeringManager is not IndoorsSteeringManager pathSteering) { return false; }
if (pathSteering.CurrentPath == null) { return false; }
if (pathSteering.CurrentPath.Unreachable) { return false; }
if (requireUnfinished && pathSteering.CurrentPath.Finished) { return false; }
if (requireNonDirty && pathSteering.IsPathDirty) { return false; }
if (nodePredicate != null)
{
return pathSteering.CurrentPath.Nodes.All(n => nodePredicate(n));
}
return true;
}
public bool IsCurrentPathNullOrUnreachable => IsCurrentPathUnreachable || steeringManager is IndoorsSteeringManager pathSteering && pathSteering.CurrentPath == null;
public bool IsCurrentPathUnreachable => steeringManager is IndoorsSteeringManager pathSteering && !pathSteering.IsPathDirty && pathSteering.CurrentPath != null && pathSteering.CurrentPath.Unreachable;
@@ -251,7 +265,7 @@ namespace Barotrauma
}
private readonly HashSet<Item> unequippedItems = new HashSet<Item>();
public bool TakeItem(Item item, CharacterInventory targetInventory, bool equip, bool wear = false, bool dropOtherIfCannotMove = true, bool allowSwapping = false, bool storeUnequipped = false)
public bool TakeItem(Item item, CharacterInventory targetInventory, bool equip, bool wear = false, bool dropOtherIfCannotMove = true, bool allowSwapping = false, bool storeUnequipped = false, IEnumerable<Identifier> targetTags = null)
{
var pickable = item.GetComponent<Pickable>();
if (pickable == null) { return false; }
@@ -265,23 +279,28 @@ namespace Barotrauma
}
else
{
var holdable = item.GetComponent<Holdable>();
if (holdable != null)
{
pickable = holdable;
}
// Not allowed to wear -> don't use the Wearable component even when it's found.
pickable = item.GetComponent<Holdable>();
}
if (item.ParentInventory is ItemInventory itemInventory)
{
if (!itemInventory.Container.HasRequiredItems(Character, addMessage: false)) { return false; }
}
if (equip)
if (equip && pickable != null)
{
int targetSlot = -1;
//check if all the slots required by the item are free
foreach (InvSlotType slots in pickable.AllowedSlots)
{
if (slots.HasFlag(InvSlotType.Any)) { continue; }
if (!wear)
{
if (slots != InvSlotType.RightHand && slots != InvSlotType.LeftHand && slots != (InvSlotType.RightHand | InvSlotType.LeftHand))
{
// Don't allow other than hand slots if not allowed to wear.
continue;
}
}
for (int i = 0; i < targetInventory.Capacity; i++)
{
if (targetInventory is CharacterInventory characterInventory)
@@ -294,7 +313,7 @@ namespace Barotrauma
var otherItem = targetInventory.GetItemAt(i);
if (otherItem == null) { continue; }
//try to move the existing item to LimbSlot.Any and continue if successful
if (otherItem.AllowedSlots.Contains(InvSlotType.Any) && targetInventory.TryPutItem(otherItem, Character, CharacterInventory.anySlot))
if (otherItem.AllowedSlots.Contains(InvSlotType.Any) && targetInventory.TryPutItem(otherItem, Character, CharacterInventory.AnySlot))
{
if (storeUnequipped && targetInventory.Owner == Character)
{
@@ -304,6 +323,11 @@ namespace Barotrauma
}
if (dropOtherIfCannotMove)
{
if (otherItem.Prefab.Identifier == item.Prefab.Identifier || otherItem.HasIdentifierOrTags(targetTags))
{
// Shouldn't try dropping identical items, because that causes infinite looping when trying to get multiple items of the same type and if can't fit them all in the inventory.
return false;
}
//if everything else fails, simply drop the existing item
otherItem.Drop(Character);
}
@@ -314,7 +338,7 @@ namespace Barotrauma
}
else
{
return targetInventory.TryPutItem(item, Character, CharacterInventory.anySlot);
return targetInventory.TryPutItem(item, Character, CharacterInventory.AnySlot);
}
}
@@ -339,7 +363,7 @@ namespace Barotrauma
if (avoidDroppingInSea && !character.IsInFriendlySub)
{
// If we are not inside a friendly sub (= same team), try to put the item in the inventory instead dropping it.
if (character.Inventory.TryPutItem(containedItem, character, CharacterInventory.anySlot))
if (character.Inventory.TryPutItem(containedItem, character, CharacterInventory.AnySlot))
{
if (unequipMax.HasValue && ++removed >= unequipMax) { return; }
continue;
@@ -449,9 +473,10 @@ namespace Barotrauma
Vector2 diff = EscapeTarget.WorldPosition - Character.WorldPosition;
float sqrDist = diff.LengthSquared();
bool isClose = sqrDist < MathUtils.Pow2(100);
if (Character.CurrentHull == null || isClose && !isClosedDoor || pathSteering == null || IsCurrentPathNullOrUnreachable || IsCurrentPathFinished)
if (Character.CurrentHull == null || isClose && !isClosedDoor || pathSteering == null || IsCurrentPathUnreachable || IsCurrentPathFinished)
{
// Very close to the target, outside, or at the end of the path -> try to steer through the gap
Character.ReleaseSecondaryItem();
SteeringManager.Reset();
pathSteering?.ResetPath();
Vector2 dir = Vector2.Normalize(diff);
@@ -480,7 +480,7 @@ namespace Barotrauma
if (SelectedAiTarget?.Entity != null || EscapeTarget != null)
{
Entity t = SelectedAiTarget?.Entity ?? EscapeTarget;
float referencePos = Vector2.DistanceSquared(Character.WorldPosition, t.WorldPosition) > 100 * 100 && HasValidPath(requireNonDirty: true) ? PathSteering.CurrentPath.CurrentNode.WorldPosition.X : t.WorldPosition.X;
float referencePos = Vector2.DistanceSquared(Character.WorldPosition, t.WorldPosition) > 100 * 100 && HasValidPath() ? PathSteering.CurrentPath.CurrentNode.WorldPosition.X : t.WorldPosition.X;
Character.AnimController.TargetDir = Character.WorldPosition.X < referencePos ? Direction.Right : Direction.Left;
}
else
@@ -3934,7 +3934,7 @@ namespace Barotrauma
{
SteerAwayFromTheEnemy();
}
else if (canAttackDoors && HasValidPath(requireNonDirty: true, requireUnfinished: true))
else if (canAttackDoors && HasValidPath())
{
var door = PathSteering.CurrentPath.CurrentNode?.ConnectedDoor ?? PathSteering.CurrentPath.NextNode?.ConnectedDoor;
if (door != null && !door.CanBeTraversed && !door.HasAccess(Character))
@@ -1,16 +1,16 @@
using Barotrauma.Networking;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
namespace Barotrauma
{
partial class HumanAIController : AIController
{
public static bool debugai;
public static bool DebugAI;
public static bool DisableCrewAI;
private readonly AIObjectiveManager objectiveManager;
@@ -55,6 +55,7 @@ namespace Barotrauma
private readonly float obstacleRaycastIntervalShort = 1, obstacleRaycastIntervalLong = 5;
private float obstacleRaycastTimer;
private bool isBlocked;
private readonly float enemyCheckInterval = 0.2f;
private readonly float enemySpotDistanceOutside = 800;
@@ -92,7 +93,10 @@ namespace Barotrauma
private readonly SteeringManager outsideSteering, insideSteering;
public bool UseIndoorSteeringOutside { get; set; } = false;
/// <summary>
/// Waypoints that are not linked to a sub (e.g. main path).
/// </summary>
public bool UseOutsideWaypoints { get; private set; }
public IndoorsSteeringManager PathSteering => insideSteering as IndoorsSteeringManager;
public HumanoidAnimController AnimController => Character.AnimController as HumanoidAnimController;
@@ -225,14 +229,15 @@ namespace Barotrauma
IgnoredItems.Clear();
}
bool IsCloseEnoughToTarget(float threshold, bool useTargetSub = true)
// Note: returns false when useTargetSub is 'true' and the target is outside (targetSub is 'null')
bool IsCloseEnoughToTarget(float threshold, bool targetSub = true)
{
Entity target = SelectedAiTarget?.Entity;
if (target == null)
{
return false;
}
if (useTargetSub)
if (targetSub)
{
if (target.Submarine is Submarine sub)
{
@@ -244,62 +249,71 @@ namespace Barotrauma
return false;
}
}
return Vector2.DistanceSquared(Character.WorldPosition, target.WorldPosition) < MathUtils.Pow(threshold, 2);
return Vector2.DistanceSquared(Character.WorldPosition, target.WorldPosition) < MathUtils.Pow2(threshold);
}
bool hasValidPath = HasValidPath();
if (Character.Submarine == null)
bool isOutside = Character.Submarine == null;
if (isOutside)
{
obstacleRaycastTimer -= deltaTime;
if (obstacleRaycastTimer <= 0)
{
bool hasValidPath = HasValidPath();
isBlocked = false;
UseOutsideWaypoints = false;
obstacleRaycastTimer = obstacleRaycastIntervalLong;
if (SelectedAiTarget?.Entity == null || SelectedAiTarget.Entity is ISpatialEntity target && target.Submarine == null || !IsCloseEnoughToTarget(2000, useTargetSub: false))
ISpatialEntity spatialTarget = SelectedAiTarget?.Entity ?? ObjectiveManager.GetLastActiveObjective<AIObjectiveGoTo>()?.Target;
if (spatialTarget != null && (spatialTarget.Submarine == null || !IsCloseEnoughToTarget(2000, targetSub: false)))
{
// If the target is behind a level wall, switch to the pathing to get around the obstacles.
ISpatialEntity spatialTarget = SelectedAiTarget?.Entity;
if (spatialTarget == null)
IEnumerable<FarseerPhysics.Dynamics.Body> ignoredBodies = null;
Vector2 rayEnd = spatialTarget.SimPosition;
Submarine targetSub = spatialTarget.Submarine;
if (targetSub != null)
{
var gotoObjective = ObjectiveManager.GetActiveObjective<AIObjectiveGoTo>();
spatialTarget = gotoObjective?.Target;
rayEnd += targetSub.SimPosition;
ignoredBodies = targetSub.PhysicsBody.FarseerBody.ToEnumerable();
}
if (spatialTarget == null)
var obstacle = Submarine.PickBody(SimPosition, rayEnd, ignoredBodies, collisionCategory: Physics.CollisionLevel | Physics.CollisionWall);
isBlocked = obstacle != null;
// Don't use outside waypoints when blocked by a sub, because we should use the waypoints linked to the sub instead.
UseOutsideWaypoints = isBlocked && (obstacle.UserData is not Submarine sub || sub.Info.IsRuin);
bool resetPath = false;
if (UseOutsideWaypoints)
{
UseIndoorSteeringOutside = false;
bool isUsingInsideWaypoints = hasValidPath && HasValidPath(nodePredicate: n => n.Submarine != null || n.Ruin != null);
if (isUsingInsideWaypoints)
{
resetPath = true;
}
}
else
{
IEnumerable<FarseerPhysics.Dynamics.Body> ignoredBodies = null;
Vector2 rayEnd = spatialTarget.SimPosition;
Submarine targetSub = spatialTarget.Submarine;
if (targetSub != null)
bool isUsingOutsideWaypoints = hasValidPath && HasValidPath(nodePredicate: n => n.Submarine == null && n.Ruin == null);
if (isUsingOutsideWaypoints)
{
rayEnd += targetSub.SimPosition;
ignoredBodies = targetSub.PhysicsBody.FarseerBody.ToEnumerable();
resetPath = true;
}
var obstacle = Submarine.PickBody(SimPosition, rayEnd, ignoredBodies, collisionCategory: Physics.CollisionLevel | Physics.CollisionWall);
UseIndoorSteeringOutside = obstacle != null;
}
if (resetPath)
{
PathSteering.ResetPath();
}
}
else
else if (hasValidPath)
{
UseIndoorSteeringOutside = false;
if (hasValidPath)
obstacleRaycastTimer = obstacleRaycastIntervalShort;
// Swimming outside and using the path finder -> check that the path is not blocked with anything (the path finder doesn't know about other subs).
foreach (var connectedSub in Submarine.MainSub.GetConnectedSubs())
{
obstacleRaycastTimer = obstacleRaycastIntervalShort;
// Swimming outside and using the path finder -> check that the path is not blocked with anything (the path finder doesn't know about other subs).
foreach (var connectedSub in Submarine.MainSub.GetConnectedSubs())
if (connectedSub == Submarine.MainSub) { continue; }
Vector2 rayStart = SimPosition - connectedSub.SimPosition;
Vector2 dir = PathSteering.CurrentPath.CurrentNode.WorldPosition - WorldPosition;
Vector2 rayEnd = rayStart + dir.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 5);
if (Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true) != null)
{
if (connectedSub == Submarine.MainSub) { continue; }
Vector2 rayStart = SimPosition - connectedSub.SimPosition;
Vector2 dir = PathSteering.CurrentPath.CurrentNode.WorldPosition - WorldPosition;
Vector2 rayEnd = rayStart + dir.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 5);
if (Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true) != null)
{
PathSteering.CurrentPath.Unreachable = true;
break;
}
PathSteering.CurrentPath.Unreachable = true;
break;
}
}
}
@@ -307,10 +321,11 @@ namespace Barotrauma
}
else
{
UseIndoorSteeringOutside = false;
UseOutsideWaypoints = false;
isBlocked = false;
}
if (Character.Submarine == null || Character.IsOnPlayerTeam && !Character.IsEscorted && !Character.IsOnFriendlyTeam(Character.Submarine.TeamID))
if (isOutside || Character.IsOnPlayerTeam && !Character.IsEscorted && !Character.IsOnFriendlyTeam(Character.Submarine.TeamID))
{
// Spot enemies while staying outside or inside an enemy ship.
// does not apply for escorted characters, such as prisoners or terrorists who have their own behavior
@@ -352,12 +367,13 @@ namespace Barotrauma
}
}
}
if (UseIndoorSteeringOutside || Character.CurrentHull?.Submarine != null || hasValidPath || IsCloseEnoughToTarget(steeringBuffer))
bool useInsideSteering = !isOutside || isBlocked || HasValidPath() || IsCloseEnoughToTarget(steeringBuffer);
if (useInsideSteering)
{
if (steeringManager != insideSteering)
{
insideSteering.Reset();
PathSteering.ResetPath();
steeringManager = insideSteering;
}
if (IsCloseEnoughToTarget(maxSteeringBuffer))
@@ -395,6 +411,8 @@ namespace Barotrauma
}
objectiveManager.UpdateObjectives(deltaTime);
UpdateDragged(deltaTime);
if (reportProblemsTimer > 0)
{
reportProblemsTimer -= deltaTime;
@@ -433,10 +451,19 @@ namespace Barotrauma
if (Character.Submarine != null && (Character.Submarine.TeamID == Character.TeamID || Character.Submarine.TeamID == Character.OriginalTeamID || Character.IsEscorted) && !Character.Submarine.Info.IsWreck)
{
ReportProblems();
}
else
{
// Allows bots to heal targets autonomously while swimming outside of the sub.
if (AIObjectiveRescueAll.IsValidTarget(Character, Character))
{
AddTargets<AIObjectiveRescueAll, Character>(Character, Character);
}
}
reportProblemsTimer = reportProblemsInterval;
}
UpdateSpeaking();
SpeakAboutIssues();
UnequipUnnecessaryItems();
reactTimer = GetReactionTime();
}
@@ -904,17 +931,67 @@ namespace Barotrauma
}
}))
{
suitableContainer = targetContainer;
return true;
if (targetContainer != null &&
character.AIController is HumanAIController humanAI &&
humanAI.PathSteering.PathFinder.FindPath(character.SimPosition, targetContainer.SimPosition, character.Submarine, errorMsgStr: $"FindSuitableContainer ({character.DisplayName})", nodeFilter: node => node.Waypoint.CurrentHull != null).Unreachable)
{
ignoredItems.Add(targetContainer);
itemIndex = 0;
return false;
}
else
{
suitableContainer = targetContainer;
return true;
}
}
return false;
}
private float draggedTimer;
private float refuseDraggingTimer;
/// <summary>
/// The bot breaks free if being dragged by a human player from another team for longer than this
/// </summary>
private const float RefuseDraggingThresholdHigh = 10.0f;
/// <summary>
/// If the RefuseDraggingDuration is active (the bot recently broke free of being dragged), the bot breaks free much faster
/// </summary>
private const float RefuseDraggingThresholdLow = 0.5f;
private const float RefuseDraggingDuration = 30.0f;
private void UpdateDragged(float deltaTime)
{
if (Character.HumanPrefab is { AllowDraggingIndefinitely: true }) { return; }
if (Character.IsEscorted) { return; }
if (Character.LockHands) { return; }
//don't allow player characters who aren't in the same team to drag us for more than x seconds
if (Character.SelectedBy == null ||
!Character.SelectedBy.IsPlayer ||
Character.SelectedBy.TeamID == Character.TeamID)
{
refuseDraggingTimer -= deltaTime;
return;
}
draggedTimer += deltaTime;
if (draggedTimer > RefuseDraggingThresholdHigh ||
(refuseDraggingTimer > 0.0f && draggedTimer > RefuseDraggingThresholdLow))
{
draggedTimer = 0.0f;
refuseDraggingTimer = RefuseDraggingDuration;
Character.SelectedBy.DeselectCharacter();
Character.Speak(TextManager.Get("dialogrefusedragging").Value, delay: 0.5f, identifier: "refusedragging".ToIdentifier(), minDurationBetweenSimilar: 5.0f);
}
}
protected void ReportProblems()
{
Order newOrder = null;
Hull targetHull = null;
bool speak = Character.SpeechImpediment < 100;
// for now, escorted characters use the report system to get targets but do not speak. escort-character specific dialogue could be implemented
bool speak = Character.SpeechImpediment < 100 && !Character.IsEscorted;
if (Character.CurrentHull != null)
{
bool isFighting = ObjectiveManager.HasActiveObjective<AIObjectiveCombat>();
@@ -1013,25 +1090,21 @@ namespace Barotrauma
}
if (newOrder != null && speak)
{
// for now, escorted characters use the report system to get targets but do not speak. escort-character specific dialogue could be implemented
if (!Character.IsEscorted)
if (Character.TeamID == CharacterTeamType.FriendlyNPC)
{
if (Character.TeamID == CharacterTeamType.FriendlyNPC)
{
Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName?.Value ?? "", givingOrderToSelf: false), ChatMessageType.Default,
identifier: $"{newOrder.Prefab.Identifier}{targetHull?.RoomName ?? "null"}".ToIdentifier(),
minDurationBetweenSimilar: 60.0f);
}
else if (Character.IsOnPlayerTeam && GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime))
{
Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName?.Value ?? "", givingOrderToSelf: false), ChatMessageType.Order);
Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName?.Value ?? "", givingOrderToSelf: false), ChatMessageType.Default,
identifier: $"{newOrder.Prefab.Identifier}{targetHull?.RoomName ?? "null"}".ToIdentifier(),
minDurationBetweenSimilar: 60.0f);
}
else if (Character.IsOnPlayerTeam && GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime))
{
Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName?.Value ?? "", givingOrderToSelf: false), ChatMessageType.Order);
#if SERVER
GameMain.Server.SendOrderChatMessage(new OrderChatMessage(newOrder
.WithManualPriority(CharacterInfo.HighestManualOrderPriority)
.WithTargetEntity(targetHull)
.WithOrderGiver(Character), "", null, Character));
GameMain.Server.SendOrderChatMessage(new OrderChatMessage(newOrder
.WithManualPriority(CharacterInfo.HighestManualOrderPriority)
.WithTargetEntity(targetHull)
.WithOrderGiver(Character), "", null, Character));
#endif
}
}
}
}
@@ -1062,21 +1135,33 @@ namespace Barotrauma
}
}
private void UpdateSpeaking()
private void SpeakAboutIssues()
{
if (!Character.IsOnPlayerTeam) { return; }
if (Character.SpeechImpediment >= 100) { return; }
if (Character.Oxygen < 20.0f)
float minDelay = 0.5f, maxDelay = 2f;
if (Character.Oxygen < CharacterHealth.InsufficientOxygenThreshold)
{
Character.Speak(TextManager.Get("DialogLowOxygen").Value, null, Rand.Range(0.5f, 5.0f), "lowoxygen".ToIdentifier(), 30.0f);
string msgId = "DialogLowOxygen";
Character.Speak(TextManager.Get(msgId).Value, delay: Rand.Range(minDelay, maxDelay), identifier: msgId.ToIdentifier(), minDurationBetweenSimilar: 30.0f);
}
if (Character.Bleeding > 2.0f)
if (Character.Bleeding > 2.0f && !Character.IsMedic)
{
Character.Speak(TextManager.Get("DialogBleeding").Value, null, Rand.Range(0.5f, 5.0f), "bleeding".ToIdentifier(), 30.0f);
string msgId = "DialogBleeding";
Character.Speak(TextManager.Get(msgId).Value, delay: Rand.Range(minDelay, maxDelay), identifier: msgId.ToIdentifier(), minDurationBetweenSimilar: 30.0f);
}
if (Character.PressureTimer > 50.0f && Character.CurrentHull?.DisplayName != null)
if ((Character.CurrentHull == null || Character.CurrentHull.LethalPressure > 0) && !Character.IsProtectedFromPressure)
{
Character.Speak(TextManager.GetWithVariable("DialogPressure", "[roomname]", Character.CurrentHull.DisplayName, FormatCapitals.Yes).Value, null, Rand.Range(0.5f, 5.0f), "pressure".ToIdentifier(), 30.0f);
if (Character.PressureProtection > 0)
{
string msgId = "DialogInsufficientPressureProtection";
Character.Speak(TextManager.Get(msgId).Value, delay: Rand.Range(minDelay, maxDelay), identifier: msgId.ToIdentifier(), minDurationBetweenSimilar: 30.0f);
}
else if (Character.CurrentHull?.DisplayName != null)
{
string msgId = "DialogPressure";
Character.Speak(TextManager.GetWithVariable(msgId, "[roomname]", Character.CurrentHull.DisplayName, FormatCapitals.Yes).Value, delay: Rand.Range(minDelay, maxDelay), identifier: msgId.ToIdentifier(), minDurationBetweenSimilar: 30.0f);
}
}
}
@@ -1205,7 +1290,7 @@ namespace Barotrauma
bool isAccidental = attacker.IsBot && !IsMentallyUnstable && !attacker.AIController.IsMentallyUnstable && attacker.CombatAction == null;
if (isAccidental)
{
if (!Character.IsSecurity && cumulativeDamage > minorDamageThreshold)
if (attacker.TeamID != Character.TeamID || (!Character.IsSecurity && cumulativeDamage > minorDamageThreshold))
{
AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker);
}
@@ -1371,7 +1456,7 @@ namespace Barotrauma
}
else
{
if (humanAI.ObjectiveManager.GetActiveObjective<AIObjectiveCombat>()?.Enemy == attacker)
if (humanAI.ObjectiveManager.GetLastActiveObjective<AIObjectiveCombat>()?.Enemy == attacker)
{
// Already targeting the attacker -> treat as a more serious threat.
cumulativeDamage *= 2;
@@ -1556,7 +1641,7 @@ namespace Barotrauma
hull.LethalPressure > 0 ||
hull.ConnectedGaps.Any(gap => !gap.IsRoomToRoom && gap.Open > 0.9f))
{
needsSuit = !Character.IsProtectedFromPressure;
needsSuit = (hull == null || hull.LethalPressure > 0) && !Character.IsImmuneToPressure;
return needsAir || needsSuit;
}
if (hull.WaterPercentage > 60 || hull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 1)
@@ -1656,7 +1741,7 @@ namespace Barotrauma
if (GameMain.GameSession?.Campaign?.Map?.CurrentLocation?.Reputation != null && character.IsPlayer)
{
var reputationLoss = damageAmount * Reputation.ReputationLossPerWallDamage;
GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.AddReputation(-reputationLoss);
GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.AddReputation(-reputationLoss, Reputation.MaxReputationLossFromWallDamage);
}
if (accumulatedDamage <= WarningThreshold) { return; }
@@ -1812,7 +1897,7 @@ namespace Barotrauma
/// </summary>
public static void PropagateHullSafety(Character character, Hull hull)
{
DoForEachCrewMember(character, (humanAi) => humanAi.RefreshHullSafety(hull));
DoForEachBot(character, (humanAi) => humanAi.RefreshHullSafety(hull));
}
private void RefreshHullSafety(Hull hull)
@@ -1885,7 +1970,7 @@ namespace Barotrauma
private static bool AddTargets<T1, T2>(Character caller, T2 target) where T1 : AIObjectiveLoop<T2>
{
bool targetAdded = false;
DoForEachCrewMember(caller, humanAI =>
DoForEachBot(caller, humanAI =>
{
if (caller != humanAI.Character && caller.SpeechImpediment >= 100) { return; }
var objective = humanAI.ObjectiveManager.GetObjective<T1>();
@@ -1902,7 +1987,7 @@ namespace Barotrauma
public static void RemoveTargets<T1, T2>(Character caller, T2 target) where T1 : AIObjectiveLoop<T2>
{
DoForEachCrewMember(caller, humanAI =>
DoForEachBot(caller, humanAI =>
humanAI.ObjectiveManager.GetObjective<T1>()?.ReportedTargets.Remove(target));
}
@@ -2024,6 +2109,10 @@ namespace Barotrauma
public float GetHullSafety(Hull hull, Character character, IEnumerable<Hull> visibleHulls = null)
{
if (hull == null)
{
return CalculateHullSafety(hull, character, visibleHulls);
}
if (!knownHulls.TryGetValue(hull, out HullSafety hullSafety))
{
hullSafety = new HullSafety(CalculateHullSafety(hull, character, visibleHulls));
@@ -2038,6 +2127,10 @@ namespace Barotrauma
public static float GetHullSafety(Hull hull, IEnumerable<Hull> visibleHulls, Character character, bool ignoreWater = false, bool ignoreOxygen = false, bool ignoreFire = false, bool ignoreEnemies = false)
{
if (hull == null)
{
return CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies);
}
HullSafety hullSafety;
if (character.AIController is HumanAIController controller)
{
@@ -2069,103 +2162,137 @@ namespace Barotrauma
if (other.IsPet)
{
// Hostile NPCs are hostile to all pets, unless they are in the same team.
if (!sameTeam && me.TeamID == CharacterTeamType.None) { return false; }
return sameTeam || me.TeamID != CharacterTeamType.None;
}
else
{
if (!me.IsSameSpeciesOrGroup(other)) { return false; }
}
if (GameMain.GameSession?.GameMode is CampaignMode campaign)
if (GameMain.GameSession?.GameMode is CampaignMode)
{
if ((me.TeamID == CharacterTeamType.FriendlyNPC && other.TeamID == CharacterTeamType.Team1) ||
if ((me.TeamID == CharacterTeamType.FriendlyNPC && other.TeamID == CharacterTeamType.Team1) ||
(me.TeamID == CharacterTeamType.Team1 && other.TeamID == CharacterTeamType.FriendlyNPC))
{
Character npc = me.TeamID == CharacterTeamType.FriendlyNPC ? me : other;
Identifier npcFaction = npc.Faction;
Identifier currentLocationFaction = campaign.Map?.CurrentLocation?.Faction?.Prefab.Identifier ?? Identifier.Empty;
if (npcFaction.IsEmpty)
//NPCs that allow some campaign interaction are not turned hostile by low reputation
if (npc.CampaignInteractionType != CampaignMode.InteractionType.None) { return true; }
if (npc.AIController is HumanAIController npcAI)
{
//if faction identifier is not specified, assume the NPC is a member of the faction that owns the outpost
npcFaction = currentLocationFaction;
}
if (!currentLocationFaction.IsEmpty && npcFaction == currentLocationFaction)
{
var reputation = campaign.Map?.CurrentLocation?.Reputation;
if (reputation != null && reputation.NormalizedValue < Reputation.HostileThreshold)
{
return false;
}
return !npcAI.IsInHostileFaction();
}
}
}
return true;
}
public static bool IsActive(Character other) => other != null && !other.Removed && !other.IsDead && !other.IsUnconscious;
public static bool IsTrueForAllCrewMembers(Character character, Func<HumanAIController, bool> predicate)
public bool IsInHostileFaction()
{
if (character == null) { return false; }
foreach (var c in Character.CharacterList)
if (GameMain.GameSession?.GameMode is not CampaignMode campaign) { return false; }
if (Character.IsEscorted) { return false; }
Identifier npcFaction = Character.Faction;
Identifier currentLocationFaction = campaign.Map?.CurrentLocation?.Faction?.Prefab.Identifier ?? Identifier.Empty;
if (npcFaction.IsEmpty)
{
if (FilterCrewMember(character, c))
{
if (!predicate(c.AIController as HumanAIController))
{
return false;
}
}
//if faction identifier is not specified, assume the NPC is a member of the faction that owns the outpost
npcFaction = currentLocationFaction;
}
return true;
}
public static bool IsTrueForAnyCrewMember(Character character, Func<HumanAIController, bool> predicate)
{
if (character == null) { return false; }
foreach (var c in Character.CharacterList)
if (!currentLocationFaction.IsEmpty && npcFaction == currentLocationFaction)
{
if (FilterCrewMember(character, c))
if (campaign.CurrentLocation is { IsFactionHostile: true })
{
if (predicate(c.AIController as HumanAIController))
{
return true;
}
return true;
}
}
return false;
}
public static int CountCrew(Character character, Func<HumanAIController, bool> predicate = null, bool onlyActive = true, bool onlyBots = false)
public static bool IsActive(Character c) => c != null && c.Enabled && !c.IsUnconscious;
public static bool IsTrueForAllBotsInTheCrew(Character character, Func<HumanAIController, bool> predicate)
{
if (character == null) { return false; }
foreach (var c in Character.CharacterList)
{
if (!IsBotInTheCrew(character, c)) { continue; }
if (!predicate(c.AIController as HumanAIController))
{
return false;
}
}
return true;
}
public static bool IsTrueForAnyBotInTheCrew(Character character, Func<HumanAIController, bool> predicate)
{
if (character == null) { return false; }
foreach (var c in Character.CharacterList)
{
if (!IsBotInTheCrew(character, c)) { continue; }
if (predicate(c.AIController as HumanAIController))
{
return true;
}
}
return false;
}
public static int CountBotsInTheCrew(Character character, Func<HumanAIController, bool> predicate = null)
{
if (character == null) { return 0; }
int count = 0;
foreach (var other in Character.CharacterList)
{
if (onlyActive && !IsActive(other))
if (!IsBotInTheCrew(character, other)) { continue; }
if (predicate == null || predicate(other.AIController as HumanAIController))
{
continue;
}
if (onlyBots && other.IsPlayer)
{
continue;
}
if (FilterCrewMember(character, other))
{
if (predicate == null || predicate(other.AIController as HumanAIController))
{
count++;
}
count++;
}
}
return count;
}
public static void DoForEachCrewMember(Character character, Action<HumanAIController> action, float range = float.PositiveInfinity)
/// <summary>
/// Including the player characters in the same team.
/// </summary>
public bool IsTrueForAnyCrewMember(Func<Character, bool> predicate, bool onlyActive = true, bool onlyConnectedSubs = false)
{
foreach (var c in Character.CharacterList)
{
if (!IsActive(c)) { continue; }
if (c.TeamID != Character.TeamID) { continue; }
if (onlyActive && c.IsIncapacitated) { continue; }
if (onlyConnectedSubs)
{
if (Character.Submarine == null)
{
if (c.Submarine != null)
{
return false;
}
}
else if (c.Submarine != Character.Submarine && !Character.Submarine.GetConnectedSubs().Contains(c.Submarine))
{
return false;
}
}
if (predicate(c))
{
return true;
}
}
return false;
}
private static void DoForEachBot(Character character, Action<HumanAIController> action, float range = float.PositiveInfinity)
{
if (character == null) { return; }
foreach (var c in Character.CharacterList)
{
if (FilterCrewMember(character, c) && CheckReportRange(character, c, range))
if (IsBotInTheCrew(character, c) && CheckReportRange(character, c, range))
{
action(c.AIController as HumanAIController);
}
@@ -2185,7 +2312,7 @@ namespace Barotrauma
}
}
private static bool FilterCrewMember(Character self, Character other) => other != null && !other.IsDead && !other.Removed && other.AIController is HumanAIController humanAi && humanAi.IsFriendly(self);
private static bool IsBotInTheCrew(Character self, Character other) => IsActive(other) && other.TeamID == self.TeamID && !other.IsIncapacitated && other.IsBot && other.AIController is HumanAIController;
public static bool IsItemTargetedBySomeone(ItemComponent target, CharacterTeamType team, out Character operatingCharacter)
{
@@ -2230,10 +2357,9 @@ namespace Barotrauma
bool isOrder = IsOrderedToOperateThis(Character.AIController);
foreach (Character c in Character.CharacterList)
{
if (!IsActive(c)) { continue; }
if (c == Character) { continue; }
if (c.Removed) { continue; }
if (c.TeamID != Character.TeamID) { continue; }
if (c.IsIncapacitated) { continue; }
if (c.IsPlayer)
{
if (c.SelectedItem == target.Item)
@@ -2301,9 +2427,9 @@ namespace Barotrauma
bool isOrder = IsOrderedToRepairThis(Character.AIController as HumanAIController);
foreach (var c in Character.CharacterList)
{
if (!IsActive(c)) { continue; }
if (c == Character) { continue; }
if (c.TeamID != Character.TeamID) { continue; }
if (c.IsIncapacitated) { continue; }
other = c;
if (c.IsPlayer)
{
@@ -2317,7 +2443,7 @@ namespace Barotrauma
{
var repairItemsObjective = operatingAI.ObjectiveManager.GetObjective<AIObjectiveRepairItems>();
if (repairItemsObjective == null) { continue; }
if (!(repairItemsObjective.SubObjectives.FirstOrDefault(o => o is AIObjectiveRepairItem) is AIObjectiveRepairItem activeObjective) || activeObjective.Item != target)
if (repairItemsObjective.SubObjectives.FirstOrDefault(o => o is AIObjectiveRepairItem) is not AIObjectiveRepairItem activeObjective || activeObjective.Item != target)
{
// Not targeting the same item.
continue;
@@ -2352,11 +2478,10 @@ namespace Barotrauma
}
#region Wrappers
public bool IsFriendly(Character other) => IsFriendly(Character, other);
public void DoForEachCrewMember(Action<HumanAIController> action) => DoForEachCrewMember(Character, action);
public bool IsTrueForAnyCrewMember(Func<HumanAIController, bool> predicate) => IsTrueForAnyCrewMember(Character, predicate);
public bool IsTrueForAllCrewMembers(Func<HumanAIController, bool> predicate) => IsTrueForAllCrewMembers(Character, predicate);
public int CountCrew(Func<HumanAIController, bool> predicate = null, bool onlyActive = true, bool onlyBots = false) => CountCrew(Character, predicate, onlyActive, onlyBots);
public bool IsFriendly(Character other, bool onlySameTeam = false) => IsFriendly(Character, other, onlySameTeam);
public bool IsTrueForAnyBotInTheCrew(Func<HumanAIController, bool> predicate) => IsTrueForAnyBotInTheCrew(Character, predicate);
public bool IsTrueForAllBotsInTheCrew(Func<HumanAIController, bool> predicate) => IsTrueForAllBotsInTheCrew(Character, predicate);
public int CountBotsInTheCrew(Func<HumanAIController, bool> predicate = null) => CountBotsInTheCrew(Character, predicate);
#endregion
}
}
@@ -180,7 +180,7 @@ namespace Barotrauma
private Vector2 CalculateSteeringSeek(Vector2 target, float weight, float minGapSize = 0, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null, bool checkVisibility = true)
{
bool needsNewPath = currentPath == null || currentPath.Unreachable || currentPath.Finished;
bool needsNewPath = currentPath == null || currentPath.Unreachable || currentPath.Finished || currentPath.CurrentNode == null;
if (!needsNewPath && character.Submarine != null && character.Params.PathFinderPriority > 0.5f)
{
Vector2 targetDiff = target - currentTarget;
@@ -205,6 +205,24 @@ namespace Barotrauma
if (needsNewPath || findPathTimer < -1.0f)
{
IsPathDirty = true;
if (!needsNewPath && findPathTimer < -1)
{
if (character.Submarine != null && Math.Abs(character.AnimController.TargetMovement.Combine()) <= 0)
{
// Not moving -> need a new path.
needsNewPath = true;
}
if (character.Submarine == null && currentPath?.CurrentNode is WayPoint wp && wp.CurrentHull != null)
{
// Current node inside, while we are outside
// -> Check that the current node is not too far (can happen e.g. if someone controls the character in the meanwhile)
float maxDist = 200;
if (Vector2.DistanceSquared(character.WorldPosition, wp.WorldPosition) > maxDist * maxDist)
{
needsNewPath = true;
}
}
}
if (findPathTimer < 0)
{
SkipCurrentPathNodes();
@@ -213,7 +231,7 @@ namespace Barotrauma
pathFinder.InsideSubmarine = character.Submarine != null && !character.Submarine.Info.IsRuin;
pathFinder.ApplyPenaltyToOutsideNodes = character.Submarine != null && !character.IsProtectedFromPressure;
var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", minGapSize, startNodeFilter, endNodeFilter, nodeFilter, checkVisibility: checkVisibility);
bool useNewPath = needsNewPath || currentPath == null || currentPath.CurrentNode == null || character.Submarine != null && findPathTimer < -1 && Math.Abs(character.AnimController.TargetMovement.Combine()) <= 0;
bool useNewPath = needsNewPath;
if (!useNewPath && currentPath?.CurrentNode != null && newPath.Nodes.Any() && !newPath.Unreachable)
{
// Check if the new path is the same as the old, in which case we just ignore it and continue using the old path (or the progress would reset).
@@ -387,49 +405,57 @@ namespace Barotrauma
}
if (character.IsClimbing && useLadders)
{
bool nextLadderSameAsCurrent = IsNextLadderSameAsCurrent;
if (nextLadderSameAsCurrent || currentLadder != null && nextLadder != null && Math.Abs(currentLadder.Item.Position.X - nextLadder.Item.Position.X) < 50)
if (currentLadder == null && nextLadder != null)
{
//climbing ladders -> don't move horizontally
diff.X = 0.0f;
// Climbing a ladder but the path is still on the node next to the ladder -> Skip the node.
NextNode(!doorsChecked);
}
//at the same height as the waypoint
float heightDiff = Math.Abs(collider.SimPosition.Y - currentPath.CurrentNode.SimPosition.Y);
float colliderSize = (collider.Height / 2 + collider.Radius) * 1.25f;
if (heightDiff < colliderSize)
else
{
float heightFromFloor = character.AnimController.GetHeightFromFloor();
// We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative.
bool isAboveFloor = heightFromFloor > -0.1f;
// If the next waypoint is horizontally far, we don't want to keep holding the ladders
if (isAboveFloor && !currentPath.IsAtEndNode && (nextLadder == null || Math.Abs(currentPath.CurrentNode.WorldPosition.X - currentPath.NextNode.WorldPosition.X) > 50))
bool nextLadderSameAsCurrent = IsNextLadderSameAsCurrent;
if (nextLadderSameAsCurrent || currentLadder != null && nextLadder != null && Math.Abs(currentLadder.Item.Position.X - nextLadder.Item.Position.X) < 50)
{
character.StopClimbing();
//climbing ladders -> don't move horizontally
diff.X = 0.0f;
}
else if (nextLadder != null && !nextLadderSameAsCurrent)
//at the same height as the waypoint
float heightDiff = Math.Abs(collider.SimPosition.Y - currentPath.CurrentNode.SimPosition.Y);
float colliderSize = (collider.Height / 2 + collider.Radius) * 1.25f;
if (heightDiff < colliderSize)
{
// Try to change the ladder (hatches between two submarines)
if (character.SelectedSecondaryItem != nextLadder.Item && character.CanInteractWith(nextLadder.Item))
float heightFromFloor = character.AnimController.GetHeightFromFloor();
// We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative.
bool isAboveFloor = heightFromFloor > -0.1f;
// If the next waypoint is horizontally far, we don't want to keep holding the ladders
if (isAboveFloor && !currentPath.IsAtEndNode && (nextLadder == null || Math.Abs(currentPath.CurrentNode.WorldPosition.X - currentPath.NextNode.WorldPosition.X) > 50))
{
if (nextLadder.Item.TryInteract(character, forceSelectKey: true))
character.StopClimbing();
}
else if (nextLadder != null && !nextLadderSameAsCurrent)
{
// Try to change the ladder (hatches between two submarines)
if (character.SelectedSecondaryItem != nextLadder.Item && character.CanInteractWith(nextLadder.Item))
{
NextNode(!doorsChecked);
if (nextLadder.Item.TryInteract(character, forceSelectKey: true))
{
NextNode(!doorsChecked);
}
}
}
if (isAboveFloor || nextLadderSameAsCurrent || nextLadder == null && Math.Abs(diff.Y) < 10)
{
NextNode(!doorsChecked);
}
}
if (!currentPath.IsAtEndNode && (isAboveFloor || nextLadderSameAsCurrent || nextLadder == null && Math.Abs(diff.Y) < 10))
else if (nextLadder != null)
{
NextNode(!doorsChecked);
}
}
else if (nextLadder != null)
{
//if the current node is below the character and the next one is above (or vice versa)
//and both are on ladders, we can skip directly to the next one
//e.g. no point in going down to reach the starting point of a path when we could go directly to the one above
if (Math.Sign(currentPath.CurrentNode.WorldPosition.Y - character.WorldPosition.Y) != Math.Sign(currentPath.NextNode.WorldPosition.Y - character.WorldPosition.Y))
{
NextNode(!doorsChecked);
if (Math.Sign(currentPath.CurrentNode.WorldPosition.Y - character.WorldPosition.Y) != Math.Sign(currentPath.NextNode.WorldPosition.Y - character.WorldPosition.Y))
{
//if the current node is below the character and the next one is above (or vice versa)
//and both are on ladders, we can skip directly to the next one
//e.g. no point in going down to reach the starting point of a path when we could go directly to the one above
NextNode(!doorsChecked);
}
}
}
return ConvertUnits.ToSimUnits(diff);
@@ -486,7 +512,7 @@ namespace Barotrauma
}
}
float targetDistance = Math.Max(colliderSize.X / 2 * margin, minWidth / 2);
if (horizontalDistance < targetDistance && !isTargetTooHigh && !isTargetTooLow && (door == null || door.CanBeTraversed))
if (horizontalDistance < targetDistance && !isTargetTooHigh && !isTargetTooLow && currentLadder == null && (door == null || door.CanBeTraversed))
{
NextNode(!doorsChecked);
}
@@ -122,7 +122,7 @@ namespace Barotrauma
foreach (Affliction affliction in afflictions)
{
var currentEffect = affliction.GetActiveEffect();
if (currentEffect != null && !string.IsNullOrEmpty(currentEffect.DialogFlag.Value) && !currentFlags.Contains(currentEffect.DialogFlag))
if (currentEffect is { DialogFlag.IsEmpty: false } && !currentFlags.Contains(currentEffect.DialogFlag))
{
currentFlags.Add(currentEffect.DialogFlag);
}
@@ -506,15 +506,29 @@ namespace Barotrauma
}
}
protected static bool CanEquip(Character character, Item item)
protected static bool CanEquip(Character character, Item item, bool allowWearing)
{
bool canEquip = item != null;
if (canEquip && !item.AllowedSlots.Contains(InvSlotType.Any))
if (item == null) { return false; }
bool canEquip = false;
if (item.AllowedSlots.Contains(InvSlotType.Any))
{
if (character.Inventory.IsAnySlotAvailable(item))
{
canEquip = true;
}
}
if (!canEquip)
{
canEquip = false;
var inv = character.Inventory;
foreach (var allowedSlot in item.AllowedSlots)
{
if (!allowWearing)
{
if (!allowedSlot.HasFlag(InvSlotType.RightHand) && !allowedSlot.HasFlag(InvSlotType.LeftHand))
{
continue;
}
}
foreach (var slotType in inv.SlotTypes)
{
if (!allowedSlot.HasFlag(slotType)) { continue; }
@@ -530,18 +544,9 @@ namespace Barotrauma
}
}
}
return canEquip;
}
protected bool CheckItemIdentifiersOrTags(Item item, ImmutableHashSet<Identifier> identifiersOrTags)
{
if (identifiersOrTags.Contains(item.Prefab.Identifier)) { return true; }
foreach (var identifier in identifiersOrTags)
{
if (item.HasTag(identifier)) { return true; }
}
return false;
return canEquip && character.Inventory.CanBePut(item);
}
protected bool CanEquip(Item item) => CanEquip(character, item);
protected bool CanEquip(Item item, bool allowWearing) => CanEquip(character, item, allowWearing);
}
}
@@ -64,7 +64,6 @@ namespace Barotrauma
if (subObjectives.Any()) { return; }
if (HumanAIController.FindSuitableContainer(character, item, ignoredContainers, ref itemIndex, out Item suitableContainer))
{
itemIndex = 0;
if (suitableContainer != null)
{
bool equip = item.GetComponent<Holdable>() != null ||
@@ -112,10 +111,7 @@ namespace Barotrauma
Abandon = true;
}
}
else
{
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
}
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
}
protected override bool CheckObjectiveSpecific()
@@ -121,7 +121,7 @@ namespace Barotrauma
{
return true;
}
return CanEquip(character, item);
return CanEquip(character, item, allowWearing: false);
}
public override void OnDeselected()
@@ -170,13 +170,33 @@ namespace Barotrauma
return Priority;
}
}
float damageFactor = MathUtils.InverseLerp(0.0f, 5.0f, character.GetDamageDoneByAttacker(Enemy) / 100.0f);
Priority = TargetEliminated ? 0 : Math.Min((95 + damageFactor) * PriorityModifier, 100);
if (Priority > 0)
if (TargetEliminated)
{
if (EnemyAIController.IsLatchedToSomeoneElse(Enemy, character))
Priority = 0;
}
else
{
// 91-100
float minPriority = AIObjectiveManager.EmergencyObjectivePriority + 1;
float maxPriority = AIObjectiveManager.MaxObjectivePriority;
float priorityScale = maxPriority - minPriority;
float xDist = Math.Abs(character.WorldPosition.X - Enemy.WorldPosition.X);
float yDist = Math.Abs(character.WorldPosition.Y - Enemy.WorldPosition.Y);
if (HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull))
{
Priority = 0;
xDist /= 2;
yDist /= 2;
}
float distanceFactor = MathUtils.InverseLerp(3000, 0, xDist + yDist * 5);
float devotion = CumulatedDevotion / 100;
float additionalPriority = MathHelper.Lerp(0, priorityScale, Math.Clamp(devotion + distanceFactor, 0, 1));
Priority = Math.Min((minPriority + additionalPriority) * PriorityModifier, maxPriority);
if (Priority > 0)
{
if (EnemyAIController.IsLatchedToSomeoneElse(Enemy, character))
{
Priority = 0;
}
}
}
return Priority;
@@ -312,12 +332,10 @@ namespace Barotrauma
}
else
{
AskHelp();
Retreat(deltaTime);
}
break;
case CombatMode.Retreat:
AskHelp();
Retreat(deltaTime);
break;
default:
@@ -352,7 +370,7 @@ namespace Barotrauma
Weapon = null;
continue;
}
if (WeaponComponent.IsNotEmpty(character))
if (!WeaponComponent.IsEmpty(character))
{
// All good, the weapon is loaded
break;
@@ -470,7 +488,7 @@ namespace Barotrauma
// Not in the inventory anymore or cannot find the weapon component
return false;
}
if (!WeaponComponent.IsNotEmpty(character))
if (WeaponComponent.IsEmpty(character))
{
// Try reloading (and seek ammo)
if (!Reload(seekAmmo))
@@ -541,7 +559,7 @@ namespace Barotrauma
priority /= 2;
}
}
if (!weapon.IsNotEmpty(character))
if (weapon.IsEmpty(character))
{
if (weapon is RangedWeapon && !isAllowedToSeekWeapons)
{
@@ -554,7 +572,6 @@ namespace Barotrauma
priority /= 2;
}
}
if (Enemy.Params.Health.StunImmunity)
{
if (weapon.Item.HasTag("stunner"))
@@ -750,7 +767,7 @@ namespace Barotrauma
private bool Equip()
{
if (character.LockHands) { return false; }
if (!WeaponComponent.HasRequiredContainedItems(character, addMessage: false))
if (WeaponComponent.IsEmpty(character))
{
return false;
}
@@ -783,6 +800,10 @@ namespace Barotrauma
private void Retreat(float deltaTime)
{
if (!Enemy.IsHuman)
{
SpeakRetreating();
}
RemoveFollowTarget();
RemoveSubObjective(ref seekAmmunitionObjective);
if (retreatObjective != null && retreatObjective.Target != retreatTarget)
@@ -793,6 +814,7 @@ namespace Barotrauma
{
// Swim away
SteeringManager.Reset();
character.ReleaseSecondaryItem();
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(character.WorldPosition - Enemy.WorldPosition));
SteeringManager.SteeringAvoid(deltaTime, 5, weight: 2);
return;
@@ -819,7 +841,8 @@ namespace Barotrauma
{
TryAddSubObjective(ref retreatObjective, () => new AIObjectiveGoTo(retreatTarget, character, objectiveManager)
{
UsePathingOutside = false
UsePathingOutside = false,
SpeakIfFails = false
},
onAbandon: () =>
{
@@ -861,6 +884,7 @@ namespace Barotrauma
{
if (sqrDistance > MathUtils.Pow2(meleeWeapon.Range))
{
character.ReleaseSecondaryItem();
// Swim towards the target
SteeringManager.Reset();
SteeringManager.SteeringSeek(character.GetRelativeSimPosition(Enemy), weight: 10);
@@ -882,7 +906,8 @@ namespace Barotrauma
UsePathingOutside = false,
IgnoreIfTargetDead = true,
TargetName = Enemy.DisplayName,
AlwaysUseEuclideanDistance = false
AlwaysUseEuclideanDistance = false,
SpeakIfFails = false
},
onAbandon: () =>
{
@@ -966,7 +991,7 @@ namespace Barotrauma
item.GetComponent<RangedWeapon>() != null)
{
item.Drop(character);
character.Inventory.TryPutItem(item, character, CharacterInventory.anySlot);
character.Inventory.TryPutItem(item, character, CharacterInventory.AnySlot);
}
}
}
@@ -1028,54 +1053,43 @@ namespace Barotrauma
if (Weapon.OwnInventory == null) { return true; }
// Eject empty ammo
HumanAIController.UnequipEmptyItems(Weapon);
RelatedItem item = null;
Item ammunition = null;
ImmutableHashSet<Identifier> ammunitionIdentifiers = null;
if (WeaponComponent.requiredItems.ContainsKey(RelatedItem.RelationType.Contained))
{
foreach (RelatedItem requiredItem in WeaponComponent.requiredItems[RelatedItem.RelationType.Contained])
{
ammunition = Weapon.OwnInventory.AllItems.FirstOrDefault(it => it.Condition > 0 && requiredItem.MatchesItem(it));
if (ammunition != null)
{
// Ammunition still remaining
return true;
}
item = requiredItem;
if (Weapon.OwnInventory.AllItems.Any(it => it.Condition > 0 && requiredItem.MatchesItem(it))) { continue; }
ammunitionIdentifiers = requiredItem.Identifiers;
break;
}
}
else if (WeaponComponent is MeleeWeapon meleeWeapon)
{
ammunitionIdentifiers = meleeWeapon.PreferredContainedItems;
}
// No ammo
if (ammunition == null)
if (ammunitionIdentifiers != null)
{
if (ammunitionIdentifiers != null)
// Try reload ammunition from inventory
static bool IsInsideHeadset(Item i) => i.ParentInventory?.Owner is Item ownerItem && ownerItem.HasTag("mobileradio");
Item ammunition = character.Inventory.FindItem(i => i.HasIdentifierOrTags(ammunitionIdentifiers) && i.Condition > 0 && !IsInsideHeadset(i), recursive: true);
if (ammunition != null)
{
// Try reload ammunition from inventory
static bool IsInsideHeadset(Item i) => i.ParentInventory?.Owner is Item ownerItem && ownerItem.HasTag("mobileradio");
ammunition = character.Inventory.FindItem(i => CheckItemIdentifiersOrTags(i, ammunitionIdentifiers) && i.Condition > 0 && !IsInsideHeadset(i), recursive: true);
if (ammunition != null)
var container = Weapon.GetComponent<ItemContainer>();
if (!container.Inventory.TryPutItem(ammunition, user: character))
{
var container = Weapon.GetComponent<ItemContainer>();
if (!container.Inventory.TryPutItem(ammunition, null))
if (ammunition.ParentInventory == character.Inventory)
{
if (ammunition.ParentInventory == character.Inventory)
{
ammunition.Drop(character);
}
ammunition.Drop(character);
}
}
}
}
if (WeaponComponent.HasRequiredContainedItems(character, addMessage: false))
if (!WeaponComponent.IsEmpty(character))
{
return true;
}
else if (ammunition == null && !HoldPosition && IsOffensiveOrArrest && seekAmmo && ammunitionIdentifiers != null)
else if (!HoldPosition && IsOffensiveOrArrest && seekAmmo && ammunitionIdentifiers != null)
{
SeekAmmunition(ammunitionIdentifiers);
}
@@ -1270,7 +1284,7 @@ namespace Barotrauma
}
private void SpeakNoWeapons() => Speak("dialogcombatnoweapons".ToIdentifier(), delay: 0, minDuration: 30);
private void AskHelp() => Speak("dialogcombatretreating".ToIdentifier(), delay: Rand.Range(0f, 1f), minDuration: 20);
private void SpeakRetreating() => Speak("dialogcombatretreating".ToIdentifier(), delay: Rand.Range(0f, 1f), minDuration: 20);
private void Speak(Identifier textIdentifier, float delay, float minDuration)
{
@@ -109,7 +109,7 @@ namespace Barotrauma
private bool CheckItem(Item item)
{
return CheckItemIdentifiersOrTags(item, itemIdentifiers) && item.ConditionPercentage >= ConditionLevel && item.HasAccess(character);
return item.HasIdentifierOrTags(itemIdentifiers) && item.ConditionPercentage >= ConditionLevel && item.HasAccess(character);
}
protected override void Act(float deltaTime)
@@ -156,15 +156,15 @@ namespace Barotrauma
Inventory originalInventory = ItemToContain.ParentInventory;
var slots = originalInventory?.FindIndices(ItemToContain);
static bool TryPutItem(Inventory inventory, int? targetSlot, Item itemToContain)
bool TryPutItem(Inventory inventory, int? targetSlot, Item itemToContain)
{
if (targetSlot.HasValue)
{
return inventory.TryPutItem(itemToContain, targetSlot.Value, allowSwapping: false, allowCombine: false, user: null);
return inventory.TryPutItem(itemToContain, targetSlot.Value, allowSwapping: false, allowCombine: false, user: character);
}
else
{
return inventory.TryPutItem(itemToContain, user: null);
return inventory.TryPutItem(itemToContain, user: character);
}
}
@@ -202,7 +202,7 @@ namespace Barotrauma
ItemToContain == null || ItemToContain.Removed ||
!ItemToContain.IsOwnedBy(character) || container.Item.GetRootInventoryOwner() is Character c && c != character,
SpeakIfFails = !objectiveManager.IsCurrentOrder<AIObjectiveCleanupItems>(),
endNodeFilter = n => Vector2.DistanceSquared(n.Waypoint.WorldPosition, container.Item.WorldPosition) <= MathUtils.Pow2(AIObjectiveGetItem.DefaultReach)
endNodeFilter = n => Vector2.DistanceSquared(n.Waypoint.WorldPosition, container.Item.WorldPosition) <= MathUtils.Pow2(AIObjectiveGetItem.MaxReach)
},
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref goToObjective));
@@ -66,7 +66,7 @@ namespace Barotrauma
else
{
float devotion = CumulatedDevotion / 100;
Priority = MathHelper.Lerp(0, 100, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1));
Priority = MathHelper.Lerp(0, AIObjectiveManager.MaxObjectivePriority, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1));
}
}
return Priority;
@@ -28,7 +28,7 @@ namespace Barotrauma
if (character.IsSecurity) { return 100; }
if (objectiveManager.IsOrder(this)) { return 100; }
// If there's any security officers onboard, leave fighting for them.
return HumanAIController.IsTrueForAnyCrewMember(c => c.Character.IsSecurity && !c.Character.IsIncapacitated && c.Character.Submarine == character.Submarine) ? 0 : 100;
return HumanAIController.IsTrueForAnyCrewMember(c => c.IsSecurity, onlyActive: true, onlyConnectedSubs: true) ? 0 : 100;
}
protected override AIObjective ObjectiveConstructor(Character target)
@@ -37,8 +37,7 @@ namespace Barotrauma
var combatObjective = new AIObjectiveCombat(character, target, combatMode, objectiveManager, PriorityModifier);
if (character.TeamID == CharacterTeamType.FriendlyNPC && target.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign)
{
var reputation = campaign.Map?.CurrentLocation?.Reputation;
if (reputation != null && reputation.NormalizedValue < Reputation.HostileThreshold)
if (campaign.CurrentLocation is { IsFactionHostile: true })
{
combatObjective.holdFireCondition = () =>
{
@@ -66,7 +65,13 @@ namespace Barotrauma
if (target.CurrentHull == null) { return false; }
if (HumanAIController.IsFriendly(character, target)) { return false; }
if (!character.Submarine.IsConnectedTo(target.Submarine)) { return false; }
if (!targetCharactersInOtherSubs && character.Submarine.TeamID != target.Submarine.TeamID && character.Submarine.TeamID != character.OriginalTeamID) { return false; }
if (!targetCharactersInOtherSubs)
{
if (character.Submarine.TeamID != target.Submarine.TeamID && character.OriginalTeamID != target.Submarine.TeamID)
{
return false;
}
}
if (target.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI)) { return false; }
if (target.IsArrested) { return false; }
if (EnemyAIController.IsLatchedToSomeoneElse(target, character)) { return false; }
@@ -79,7 +79,7 @@ namespace Barotrauma
{
if (mask != targetItem)
{
character.Inventory.TryPutItem(mask, character, CharacterInventory.anySlot);
character.Inventory.TryPutItem(mask, character, CharacterInventory.AnySlot);
}
}
}
@@ -52,17 +52,14 @@ namespace Barotrauma
objectiveManager.HasOrder<AIObjectiveReturn>(o => o.Priority > 0) ||
objectiveManager.HasActiveObjective<AIObjectiveRescue>() ||
objectiveManager.Objectives.Any(o => o is AIObjectiveCombat && o.Priority > 0))
&& ((character.IsImmuneToPressure && !character.IsLowInOxygen)|| HumanAIController.HasDivingSuit(character)) ? 0 : 100;
&& ((!character.IsLowInOxygen && character.IsImmuneToPressure)|| HumanAIController.HasDivingSuit(character)) ? 0 : AIObjectiveManager.EmergencyObjectivePriority - 10;
}
else
{
if ((character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false)) ||
(HumanAIController.NeedsDivingGear(character.CurrentHull, out bool needsSuit) &&
(needsSuit ?
!HumanAIController.HasDivingSuit(character, conditionPercentage: AIObjectiveFindDivingGear.GetMinOxygen(character)) :
!HumanAIController.HasDivingGear(character, conditionPercentage: AIObjectiveFindDivingGear.GetMinOxygen(character)))))
NeedMoreDivingGear(character.CurrentHull, AIObjectiveFindDivingGear.GetMinOxygen(character)))
{
Priority = 100;
Priority = AIObjectiveManager.MaxObjectivePriority;
}
else if ((objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() || objectiveManager.IsCurrentOrder<AIObjectiveReturn>()) &&
character.Submarine != null && !character.IsOnFriendlyTeam(character.Submarine.TeamID))
@@ -75,11 +72,11 @@ namespace Barotrauma
{
Priority = 0;
}
Priority = MathHelper.Clamp(Priority, 0, 100);
Priority = MathHelper.Clamp(Priority, 0, AIObjectiveManager.MaxObjectivePriority);
if (divingGearObjective != null && !divingGearObjective.IsCompleted && divingGearObjective.CanBeCompleted)
{
// Boost the priority while seeking the diving gear
Priority = Math.Max(Priority, Math.Min(AIObjectiveManager.HighestOrderPriority + 20, 100));
Priority = Math.Max(Priority, Math.Min(AIObjectiveManager.EmergencyObjectivePriority - 1, AIObjectiveManager.MaxObjectivePriority));
}
}
return Priority;
@@ -111,7 +108,7 @@ namespace Barotrauma
if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD)
{
Priority -= priorityDecrease * deltaTime;
if (currenthullSafety >= 100)
if (currenthullSafety >= 100 && !character.IsLowInOxygen)
{
// Reduce the priority to zero so that the bot can get switch to other objectives immediately, e.g. when entering the airlock.
Priority = 0;
@@ -122,7 +119,7 @@ namespace Barotrauma
float dangerFactor = (100 - currenthullSafety) / 100;
Priority += dangerFactor * priorityIncrease * deltaTime;
}
Priority = MathHelper.Clamp(Priority, 0, 100);
Priority = MathHelper.Clamp(Priority, 0, AIObjectiveManager.MaxObjectivePriority);
}
}
@@ -138,7 +135,7 @@ namespace Barotrauma
{
if (resetPriority) { return; }
var currentHull = character.CurrentHull;
bool dangerousPressure = !character.IsProtectedFromPressure && (currentHull == null || currentHull.LethalPressure > 0);
bool dangerousPressure = (currentHull == null || currentHull.LethalPressure > 0) && !character.IsProtectedFromPressure;
bool shouldActOnSuffocation = character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false);
if (!character.LockHands && (!dangerousPressure || shouldActOnSuffocation || cannotFindSafeHull))
{
@@ -200,16 +197,11 @@ namespace Barotrauma
UpdateSimpleEscape(deltaTime);
return;
}
searchHullTimer = SearchHullInterval * Rand.Range(0.9f, 1.1f);
previousSafeHull = currentSafeHull;
currentSafeHull = potentialSafeHull;
cannotFindSafeHull = currentSafeHull == null || HumanAIController.NeedsDivingGear(currentSafeHull, out _);
if (currentSafeHull == null)
{
currentSafeHull = previousSafeHull;
}
cannotFindSafeHull = currentSafeHull == null || NeedMoreDivingGear(currentSafeHull);
currentSafeHull ??= previousSafeHull;
if (currentSafeHull != null && currentSafeHull != currentHull)
{
if (goToObjective?.Target != currentSafeHull)
@@ -219,6 +211,7 @@ namespace Barotrauma
TryAddSubObjective(ref goToObjective,
constructor: () => new AIObjectiveGoTo(currentSafeHull, character, objectiveManager, getDivingGearIfNeeded: true)
{
SpeakIfFails = false,
AllowGoingOutside =
character.IsProtectedFromPressure ||
character.CurrentHull == null ||
@@ -300,6 +293,7 @@ namespace Barotrauma
//only move if we haven't reached the edge of the room
if (escapeVel.X < 0 && character.Position.X > left || escapeVel.X > 0 && character.Position.X < right)
{
character.ReleaseSecondaryItem();
character.AIController.SteeringManager.SteeringManual(deltaTime, escapeVel);
}
else
@@ -349,7 +343,6 @@ namespace Barotrauma
if (ignoredHulls != null && ignoredHulls.Contains(hull)) { continue; }
if (HumanAIController.UnreachableHulls.Contains(hull)) { continue; }
if (connectedSubs != null && !connectedSubs.Contains(hull.Submarine)) { continue; }
//sort the hulls based on distance and which sub they're in
//tends to make the method much faster, because we find a potential hull earlier and can discard further-away hulls more easily
//(for instance, an NPC in an outpost might otherwise go through all the hulls in the main sub first and do tons of expensive
@@ -493,5 +486,18 @@ namespace Barotrauma
cannotFindDivingGear = false;
cannotFindSafeHull = false;
}
private bool NeedMoreDivingGear(Hull targetHull, float minOxygen = 0)
{
if (!HumanAIController.NeedsDivingGear(targetHull, out bool needsSuit)) { return false; }
if (needsSuit)
{
return !HumanAIController.HasDivingSuit(character, minOxygen);
}
else
{
return !HumanAIController.HasDivingGear(character, minOxygen);
}
}
}
}
@@ -37,40 +37,56 @@ namespace Barotrauma
{
Priority = 0;
Abandon = true;
return Priority;
}
else if (HumanAIController.IsTrueForAnyCrewMember(
other => other != HumanAIController &&
other.Character.IsBot &&
other.ObjectiveManager.GetActiveObjective<AIObjectiveFixLeaks>() is AIObjectiveFixLeaks fixLeaks &&
fixLeaks.SubObjectives.Any(so => so is AIObjectiveFixLeak fixObjective && fixObjective.Leak == Leak)))
float coopMultiplier = 1;
foreach (var c in Character.CharacterList)
{
Priority = 0;
if (!HumanAIController.IsActive(c)) { continue; }
if (c.TeamID != character.TeamID) { continue; }
if (c == character) { continue; }
if (c.IsPlayer) { continue; }
if (c.AIController is HumanAIController otherAI )
{
if (otherAI.ObjectiveManager.GetFirstActiveObjective<AIObjectiveFixLeak>() is AIObjectiveFixLeak fixLeak)
{
if (fixLeak.Leak == Leak)
{
// Ignore leaks that others are already targeting
Priority = 0;
return Priority;
}
if (fixLeak.Leak.FlowTargetHull == Leak.FlowTargetHull)
{
// Reduce the priority of leaks that others should be targeting
coopMultiplier = 0.1f;
break;
}
}
}
}
float reduction = isPriority ? 1 : 2;
float maxPriority = AIObjectiveManager.LowestOrderPriority - reduction;
if (operateObjective != null && objectiveManager.GetFirstActiveObjective<AIObjectiveFixLeaks>() is AIObjectiveFixLeaks fixLeaks && fixLeaks.CurrentSubObjective == this)
{
// Prioritize leaks that we are already fixing
Priority = maxPriority;
}
else
{
float reduction = isPriority ? 1 : 2;
float maxPriority = AIObjectiveManager.LowestOrderPriority - reduction;
if (operateObjective != null && objectiveManager.GetActiveObjective<AIObjectiveFixLeaks>() is AIObjectiveFixLeaks fixLeaks && fixLeaks.CurrentSubObjective == this)
float xDist = Math.Abs(character.WorldPosition.X - Leak.WorldPosition.X);
float yDist = Math.Abs(character.WorldPosition.Y - Leak.WorldPosition.Y);
// Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally).
// If the target is close, ignore the distance factor alltogether so that we keep fixing the leaks that are nearby.
float distanceFactor = isPriority || xDist < 200 && yDist < 100 ? 1 : MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 3000, xDist + yDist * 3.0f));
if (Leak.linkedTo.Any(e => e is Hull h && h == character.CurrentHull))
{
// Prioritize leaks that we are already fixing
Priority = maxPriority;
}
else
{
float xDist = Math.Abs(character.WorldPosition.X - Leak.WorldPosition.X);
float yDist = Math.Abs(character.WorldPosition.Y - Leak.WorldPosition.Y);
// Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally).
// If the target is close, ignore the distance factor alltogether so that we keep fixing the leaks that are nearby.
float distanceFactor = isPriority || xDist < 200 && yDist < 100 ? 1 : MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 3000, xDist + yDist * 3.0f));
if (Leak.linkedTo.Any(e => e is Hull h && h == character.CurrentHull))
{
// Double the distance when the leak can be accessed from the current hull.
distanceFactor *= 2;
}
float severity = isPriority ? 1 : AIObjectiveFixLeaks.GetLeakSeverity(Leak) / 100;
float devotion = CumulatedDevotion / 100;
Priority = MathHelper.Lerp(0, maxPriority, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1));
// Double the distance when the leak can be accessed from the current hull.
distanceFactor *= 2;
}
float severity = isPriority ? 1 : AIObjectiveFixLeaks.GetLeakSeverity(Leak) / 100;
float devotion = CumulatedDevotion / 100;
Priority = MathHelper.Lerp(0, maxPriority, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier * coopMultiplier), 0, 1));
}
return Priority;
}
@@ -38,9 +38,9 @@ namespace Barotrauma
protected override float TargetEvaluation()
{
int totalLeaks = Targets.Count();
int totalLeaks = Targets.Count;
if (totalLeaks == 0) { return 0; }
int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveFixLeaks>() && !c.Character.IsIncapacitated && c.Character.Submarine == character.Submarine, onlyBots: true);
int otherFixers = HumanAIController.CountBotsInTheCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveFixLeaks>() && c.Character.Submarine == character.Submarine);
bool anyFixers = otherFixers > 0;
if (objectiveManager.IsOrder(this))
{
@@ -52,7 +52,7 @@ namespace Barotrauma
int secondaryLeaks = Targets.Count(l => l.IsRoomToRoom);
int leaks = totalLeaks - secondaryLeaks;
float ratio = leaks == 0 ? 1 : anyFixers ? leaks / (float)otherFixers : 1;
if (anyFixers && (ratio <= 1 || otherFixers > 5 || otherFixers / (float)HumanAIController.CountCrew(onlyBots: true) > 0.75f))
if (anyFixers && (ratio <= 1 || otherFixers > 5 || otherFixers / (float)HumanAIController.CountBotsInTheCrew() > 0.75f))
{
// Enough fixers
return 0;
@@ -1,9 +1,11 @@
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Immutable;
using System.Collections.Generic;
using System.Linq;
using System.Diagnostics;
namespace Barotrauma
{
@@ -31,14 +33,15 @@ namespace Barotrauma
private ISpatialEntity moveToTarget;
private bool isDoneSeeking;
public Item TargetItem => targetItem;
private int currSearchIndex;
private int currentSearchIndex;
public ImmutableHashSet<Identifier> ignoredContainerIdentifiers;
public ImmutableHashSet<Identifier> ignoredIdentifiersOrTags;
private AIObjectiveGoTo goToObjective;
private float currItemPriority;
private readonly bool checkInventory;
public static float DefaultReach = 100;
public const float DefaultReach = 100;
public const float MaxReach = 150;
public bool AllowToFindDivingGear { get; set; } = true;
public bool MustBeSpecificItem { get; set; }
@@ -76,7 +79,7 @@ namespace Barotrauma
public AIObjectiveGetItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, bool equip = true, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
currSearchIndex = -1;
currentSearchIndex = 0;
Equip = equip;
originalTarget = targetItem;
this.targetItem = targetItem;
@@ -89,7 +92,7 @@ namespace Barotrauma
public AIObjectiveGetItem(Character character, IEnumerable<Identifier> identifiersOrTags, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1, bool spawnItemIfNotFound = false)
: base(character, objectiveManager, priorityModifier)
{
currSearchIndex = -1;
currentSearchIndex = 0;
Equip = equip;
this.spawnItemIfNotFound = spawnItemIfNotFound;
this.checkInventory = checkInventory;
@@ -125,7 +128,7 @@ namespace Barotrauma
public static Func<PathNode, bool> CreateEndNodeFilter(ISpatialEntity targetEntity)
{
return n => (n.Waypoint.Ladders == null || n.Waypoint.IsInWater) && Vector2.DistanceSquared(n.Waypoint.WorldPosition, targetEntity.WorldPosition) <= MathUtils.Pow2(DefaultReach);
return n => (n.Waypoint.Ladders == null || n.Waypoint.IsInWater) && Vector2.DistanceSquared(n.Waypoint.WorldPosition, targetEntity.WorldPosition) <= MathUtils.Pow2(MaxReach);
}
private bool CheckInventory()
@@ -246,7 +249,7 @@ namespace Barotrauma
else
{
character.SelectCharacter(c);
canInteract = character.CanInteractWith(c, maxDist: DefaultReach);
canInteract = character.CanInteractWith(c);
character.DeselectCharacter();
}
}
@@ -268,7 +271,7 @@ namespace Barotrauma
Inventory itemInventory = targetItem.ParentInventory;
var slots = itemInventory?.FindIndices(targetItem);
if (HumanAIController.TakeItem(targetItem, character.Inventory, Equip, Wear, storeUnequipped: true))
if (HumanAIController.TakeItem(targetItem, character.Inventory, Equip, Wear, storeUnequipped: true, targetTags: IdentifiersOrTags))
{
if (TakeWholeStack && slots != null)
{
@@ -298,8 +301,11 @@ namespace Barotrauma
if (!Equip)
{
// Try equipping and wearing the item
Wear = true;
Equip = true;
if (!objectiveManager.HasActiveObjective<AIObjectiveCleanupItem>() && !objectiveManager.HasActiveObjective<AIObjectiveLoadItem>())
{
Wear = true;
}
return;
}
#if DEBUG
@@ -342,6 +348,10 @@ namespace Barotrauma
}
}
private Stopwatch sw;
private Stopwatch StopWatch => sw ??= new Stopwatch();
private readonly List<(Item item, float priority)> itemCandidates = new List<(Item, float)>();
private List<Item> itemList;
private void FindTargetItem()
{
if (IdentifiersOrTags == null)
@@ -349,13 +359,16 @@ namespace Barotrauma
if (targetItem == null)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot find an item, because neither identifiers nor item was defined.", Color.Red);
DebugConsole.AddWarning($"{character.Name}: Cannot find an item, because neither identifiers nor item was defined.");
#endif
Abandon = true;
}
return;
}
if (HumanAIController.DebugAI)
{
StopWatch.Restart();
}
float priority = Math.Clamp(objectiveManager.GetCurrentPriority(), 10, 100);
if (!CheckPathForEachItem)
{
@@ -366,12 +379,22 @@ namespace Barotrauma
CheckPathForEachItem = priority >= AIObjectiveManager.LowestOrderPriority && (objectiveManager.IsCurrentOrder<AIObjectiveFixLeaks>() || objectiveManager.CurrentOrder is AIObjectiveGoTo gotoOrder && gotoOrder.IsFollowOrderObjective);
}
bool checkPath = CheckPathForEachItem;
bool hasCalledPathFinder = false;
int itemsPerFrame = (int)priority;
for (int i = 0; i < itemsPerFrame && currSearchIndex < Item.ItemList.Count - 1; i++)
// Reset if the character has switched subs.
if (itemList != null && !character.Submarine.IsEntityFoundOnThisSub(itemList.FirstOrDefault(), includingConnectedSubs: true))
{
currSearchIndex++;
var item = Item.ItemList[currSearchIndex];
currentSearchIndex = 0;
}
if (currentSearchIndex == 0)
{
itemCandidates.Clear();
itemList = character.Submarine.GetItems(alsoFromConnectedSubs: true);
}
int itemsPerFrame = (int)MathHelper.Lerp(30, 300, MathUtils.InverseLerp(10, 100, priority));
int checkedItems = 0;
for (int i = 0; i < itemsPerFrame && currentSearchIndex < itemList.Count; i++, currentSearchIndex++)
{
checkedItems++;
var item = itemList[currentSearchIndex];
Submarine itemSub = item.Submarine ?? item.ParentInventory?.Owner?.Submarine;
if (itemSub == null) { continue; }
Submarine mySub = character.Submarine;
@@ -395,8 +418,6 @@ namespace Barotrauma
if (ignoredContainerIdentifiers.Contains(item.ContainerIdentifier)) { continue; }
}
}
// Don't allow going into another sub, unless it's connected and of the same team and type.
if (!character.Submarine.IsEntityFoundOnThisSub(item, includingConnectedSubs: true)) { continue; }
if (character.IsItemTakenBySomeoneElse(item)) { continue; }
if (item.ParentInventory is ItemInventory itemInventory)
{
@@ -411,11 +432,14 @@ namespace Barotrauma
if (rootInventoryOwner is Item ownerItem)
{
if (!ownerItem.IsInteractable(character)) { continue; }
if (!(ownerItem.GetComponent<ItemContainer>()?.HasRequiredItems(character, addMessage: false) ?? true)) { continue; }
//the item is inside an item inside an item (e.g. fuel tank in a welding tool in a cabinet -> reduce priority to prefer items that aren't inside a tool)
if (ownerItem != item.Container)
if (ownerItem != item)
{
itemPriority *= 0.1f;
if (!(ownerItem.GetComponent<ItemContainer>()?.HasRequiredItems(character, addMessage: false) ?? true)) { continue; }
//the item is inside an item inside an item (e.g. fuel tank in a welding tool in a cabinet -> reduce priority to prefer items that aren't inside a tool)
if (ownerItem != item.Container)
{
itemPriority *= 0.1f;
}
}
}
Vector2 itemPos = (rootInventoryOwner ?? item).WorldPosition;
@@ -463,22 +487,69 @@ namespace Barotrauma
{
itemPriority *= item.Condition / item.MaxCondition;
}
if (checkPath)
{
itemCandidates.Add((item, itemPriority));
}
// Ignore if the item has a lower priority than the currently selected one
if (itemPriority < currItemPriority) { continue; }
if (!hasCalledPathFinder && PathSteering != null && checkPath)
if (EvaluateCombatPriority && itemPriority <= 0)
{
hasCalledPathFinder = true;
var path = PathSteering.PathFinder.FindPath(character.SimPosition, item.SimPosition, character.Submarine, errorMsgStr: $"AIObjectiveGetItem {character.DisplayName}", nodeFilter: node => node.Waypoint.CurrentHull != null);
if (path.Unreachable) { continue; }
// Not good enough
continue;
}
currItemPriority = itemPriority;
targetItem = item;
moveToTarget = rootInventoryOwner ?? item;
}
if (currSearchIndex >= Item.ItemList.Count - 1)
if (currentSearchIndex >= itemList.Count - 1)
{
isDoneSeeking = true;
if (targetItem == null)
}
if (checkedItems > 0)
{
if (isDoneSeeking && itemCandidates.Any())
{
itemCandidates.Sort((x, y) => y.priority.CompareTo(x.priority));
}
if (HumanAIController.DebugAI && targetItem != null && StopWatch.ElapsedMilliseconds > 2)
{
var msg = $"Went through {checkedItems} of total {itemList.Count} items. Found item {targetItem.Name} in {StopWatch.ElapsedMilliseconds} ms. Completed: {isDoneSeeking}";
if (StopWatch.ElapsedMilliseconds > 5)
{
DebugConsole.ThrowError(msg);
}
else
{
// An occasional warning now and then can be ignored, but multiple at the same time might indicate a performance issue.
DebugConsole.AddWarning(msg);
}
}
}
if (isDoneSeeking)
{
if (PathSteering == null)
{
itemCandidates.Clear();
}
if (itemCandidates.Any())
{
if (itemCandidates.FirstOrDefault() is { } itemCandidate)
{
var path = PathSteering.PathFinder.FindPath(character.SimPosition, itemCandidate.item.SimPosition, character.Submarine, errorMsgStr: $"AIObjectiveGetItem {character.DisplayName}", nodeFilter: node => node.Waypoint.CurrentHull != null);
if (path.Unreachable)
{
// Remove the invalid candidates and continue on the next frame.
itemCandidates.Remove(itemCandidate);
}
else
{
// The path was valid -> we are done.
itemCandidates.Clear();
}
}
}
if (targetItem == null && itemCandidates.None())
{
if (spawnItemIfNotFound)
{
@@ -569,11 +640,11 @@ namespace Barotrauma
{
if (!item.HasAccess(character)) { return false; }
if (ignoredItems.Contains(item)) { return false; };
if (ignoredIdentifiersOrTags != null && CheckItemIdentifiersOrTags(item, ignoredIdentifiersOrTags)) { return false; }
if (ignoredIdentifiersOrTags != null && item.HasIdentifierOrTags(ignoredIdentifiersOrTags)) { return false; }
if (item.Condition < TargetCondition) { return false; }
if (ItemFilter != null && !ItemFilter(item)) { return false; }
if (RequireNonEmpty && item.Components.Any(i => !i.IsNotEmpty(character))) { return false; }
return CheckItemIdentifiersOrTags(item, IdentifiersOrTags) || (AllowVariants && !item.Prefab.VariantOf.IsEmpty && IdentifiersOrTags.Contains(item.Prefab.VariantOf));
if (RequireNonEmpty && item.Components.Any(i => i.IsEmpty(character))) { return false; }
return item.HasIdentifierOrTags(IdentifiersOrTags) || (AllowVariants && !item.Prefab.VariantOf.IsEmpty && IdentifiersOrTags.Contains(item.Prefab.VariantOf));
}
public override void Reset()
@@ -591,7 +662,7 @@ namespace Barotrauma
targetItem = originalTarget;
moveToTarget = targetItem?.GetRootInventoryOwner();
isDoneSeeking = false;
currSearchIndex = 0;
currentSearchIndex = 0;
currItemPriority = 0;
}
@@ -257,7 +257,7 @@ namespace Barotrauma
}
}
}
else if (HumanAIController.HasValidPath(requireNonDirty: true, requireUnfinished: false))
else if (HumanAIController.HasValidPath(requireUnfinished: false))
{
waitUntilPathUnreachable = pathWaitingTime;
}
@@ -364,7 +364,8 @@ namespace Barotrauma
else
{
bool isRuins = character.Submarine?.Info.IsRuin != null || Target.Submarine?.Info.IsRuin != null;
if (!isRuins || !HumanAIController.HasValidPath(requireNonDirty: true, requireUnfinished: true))
bool isEitherOneInside = isInside || Target.Submarine != null;
if (isEitherOneInside && (!isRuins || !HumanAIController.HasValidPath()))
{
SeekGaps(maxGapDistance);
seekGapsTimer = seekGapsInterval * Rand.Range(0.1f, 1.1f);
@@ -388,6 +389,10 @@ namespace Barotrauma
}
}
}
else
{
TargetGap = null;
}
}
}
else
@@ -436,9 +441,9 @@ namespace Barotrauma
{
var leftHandItem = character.GetEquippedItem(slotType: InvSlotType.LeftHand);
var rightHandItem = character.GetEquippedItem(slotType: InvSlotType.RightHand);
bool handsFull =
(leftHandItem != null && character.Inventory.CheckIfAnySlotAvailable(leftHandItem, inWrongSlot: false) == -1) ||
(rightHandItem != null && character.Inventory.CheckIfAnySlotAvailable(rightHandItem, inWrongSlot: false) == -1);
bool handsFull =
(leftHandItem != null && !character.Inventory.IsAnySlotAvailable(leftHandItem)) ||
(rightHandItem != null && !character.Inventory.IsAnySlotAvailable(rightHandItem));
if (!handsFull)
{
bool hasBattery = false;
@@ -473,7 +478,7 @@ namespace Barotrauma
// Try to switch batteries
if (HumanAIController.HasItem(character, batteryTag, out IEnumerable<Item> batteries, conditionPercentage: 1, recursive: false))
{
scooter.ContainedItems.ForEachMod(emptyBattery => character.Inventory.TryPutItem(emptyBattery, character, CharacterInventory.anySlot));
scooter.ContainedItems.ForEachMod(emptyBattery => character.Inventory.TryPutItem(emptyBattery, character, CharacterInventory.AnySlot));
if (!scooter.Combine(batteries.OrderByDescending(b => b.Condition).First(), character))
{
useScooter = false;
@@ -488,7 +493,7 @@ namespace Barotrauma
if (!useScooter)
{
// Unequip
character.Inventory.TryPutItem(scooter, character, CharacterInventory.anySlot);
character.Inventory.TryPutItem(scooter, character, CharacterInventory.AnySlot);
}
}
}
@@ -511,13 +516,20 @@ namespace Barotrauma
{
nodeFilter = n => n.Waypoint.CurrentHull != null;
}
else if (!isInside && HumanAIController.UseIndoorSteeringOutside)
else if (!isInside)
{
nodeFilter = n => n.Waypoint.Submarine == null;
if (HumanAIController.UseOutsideWaypoints)
{
nodeFilter = n => n.Waypoint.Submarine == null;
}
else
{
nodeFilter = n => n.Waypoint.Submarine != null || n.Waypoint.Ruin != null;
}
}
if (!isInside && !UsePathingOutside)
{
character.ReleaseSecondaryItem();
PathSteering.SteeringSeekSimple(character.GetRelativeSimPosition(Target), 10);
if (character.AnimController.InWater)
{
@@ -540,6 +552,7 @@ namespace Barotrauma
}
else
{
character.ReleaseSecondaryItem();
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(Target.WorldPosition - character.WorldPosition));
if (character.AnimController.InWater)
{
@@ -560,6 +573,7 @@ namespace Barotrauma
}
else
{
character.ReleaseSecondaryItem();
SteeringManager.SteeringSeek(character.GetRelativeSimPosition(Target), 10);
if (character.AnimController.InWater)
{
@@ -573,6 +587,7 @@ namespace Barotrauma
{
if (!character.HasEquippedItem("scooter".ToIdentifier())) { return; }
SteeringManager.Reset();
character.ReleaseSecondaryItem();
character.CursorPosition = targetWorldPos;
if (character.Submarine != null)
{
@@ -624,7 +639,7 @@ namespace Barotrauma
}
else if (target is Character c)
{
return c.CurrentHull;
return c.CurrentHull ?? c.AnimController.CurrentHull;
}
else if (target is Structure structure)
{
@@ -772,6 +787,14 @@ namespace Barotrauma
{
StopMovement();
HumanAIController.FaceTarget(Target);
if (Target is WayPoint { Ladders: null })
{
// Release ladders when ordered to wait at a spawnpoint.
// This is a special case specifically meant for NPCs that spawn in outposts with a wait order.
// Otherwise they might keep holding to the ladders when the target is just next to it.
// Releasing too early should be handled inside the IsCloseEnough property.
character.ReleaseSecondaryItem();
}
base.OnCompleted();
}
@@ -781,6 +804,10 @@ namespace Barotrauma
findDivingGear = null;
seekGapsTimer = 0;
TargetGap = null;
if (SteeringManager is IndoorsSteeringManager pathSteering)
{
pathSteering.ResetPath();
}
}
}
}
@@ -163,15 +163,22 @@ namespace Barotrauma
character.SelectedItem = null;
CleanupItems(deltaTime);
if (!character.IsClimbing)
{
CleanupItems(deltaTime);
}
if (behavior == BehaviorType.StayInHull && TargetHull == null && character.CurrentHull != null)
{
TargetHull = character.CurrentHull;
}
bool currentTargetIsInvalid = currentTarget == null || IsForbidden(currentTarget) || (PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull)));
if (behavior == BehaviorType.StayInHull && !currentTargetIsInvalid)
bool currentTargetIsInvalid =
currentTarget == null ||
IsForbidden(currentTarget) ||
(PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull)));
if (behavior == BehaviorType.StayInHull && !currentTargetIsInvalid && !HumanAIController.UnsafeHulls.Contains(TargetHull))
{
currentTarget = TargetHull;
bool stayInHull = character.CurrentHull == currentTarget && IsSteeringFinished() && !character.IsClimbing;
@@ -305,12 +312,8 @@ namespace Barotrauma
{
if (character.IsClimbing)
{
if (character.AnimController.GetHeightFromFloor() < 0.1f)
{
character.AnimController.Anim = AnimController.Animation.None;
character.SelectedSecondaryItem = null;
}
return;
PathSteering.Reset();
character.StopClimbing();
}
var currentHull = character.CurrentHull;
if (!character.AnimController.InWater && currentHull != null)
@@ -362,6 +365,7 @@ namespace Barotrauma
}
else
{
character.ReleaseSecondaryItem();
PathSteering.SteeringManual(deltaTime, Vector2.Normalize(diff));
}
return;
@@ -24,7 +24,7 @@ namespace Barotrauma
private ImmutableHashSet<Identifier> ValidContainableItemIdentifiers { get; }
private static Dictionary<ItemPrefab, ImmutableHashSet<Identifier>> AllValidContainableItemIdentifiers { get; } = new Dictionary<ItemPrefab, ImmutableHashSet<Identifier>>();
private int itemIndex = 0;
private int itemIndex;
private AIObjectiveDecontainItem decontainObjective;
private readonly HashSet<Item> ignoredItems = new HashSet<Item>();
private Item targetItem;
@@ -219,9 +219,8 @@ namespace Barotrauma
return Priority;
}
public override void Update(float deltaTime)
protected override void Act(float deltaTime)
{
base.Update(deltaTime);
if (targetItem == null)
{
if (character.FindItem(ref itemIndex, out Item item, identifiers: ValidContainableItemIdentifiers, ignoreBroken: false, customPredicate: IsValidContainable, customPriorityFunction: GetPriority))
@@ -233,6 +232,7 @@ namespace Barotrauma
}
targetItem = item;
}
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
float GetPriority(Item item)
{
try
@@ -256,11 +256,7 @@ namespace Barotrauma
}
}
}
}
protected override void Act(float deltaTime)
{
if (targetItem != null)
else
{
if(decontainObjective == null && !IsValidContainable(targetItem))
{
@@ -290,10 +286,6 @@ namespace Barotrauma
Reset();
});
}
else
{
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
}
}
private bool IsValidContainable(Item item)
@@ -310,7 +302,7 @@ namespace Barotrauma
if (parentItem.HasTag("donttakeitems")) { return false; }
}
if (!item.HasAccess(character)) { return false; }
if (!character.HasItem(item) && !CanEquip(item)) { return false; }
if (!character.HasItem(item) && !CanEquip(item, allowWearing: false)) { return false; }
if (!ItemContainer.CanBeContained(item)) { return false; }
if (AIObjectiveLoadItems.ItemMatchesTargetCondition(item, TargetItemCondition)) { return false; }
if (TargetItemCondition == AIObjectiveLoadItems.ItemCondition.Full)
@@ -63,7 +63,7 @@ namespace Barotrauma
{
if (item == null || item.Removed) { return false; }
if (targetContainerTags.HasValue && !OrderPrefab.TargetItemsMatchItem(targetContainerTags.Value, item)) { return false; }
if (!(item.GetComponent<ItemContainer>() is ItemContainer container)) { return false; }
if ((item.GetComponent<ItemContainer>() is not ItemContainer container)) { return false; }
if (container.Inventory == null) { return false; }
if (targetCondition.HasValue && container.Inventory.IsFull() && container.Inventory.AllItems.None(i => ItemMatchesTargetCondition(i, targetCondition.Value))) { return false; }
if (!AIObjectiveCleanupItems.IsItemInsideValidSubmarine(item, character)) { return false; }
@@ -89,12 +89,7 @@ namespace Barotrauma
var target = objective.Key;
if (!Targets.Contains(target))
{
var subObjective = objective.Value;
if (CurrentSubObjective == subObjective)
{
CurrentSubObjective.Abandon = !CurrentSubObjective.IsCompleted;
}
subObjectives.Remove(subObjective);
subObjectives.Remove(objective.Value);
}
}
SyncRemovedObjectives(Objectives, GetList());
@@ -157,6 +152,11 @@ namespace Barotrauma
else
{
float max = AIObjectiveManager.LowestOrderPriority - 1;
if (this is AIObjectiveRescueAll rescueObjective && rescueObjective.Targets.Contains(character))
{
// Allow higher prio
max = AIObjectiveManager.EmergencyObjectivePriority;
}
float value = MathHelper.Clamp((CumulatedDevotion + (targetValue * PriorityModifier)) / 100, 0, 1);
Priority = MathHelper.Lerp(0, max, value);
}
@@ -20,6 +20,8 @@ namespace Barotrauma
MaxValue = 2
}
public const float MaxObjectivePriority = 100;
public const float EmergencyObjectivePriority = 90;
public const float HighestOrderPriority = 70;
public const float LowestOrderPriority = 60;
public const float RunPriority = 50;
@@ -125,11 +127,21 @@ namespace Barotrauma
{
CoroutineManager.StopCoroutines(delayedObjective.Value);
}
var prevIdleObjective = GetObjective<AIObjectiveIdle>();
DelayedObjectives.Clear();
Objectives.Clear();
FailedAutonomousObjectives = false;
AddObjective(new AIObjectiveFindSafety(character, this));
AddObjective(new AIObjectiveIdle(character, this));
var newIdleObjective = new AIObjectiveIdle(character, this);
if (prevIdleObjective != null)
{
newIdleObjective.TargetHull = prevIdleObjective.TargetHull;
newIdleObjective.Behavior = prevIdleObjective.Behavior;
prevIdleObjective.PreferredOutpostModuleTypes.ForEach(t => newIdleObjective.PreferredOutpostModuleTypes.Add(t));
}
AddObjective(newIdleObjective);
int objectiveCount = Objectives.Count;
foreach (var autonomousObjective in character.Info.Job.Prefab.AutonomousObjectives)
{
@@ -155,7 +167,7 @@ namespace Barotrauma
AddObjective(objective, delay: Rand.Value() / 2);
objectiveCount++;
}
}
}
_waitTimer = Math.Max(_waitTimer, Rand.Range(0.5f, 1f) * objectiveCount);
}
@@ -410,7 +422,7 @@ namespace Barotrauma
newObjective = new AIObjectiveGoTo(order.OrderGiver, character, this, repeat: true, priorityModifier: priorityModifier)
{
CloseEnough = Rand.Range(80f, 100f),
CloseEnoughMultiplier = Math.Min(1 + HumanAIController.CountCrew(c => c.ObjectiveManager.HasOrder<AIObjectiveGoTo>(o => o.Target == order.OrderGiver), onlyBots: true) * Rand.Range(0.8f, 1f), 4),
CloseEnoughMultiplier = Math.Min(1 + HumanAIController.CountBotsInTheCrew(c => c.ObjectiveManager.HasOrder<AIObjectiveGoTo>(o => o.Target == order.OrderGiver)) * Rand.Range(0.8f, 1f), 4),
ExtraDistanceOutsideSub = 100,
ExtraDistanceWhileSwimming = 100,
AllowGoingOutside = true,
@@ -633,10 +645,11 @@ namespace Barotrauma
public AIObjective GetActiveObjective() => CurrentObjective?.GetActiveObjective();
public T GetOrder<T>() where T : AIObjective => CurrentOrders.FirstOrDefault(o => o.Objective is T)?.Objective as T;
/// <summary>
/// Returns the last active objective of the specific type.
/// </summary>
public T GetActiveObjective<T>() where T : AIObjective => CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).LastOrDefault(so => so is T) as T;
public T GetLastActiveObjective<T>() where T : AIObjective
=> CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).LastOrDefault(so => so is T) as T;
public T GetFirstActiveObjective<T>() where T : AIObjective
=> CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).FirstOrDefault(so => so is T) as T;
/// <summary>
/// Returns all active objectives of the specific type. Creates a new collection -> don't use too frequently.
@@ -122,7 +122,7 @@ namespace Barotrauma
else if (!isOrder)
{
var steering = component?.Item.GetComponent<Steering>();
if (steering != null && (steering.AutoPilot || HumanAIController.IsTrueForAnyCrewMember(c => c != HumanAIController && c.Character.IsCaptain)))
if (steering != null && (steering.AutoPilot || HumanAIController.IsTrueForAnyCrewMember(c => c != character && c.IsCaptain, onlyActive: true, onlyConnectedSubs: true)))
{
// Ignore if already set to autopilot or if there's a captain onboard
Priority = 0;
@@ -204,7 +204,7 @@ namespace Barotrauma
}
if (operateTarget != null)
{
if (HumanAIController.IsTrueForAnyCrewMember(other => other != HumanAIController && other.Character.IsBot && other.ObjectiveManager.GetActiveObjective() is AIObjectiveOperateItem operateObjective && operateObjective.operateTarget == operateTarget))
if (HumanAIController.IsTrueForAnyBotInTheCrew(other => other != HumanAIController && other.ObjectiveManager.GetActiveObjective() is AIObjectiveOperateItem operateObjective && operateObjective.operateTarget == operateTarget))
{
// Another crew member is already targeting this entity (leak).
Abandon = true;
@@ -69,16 +69,6 @@ namespace Barotrauma
if (subObjective != null && subObjective.IsCompleted)
{
Priority = 0;
items.RemoveWhere(i => i == null || i.Removed || !i.IsOwnedBy(character));
if (items.None())
{
Abandon = true;
}
else if (items.Any(i => i.Components.Any(i => !i.IsNotEmpty(character))))
{
Reset();
}
}
return Priority;
}
@@ -162,25 +152,25 @@ namespace Barotrauma
};
}
if (!TryAddSubObjective(ref getSingleItemObjective, getItemConstructor,
onCompleted: () =>
{
if (KeepActiveWhenReady)
onCompleted: () =>
{
if (getSingleItemObjective != null)
if (KeepActiveWhenReady)
{
var item = getSingleItemObjective?.TargetItem;
if (item?.IsOwnedBy(character) != null)
if (getSingleItemObjective != null)
{
items.Add(item);
var item = getSingleItemObjective?.TargetItem;
if (item?.IsOwnedBy(character) != null)
{
items.Add(item);
}
}
}
}
else
{
IsCompleted = true;
}
},
onAbandon: () => Abandon = true))
else
{
IsCompleted = true;
}
},
onAbandon: () => Abandon = true))
{
Abandon = true;
}
@@ -102,7 +102,7 @@ namespace Barotrauma
// Don't stop fixing until completely done
return 100;
}
int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveRepairItems>() && !c.Character.IsIncapacitated, onlyBots: true);
int otherFixers = HumanAIController.CountBotsInTheCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveRepairItems>());
int items = Targets.Count;
if (items == 0)
{
@@ -116,7 +116,7 @@ namespace Barotrauma
}
else
{
if (anyFixers && (ratio <= 1 || otherFixers > 5 || otherFixers / (float)HumanAIController.CountCrew(onlyBots: true) > 0.75f))
if (anyFixers && (ratio <= 1 || otherFixers > 5 || otherFixers / (float)HumanAIController.CountBotsInTheCrew() > 0.75f))
{
// Enough fixers
return 0;
@@ -139,14 +139,14 @@ namespace Barotrauma
recursive: true);
}
}
if (character.Submarine != null)
if (character.Submarine != null && targetCharacter.CurrentHull != null)
{
if (HumanAIController.GetHullSafety(targetCharacter.CurrentHull, targetCharacter) < HumanAIController.HULL_SAFETY_THRESHOLD)
{
// Incapacitated target is not in a safe place -> Move to a safe place first
if (character.SelectedCharacter != targetCharacter)
{
if (targetCharacter.CurrentHull != null && HumanAIController.VisibleHulls.Contains(targetCharacter.CurrentHull) && targetCharacter.CurrentHull.DisplayName != null)
if (HumanAIController.VisibleHulls.Contains(targetCharacter.CurrentHull) && targetCharacter.CurrentHull.DisplayName != null)
{
character.Speak(TextManager.GetWithVariables("DialogFoundUnconsciousTarget",
("[targetname]", targetCharacter.Name, FormatCapitals.No),
@@ -293,6 +293,11 @@ namespace Barotrauma
currentTreatmentSuitabilities[treatmentSuitability.Key] > bestSuitability)
{
Item matchingItem = character.Inventory.FindItemByIdentifier(treatmentSuitability.Key, true);
//allow taking items from the target's inventory too if the target is unconscious
if (matchingItem == null && targetCharacter.IsIncapacitated)
{
matchingItem ??= targetCharacter.Inventory?.FindItemByIdentifier(treatmentSuitability.Key, true);
}
if (matchingItem != null)
{
bestItem = matchingItem;
@@ -379,24 +384,24 @@ namespace Barotrauma
onAbandon: () =>
{
Abandon = true;
if (character != targetCharacter && character.IsOnPlayerTeam)
if (character.IsOnPlayerTeam)
{
character.Speak(TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, FormatCapitals.No).Value, identifier: "cannottreatpatient".ToIdentifier(), minDurationBetweenSimilar: 20.0f);
SpeakCannotTreat();
}
});
}
else if (cprSuitability <= 0)
{
character.Speak(TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, formatCapitals: FormatCapitals.No).Value, identifier: "cannottreatpatient".ToIdentifier(), minDurationBetweenSimilar: 20.0f);
Abandon = true;
SpeakCannotTreat();
}
}
}
else if (!targetCharacter.IsUnconscious)
{
//no suitable treatments found, not inside our own sub (= can't search for more treatments), the target isn't unconscious (= can't give CPR)
character.Speak(TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, formatCapitals: FormatCapitals.No).Value, identifier: "cannottreatpatient".ToIdentifier(), minDurationBetweenSimilar: 20.0f);
Abandon = true;
//no suitable treatments found, not inside our own sub (= can't search for more treatments), the target isn't unconscious (= can't give CPR)
SpeakCannotTreat();
return;
}
if (character != targetCharacter)
@@ -414,6 +419,14 @@ namespace Barotrauma
}
}
private void SpeakCannotTreat()
{
LocalizedString msg = character == targetCharacter ?
TextManager.Get("dialogcannottreatself") :
TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, FormatCapitals.No);
character.Speak(msg.Value, identifier: "cannottreatpatient".ToIdentifier(), minDurationBetweenSimilar: 20.0f);
}
private void ApplyTreatment(Affliction affliction, Item item)
{
item.ApplyTreatment(character, targetCharacter, targetCharacter.CharacterHealth.GetAfflictionLimb(affliction));
@@ -433,50 +446,36 @@ namespace Barotrauma
protected override float GetPriority()
{
if (!IsAllowed)
if (!IsAllowed || targetCharacter == null)
{
Priority = 0;
Abandon = true;
return Priority;
}
if (character.CurrentHull == null)
if (character.CurrentHull != null)
{
if (!objectiveManager.HasOrder<AIObjectiveRescueAll>())
if (Character.CharacterList.Any(c => c.CurrentHull == targetCharacter.CurrentHull && !HumanAIController.IsFriendly(character, c) && HumanAIController.IsActive(c)))
{
// Don't go into rooms that have enemies
Priority = 0;
Abandon = true;
return Priority;
}
}
else if (Character.CharacterList.Any(c => c.CurrentHull == targetCharacter.CurrentHull && !HumanAIController.IsFriendly(character, c) && HumanAIController.IsActive(c)))
float horizontalDistance = Math.Abs(character.WorldPosition.X - targetCharacter.WorldPosition.X);
float verticalDistance = Math.Abs(character.WorldPosition.Y - targetCharacter.WorldPosition.Y);
if (character.Submarine?.Info is { IsRuin: false })
{
// Don't go into rooms that have enemies
Priority = 0;
Abandon = true;
return Priority;
verticalDistance *= 2;
}
if (targetCharacter == null)
float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, horizontalDistance + verticalDistance));
if (character.CurrentHull != null && targetCharacter.CurrentHull == character.CurrentHull)
{
Priority = 0;
Abandon = true;
}
else
{
float horizontalDistance = Math.Abs(character.WorldPosition.X - targetCharacter.WorldPosition.X);
float verticalDistance = Math.Abs(character.WorldPosition.Y - targetCharacter.WorldPosition.Y);
if (character.Submarine?.Info is { IsRuin: false })
{
verticalDistance *= 2;
}
float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, horizontalDistance + verticalDistance));
if (targetCharacter.CurrentHull == character.CurrentHull)
{
distanceFactor = 1;
}
float vitalityFactor = 1 - AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) / 100;
float devotion = CumulatedDevotion / 100;
Priority = MathHelper.Lerp(0, 100, MathHelper.Clamp(devotion + (vitalityFactor * distanceFactor * PriorityModifier), 0, 1));
distanceFactor = 1;
}
float vitalityFactor = 1 - AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) / 100;
float devotion = CumulatedDevotion / 100;
Priority = MathHelper.Lerp(0, AIObjectiveManager.EmergencyObjectivePriority, MathHelper.Clamp(devotion + (vitalityFactor * distanceFactor * PriorityModifier), 0, 1));
return Priority;
}
@@ -42,15 +42,16 @@ namespace Barotrauma
if (Targets.None()) { return 100; }
if (!objectiveManager.IsOrder(this))
{
if (!character.IsMedic && HumanAIController.IsTrueForAnyCrewMember(c => c != HumanAIController && c.Character.IsMedic && !c.Character.IsUnconscious))
if (!character.IsMedic && HumanAIController.IsTrueForAnyCrewMember(c => c != character && c.IsMedic, onlyActive: true, onlyConnectedSubs: true))
{
// Don't do anything if there's a medic on board and we are not a medic
// Don't do anything if there's a medic on board actively treating and we are not a medic
return 100;
}
}
float worstCondition = Targets.Min(t => GetVitalityFactor(t));
if (Targets.Contains(character))
{
// Targeting self -> higher prio
if (character.Bleeding > 10)
{
// Enforce the highest priority when bleeding out.
@@ -117,7 +118,7 @@ namespace Barotrauma
{
if (!character.IsMedic && target != character)
{
// Don't allow to treat others autonomously
// Don't allow to treat others autonomously, unless we are a medic
return false;
}
// Ignore unsafe hulls, unless ordered
@@ -136,17 +137,17 @@ namespace Barotrauma
// Don't allow going into another sub, unless it's connected and of the same team and type.
if (!character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, includingConnectedSubs: true)) { return false; }
}
else
else if (target.Submarine != null)
{
return target.Submarine == null;
// We are outside, but the target is inside.
return false;
}
if (target != character && target.IsBot && HumanAIController.IsActive(target) && target.AIController is HumanAIController targetAI)
{
// Ignore all concious targets that are currently fighting, fleeing, fixing, or treating characters
// Ignore all concious targets that are currently fighting, fleeing, or treating characters
if (targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveCombat>() ||
targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveFindSafety>() ||
targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveRescue>() ||
targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveFixLeak>())
targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveRescue>())
{
return false;
}
@@ -228,8 +228,8 @@ namespace Barotrauma
{
continue;
}
//optimization: node extremely close (< 1 m). If it's valid, choose it as the start node and skip the more exhaustive search for the closest one
if (node.TempDistance < 1.0f)
//optimization: node close enough. If it's valid, choose it as the start node and skip the more exhaustive search for the closest one
if (node.TempDistance < FarseerPhysics.ConvertUnits.ToSimUnits(AIObjectiveGetItem.DefaultReach))
{
if (IsValidStartNode(node))
{
@@ -47,7 +47,8 @@ namespace Barotrauma
public void SetOrder(Character orderedCharacter)
{
OrderedCharacter = orderedCharacter;
if (OrderedCharacter.AIController is HumanAIController humanAI && humanAI.ObjectiveManager.CurrentOrders.None(o => o.MatchesOrder(SuggestedOrder.Identifier, Option)))
if (OrderedCharacter.AIController is HumanAIController humanAI &&
humanAI.ObjectiveManager.CurrentOrders.None(o => o.MatchesOrder(SuggestedOrder.Identifier, Option) && o.TargetEntity == TargetItem))
{
if (orderedCharacter != CommandingCharacter)
{
@@ -1,5 +1,6 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -7,7 +8,7 @@ namespace Barotrauma
{
class ShipIssueWorkerOperateWeapons : ShipIssueWorkerItem
{
public override float RedundantIssueModifier => 0.65f;
public override float RedundantIssueModifier => 0.8f;
private readonly List<float> targetingImportances = new List<float>();
public override bool AllowEasySwitching => true;
@@ -17,30 +18,52 @@ namespace Barotrauma
float GetTargetingImportance(Entity entity)
{
float currentDistanceToEnemy = Vector2.Distance(entity.WorldPosition, TargetItem.WorldPosition);
return MathHelper.Clamp(100 - (currentDistanceToEnemy / 100f), MinImportance, MaxImportance);
if (currentDistanceToEnemy > Sonar.DefaultSonarRange) { return 0.0f; }
float importance = MathHelper.Clamp(100 - (currentDistanceToEnemy / 100f), MaxImportance * 0.1f, MaxImportance * 0.5f);
if (TargetItem.Submarine != null && importance > 0.0f)
{
if (TargetItemComponent is Turret turret)
{
if (!turret.CheckTurretAngle(entity.WorldPosition))
{
importance *= 0.1f;
}
}
else
{
Vector2 dir = entity.WorldPosition - TargetItem.WorldPosition;
Vector2 submarineDir = TargetItem.WorldPosition - TargetItem.Submarine.WorldPosition;
if (Vector2.Dot(dir, submarineDir) < 0)
{
//direction from the weapon to the target is opposite to the direction from the sub to the weapon
// = the turret is most likely on the wrong side of the sub, reduce importance
importance *= 0.1f;
}
}
}
return importance;
}
public override void CalculateImportanceSpecific()
{
if (TargetItemComponent is Turret turret && !turret.HasPowerToShoot())
{
//operate (= recharge the turrets) with low priority if they're out of power
//if something else (issues with reactor or the electrical grid) is preventing them from being charged, fixing those issues should take priority
Importance = ShipCommandManager.MinimumIssueThreshold * 1.05f;
return;
}
targetingImportances.Clear();
foreach (Character character in shipCommandManager.EnemyCharacters)
{
targetingImportances.Add(GetTargetingImportance(character));
}
// there should maybe be additional logic for targeting and destroying spires, because they currently cause some issues with pathing
if (targetingImportances.Any(i => i > 0))
{
targetingImportances.Sort();
Importance = targetingImportances.TakeLast(3).Sum();
Importance = Math.Max(targetingImportances.TakeLast(3).Sum(), ShipCommandManager.MinimumIssueThreshold);
}
if (TargetItemComponent is Turret turret && !turret.HasPowerToShoot())
{
//operate (= recharge the turrets) with low priority if they're out of power
//if something else (issues with reactor or the electrical grid) is preventing them from being charged, fixing those issues should take priority
Importance = Math.Max(ShipCommandManager.MinimumIssueThreshold / RedundantIssueModifier, Importance);
return;
}
}
}
@@ -229,7 +229,7 @@ namespace Barotrauma
#if DEBUG
ShipCommandLog("Current importance for " + shipIssueWorker + " was " + importance + " and it was already being attended by " + shipIssueWorker.OrderedCharacter);
#endif
attendedIssues.Add(shipIssueWorker);
InsertIssue(shipIssueWorker, attendedIssues);
}
else
{
@@ -237,19 +237,26 @@ namespace Barotrauma
ShipCommandLog("Current importance for " + shipIssueWorker + " was " + importance + " and it is not attended to");
#endif
shipIssueWorker.RemoveOrder();
availableIssues.Add(shipIssueWorker);
InsertIssue(shipIssueWorker, availableIssues);
}
}
availableIssues.Sort((x, y) => y.Importance.CompareTo(x.Importance));
attendedIssues.Sort((x, y) => x.Importance.CompareTo(y.Importance));
static void InsertIssue(ShipIssueWorker issue, List<ShipIssueWorker> list)
{
int index = 0;
while (index < list.Count && list[index].Importance > issue.Importance)
{
index++;
}
list.Insert(index, issue);
}
ShipIssueWorker mostImportantIssue = availableIssues.FirstOrDefault();
float bestValue = 0f;
Character bestCharacter = null;
if (mostImportantIssue != null && mostImportantIssue.Importance > MinimumIssueThreshold)
if (mostImportantIssue != null && mostImportantIssue.Importance >= MinimumIssueThreshold)
{
IEnumerable<Character> bestCharacters = CrewManager.GetCharactersSortedForOrder(mostImportantIssue.SuggestedOrder, AlliedCharacters, character, true);
@@ -480,7 +480,10 @@ namespace Barotrauma
t = MathHelper.Clamp((1 + dot) / 10, 0.01f, 0.1f);
}
}
Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, t);
if (Collider.BodyType == BodyType.Dynamic)
{
Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, t);
}
//limbs are disabled when simple physics is enabled, no need to move them
if (SimplePhysicsEnabled) { return; }
mainLimb.PullJointEnabled = true;
@@ -717,9 +720,12 @@ namespace Barotrauma
{
movement = MathUtils.SmoothStep(movement, TargetMovement, 0.2f);
Collider.LinearVelocity = new Vector2(
movement.X,
Collider.LinearVelocity.Y > 0.0f ? Collider.LinearVelocity.Y * 0.5f : Collider.LinearVelocity.Y);
if (Collider.BodyType == BodyType.Dynamic)
{
Collider.LinearVelocity = new Vector2(
movement.X,
Collider.LinearVelocity.Y > 0.0f ? Collider.LinearVelocity.Y * 0.5f : Collider.LinearVelocity.Y);
}
//limbs are disabled when simple physics is enabled, no need to move them
if (SimplePhysicsEnabled) { return; }
@@ -841,14 +841,11 @@ namespace Barotrauma
if (head == null) { return; }
if (torso == null) { return; }
const float DisableMovementAboveSurfaceThreshold = 50.0f;
if (currentHull != null && character.CurrentHull != null)
{
float surfacePos = GetSurfaceY();
float surfaceThreshold = ConvertUnits.ToDisplayUnits(Collider.SimPosition.Y + 1.0f);
surfaceLimiter = Math.Max(1.0f, surfaceThreshold - surfacePos);
if (surfaceLimiter > DisableMovementAboveSurfaceThreshold) { return; }
}
Limb leftHand = GetLimb(LimbType.LeftHand);
@@ -918,6 +915,7 @@ namespace Barotrauma
RotateHead(head);
}
const float DisableMovementAboveSurfaceThreshold = 50.0f;
//dont try to move upwards if head is already out of water
if (surfaceLimiter > 1.0f && TargetMovement.Y > 0.0f)
{
@@ -937,8 +935,8 @@ namespace Barotrauma
//turn head above the water
head.body.ApplyTorque(Dir);
}
movement.Y *= Math.Max(0, 1.0f - ((surfaceLimiter - 1.0f) / DisableMovementAboveSurfaceThreshold));
movement.Y = movement.Y * (1.0f - ((surfaceLimiter - 1.0f) / DisableMovementAboveSurfaceThreshold));
}
bool isNotRemote = true;
@@ -957,7 +955,13 @@ namespace Barotrauma
t = MathHelper.Clamp((1 + dot) / 10, 0.01f, 0.1f);
}
}
Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, t);
Vector2 targetVelocity = movement;
//if we're too high above the surface, don't touch the vertical velocity of the collider unless we're heading down
if (surfaceLimiter > DisableMovementAboveSurfaceThreshold)
{
targetVelocity.Y = Math.Min(Collider.LinearVelocity.Y, movement.Y);
};
Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, targetVelocity, t);
}
WalkPos += movement.Length();
@@ -1255,7 +1259,7 @@ namespace Barotrauma
if (ladder.Item.Prefab.Triggers.None())
{
character.SelectedSecondaryItem = null;
character.ReleaseSecondaryItem();
return;
}
@@ -1525,13 +1529,15 @@ namespace Barotrauma
Limb leftHand = GetLimb(LimbType.LeftHand);
Limb rightHand = GetLimb(LimbType.RightHand);
Limb targetLeftHand = target.AnimController.GetLimb(LimbType.LeftForearm);
if (targetLeftHand == null) { targetLeftHand = target.AnimController.GetLimb(LimbType.Torso); }
if (targetLeftHand == null) { targetLeftHand = target.AnimController.MainLimb; }
Limb targetLeftHand =
target.AnimController.GetLimb(LimbType.LeftForearm) ??
target.AnimController.GetLimb(LimbType.Torso) ??
target.AnimController.MainLimb;
Limb targetRightHand = target.AnimController.GetLimb(LimbType.RightForearm);
if (targetRightHand == null) { targetRightHand = target.AnimController.GetLimb(LimbType.Torso); }
if (targetRightHand == null) { targetRightHand = target.AnimController.MainLimb; }
Limb targetRightHand =
target.AnimController.GetLimb(LimbType.RightForearm) ??
target.AnimController.GetLimb(LimbType.Torso) ??
target.AnimController.MainLimb;
if (!target.AllowInput)
{
@@ -1547,10 +1553,7 @@ namespace Barotrauma
return;
}
Limb targetTorso = target.AnimController.GetLimb(LimbType.Torso);
if (targetTorso == null)
{
targetTorso = target.AnimController.MainLimb;
}
targetTorso ??= target.AnimController.MainLimb;
if (target.AnimController.Dir != Dir)
{
target.AnimController.Flip();
@@ -1299,21 +1299,32 @@ namespace Barotrauma
limb.Update(deltaTime);
}
if (!inWater && character.AllowInput && levitatingCollider && Collider.LinearVelocity.Y > -ImpactTolerance && onGround)
if (!inWater && character.AllowInput && levitatingCollider)
{
float targetY = standOnFloorY + ((float)Math.Abs(Math.Cos(Collider.Rotation)) * Collider.Height * 0.5f) + Collider.Radius + ColliderHeightFromFloor;
if (Math.Abs(Collider.SimPosition.Y - targetY) > 0.01f && onGround)
if (onGround && Collider.LinearVelocity.Y > -ImpactTolerance)
{
if (Stairs != null)
float targetY = standOnFloorY + ((float)Math.Abs(Math.Cos(Collider.Rotation)) * Collider.Height * 0.5f) + Collider.Radius + ColliderHeightFromFloor;
if (Math.Abs(Collider.SimPosition.Y - targetY) > 0.01f)
{
Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X,
(targetY < Collider.SimPosition.Y ? Math.Sign(targetY - Collider.SimPosition.Y) : (targetY - Collider.SimPosition.Y)) * 5.0f);
if (Stairs != null)
{
Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X,
(targetY < Collider.SimPosition.Y ? Math.Sign(targetY - Collider.SimPosition.Y) : (targetY - Collider.SimPosition.Y)) * 5.0f);
}
else
{
Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X, (targetY - Collider.SimPosition.Y) * 5.0f);
}
}
else
}
else
{
// Falling -> ragdoll briefly if we are not moving at all, because we are probably stuck.
if (Collider.LinearVelocity == Vector2.Zero)
{
Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X, (targetY - Collider.SimPosition.Y) * 5.0f);
character.IsRagdolled = true;
}
}
}
}
UpdateProjSpecific(deltaTime, cam);
forceNotStanding = false;
@@ -78,6 +78,11 @@ namespace Barotrauma
}
}
/// <summary>
/// Attacks are used to deal damage to characters, structures and items.
/// They can be defined in the weapon components of the items or the limb definitions of the characters.
/// The limb attacks can also be used by the player, when they control a monster or have some appendage, like a husk stinger.
/// </summary>
partial class Attack : ISerializableEntity
{
[Serialize(AttackContext.Any, IsPropertySaveable.Yes, description: "The attack will be used only in this context."), Editable]
@@ -123,7 +128,7 @@ namespace Barotrauma
set => _damageRange = value;
}
[Serialize(0.0f, IsPropertySaveable.Yes, description: ""), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)]
[Serialize(0.0f, IsPropertySaveable.Yes, description: "Used by enemy AI to determine the minimum range required for the attack to hit."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)]
public float MinRange { get; private set; }
[Serialize(0.25f, IsPropertySaveable.Yes, description: "An approximation of the attack duration. Effectively defines the time window in which the hit can be registered. If set to too low value, it's possible that the attack won't hit the target in time."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f, DecimalCount = 2)]
@@ -138,22 +143,22 @@ namespace Barotrauma
[Serialize(0f, IsPropertySaveable.Yes, description: "A random factor applied to all cooldowns. Example: 0.1 -> adds a random value between -10% and 10% of the cooldown. Min 0 (default), Max 1 (could disable or double the cooldown in extreme cases)."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)]
public float CoolDownRandomFactor { get; private set; } = 0;
[Serialize(false, IsPropertySaveable.Yes), Editable]
[Serialize(false, IsPropertySaveable.Yes, description: "When set to true, causes the enemy AI to use the fast movement animations when the attack is on cooldown."), Editable]
public bool FullSpeedAfterAttack { get; private set; }
private float _structureDamage;
[Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)]
[Serialize(0.0f, IsPropertySaveable.Yes, description: "How much damage the attack does to submarine walls."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)]
public float StructureDamage
{
get => _structureDamage * DamageMultiplier;
set => _structureDamage = value;
}
[Serialize(true, IsPropertySaveable.Yes), Editable]
[Serialize(true, IsPropertySaveable.Yes, description: "Whether or not damaging structures with the attack causes damage particles to emit."), Editable]
public bool EmitStructureDamageParticles { get; private set; }
private float _itemDamage;
[Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)]
[Serialize(0.0f, IsPropertySaveable.Yes, description: "How much damage the attack does to items."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)]
public float ItemDamage
{
get =>_itemDamage * DamageMultiplier;
@@ -178,16 +183,16 @@ namespace Barotrauma
/// </summary>
public float ImpactMultiplier { get; set; } = 1;
[Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)]
[Serialize(0.0f, IsPropertySaveable.Yes, description: "How much damage the attack does to level walls."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)]
public float LevelWallDamage { get; set; }
[Serialize(false, IsPropertySaveable.Yes), Editable]
[Serialize(false, IsPropertySaveable.Yes, description: "Sets whether or not the attack is ranged or not."), Editable]
public bool Ranged { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description:"Only affects ranged attacks."), Editable]
[Serialize(false, IsPropertySaveable.Yes, description:"When enabled the attack will not be launched when there's a friendly character in the way. Only affects ranged attacks."), Editable]
public bool AvoidFriendlyFire { get; set; }
[Serialize(20f, IsPropertySaveable.Yes, description: "Only affects ranged attacks."), Editable]
[Serialize(20f, IsPropertySaveable.Yes, description: "Used by enemy AI to determine how accurately the attack needs to be aimed for the attack to trigger. Only affects ranged attacks."), Editable]
public float RequiredAngle { get; set; }
[Serialize(0f, IsPropertySaveable.Yes, description: "By default uses the same value as RequiredAngle. Use if you want to allow selecting the attack but not shooting until the angle is smaller. Only affects ranged attacks."), Editable]
@@ -205,16 +210,13 @@ namespace Barotrauma
[Serialize(5f, IsPropertySaveable.Yes, description: "How fast the held weapon is swayed back and forth while aiming. Only affects monsters using ranged weapons (items)."), Editable]
public float SwayFrequency { get; set; }
/// <summary>
/// Legacy support. Use Afflictions.
/// </summary>
[Serialize(0.0f, IsPropertySaveable.No)]
[Serialize(0.0f, IsPropertySaveable.No, description: "Legacy support. Use Afflictions.")]
public float Stun { get; private set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Can damage only Humans."), Editable]
public bool OnlyHumans { get; set; }
[Serialize("", IsPropertySaveable.Yes), Editable]
[Serialize("", IsPropertySaveable.Yes, description: "List of limb indices to apply the force into."), Editable]
public string ApplyForceOnLimbs
{
get
@@ -240,20 +242,20 @@ namespace Barotrauma
[Serialize("0.0, 0.0", IsPropertySaveable.Yes, description: "Applied to the main limb. In world space coordinates(i.e. 0, 1 pushes the character upwards a bit). The attacker's facing direction is taken into account."), Editable]
public Vector2 RootForceWorldStart { get; private set; }
[Serialize("0.0, 0.0", IsPropertySaveable.Yes, description: "Applied to the main limb. In world space coordinates(i.e. 0, 1 pushes the character upwards a bit). The attacker's facing direction is taken into account."), Editable]
public Vector2 RootForceWorldMiddle { get; private set; }
[Serialize("0.0, 0.0", IsPropertySaveable.Yes, description: "Applied to the main limb. In world space coordinates(i.e. 0, 1 pushes the character upwards a bit). The attacker's facing direction is taken into account."), Editable]
public Vector2 RootForceWorldEnd { get; private set; }
[Serialize(TransitionMode.Linear, IsPropertySaveable.Yes, description:""), Editable]
[Serialize(TransitionMode.Linear, IsPropertySaveable.Yes, description:"Applied to the main limb. The transition smoothing of the applied force."), Editable]
public TransitionMode RootTransitionEasing { get; private set; }
[Serialize(0.0f, IsPropertySaveable.Yes, description: "Applied to the attacking limb (or limbs defined using ApplyForceOnLimbs)"), Editable(MinValueFloat = -10000.0f, MaxValueFloat = 10000.0f)]
public float Torque { get; private set; }
[Serialize(false, IsPropertySaveable.Yes), Editable]
[Serialize(false, IsPropertySaveable.Yes, description: "Only apply the force once during the attacks lifetime."), Editable]
public bool ApplyForcesOnlyOnce { get; private set; }
[Serialize(0.0f, IsPropertySaveable.Yes, description: "Applied to the target the attack hits. The direction of the impulse is from this limb towards the target (use negative values to pull the target closer)."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)]
@@ -279,10 +281,10 @@ namespace Barotrauma
//public float StickChance { get; set; }
public float StickChance => 0f;
[Serialize(0.0f, IsPropertySaveable.Yes, description: ""), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)]
[Serialize(0.0f, IsPropertySaveable.Yes, description: "Used by enemy AI to determine the priority when selecting attacks. When random attacks are disabled on the character it is multiplied with distance to determine the which attack to use. Only attacks that are currently valid are taken into consideration when making the decision."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)]
public float Priority { get; private set; }
[Serialize(false, IsPropertySaveable.Yes, description: ""), Editable]
[Serialize(false, IsPropertySaveable.Yes, description: "Triggers the 'blink' animation on the attacking limbs when the attack executes. Used e.g. by abyss monsters to make their jaws close when attacking."), Editable]
public bool Blink { get; private set; }
public IEnumerable<StatusEffect> StatusEffects
@@ -298,7 +300,7 @@ namespace Barotrauma
private set;
} = new Dictionary<Identifier, SerializableProperty>();
//the indices of the limbs Force is applied on
//the indices of the limbs Force is applied on
//(if none, force is applied only to the limb the attack is attached to)
public readonly List<int> ForceOnLimbIndices = new List<int>();
@@ -309,6 +311,10 @@ namespace Barotrauma
/// </summary>
public List<PropertyConditional> Conditionals { get; private set; } = new List<PropertyConditional>();
/// <summary>
/// StatusEffects to apply when the attack triggers.
/// StatusEffect types of 'OnUse' are executed always, 'OnFailure' only when the attack doesn't deal damage and 'OnSuccess' executes when some damage is dealt.
/// </summary>
private readonly List<StatusEffect> statusEffects = new List<StatusEffect>();
public void SetUser(Character user)
@@ -322,7 +328,7 @@ namespace Barotrauma
// used for talents/ability conditions
public Item SourceItem { get; set; }
public List<Affliction> GetMultipliedAfflictions(float multiplier)
{
List<Affliction> multipliedAfflictions = new List<Affliction>();
@@ -11,6 +11,7 @@ using FarseerPhysics.Dynamics;
using Barotrauma.Extensions;
using System.Collections.Immutable;
using Barotrauma.Abilities;
using System.Diagnostics;
#if SERVER
using System.Text;
#endif
@@ -756,7 +757,7 @@ namespace Barotrauma
/// </summary>
public bool InPressure
{
get { return CurrentHull == null || CurrentHull.LethalPressure > 5.0f; }
get { return CurrentHull == null || CurrentHull.LethalPressure > 0.0f; }
}
/// <summary>
@@ -964,6 +965,7 @@ namespace Barotrauma
/// The secondary selected item. It's an item other than a device (see <see cref="SelectedItem"/>), e.g. a ladder or a chair.
/// </summary>
public Item SelectedSecondaryItem { get; set; }
public void ReleaseSecondaryItem() => SelectedSecondaryItem = null;
/// <summary>
/// Has the characters selected a primary or a secondary item?
/// </summary>
@@ -1823,20 +1825,8 @@ namespace Barotrauma
public void StackSpeedMultiplier(float val)
{
if (val < 1f)
{
if (val < greatestNegativeSpeedMultiplier)
{
greatestNegativeSpeedMultiplier = val;
}
}
else
{
if (val > greatestPositiveSpeedMultiplier)
{
greatestPositiveSpeedMultiplier = val;
}
}
greatestNegativeSpeedMultiplier = Math.Min(val, greatestNegativeSpeedMultiplier);
greatestPositiveSpeedMultiplier = Math.Max(val, greatestPositiveSpeedMultiplier);
}
public void ResetSpeedMultiplier()
@@ -1859,20 +1849,8 @@ namespace Barotrauma
public void StackHealthMultiplier(float val)
{
if (val < 1f)
{
if (val < greatestNegativeHealthMultiplier)
{
greatestNegativeHealthMultiplier = val;
}
}
else
{
if (val > greatestPositiveHealthMultiplier)
{
greatestPositiveHealthMultiplier = val;
}
}
greatestNegativeHealthMultiplier = Math.Min(val, greatestNegativeHealthMultiplier);
greatestPositiveHealthMultiplier = Math.Max(val, greatestPositiveHealthMultiplier);
}
private void CalculateHealthMultiplier()
@@ -2456,6 +2434,8 @@ namespace Barotrauma
return true;
}
private Stopwatch sw;
private Stopwatch StopWatch => sw ??= new Stopwatch();
private float _selectedItemPriority;
private Item _foundItem;
/// <summary>
@@ -2469,14 +2449,20 @@ namespace Barotrauma
IEnumerable<Item> ignoredItems = null, IEnumerable<Identifier> ignoredContainerIdentifiers = null,
Func<Item, bool> customPredicate = null, Func<Item, float> customPriorityFunction = null, float maxItemDistance = 10000, ISpatialEntity positionalReference = null)
{
if (HumanAIController.DebugAI)
{
StopWatch.Restart();
}
if (itemIndex == 0)
{
_foundItem = null;
_selectedItemPriority = 0;
}
for (int i = 0; i < 10 && itemIndex < Item.ItemList.Count - 1; i++)
int itemsPerFrame = IsOnPlayerTeam ? 100 : 10;
int checkedItemCount = 0;
for (int i = 0; i < itemsPerFrame && itemIndex < Item.ItemList.Count; i++, itemIndex++)
{
itemIndex++;
checkedItemCount++;
var item = Item.ItemList[itemIndex];
if (!item.IsInteractable(this)) { continue; }
if (ignoredItems != null && ignoredItems.Contains(item)) { continue; }
@@ -2516,7 +2502,21 @@ namespace Barotrauma
}
}
targetItem = _foundItem;
return itemIndex >= Item.ItemList.Count - 1;
bool completed = itemIndex >= Item.ItemList.Count - 1;
if (HumanAIController.DebugAI && checkedItemCount > 0 && targetItem != null && StopWatch.ElapsedMilliseconds > 1)
{
var msg = $"Went through {checkedItemCount} of total {Item.ItemList.Count} items. Found item {targetItem.Name} in {StopWatch.ElapsedMilliseconds} ms. Completed: {completed}";
if (StopWatch.ElapsedMilliseconds > 5)
{
DebugConsole.ThrowError(msg);
}
else
{
// An occasional warning now and then can be ignored, but multiple at the same time might indicate a performance issue.
DebugConsole.AddWarning(msg);
}
}
return completed;
}
public bool IsItemTakenBySomeoneElse(Item item) => item.FindParentInventory(i => i.Owner != this && i.Owner is Character owner && !owner.IsDead && !owner.Removed) != null;
@@ -2536,7 +2536,7 @@ namespace Barotrauma
}
}
return checkVisibility ? CanSeeCharacter(c) : true;
return !checkVisibility || CanSeeCharacter(c);
}
public bool CanInteractWith(Item item, bool checkLinked = true)
@@ -2930,7 +2930,7 @@ namespace Barotrauma
else if (IsKeyHit(InputType.Deselect) && SelectedSecondaryItem != null && SelectedSecondaryItem.GetComponent<Ladder>() == null &&
(focusedItem == null || focusedItem == SelectedSecondaryItem || !selectInputSameAsDeselect))
{
SelectedSecondaryItem = null;
ReleaseSecondaryItem();
#if CLIENT
CharacterHealth.OpenHealthWindow = null;
#endif
@@ -3270,7 +3270,7 @@ namespace Barotrauma
}
if (SelectedSecondaryItem != null && !CanInteractWith(SelectedSecondaryItem))
{
SelectedSecondaryItem = null;
ReleaseSecondaryItem();
}
if (!IsDead) { LockHands = false; }
@@ -3484,14 +3484,19 @@ namespace Barotrauma
despawnTimer += deltaTime * despawnPriority;
if (despawnTimer < GameSettings.CurrentConfig.CorpseDespawnDelay) { return; }
if (IsHuman)
Identifier despawnContainerId =
IsHuman ?
"despawncontainer".ToIdentifier() :
Params.DespawnContainer;
if (!despawnContainerId.IsEmpty)
{
var containerPrefab =
ItemPrefab.Prefabs.Find(me => me.Tags.Contains("despawncontainer")) ??
MapEntityPrefab.FindByIdentifier(despawnContainerId) as ItemPrefab ??
ItemPrefab.Prefabs.Find(me => me?.Tags != null && me.Tags.Contains(despawnContainerId)) ??
(MapEntityPrefab.FindByIdentifier("metalcrate".ToIdentifier()) as ItemPrefab);
if (containerPrefab == null)
{
DebugConsole.NewMessage("Could not spawn a container for a despawned character's items. No item with the tag \"despawncontainer\" or the identifier \"metalcrate\" found.", Color.Red);
DebugConsole.NewMessage($"Could not spawn a container for a despawned character's items. No item with the tag \"{despawnContainerId}\" or the identifier \"metalcrate\" found.", Color.Red);
}
else
{
@@ -3615,7 +3620,7 @@ namespace Barotrauma
{
if (character == this) { continue; }
if (character.TeamID != TeamID) { continue; }
if (!(character.AIController is HumanAIController)) { continue; }
if (character.AIController is not HumanAIController) { continue; }
if (!HumanAIController.IsActive(character)) { continue; }
foreach (var currentOrder in character.CurrentOrders)
{
@@ -4507,9 +4512,12 @@ namespace Barotrauma
OnDeath?.Invoke(this, CauseOfDeath);
var abilityCharacterKiller = new AbilityCharacterKiller(CauseOfDeath.Killer);
CheckTalents(AbilityEffectType.OnDieToCharacter, abilityCharacterKiller);
CauseOfDeath.Killer?.RecordKill(this);
if (CauseOfDeath.Type != CauseOfDeathType.Disconnected)
{
var abilityCharacterKiller = new AbilityCharacterKiller(CauseOfDeath.Killer);
CheckTalents(AbilityEffectType.OnDieToCharacter, abilityCharacterKiller);
CauseOfDeath.Killer?.RecordKill(this);
}
if (GameMain.GameSession != null && Screen.Selected == GameMain.GameScreen)
{
@@ -4724,7 +4732,7 @@ namespace Barotrauma
if (!MathUtils.NearlyEqual(newItem.Condition, newItem.MaxCondition) &&
GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
newItem.CreateStatusEvent();
newItem.CreateStatusEvent(loadingRound: true);
}
#if SERVER
newItem.GetComponent<Terminal>()?.SyncHistory();
@@ -4981,6 +4989,8 @@ namespace Barotrauma
#region Talents
private readonly List<CharacterTalent> characterTalents = new List<CharacterTalent>();
public IReadOnlyCollection<CharacterTalent> CharacterTalents => characterTalents;
public void LoadTalents()
{
List<Identifier> toBeRemoved = null;
@@ -5077,7 +5087,7 @@ namespace Barotrauma
public void CheckTalents(AbilityEffectType abilityEffectType, AbilityObject abilityObject)
{
foreach (var characterTalent in characterTalents)
foreach (CharacterTalent characterTalent in CharacterTalents)
{
characterTalent.CheckTalent(abilityEffectType, abilityObject);
}
@@ -5373,7 +5383,7 @@ namespace Barotrauma
public void StopClimbing()
{
AnimController.StopClimbing();
SelectedSecondaryItem = null;
ReleaseSecondaryItem();
}
}
@@ -4,6 +4,7 @@ using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using Barotrauma.IO;
using System.Linq;
using System.Xml.Linq;
@@ -11,6 +12,31 @@ using Barotrauma.Abilities;
namespace Barotrauma
{
[NetworkSerialize]
internal readonly record struct NetJobVariant(Identifier Identifier, byte Variant) : INetSerializableStruct
{
[return: MaybeNull]
public JobVariant ToJobVariant()
{
if (!JobPrefab.Prefabs.TryGet(Identifier, out JobPrefab jobPrefab) || jobPrefab.HiddenJob) { return null; }
return new JobVariant(jobPrefab, Variant);
}
public static NetJobVariant FromJobVariant(JobVariant jobVariant) => new NetJobVariant(jobVariant.Prefab.Identifier, (byte)jobVariant.Variant);
}
[NetworkSerialize(ArrayMaxSize = byte.MaxValue)]
internal readonly record struct NetCharacterInfo(string NewName,
ImmutableArray<Identifier> Tags,
byte HairIndex,
byte BeardIndex,
byte MoustacheIndex,
byte FaceAttachmentIndex,
Color SkinColor,
Color HairColor,
Color FacialHairColor,
ImmutableArray<NetJobVariant> JobVariants) : INetSerializableStruct;
class CharacterInfoPrefab
{
public readonly ImmutableArray<CharacterInfo.HeadPreset> Heads;
@@ -879,10 +905,19 @@ namespace Barotrauma
Identifier talentIdentifier = talentElement.GetAttributeIdentifier("identifier", Identifier.Empty);
if (talentIdentifier == Identifier.Empty) { continue; }
if (TalentPrefab.TalentPrefabs.TryGet(talentIdentifier, out TalentPrefab prefab))
{
foreach (TalentMigration migration in prefab.Migrations)
{
migration.TryApply(version, this);
}
}
UnlockedTalents.Add(talentIdentifier);
}
}
}
LoadHeadAttachments();
}
@@ -1856,6 +1891,21 @@ namespace Barotrauma
}
}
public float GetSavedStatValueWithBotsInMp(StatTypes statType, Identifier statIdentifier)
{
float statValue = GetSavedStatValue(statType, statIdentifier);
if (GameMain.NetworkMember is null) { return statValue; }
foreach (Character bot in GameSession.GetSessionCrewCharacters(CharacterType.Bot))
{
int botStatValue = (int)bot.Info.GetSavedStatValue(statType, statIdentifier);
statValue = Math.Max(statValue, botStatValue);
}
return statValue;
}
public void ChangeSavedStatValue(StatTypes statType, float value, Identifier statIdentifier, bool removeOnDeath, float maxValue = float.MaxValue, bool setValue = false)
{
if (!SavedStatValues.ContainsKey(statType))
@@ -1,4 +1,5 @@
using Microsoft.Xna.Framework;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -14,8 +15,8 @@ namespace Barotrauma
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; set; }
public float PendingAdditionStrength { get; set; }
public float AdditionStrength { get; set; }
public float PendingGrainEffectStrength { get; set; }
public float GrainEffectStrength { get; set; }
private float fluctuationTimer;
@@ -46,7 +47,7 @@ namespace Barotrauma
float newValue = MathHelper.Clamp(value, 0.0f, Prefab.MaxStrength);
if (newValue > _strength)
{
PendingAdditionStrength = Prefab.GrainBurst;
PendingGrainEffectStrength = Prefab.GrainBurst;
Duration = Prefab.Duration;
}
_strength = newValue;
@@ -73,8 +74,7 @@ namespace Barotrauma
public float DamagePerSecondTimer;
public float PreviousVitalityDecrease;
public float StrengthDiminishMultiplier = 1.0f;
public Affliction MultiplierSource;
public (float Value, Affliction Source) StrengthDiminishMultiplier = (1.0f, null);
public readonly Dictionary<AfflictionPrefab.PeriodicEffect, float> PeriodicEffectTimers = new Dictionary<AfflictionPrefab.PeriodicEffect, float>();
@@ -100,7 +100,7 @@ namespace Barotrauma
prefab?.ReloadSoundsIfNeeded();
#endif
Prefab = prefab;
PendingAdditionStrength = Prefab.GrainBurst;
PendingGrainEffectStrength = Prefab.GrainBurst;
_strength = strength;
Identifier = prefab.Identifier;
@@ -179,7 +179,7 @@ namespace Barotrauma
float currVitalityDecrease = MathHelper.Lerp(
currentEffect.MinVitalityDecrease,
currentEffect.MaxVitalityDecrease,
(strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
currentEffect.GetStrengthFactor(strength));
if (currentEffect.MultiplyByMaxVitality)
{
@@ -200,11 +200,11 @@ namespace Barotrauma
float amount = MathHelper.Lerp(
currentEffect.MinGrainStrength,
currentEffect.MaxGrainStrength,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)) * GetScreenEffectFluctuation(currentEffect);
currentEffect.GetStrengthFactor(this)) * GetScreenEffectFluctuation(currentEffect);
if (Prefab.GrainBurst > 0 && AdditionStrength > amount)
if (Prefab.GrainBurst > 0 && GrainEffectStrength > amount)
{
return Math.Min(AdditionStrength, 1.0f);
return Math.Min(GrainEffectStrength, 1.0f);
}
return amount;
@@ -220,7 +220,7 @@ namespace Barotrauma
return MathHelper.Lerp(
currentEffect.MinScreenDistort,
currentEffect.MaxScreenDistort,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)) * GetScreenEffectFluctuation(currentEffect);
currentEffect.GetStrengthFactor(this)) * GetScreenEffectFluctuation(currentEffect);
}
public float GetRadialDistortStrength()
@@ -233,7 +233,7 @@ namespace Barotrauma
return MathHelper.Lerp(
currentEffect.MinRadialDistort,
currentEffect.MaxRadialDistort,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)) * GetScreenEffectFluctuation(currentEffect);
currentEffect.GetStrengthFactor(this)) * GetScreenEffectFluctuation(currentEffect);
}
public float GetChromaticAberrationStrength()
@@ -246,7 +246,7 @@ namespace Barotrauma
return MathHelper.Lerp(
currentEffect.MinChromaticAberration,
currentEffect.MaxChromaticAberration,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)) * GetScreenEffectFluctuation(currentEffect);
currentEffect.GetStrengthFactor(this)) * GetScreenEffectFluctuation(currentEffect);
}
public float GetAfflictionOverlayMultiplier()
@@ -261,7 +261,7 @@ namespace Barotrauma
return MathHelper.Lerp(
currentEffect.MinAfflictionOverlayAlphaMultiplier,
currentEffect.MaxAfflictionOverlayAlphaMultiplier,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
currentEffect.GetStrengthFactor(this));
}
public Color GetFaceTint()
@@ -273,7 +273,7 @@ namespace Barotrauma
return Color.Lerp(
currentEffect.MinFaceTint,
currentEffect.MaxFaceTint,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
currentEffect.GetStrengthFactor(this));
}
public Color GetBodyTint()
@@ -285,7 +285,7 @@ namespace Barotrauma
return Color.Lerp(
currentEffect.MinBodyTint,
currentEffect.MaxBodyTint,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
currentEffect.GetStrengthFactor(this));
}
public float GetScreenBlurStrength()
@@ -298,7 +298,7 @@ namespace Barotrauma
return MathHelper.Lerp(
currentEffect.MinScreenBlur,
currentEffect.MaxScreenBlur,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)) * GetScreenEffectFluctuation(currentEffect);
currentEffect.GetStrengthFactor(this)) * GetScreenEffectFluctuation(currentEffect);
}
private float GetScreenEffectFluctuation(AfflictionPrefab.Effect currentEffect)
@@ -316,7 +316,7 @@ namespace Barotrauma
float amount = MathHelper.Lerp(
currentEffect.MinSkillMultiplier,
currentEffect.MaxSkillMultiplier,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
currentEffect.GetStrengthFactor(this));
return amount;
}
@@ -347,7 +347,7 @@ namespace Barotrauma
return MathHelper.Lerp(
currentEffect.MinResistance,
currentEffect.MaxResistance,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
currentEffect.GetStrengthFactor(this));
}
public float GetSpeedMultiplier()
@@ -358,21 +358,16 @@ namespace Barotrauma
return MathHelper.Lerp(
currentEffect.MinSpeedMultiplier,
currentEffect.MaxSpeedMultiplier,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
currentEffect.GetStrengthFactor(this));
}
public float GetStatValue(StatTypes statType)
{
if (GetViableEffect() is not AfflictionPrefab.Effect currentEffect) { return 0.0f; }
if (currentEffect.AfflictionStatValues.TryGetValue(statType, out var value))
{
return MathHelper.Lerp(
value.minValue,
value.maxValue,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
}
return 0.0f;
if (!currentEffect.AfflictionStatValues.TryGetValue(statType, out var appliedStat)) { return 0.0f; }
return MathHelper.Lerp(appliedStat.MinValue, appliedStat.MaxValue, currentEffect.GetStrengthFactor(this));
}
public bool HasFlag(AbilityFlags flagType)
@@ -415,13 +410,16 @@ namespace Barotrauma
fluctuationTimer += deltaTime * currentEffect.ScreenEffectFluctuationFrequency;
fluctuationTimer %= 1.0f;
if (currentEffect.StrengthChange < 0) // Reduce diminishing of buffs if boosted
if (currentEffect.StrengthChange < 0) // Only apply StrengthDiminish.Multiplier if affliction is being weakened
{
float durationMultiplier = 1 / (1 + (Prefab.IsBuff ? characterHealth.Character.GetStatValue(StatTypes.BuffDurationMultiplier)
: characterHealth.Character.GetStatValue(StatTypes.DebuffDurationMultiplier)));
float stat = characterHealth.Character.GetStatValue(
Prefab.IsBuff
? StatTypes.BuffDurationMultiplier
: StatTypes.DebuffDurationMultiplier);
_strength += currentEffect.StrengthChange * deltaTime * StrengthDiminishMultiplier * durationMultiplier;
float durationMultiplier = 1f / (1f + stat);
_strength += currentEffect.StrengthChange * deltaTime * StrengthDiminishMultiplier.Value * durationMultiplier;
}
else if (currentEffect.StrengthChange > 0) // Reduce strengthening of afflictions if resistant
{
@@ -441,14 +439,14 @@ namespace Barotrauma
{
amount /= Prefab.GrainBurst;
}
if (PendingAdditionStrength >= 0)
if (PendingGrainEffectStrength >= 0)
{
AdditionStrength += amount;
PendingAdditionStrength -= deltaTime;
GrainEffectStrength += amount;
PendingGrainEffectStrength -= deltaTime;
}
else if (AdditionStrength > 0)
else if (GrainEffectStrength > 0)
{
AdditionStrength -= amount;
GrainEffectStrength -= amount;
}
}
@@ -1,5 +1,8 @@
namespace Barotrauma
{
/// <summary>
/// A special affliction type that increases the character's Bloodloss affliction with a rate relative to the strength of the bleeding.
/// </summary>
class AfflictionBleeding : Affliction
{
public AfflictionBleeding(AfflictionPrefab prefab, float strength) :
@@ -7,6 +7,10 @@ using Microsoft.Xna.Framework;
namespace Barotrauma
{
/// <summary>
/// A special affliction type that gradually makes the character turn into another type of character.
/// See <see cref="AfflictionPrefabHusk"/> for more details.
/// </summary>
partial class AfflictionHusk : Affliction
{
public enum InfectionState
@@ -63,6 +67,7 @@ namespace Barotrauma
private float DormantThreshold => HuskPrefab.DormantThreshold;
private float ActiveThreshold => HuskPrefab.ActiveThreshold;
private float TransitionThreshold => HuskPrefab.TransitionThreshold;
private float TransformThresholdOnDeath => HuskPrefab.TransformThresholdOnDeath;
public AfflictionHusk(AfflictionPrefab prefab, float strength) : base(prefab, strength)
@@ -6,6 +6,7 @@ using System.Reflection;
using System.Xml.Linq;
using Barotrauma.Extensions;
using System.Collections.Immutable;
using Barotrauma.Items.Components;
namespace Barotrauma
{
@@ -56,8 +57,77 @@ namespace Barotrauma
public override void Dispose() { }
}
/// <summary>
/// AfflictionPrefabHusk is a special type of affliction that has added functionality for husk infection.
/// </summary>
class AfflictionPrefabHusk : AfflictionPrefab
{
// Use any of these to define which limb the appendage is attached to.
// If multiple are defined, the order of preference is: id, name, type.
public readonly int AttachLimbId;
public readonly string AttachLimbName;
public readonly LimbType AttachLimbType;
/// <summary>
/// The minimum strength at which husk infection will be in the dormant stage.
/// It must be less than or equal to ActiveThreshold.
/// </summary>
public readonly float DormantThreshold;
/// <summary>
/// The minimum strength at which husk infection will be in the active stage.
/// It must be greater than or equal to DormantThreshold and less than or equal to TransitionThreshold.
/// </summary>
public readonly float ActiveThreshold;
/// <summary>
/// The minimum strength at which husk infection will be in its final stage.
/// It must be greater than or equal to ActiveThreshold.
/// </summary>
public readonly float TransitionThreshold;
/// <summary>
/// The minimum strength the affliction must have for the affected character
/// to transform into a husk upon death.
/// </summary>
public readonly float TransformThresholdOnDeath;
/// <summary>
/// The species of husk to convert the affected character to
/// once husk infection reaches its final stage.
/// </summary>
public readonly Identifier HuskedSpeciesName;
/// <summary>
/// If set to true, all buffs are transferred to the converted
/// character after husk transformation is complete.
/// </summary>
public readonly bool TransferBuffs;
/// <summary>
/// If set to true, the affected player will see on-screen messages describing husk infection symptoms
/// and affected bots will speak about their current husk infection stage.
/// </summary>
public readonly bool SendMessages;
/// <summary>
/// If set to true, affected characters will have their speech impeded once the affliction
/// reaches the dormant stage.
/// </summary>
public readonly bool CauseSpeechImpediment;
/// <summary>
/// If set to false, affected characters will no longer require air
/// once the affliction reaches the active stage.
/// </summary>
public readonly bool NeedsAir;
/// <summary>
/// If set to true, affected players will retain control of their character
/// after transforming into a husk.
/// </summary>
public readonly bool ControlHusk;
public AfflictionPrefabHusk(ContentXElement element, AfflictionsFile file, Type type = null) : base(element, file, type)
{
HuskedSpeciesName = element.GetAttributeIdentifier("huskedspeciesname", Identifier.Empty);
@@ -78,7 +148,7 @@ namespace Barotrauma
{
AttachLimbId = attachElement.GetAttributeInt("id", -1);
AttachLimbName = attachElement.GetAttributeString("name", null);
AttachLimbType = Enum.TryParse(attachElement.GetAttributeString("type", "none"), true, out LimbType limbType) ? limbType : LimbType.None;
AttachLimbType = attachElement.GetAttributeEnum("type", LimbType.None);
}
else
{
@@ -96,174 +166,282 @@ namespace Barotrauma
DormantThreshold = element.GetAttributeFloat("dormantthreshold", MaxStrength * 0.5f);
ActiveThreshold = element.GetAttributeFloat("activethreshold", MaxStrength * 0.75f);
TransitionThreshold = element.GetAttributeFloat("transitionthreshold", MaxStrength);
if (DormantThreshold > ActiveThreshold)
{
DebugConsole.ThrowError($"Error in \"{Identifier}\": {nameof(DormantThreshold)} is greater than {nameof(ActiveThreshold)} ({DormantThreshold} > {ActiveThreshold})");
}
if (ActiveThreshold > TransitionThreshold)
{
DebugConsole.ThrowError($"Error in \"{Identifier}\": {nameof(ActiveThreshold)} is greater than {nameof(TransitionThreshold)} ({ActiveThreshold} > {TransitionThreshold})");
}
TransformThresholdOnDeath = element.GetAttributeFloat("transformthresholdondeath", ActiveThreshold);
}
// Use any of these to define which limb the appendage is attached to.
// If multiple are defined, the order of preference is: id, name, type.
public readonly int AttachLimbId;
public readonly string AttachLimbName;
public readonly LimbType AttachLimbType;
public float ActiveThreshold, DormantThreshold, TransitionThreshold;
public float TransformThresholdOnDeath;
public readonly Identifier HuskedSpeciesName;
public readonly bool TransferBuffs;
public readonly bool SendMessages;
public readonly bool CauseSpeechImpediment;
public readonly bool NeedsAir;
public readonly bool ControlHusk;
}
/// <summary>
/// AfflictionPrefab is a prefab that defines a type of affliction that can be applied to a character.
/// There are multiple sub-types of afflictions such as AfflictionPrefabHusk, AfflictionPsychosis and AfflictionBleeding that can be used for additional functionality.
///
/// When defining a new affliction, the type will be determined by the element name.
/// </summary>
/// <example>
/// <code language="xml">
/// <Afflictions>
/// <!-- Defines a regular affliction. -->
/// <Affliction identifier="mycoolaffliction1" />
///
/// <!-- Defines an AfflictionPrefabHusk affliction. -->
/// <AfflictionPrefabHusk identifier="mycoolaffliction2"/>
///
/// <!-- Defines an AfflictionBleeding affliction. -->
/// <AfflictionBleeding identifier="mycoolaffliction3"/>
/// </Afflictions>
/// </code>
/// </example>
class AfflictionPrefab : PrefabWithUintIdentifier
{
public class Effect
/// <summary>
/// Effects are the primary way to add functionality to afflictions.
/// </summary>
/// <doc>
/// <Ignore type="SubElement" identifier="AbilityFlag" />
/// <SubElement identifier="abilityflag" type="AppliedAbilityFlag">
/// Enables the specified flag on the character as long as the effect is active.
/// </SubElement>
/// <Type identifier="AppliedAbilityFlag">
/// <Summary>
/// Flag that will be enabled for the character as long as the effect is active.
/// <example>
/// <code language="xml">
/// <Effect minstrength="0" maxstrength="100">
/// <!-- Grants pressure immunity to the character while the effect is active. -->
/// <AbilityFlag flagtype="ImmuneToPressure" />
/// </Effect>
/// </code>
/// </example>
/// </Summary>
/// <Field identifier="FlagType" type="AbilityFlags" defaultValue="None">
/// Which ability flag to enable.
/// </Field>
/// </Type>
/// </doc>
public sealed class Effect
{
//this effect is applied when the strength is within this range
[Serialize(0.0f, IsPropertySaveable.No)]
[Serialize(0.0f, IsPropertySaveable.No, description: "Minimum affliction strength required for this effect to be active.")]
public float MinStrength { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No)]
[Serialize(0.0f, IsPropertySaveable.No, description: "Maximum affliction strength for which this effect will be active.")]
public float MaxStrength { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No)]
[Serialize(0.0f, IsPropertySaveable.No, description: "The amount of vitality that is lost at this effect's lowest strength.")]
public float MinVitalityDecrease { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No)]
[Serialize(0.0f, IsPropertySaveable.No, description: "The amount of vitality that is lost at this effect's highest strength.")]
public float MaxVitalityDecrease { get; private set; }
//how much the strength of the affliction changes per second
[Serialize(0.0f, IsPropertySaveable.No)]
[Serialize(0.0f, IsPropertySaveable.No, description: "How much the affliction's strength changes every second while this effect is active.")]
public float StrengthChange { get; private set; }
[Serialize(false, IsPropertySaveable.No)]
[Serialize(false, IsPropertySaveable.No, description:
"If set to true, MinVitalityDecrease and MaxVitalityDecrease represent a fraction of the affected character's maximum " +
"vitality, with 1 meaning 100%, instead of the same amount for all species.")]
public bool MultiplyByMaxVitality { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No)]
[Serialize(0.0f, IsPropertySaveable.No, description: "Blur effect strength at this effect's lowest strength.")]
public float MinScreenBlur { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No)]
[Serialize(0.0f, IsPropertySaveable.No, description: "Blur effect strength at this effect's highest strength.")]
public float MaxScreenBlur { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No)]
[Serialize(0.0f, IsPropertySaveable.No, description: "Generic distortion effect strength at this effect's lowest strength.")]
public float MinScreenDistort { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No)]
[Serialize(0.0f, IsPropertySaveable.No, description: "Generic distortion effect strength at this effect's highest strength.")]
public float MaxScreenDistort { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No)]
[Serialize(0.0f, IsPropertySaveable.No, description: "Radial distortion effect strength at this effect's lowest strength.")]
public float MinRadialDistort { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No)]
[Serialize(0.0f, IsPropertySaveable.No, description: "Radial distortion effect strength at this effect's highest strength.")]
public float MaxRadialDistort { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No)]
[Serialize(0.0f, IsPropertySaveable.No, description: "Chromatic aberration effect strength at this effect's lowest strength.")]
public float MinChromaticAberration { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No)]
[Serialize(0.0f, IsPropertySaveable.No, description: "Chromatic aberration effect strength at this effect's highest strength.")]
public float MaxChromaticAberration { get; private set; }
[Serialize("255,255,255,255", IsPropertySaveable.No)]
[Serialize("255,255,255,255", IsPropertySaveable.No, description: "Radiation grain effect color.")]
public Color GrainColor { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No)]
[Serialize(0.0f, IsPropertySaveable.No, description: "Radiation grain effect strength at this effect's lowest strength.")]
public float MinGrainStrength { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No)]
[Serialize(0.0f, IsPropertySaveable.No, description: "Radiation grain effect strength at this effect's highest strength.")]
public float MaxGrainStrength { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No)]
[Serialize(0.0f, IsPropertySaveable.No, description:
"The maximum rate of fluctuation to apply to visual effects caused by this affliction effect. " +
"Effective fluctuation is proportional to the affliction's current strength.")]
public float ScreenEffectFluctuationFrequency { get; private set; }
[Serialize(1.0f, IsPropertySaveable.No)]
[Serialize(1.0f, IsPropertySaveable.No, description:
"Multiplier for the affliction overlay's opacity at this effect's lowest strength. " +
"See the list of elements for more details.")]
public float MinAfflictionOverlayAlphaMultiplier { get; private set; }
[Serialize(1.0f, IsPropertySaveable.No)]
[Serialize(1.0f, IsPropertySaveable.No, description:
"Multiplier for the affliction overlay's opacity at this effect's highest strength. " +
"See the list of elements for more details.")]
public float MaxAfflictionOverlayAlphaMultiplier { get; private set; }
[Serialize(1.0f, IsPropertySaveable.No)]
[Serialize(1.0f, IsPropertySaveable.No, description:
"Multiplier for every buff's decay rate at this effect's lowest strength. " +
"Only applies to afflictions of class BuffDurationIncrease.")]
public float MinBuffMultiplier { get; private set; }
[Serialize(1.0f, IsPropertySaveable.No)]
[Serialize(1.0f, IsPropertySaveable.No, description:
"Multiplier for every buff's decay rate at this effect's highest strength. " +
"Only applies to afflictions of class BuffDurationIncrease.")]
public float MaxBuffMultiplier { get; private set; }
[Serialize(1.0f, IsPropertySaveable.No)]
[Serialize(1.0f, IsPropertySaveable.No, description: "Multiplier to apply to the affected character's speed at this effect's lowest strength.")]
public float MinSpeedMultiplier { get; private set; }
[Serialize(1.0f, IsPropertySaveable.No)]
[Serialize(1.0f, IsPropertySaveable.No, description: "Multiplier to apply to the affected character's speed at this effect's highest strength.")]
public float MaxSpeedMultiplier { get; private set; }
[Serialize(1.0f, IsPropertySaveable.No)]
[Serialize(1.0f, IsPropertySaveable.No, description: "Multiplier to apply to all of the affected character's skill levels at this effect's lowest strength.")]
public float MinSkillMultiplier { get; private set; }
[Serialize(1.0f, IsPropertySaveable.No)]
[Serialize(1.0f, IsPropertySaveable.No, description: "Multiplier to apply to all of the affected character's skill levels at this effect's highest strength.")]
public float MaxSkillMultiplier { get; private set; }
private readonly Identifier[] resistanceFor;
public IReadOnlyList<Identifier> ResistanceFor => resistanceFor;
/// <summary>
/// A list of identifiers of afflictions that the affected character will be
/// resistant to when this effect is active.
/// </summary>
public readonly ImmutableArray<Identifier> ResistanceFor;
[Serialize(0.0f, IsPropertySaveable.No)]
[Serialize(0.0f, IsPropertySaveable.No,
description: "The amount of resistance to the afflictions specified by ResistanceFor to apply at this effect's lowest strength.")]
public float MinResistance { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No)]
[Serialize(0.0f, IsPropertySaveable.No,
description: "The amount of resistance to the afflictions specified by ResistanceFor to apply at this effect's highest strength.")]
public float MaxResistance { get; private set; }
[Serialize("", IsPropertySaveable.No)]
[Serialize("", IsPropertySaveable.No, description: "Identifier used by AI to determine conversation lines to say when this effect is active.")]
public Identifier DialogFlag { get; private set; }
[Serialize("", IsPropertySaveable.No)]
[Serialize("", IsPropertySaveable.No, description: "Tag that enemy AI may use to target the affected character when this effect is active.")]
public Identifier Tag { get; private set; }
[Serialize("0,0,0,0", IsPropertySaveable.No)]
[Serialize("0,0,0,0", IsPropertySaveable.No,
description: "Color to tint the affected character's face with at this effect's lowest strength. The alpha channel is used to determine how much to tint the character's face.")]
public Color MinFaceTint { get; private set; }
[Serialize("0,0,0,0", IsPropertySaveable.No)]
[Serialize("0,0,0,0", IsPropertySaveable.No,
description: "Color to tint the affected character's face with at this effect's highest strength. The alpha channel is used to determine how much to tint the character's face.")]
public Color MaxFaceTint { get; private set; }
[Serialize("0,0,0,0", IsPropertySaveable.No)]
[Serialize("0,0,0,0", IsPropertySaveable.No,
description: "Color to tint the affected character's entire body with at this effect's lowest strength. The alpha channel is used to determine how much to tint the character.")]
public Color MinBodyTint { get; private set; }
[Serialize("0,0,0,0", IsPropertySaveable.No)]
[Serialize("0,0,0,0", IsPropertySaveable.No,
description: "Color to tint the affected character's entire body with at this effect's highest strength. The alpha channel is used to determine how much to tint the character.")]
public Color MaxBodyTint { get; private set; }
/// <summary>
/// Prevents AfflictionHusks with the specified identifier(s) from transforming the character into an AI-controlled character
/// StatType that will be applied to the affected character when the effect is active that is proportional to the effect's strength.
/// </summary>
public Identifier[] BlockTransformation { get; private set; }
/// <example>
/// <code language="xml">
/// <Effect minstrength="0" maxstrength="100">
/// <!-- Walking speed will be increased by 10% at strength 0, 20% at 50 and 30% at 100 -->
/// <StatValue stattype="WalkingSpeed" minvalue="0.1" maxvalue="0.3" />
/// <!-- Maximum health will be increased by 20% regardless of the effect strength -->
/// <StatValue stattype="MaximumHealthMultiplier" value="0.2" />
/// </Effect>
/// </code>
/// </example>
public readonly struct AppliedStatValue
{
/// <summary>
/// Which StatType to apply
/// </summary>
public readonly StatTypes StatType;
public readonly Dictionary<StatTypes, (float minValue, float maxValue)> AfflictionStatValues = new Dictionary<StatTypes, (float minValue, float maxValue)>();
public AbilityFlags AfflictionAbilityFlags;
/// <summary>
/// Minimum value to apply
/// </summary>
public readonly float MinValue;
/// <summary>
/// Minimum value to apply
/// </summary>
public readonly float MaxValue;
/// <summary>
/// Constant value to apply, will be ignored if MinValue or MaxValue are set
/// </summary>
private readonly float Value;
public AppliedStatValue(ContentXElement element)
{
Value = element.GetAttributeFloat("value", 0.0f);
StatType = element.GetAttributeEnum("stattype", StatTypes.None);
MinValue = element.GetAttributeFloat("minvalue", Value);
MaxValue = element.GetAttributeFloat("maxvalue", Value);
}
}
/// <summary>
/// Prevents AfflictionHusks with the specified identifier(s) from transforming the character into an AI-controlled character.
/// </summary>
public readonly ImmutableArray<Identifier> BlockTransformation;
/// <summary>
/// StatType that will be applied to the affected character when the effect is active that is proportional to the effect's strength.
/// </summary>
public readonly ImmutableDictionary<StatTypes, AppliedStatValue> AfflictionStatValues;
public readonly AbilityFlags AfflictionAbilityFlags;
//statuseffects applied on the character when the affliction is active
public readonly List<StatusEffect> StatusEffects = new List<StatusEffect>();
public readonly ImmutableArray<StatusEffect> StatusEffects;
public Effect(ContentXElement element, string parentDebugName)
{
SerializableProperty.DeserializeProperties(this, element);
resistanceFor = element.GetAttributeIdentifierArray("resistancefor", Array.Empty<Identifier>());
BlockTransformation = element.GetAttributeIdentifierArray("blocktransformation", Array.Empty<Identifier>());
ResistanceFor = element.GetAttributeIdentifierArray("resistancefor", Array.Empty<Identifier>())!.ToImmutableArray();
BlockTransformation = element.GetAttributeIdentifierArray("blocktransformation", Array.Empty<Identifier>())!.ToImmutableArray();
var afflictionStatValues = new Dictionary<StatTypes, AppliedStatValue>();
var statusEffects = new List<StatusEffect>();
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "statuseffect":
StatusEffects.Add(StatusEffect.Load(subElement, parentDebugName));
statusEffects.Add(StatusEffect.Load(subElement, parentDebugName));
break;
case "statvalue":
var statType = CharacterAbilityGroup.ParseStatType(subElement.GetAttributeString("stattype", ""), parentDebugName);
float defaultValue = subElement.GetAttributeFloat("value", 0f);
float minValue = subElement.GetAttributeFloat("minvalue", defaultValue);
float maxValue = subElement.GetAttributeFloat("maxvalue", defaultValue);
AfflictionStatValues.TryAdd(statType, (minValue, maxValue));
var newStatValue = new AppliedStatValue(subElement);
afflictionStatValues.Add(newStatValue.StatType, newStatValue);
break;
case "abilityflag":
var flagType = CharacterAbilityGroup.ParseFlagType(subElement.GetAttributeString("flagtype", ""), parentDebugName);
AbilityFlags flagType = subElement.GetAttributeEnum("flagtype", AbilityFlags.None);
if (flagType is AbilityFlags.None)
{
DebugConsole.ThrowError($"Error in affliction \"{parentDebugName}\" - invalid ability flag type \"{subElement.GetAttributeString("flagtype", "")}\".");
continue;
}
AfflictionAbilityFlags |= flagType;
break;
case "affliction":
@@ -271,21 +449,77 @@ namespace Barotrauma
break;
}
}
AfflictionStatValues = afflictionStatValues.ToImmutableDictionary();
StatusEffects = statusEffects.ToImmutableArray();
}
/// <summary>
/// Returns 0 if affliction.Strength is MinStrength,
/// 1 if affliction.Strength is MaxStrength
/// </summary>
public float GetStrengthFactor(Affliction affliction) => GetStrengthFactor(affliction.Strength);
/// <summary>
/// Returns 0 if affliction.Strength is MinStrength,
/// 1 if affliction.Strength is MaxStrength
/// </summary>
public float GetStrengthFactor(float strength)
=> MathUtils.InverseLerp(
MinStrength,
MaxStrength,
strength);
}
public class Description
/// <summary>
/// The description element can be used to define descriptions for the affliction which are shown under specific conditions;
/// for example a description that only shows to other players or only at certain strength levels.
/// </summary>
/// <doc>
/// <Field identifier="Text" type="string" defaultValue="&quot;&quot;">
/// Raw text for the description.
/// </Field>
/// </doc>
public sealed class Description
{
public enum TargetType
{
/// <summary>
/// Everyone can see the description.
/// </summary>
Any,
/// <summary>
/// Only the affected character can see the description.
/// </summary>
Self,
/// <summary>
/// The affected character cannot see the description but others can.
/// </summary>
OtherCharacter
}
/// <summary>
/// Raw text for the description.
/// </summary>
public readonly LocalizedString Text;
/// <summary>
/// Text tag used to set the text from the localization files.
/// </summary>
public readonly Identifier TextTag;
public readonly float MinStrength, MaxStrength;
/// <summary>
/// Minimum strength required for the description to be shown.
/// </summary>
public readonly float MinStrength;
/// <summary>
/// Maximum strength required for the description to be shown.
/// </summary>
public readonly float MaxStrength;
/// <summary>
/// Who can see the description.
/// </summary>
public readonly TargetType Target;
public Description(ContentXElement element, AfflictionPrefab affliction)
@@ -315,7 +549,23 @@ namespace Barotrauma
}
}
public class PeriodicEffect
/// <summary>
/// PeriodicEffect applies StatusEffects to the character periodically.
/// </summary>
/// <doc>
/// <SubElement identifier="StatusEffect" type="StatusEffect" />
/// <Field identifier="Interval" type="float" defaultValue="1.0">
/// How often the status effect is applied in seconds.
/// Setting this attribute will set both the min and max interval to the specified value.
/// </Field>
/// <Field identifier="MinInterval" type="float" defaultValue="1.0">
/// Minimum interval between applying the status effect in seconds.
/// </Field>
/// <Field identifier="MaxInterval" type="float" defaultValue="1.0">
/// Maximum interval between applying the status effect in seconds.
/// </Field>
/// </doc>
public sealed class PeriodicEffect
{
public readonly List<StatusEffect> StatusEffects = new List<StatusEffect>();
public readonly float MinInterval, MaxInterval;
@@ -368,52 +618,124 @@ namespace Barotrauma
public static readonly PrefabCollection<AfflictionPrefab> Prefabs = new PrefabCollection<AfflictionPrefab>();
public override void Dispose() { }
public static IEnumerable<AfflictionPrefab> List => Prefabs;
// Arbitrary string that is used to identify the type of the affliction.
public readonly Identifier AfflictionType;
public override void Dispose() { }
private readonly ContentXElement configElement;
//Does the affliction affect a specific limb or the whole character
public readonly bool LimbSpecific;
//If not a limb-specific affliction, which limb is the indicator shown on in the health menu
//(e.g. mental health problems on head, lack of oxygen on torso...)
public readonly LimbType IndicatorLimb;
public readonly LocalizedString Name;
public readonly Identifier TranslationIdentifier;
public readonly bool IsBuff;
public readonly bool AffectMachines;
public readonly bool HealableInMedicalClinic;
public readonly float HealCostMultiplier;
public readonly int BaseHealCost;
public readonly bool ShowBarInHealthMenu;
public readonly LocalizedString CauseOfDeathDescription, SelfCauseOfDeathDescription;
private readonly LocalizedString defaultDescription;
public readonly ImmutableList<Description> Descriptions;
/// <summary>
/// Arbitrary string that is used to identify the type of the affliction.
/// </summary>
public readonly Identifier AfflictionType;
/// <summary>
/// If set to true, the affliction affects individual limbs. Otherwise, it affects the whole character.
/// </summary>
public readonly bool LimbSpecific;
/// <summary>
/// If the affliction doesn't affect individual limbs, this attribute determines
/// where the game will render the affliction's indicator when viewed in the
/// in-game health UI.
///
/// For example, the psychosis indicator is rendered on the head, and low oxygen
/// is rendered on the torso.
/// </summary>
public readonly LimbType IndicatorLimb;
/// <summary>
/// Can be set to the identifier of another affliction to make this affliction
/// reuse the same name and description.
/// </summary>
public readonly Identifier TranslationIdentifier;
/// <summary>
/// If set to true, the game will recognize this affliction as a buff.
/// This means, among other things, that bots won't attempt to treat it,
/// and the health UI will render the affected limb in green rather than red.
/// </summary>
public readonly bool IsBuff;
/// <summary>
/// If set to true, this affliction can affect characters that are marked as
/// machines, such as the Fractal Guardian.
/// </summary>
public readonly bool AffectMachines;
/// <summary>
/// If set to true, this affliction can be healed at the medical clinic.
/// </summary>
/// <doc>
/// <override type="DefaultValue">
/// false if the affliction is a buff or has the type "geneticmaterialbuff" or "geneticmaterialdebuff", true otherwise.
/// </override>
/// </doc>
public readonly bool HealableInMedicalClinic;
/// <summary>
/// How much each unit of this affliction's strength will add
/// to the cost of healing at the medical clinic.
/// </summary>
public readonly float HealCostMultiplier;
/// <summary>
/// The minimum cost of healing this affliction at the medical clinic.
/// </summary>
public readonly int BaseHealCost;
/// <summary>
/// If set to false, the health UI will not show the strength of the affliction
/// as a bar under its indicator.
/// </summary>
public readonly bool ShowBarInHealthMenu;
/// <summary>
/// If set to true, this affliction's icon will be hidden from the HUD after 5 seconds.
/// </summary>
public readonly bool HideIconAfterDelay;
//how high the strength has to be for the affliction to take affect
/// <summary>
/// How high the strength has to be for the affliction to take effect
/// </summary>
public readonly float ActivationThreshold = 0.0f;
//how high the strength has to be for the affliction icon to be shown in the UI
/// <summary>
/// How high the strength has to be for the affliction icon to be shown in the UI
/// </summary>
public readonly float ShowIconThreshold = 0.05f;
//how high the strength has to be for the affliction icon to be shown to others with a health scanner or via the health interface
/// <summary>
/// How high the strength has to be for the affliction icon to be shown to others with a health scanner or via the health interface
/// </summary>
public readonly float ShowIconToOthersThreshold = 0.05f;
/// <summary>
/// The maximum strength this affliction can have.
/// </summary>
public readonly float MaxStrength = 100.0f;
/// <summary>
/// The strength of the radiation grain effect to apply when the strength of this affliction increases.
/// </summary>
public readonly float GrainBurst;
//how high the strength has to be for the affliction icon to be shown with a health scanner
/// <summary>
/// How high the strength has to be for the affliction icon to be shown with a health scanner
/// </summary>
public readonly float ShowInHealthScannerThreshold = 0.05f;
//how strong the affliction needs to be before bots attempt to treat it
/// <summary>
/// How strong the affliction needs to be before bots attempt to treat it.
/// Also effects when the affliction is shown in the suitable treatments list.
/// </summary>
public readonly float TreatmentThreshold = 5.0f;
/// <summary>
@@ -422,43 +744,84 @@ namespace Barotrauma
public ImmutableHashSet<Identifier> IgnoreTreatmentIfAfflictedBy;
/// <summary>
/// The affliction is automatically removed after this time. 0 = unlimited
/// The duration of the affliction, in seconds. If set to 0, the affliction does not expire.
/// </summary>
public readonly float Duration;
//how much karma changes when a player applies this affliction to someone (per strength of the affliction)
/// <summary>
/// How much karma changes when a player applies this affliction to someone (per strength of the affliction)
/// </summary>
public float KarmaChangeOnApplied;
/// <summary>
/// Opacity of the burn effect (darker tint) on limbs affected by this affliction. 1 = full strength.
/// </summary>
public readonly float BurnOverlayAlpha;
/// <summary>
/// Opacity of the bloody damage overlay on limbs affected by this affliction. 1 = full strength.
/// </summary>
public readonly float DamageOverlayAlpha;
//steam achievement given when the controlled character receives the affliction
/// Steam achievement given when the controlled character receives the affliction.
/// </summary>
public readonly Identifier AchievementOnReceived;
//steam achievement given when the affliction is removed from the controlled character
/// <summary>
/// Steam achievement given when the affliction is removed from the controlled character.
/// </summary>
public readonly Identifier AchievementOnRemoved;
public readonly Sprite Icon;
/// <summary>
/// A gradient that defines which color to render this affliction's icon
/// with, based on the affliction's current strength.
/// </summary>
public readonly Color[] IconColors;
public readonly Sprite AfflictionOverlay;
/// <summary>
/// If set to true and the affliction has an AfflictionOverlay element, the overlay's opacity will be strictly proportional to its strength.
/// Otherwise, the overlay's opacity will be determined based on its activation threshold and effects.
/// </summary>
public readonly bool AfflictionOverlayAlphaIsLinear;
/// <summary>
/// If set to true, this affliction will not persist between rounds.
/// </summary>
public readonly bool ResetBetweenRounds;
/// <summary>
/// Should damage particles be emitted when a character receives this affliction?
/// Only relevant if the affliction is of the type "bleeding" or "damage".
/// </summary>
public readonly bool DamageParticles;
/// <summary>
/// An arbitrary modifier that affects how much medical skill is increased when you apply the affliction on a target.
/// If the affliction causes damage or is of type poison or paralysis, the skill is increased only when the target is hostile.
/// If the affliction is of type buff, the skill is increased only when the target is friendly.
/// If the affliction causes damage or is of the 'poison' or 'paralysis' type, the skill is increased only when the target is hostile.
/// If the affliction is of the 'buff' type, the skill is increased only when the target is friendly.
/// </summary>
public readonly float MedicalSkillGain;
/// <summary>
/// An arbitrary modifier that affects how much weapons skill is increased when you apply the affliction on a target.
/// The skill is increased only when the target is hostile.
/// </summary>
public readonly float WeaponsSkillGain;
/// <summary>
/// A list of species this affliction is allowed to affect.
/// </summary>
public Identifier[] TargetSpecies { get; protected set; }
/// <summary>
/// Effects to apply at various strength levels.
/// Only one effect can be applied at any given moment, so their ranges should be defined with no overlap.
/// </summary>
private readonly List<Effect> effects = new List<Effect>();
/// <summary>
/// PeriodicEffect applies StatusEffects to the character periodically.
/// </summary>
private readonly List<PeriodicEffect> periodicEffects = new List<PeriodicEffect>();
public IEnumerable<Effect> Effects => effects;
@@ -467,9 +830,16 @@ namespace Barotrauma
private readonly ConstructorInfo constructor;
public Identifier[] TargetSpecies { get; protected set; }
/// <summary>
/// An icon thats used in the UI to represent this affliction.
/// </summary>
public readonly Sprite Icon;
public readonly bool ResetBetweenRounds;
/// <summary>
/// A sprite that covers the affected player's entire screen when this affliction is active.
/// Its opacity is controlled by the active effect's MinAfflictionOverlayAlphaMultiplier and MaxAfflictionOverlayAlphaMultiplier
/// </summary>
public readonly Sprite AfflictionOverlay;
public IEnumerable<KeyValuePair<Identifier, float>> TreatmentSuitability
{
@@ -497,7 +867,7 @@ namespace Barotrauma
if (!string.IsNullOrEmpty(fallbackName))
{
Name = Name.Fallback(fallbackName);
}
}
defaultDescription = TextManager.Get($"AfflictionDescription.{TranslationIdentifier}");
string fallbackDescription = element.GetAttributeString("description", "");
if (!string.IsNullOrEmpty(fallbackDescription))
@@ -1,5 +1,8 @@
namespace Barotrauma
{
/// <summary>
/// A special affliction type that makes the character see and hear things that aren't there.
/// </summary>
partial class AfflictionPsychosis : Affliction
{
@@ -1,11 +1,12 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Barotrauma
{
/// <summary>
/// A special affliction type that periodically inverts the character's controls and stuns the character.
/// The frequency and duration of the effects increases the higher the strength of the affliction is.
/// </summary>
class AfflictionSpaceHerpes : Affliction
{
private float invertControlsCooldown = 60.0f;
@@ -3,6 +3,10 @@ using System;
namespace Barotrauma
{
/// <summary>
/// A special affliction type that increases the duration of buffs (afflictions of the type "buff"). The increase is defined using the
/// <see cref="AfflictionPrefab.Effect.MinBuffMultiplier"/> and <see cref="AfflictionPrefab.Effect.MaxBuffMultiplier"/> attributes of the affliction effect.
/// </summary>
class BuffDurationIncrease : Affliction
{
public BuffDurationIncrease(AfflictionPrefab prefab, float strength) : base(prefab, strength)
@@ -20,9 +24,9 @@ namespace Barotrauma
{
foreach (Affliction affliction in afflictions)
{
if (!affliction.Prefab.IsBuff || affliction == this || affliction.MultiplierSource != this) { continue; }
affliction.MultiplierSource = null;
affliction.StrengthDiminishMultiplier = 1f;
if (!affliction.Prefab.IsBuff || affliction == this || affliction.StrengthDiminishMultiplier.Source != this) { continue; }
affliction.StrengthDiminishMultiplier.Source = null;
affliction.StrengthDiminishMultiplier.Value = 1f;
}
}
else
@@ -31,10 +35,10 @@ namespace Barotrauma
{
if (!affliction.Prefab.IsBuff || affliction == this) { continue; }
float multiplier = GetDiminishMultiplier();
if (affliction.StrengthDiminishMultiplier < multiplier && affliction.MultiplierSource != this) { continue; }
if (affliction.StrengthDiminishMultiplier.Value < multiplier && affliction.StrengthDiminishMultiplier.Source != this) { continue; }
affliction.MultiplierSource = this;
affliction.StrengthDiminishMultiplier = multiplier;
affliction.StrengthDiminishMultiplier.Source = this;
affliction.StrengthDiminishMultiplier.Value = multiplier;
}
}
}
@@ -48,7 +52,7 @@ namespace Barotrauma
float multiplier = MathHelper.Lerp(
currentEffect.MinBuffMultiplier,
currentEffect.MaxBuffMultiplier,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
currentEffect.GetStrengthFactor(this));
return 1.0f / Math.Max(multiplier, 0.001f);
}
}
@@ -184,7 +184,7 @@ namespace Barotrauma
}
}
public Color DefaultFaceTint = Color.TransparentBlack;
public static readonly Color DefaultFaceTint = Color.TransparentBlack;
public Color FaceTint
{
@@ -449,7 +449,11 @@ namespace Barotrauma
var affliction = kvp.Key;
resistance += affliction.GetResistance(afflictionPrefab.Identifier);
}
return 1 - ((1 - resistance) * Character.GetAbilityResistance(afflictionPrefab));
resistance = 1 - ((1 - resistance) * Character.GetAbilityResistance(afflictionPrefab));
if (resistance > 1f) { resistance = 1f; }
return resistance;
}
public float GetStatValue(StatTypes statType)
@@ -1151,16 +1155,14 @@ namespace Barotrauma
}
}
public IEnumerable<Identifier> GetActiveAfflictionTags() => GetActiveAfflictionTags(afflictions.Keys);
private readonly HashSet<Identifier> afflictionTags = new HashSet<Identifier>();
public IEnumerable<Identifier> GetActiveAfflictionTags(IEnumerable<Affliction> afflictions)
public IEnumerable<Identifier> GetActiveAfflictionTags()
{
afflictionTags.Clear();
foreach (Affliction affliction in afflictions)
foreach (Affliction affliction in afflictions.Keys)
{
var currentEffect = affliction.GetActiveEffect();
if (currentEffect != null && !currentEffect.Tag.IsEmpty)
if (currentEffect is { Tag.IsEmpty: false })
{
afflictionTags.Add(currentEffect.Tag);
}
@@ -111,6 +111,9 @@ namespace Barotrauma
[Serialize("", IsPropertySaveable.No)]
public Identifier Group { get; set; }
[Serialize(false, IsPropertySaveable.No)]
public bool AllowDraggingIndefinitely { get; set; }
public XElement Element { get; protected set; }
@@ -214,7 +217,8 @@ namespace Barotrauma
CharacterInfo characterInfo;
if (characterElement == null)
{
characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: GetJobPrefab(randSync), npcIdentifier: Identifier, randSync: randSync);}
characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: GetJobPrefab(randSync), npcIdentifier: Identifier, randSync: randSync);
}
else
{
characterInfo = new CharacterInfo(characterElement, Identifier);
@@ -50,7 +50,7 @@ namespace Barotrauma
public override void Dispose() { }
}
class JobVariant
internal class JobVariant
{
public JobPrefab Prefab;
public int Variant;
@@ -113,7 +113,7 @@ namespace Barotrauma
public readonly LocalizedString Name;
[Serialize(AIObjectiveIdle.BehaviorType.Passive, IsPropertySaveable.No)]
[Serialize(AIObjectiveIdle.BehaviorType.Passive, IsPropertySaveable.No, description: "How should the character behave when idling (not doing any particular task)?")]
public AIObjectiveIdle.BehaviorType IdleBehavior
{
get;
@@ -122,78 +122,63 @@ namespace Barotrauma
public readonly LocalizedString Description;
[Serialize(false, IsPropertySaveable.No)]
[Serialize(false, IsPropertySaveable.No, description: "Can the character speak any random lines, or just ones specifically meant for the job?")]
public bool OnlyJobSpecificDialog
{
get;
private set;
}
//the number of these characters in the crew the player starts with in the single player campaign
[Serialize(0, IsPropertySaveable.No)]
[Serialize(0, IsPropertySaveable.No, description: "The number of these characters in the crew the player starts with in the single player campaign.")]
public int InitialCount
{
get;
private set;
}
//if set to true, a client that has chosen this as their preferred job will get it no matter what
[Serialize(false, IsPropertySaveable.No)]
[Serialize(false, IsPropertySaveable.No, description: "If set to true, a client that has chosen this as their preferred job will get it regardless of the maximum number or the amount of spawnpoints in the sub.")]
public bool AllowAlways
{
get;
private set;
}
//how many crew members can have the job (only one captain etc)
[Serialize(100, IsPropertySaveable.No)]
[Serialize(100, IsPropertySaveable.No, description: "How many crew members can have the job (e.g. only one captain etc).")]
public int MaxNumber
{
get;
private set;
}
//how many crew members are REQUIRED to have the job
//(i.e. if one captain is required, one captain is chosen even if all the players have set captain to lowest preference)
[Serialize(0, IsPropertySaveable.No)]
[Serialize(0, IsPropertySaveable.No, description: "How many crew members are required to have the job. I.e. if one captain is required, one captain is chosen even if all the players have set captain to lowest preference.")]
public int MinNumber
{
get;
private set;
}
[Serialize(0.0f, IsPropertySaveable.No)]
[Serialize(0.0f, IsPropertySaveable.No, description: "Minimum amount of karma a player must have to get assigned this job.")]
public float MinKarma
{
get;
private set;
}
[Serialize(1.0f, IsPropertySaveable.No)]
[Serialize(1.0f, IsPropertySaveable.No, description: "Multiplier on the base hiring cost when hiring the character from an outpost.")]
public float PriceMultiplier
{
get;
private set;
}
// TODO: not used
[Serialize(10.0f, IsPropertySaveable.No)]
public float Commonness
{
get;
private set;
}
//how much the vitality of the character is increased/reduced from the default value
[Serialize(0.0f, IsPropertySaveable.No)]
[Serialize(0.0f, IsPropertySaveable.No, description: "How much the vitality of the character is increased/reduced from the default value (e.g. 10 = 110 total vitality if the default vitality is 100.).")]
public float VitalityModifier
{
get;
private set;
}
//whether the job should be available to NPCs
[Serialize(false, IsPropertySaveable.No)]
[Serialize(false, IsPropertySaveable.No, description: "Hidden jobs are not selectable by players, but can be used by e.g. outpost NPCs.")]
public bool HiddenJob
{
get;
@@ -119,6 +119,9 @@ namespace Barotrauma
[Serialize(1.0f, IsPropertySaveable.Yes, "Tells the bots how much they should prefer targeting this character with submarine weapons tagged as \"slowturret\", like railguns. The tag is arbitrary and can be added to any turrets, just like the priority. Defaults to 1. Not used if AITurretPriority is 0. Distance to the target affects the decision making."), Editable]
public float AISlowTurretPriority { get; set; }
[Serialize("", IsPropertySaveable.Yes, description: "Identifier or tag of the item the character's items are placed inside when the character despawns."), Editable]
public Identifier DespawnContainer { get; private set; }
public readonly CharacterFile File;
public XDocument VariantFile { get; private set; }
@@ -6,11 +6,13 @@ namespace Barotrauma.Abilities
{
private readonly ItemTalentStats stat;
private readonly float value;
private readonly bool stackable;
public CharacterAbilityGiveItemStat(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
stat = abilityElement.GetAttributeEnum("stattype", ItemTalentStats.None);
value = abilityElement.GetAttributeFloat("value", 0f);
stackable = abilityElement.GetAttributeBool("stackable", true);
}
protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate)
@@ -25,7 +27,7 @@ namespace Barotrauma.Abilities
{
if (abilityObject is not IAbilityItem ability) { return; }
ability.Item.StatManager.ApplyStat(stat, value, CharacterTalent);
ability.Item.StatManager.ApplyStat(stat, stackable, value, CharacterTalent);
}
}
}
@@ -9,12 +9,14 @@ namespace Barotrauma.Abilities
private readonly ItemTalentStats stat;
private readonly float value;
private readonly ImmutableHashSet<Identifier> tags;
private readonly bool stackable;
public CharacterAbilityGiveItemStatToTags(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
stat = abilityElement.GetAttributeEnum("stattype", ItemTalentStats.None);
value = abilityElement.GetAttributeFloat("value", 0f);
tags = abilityElement.GetAttributeIdentifierImmutableHashSet("tags", ImmutableHashSet<Identifier>.Empty);
stackable = abilityElement.GetAttributeBool("stackable", true);
}
public override void InitializeAbility(bool addingFirstTime)
@@ -41,7 +43,7 @@ namespace Barotrauma.Abilities
{
if (item.HasTag(tags) || tags.Contains(item.Prefab.Identifier))
{
item.StatManager.ApplyStat(stat, value, CharacterTalent);
item.StatManager.ApplyStat(stat, stackable, value, CharacterTalent);
}
}
}
@@ -217,6 +217,11 @@ namespace Barotrauma.Abilities
public static StatTypes ParseStatType(string statTypeString, string debugIdentifier)
{
//backwards compatibility
if (statTypeString.Equals("MedicalItemDurationMultiplier", StringComparison.OrdinalIgnoreCase))
{
statTypeString = "BuffItemApplyingMultiplier";
}
if (!Enum.TryParse(statTypeString, true, out StatTypes statType))
{
DebugConsole.ThrowError("Invalid stat type type \"" + statTypeString + "\" in CharacterTalent (" + debugIdentifier + ")");
@@ -71,6 +71,29 @@ namespace Barotrauma
}
}
private static readonly HashSet<Identifier> checkedNonStackableTalents = new();
/// <summary>
/// Checks talents for a given AbilityObject taking into account non-stackable talents.
/// </summary>
public static void CheckTalentsForCrew(IEnumerable<Character> crew, AbilityEffectType type, AbilityObject abilityObject)
{
checkedNonStackableTalents.Clear();
foreach (Character character in crew)
{
foreach (CharacterTalent characterTalent in character.CharacterTalents)
{
if (!characterTalent.Prefab.AbilityEffectsStackWithSameTalent)
{
if (checkedNonStackableTalents.Contains(characterTalent.Prefab.Identifier)) { continue; }
checkedNonStackableTalents.Add(characterTalent.Prefab.Identifier);
}
characterTalent.CheckTalent(type, abilityObject);
}
}
}
public void CheckTalent(AbilityEffectType abilityEffectType, AbilityObject abilityObject)
{
if (characterAbilityGroupEffectDictionary.TryGetValue(abilityEffectType, out var characterAbilityGroups))
@@ -0,0 +1,109 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
internal abstract class TalentMigration
{
private readonly Version version;
private delegate TalentMigration TalentMigrationCtor(Version version, ContentXElement element);
private static readonly Dictionary<Identifier, TalentMigrationCtor> migrationTemplates =
new()
{
[new Identifier("AddStat")] =
static (version, element) => new TalentMigrationAddStat(version, element),
[new Identifier("UpdateStatIdentifier")] =
static (version, element) => new TalentMigrationUpdateStatIdentifier(version, element)
};
public bool TryApply(Version savedVersion, CharacterInfo info)
{
if (version <= savedVersion) { return false; }
Apply(info);
return true;
}
protected abstract void Apply(CharacterInfo info);
protected TalentMigration(Version targetVersion)
{
version = targetVersion;
}
public static TalentMigration FromXML(ContentXElement element)
{
Version? version = XMLExtensions.GetAttributeVersion(element, "version", null);
if (version is null)
{
throw new Exception("Talent migration version not defined.");
}
Identifier name = element.Name.ToString().ToIdentifier();
if (!migrationTemplates.TryGetValue(name, out TalentMigrationCtor? ctor))
{
throw new Exception($"Unknown talent migration type: {name}.");
}
return ctor(version, element);
}
}
/// <summary>
/// Migration that adds a missing permanent stat to the character.
/// </summary>
internal sealed class TalentMigrationAddStat : TalentMigration
{
[Serialize(StatTypes.None, IsPropertySaveable.Yes)]
public StatTypes StatType { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier StatIdentifier { get; set; }
[Serialize(0f, IsPropertySaveable.Yes)]
public float Value { get; set; }
[Serialize(false, IsPropertySaveable.Yes)]
public bool RemoveOnDeath { get; set; }
public TalentMigrationAddStat(Version targetVersion, ContentXElement element) : base(targetVersion)
=> SerializableProperty.DeserializeProperties(this, element);
protected override void Apply(CharacterInfo info)
{
info.ChangeSavedStatValue(StatType, Value, StatIdentifier, RemoveOnDeath);
}
}
/// <summary>
/// Migration that updates permanent stat identifiers.
/// </summary>
internal class TalentMigrationUpdateStatIdentifier : TalentMigration
{
[Serialize("", IsPropertySaveable.Yes, "The old identifier to update.")]
public Identifier Old { get; set; }
[Serialize("", IsPropertySaveable.Yes, "What to change the old identifier to.")]
public Identifier New { get; set; }
public TalentMigrationUpdateStatIdentifier(Version targetVersion, ContentXElement element) : base(targetVersion)
=> SerializableProperty.DeserializeProperties(this, element);
protected override void Apply(CharacterInfo info)
{
foreach (SavedStatValue statValue in info.SavedStatValues.Values.SelectMany(static s => s))
{
if (statValue.StatIdentifier != Old) { continue; }
statValue.StatIdentifier = New;
}
}
}
}
@@ -1,4 +1,7 @@
#if CLIENT
using System;
using System.Collections.Immutable;
using System.Xml.Linq;
#if CLIENT
using Microsoft.Xna.Framework;
#endif
@@ -12,6 +15,11 @@ namespace Barotrauma
public LocalizedString Description { get; private set; }
/// <summary>
/// When set to false the AbilityEffects of multiple of the same talent will not be checked and only the first one.
/// </summary>
public bool AbilityEffectsStackWithSameTalent;
public readonly Sprite Icon;
#if CLIENT
@@ -20,6 +28,8 @@ namespace Barotrauma
public static readonly PrefabCollection<TalentPrefab> TalentPrefabs = new PrefabCollection<TalentPrefab>();
public readonly ImmutableHashSet<TalentMigration> Migrations;
public ContentXElement ConfigElement
{
get;
@@ -32,6 +42,8 @@ namespace Barotrauma
DisplayName = TextManager.Get($"talentname.{Identifier}").Fallback(Identifier.Value);
AbilityEffectsStackWithSameTalent = element.GetAttributeBool("abilityeffectsstackwithsametalent", true);
Identifier nameIdentifier = element.GetAttributeIdentifier("nameidentifier", Identifier.Empty);
if (!nameIdentifier.IsEmpty)
{
@@ -48,6 +60,8 @@ namespace Barotrauma
: Option<Color>.None();
#endif
var migrations = ImmutableHashSet.CreateBuilder<TalentMigration>();
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
@@ -60,9 +74,25 @@ namespace Barotrauma
TextManager.ConstructDescription(ref tempDescription, subElement);
Description = tempDescription;
break;
case "migrations":
foreach (var migrationElement in subElement.Elements())
{
try
{
var migration = TalentMigration.FromXML(migrationElement);
migrations.Add(migration);
}
catch (Exception e)
{
DebugConsole.ThrowError($"Error while loading talent migration for talent \"{Identifier}\".", e);
}
}
break;
}
}
Migrations = migrations.ToImmutable();
if (element.GetAttribute("description") != null)
{
string description = element.GetAttributeString("description", string.Empty);