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);
@@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
@@ -22,7 +23,7 @@ namespace Barotrauma
: string.Empty);
}
public static readonly Version MinimumHashCompatibleVersion = new Version(0, 18, 13, 0);
public static readonly Version MinimumHashCompatibleVersion = new Version(1, 0, 11, 0);
public const string LocalModsDir = "LocalMods";
public static readonly string WorkshopModsDir = Barotrauma.IO.Path.Combine(
@@ -203,7 +204,13 @@ namespace Barotrauma
break;
}
}
if (UgcId.TryUnwrap(out ContentPackageId? id) && id != null)
{
incrementalHash.AppendData(Encoding.UTF8.GetBytes(id.StringRepresentation));
}
incrementalHash.AppendData(Encoding.UTF8.GetBytes(ModVersion));
var md5Hash = Md5Hash.BytesAsHash(incrementalHash.GetHashAndReset());
if (logging)
{
@@ -116,9 +116,12 @@ namespace Barotrauma
public static void ThrowIfDuplicates(IEnumerable<ContentPackage> pkgs)
{
var contentPackages = pkgs as IList<ContentPackage> ?? pkgs.ToArray();
if (contentPackages.Any(p1 => contentPackages.AtLeast(2, p2 => p1 == p2)))
foreach (ContentPackage cp in contentPackages)
{
throw new InvalidOperationException($"Input contains duplicate packages");
if (contentPackages.AtLeast(2, cp2 => cp == cp2))
{
throw new InvalidOperationException($"There are duplicates in the list of selected content packages (\"{cp.Name}\", hash: {cp.Hash?.ShortRepresentation ?? "none"})");
}
}
}
@@ -384,7 +384,7 @@ namespace Barotrauma
return new string[][]
{
GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray(),
PermissionPreset.List.Select(pp => pp.Name.Value).ToArray()
PermissionPreset.List.Select(pp => pp.DisplayName.Value).ToArray()
};
}));
+373 -16
View File
@@ -75,88 +75,390 @@ namespace Barotrauma
OnStatusEffectIdentifier,
}
/// <summary>
/// StatTypes are used to alter several traits of a character. They are mostly used by talents.
///
/// A lot of StatTypes use a "percentage" value. The way this works is that the value is 0 by default and 1 is added to the value of the stat type to get the final multiplier.
/// For example if the value is set to 0.2 then 1 is added to it making it 1.2 and that is used as a multiplier.
/// This makes it so values between -100% and +100% can be easily represented as -1 and 1 respectively. For example 0.5 would translate to 1.5 for +50% and -0.2 would translate to 0.8 for -20% multiplier.
/// </summary>
public enum StatTypes
{
/// <summary>
/// Used to indicate an invalid stat type. Should not be used.
/// </summary>
None,
// Skills
/// <summary>
/// Boosts electrical skill by a flat amount.
/// </summary>
ElectricalSkillBonus,
/// <summary>
/// Boosts helm skill by a flat amount.
/// </summary>
HelmSkillBonus,
HelmSkillOverride,
MedicalSkillOverride,
WeaponsSkillOverride,
ElectricalSkillOverride,
MechanicalSkillOverride,
/// <summary>
/// Boosts mechanical skill by a flat amount.
/// </summary>
MechanicalSkillBonus,
/// <summary>
/// Boosts medical skill by a flat amount.
/// </summary>
MedicalSkillBonus,
/// <summary>
/// Boosts weapons skill by a flat amount.
/// </summary>
WeaponsSkillBonus,
// Character attributes
/// <summary>
/// Boosts the character's helm skill to the given value if it's lower than the given value.
/// </summary>
HelmSkillOverride,
/// <summary>
/// Boosts the character's medical skill to the given value if it's lower than the given value.
/// </summary>
MedicalSkillOverride,
/// <summary>
/// Boosts the character's weapons skill to the given value if it's lower than the given value.
/// </summary>
WeaponsSkillOverride,
/// <summary>
/// Boosts the character's electrical skill to the given value if it's lower than the given value.
/// </summary>
ElectricalSkillOverride,
/// <summary>
/// Boosts the character's mechanical skill to the given value if it's lower than the given value.
/// </summary>
MechanicalSkillOverride,
/// <summary>
/// Increases character's maximum vitality by a percentage.
/// </summary>
MaximumHealthMultiplier,
/// <summary>
/// Increases both walking and swimming speed of the character by a percentage.
/// </summary>
MovementSpeed,
/// <summary>
/// Increases the character's walking speed by a percentage.
/// </summary>
WalkingSpeed,
/// <summary>
/// Increases the character's swimming speed by a percentage.
/// </summary>
SwimmingSpeed,
/// <summary>
/// Decreases how long it takes for buffs applied to the character decay over time by a percentage.
/// Buffs are afflictions that have isBuff set to true.
/// </summary>
BuffDurationMultiplier,
/// <summary>
/// Decreases how long it takes for debuff applied to the character decay over time by a percentage.
/// Debuffs are afflictions that have isBuff set to false.
/// </summary>
DebuffDurationMultiplier,
/// <summary>
/// Increases the strength of afflictions that are applied to the character by a percentage.
/// Medicines are items that have the "medical" tag.
/// </summary>
MedicalItemEffectivenessMultiplier,
/// <summary>
/// Increases the resistance to pushing force caused by flowing water by a percentage. The resistance cannot be below 0% or higher than 100%.
/// </summary>
FlowResistance,
// Combat
/// <summary>
/// Increases how much damage the character deals via all attacks by a percentage.
/// </summary>
AttackMultiplier,
/// <summary>
/// Increases how much damage the character deals to other characters on the same team by a percentage.
/// </summary>
TeamAttackMultiplier,
/// <summary>
/// Decreases the reload time of ranged weapons held by the character by a percentage.
/// </summary>
RangedAttackSpeed,
/// <summary>
/// Decreases the reload time of submarine turrets operated by the character by a percentage.
/// </summary>
TurretAttackSpeed,
/// <summary>
/// Decreases the power consumption of submarine turrets operated by the character by a percentage.
/// </summary>
TurretPowerCostReduction,
/// <summary>
/// Increases how fast submarine turrets operated by the character charge up by a percentage. Affects turrets like pulse laser.
/// </summary>
TurretChargeSpeed,
/// <summary>
/// Increases how fast the character can swing melee weapons by a percentage.
/// </summary>
MeleeAttackSpeed,
/// <summary>
/// Increases the damage dealt by melee weapons held by the character by a percentage.
/// </summary>
MeleeAttackMultiplier,
RangedAttackMultiplier,
/// <summary>
/// Decreases the spread of ranged weapons held by the character by a percentage.
/// </summary>
RangedSpreadReduction,
// Utility
/// <summary>
/// Increases the repair speed of the character by a percentage.
/// </summary>
RepairSpeed,
/// <summary>
/// Increases the repair speed of the character when repairing mechanical items by a percentage.
/// </summary>
MechanicalRepairSpeed,
/// <summary>
/// Increase deconstruction speed of deconstructor operated by the character by a percentage.
/// </summary>
DeconstructorSpeedMultiplier,
/// <summary>
/// Increases the repair speed of repair tools that fix submarine walls by a percentage.
/// </summary>
RepairToolStructureRepairMultiplier,
/// <summary>
/// Increases the wall damage of tools that destroy submarine walls like plasma cutter by a percentage.
/// </summary>
RepairToolStructureDamageMultiplier,
/// <summary>
/// Increase the detach speed of items like minerals that require a tool to detach from the wall by a percentage.
/// </summary>
RepairToolDeattachTimeMultiplier,
/// <summary>
/// Allows the character to repair mechanical items past the maximum condition by a flat percentage amount. For example setting this to 0.1 allows the character to repair mechanical items to 110% condition.
/// </summary>
MaxRepairConditionMultiplierMechanical,
/// <summary>
/// Allows the character to repair electrical items past the maximum condition by a flat percentage amount. For example setting this to 0.1 allows the character to repair electrical items to 110% condition.
/// </summary>
MaxRepairConditionMultiplierElectrical,
/// <summary>
/// Increase the the quality of items crafted by the character by a flat amount.
/// Can be made to only affect certain item with a given tag types by specifying a tag via CharacterAbilityGivePermanentStat, when no tag is specified the ability affects all items.
/// </summary>
IncreaseFabricationQuality,
/// <summary>
/// Boosts the condition of genes combined by the character by a flat amount.
/// </summary>
GeneticMaterialRefineBonus,
/// <summary>
/// Reduces the chance to taint a gene when combining genes by a percentage. Tainting probability can not go below 0% or above 100%.
/// </summary>
GeneticMaterialTaintedProbabilityReductionOnCombine,
/// <summary>
/// Increases the speed at which the character gains skills by a percentage.
/// </summary>
SkillGainSpeed,
/// <summary>
/// Whenever the character's skill level up add a flat amount of more skill levels to the character.
/// </summary>
ExtraLevelGain,
/// <summary>
/// Increases the speed at which the character gains helm skill by a percentage.
/// </summary>
HelmSkillGainSpeed,
/// <summary>
/// Increases the speed at which the character gains weapons skill by a percentage.
/// </summary>
WeaponsSkillGainSpeed,
/// <summary>
/// Increases the speed at which the character gains medical skill by a percentage.
/// </summary>
MedicalSkillGainSpeed,
/// <summary>
/// Increases the speed at which the character gains electrical skill by a percentage.
/// </summary>
ElectricalSkillGainSpeed,
/// <summary>
/// Increases the speed at which the character gains mechanical skill by a percentage.
/// </summary>
MechanicalSkillGainSpeed,
/// <summary>
/// Increases the strength of afflictions the character applies to other characters via medicine by a percentage.
/// Medicines are items that have the "medical" tag.
/// </summary>
MedicalItemApplyingMultiplier,
MedicalItemDurationMultiplier,
/// <summary>
/// Increases the strength of afflictions the character applies to other characters via medicine by a percentage.
/// Works only for afflictions that have isBuff set to true.
/// </summary>
BuffItemApplyingMultiplier,
/// <summary>
/// Increases the strength of afflictions the character applies to other characters via medicine by a percentage.
/// Works only for afflictions that have "poison" type.
/// </summary>
PoisonMultiplier,
// Tinker
/// <summary>
/// Increases how long the character can tinker with items by a flat amount where 1 = 1 second.
/// </summary>
TinkeringDuration,
/// <summary>
/// Increases the effectiveness of the character's tinkerings by a percentage.
/// Tinkering strength affects the speed and effectiveness of the item that is being tinkered with.
/// </summary>
TinkeringStrength,
/// <summary>
/// Increases how much condition tinkered items lose when the character tinkers with them by a percentage.
/// </summary>
TinkeringDamage,
// Misc
/// <summary>
/// Increases how much reputation the character gains by a percentage.
/// Can be made to only affect certain factions with a given tag types by specifying a tag via CharacterAbilityGivePermanentStat, when no tag is specified the ability affects all factions.
/// </summary>
ReputationGainMultiplier,
/// <summary>
/// Increases how much reputation the character loses by a percentage.
/// Can be made to only affect certain factions with a given tag types by specifying a tag via CharacterAbilityGivePermanentStat, when no tag is specified the ability affects all factions.
/// </summary>
ReputationLossMultiplier,
/// <summary>
/// Increases how much money the character gains from missions by a percentage.
/// </summary>
MissionMoneyGainMultiplier,
/// <summary>
/// Increases how much talent experience the character gains from all sources by a percentage.
/// </summary>
ExperienceGainMultiplier,
/// <summary>
/// Increases how much talent experience the character gains from missions by a percentage.
/// </summary>
MissionExperienceGainMultiplier,
/// <summary>
/// Increases how many missions the characters crew can have at the same time by a flat amount.
/// </summary>
ExtraMissionCount,
/// <summary>
/// Increases how many items are in stock in special sales in the store by a flat amount.
/// </summary>
ExtraSpecialSalesCount,
/// <summary>
/// Increases how much money is gained from selling items to the store by a percentage.
/// </summary>
StoreSellMultiplier,
/// <summary>
/// Decreases the prices of items in affiliated store by a percentage.
/// </summary>
StoreBuyMultiplierAffiliated,
/// <summary>
/// Decreases the prices of items in all stores by a percentage.
/// </summary>
StoreBuyMultiplier,
/// <summary>
/// Decreases the price of upgrades and submarines in affiliated outposts by a percentage.
/// </summary>
ShipyardBuyMultiplierAffiliated,
/// <summary>
/// Decreases the price of upgrades and submarines in all outposts by a percentage.
/// </summary>
ShipyardBuyMultiplier,
/// <summary>
/// Limits how many of a certain item can be attached to the wall in the submarine at the same time.
/// Has to be used with CharacterAbilityGivePermanentStat to specify the tag of the item that is affected. Does nothing if no tag is specified.
/// </summary>
MaxAttachableCount,
/// <summary>
/// Increase the radius of explosions caused by the character by a percentage.
/// </summary>
ExplosionRadiusMultiplier,
/// <summary>
/// Increases the damage of explosions caused by the character by a percentage.
/// </summary>
ExplosionDamageMultiplier,
/// <summary>
/// Decreases the time it takes to fabricate items on fabricators operated by the character by a percentage.
/// </summary>
FabricationSpeed,
/// <summary>
/// Increases how much damage the character deals to ballast flora by a percentage.
/// </summary>
BallastFloraDamageMultiplier,
/// <summary>
/// Increases the time it takes for the character to pass out when out of oxygen.
/// </summary>
HoldBreathMultiplier,
/// <summary>
/// Used to set the character's apprencticeship to a certain job.
/// Used by the "apprenticeship" talent and requires a job to be specified via CharacterAbilityGivePermanentStat.
/// </summary>
Apprenticeship,
Affiliation,
/// <summary>
/// Increases the revival chance of the character when performing CPR by a percentage.
/// </summary>
CPRBoost,
/// <summary>
/// Can be used to prevent certain talents from being unlocked by specifying the talent's identifier via CharacterAbilityGivePermanentStat.
/// </summary>
LockedTalents
}
@@ -175,22 +477,77 @@ namespace Barotrauma
FabricationSpeed
}
/// <summary>
/// AbilityFlags are a set of toggleable flags that can be applied to characters.
/// </summary>
[Flags]
public enum AbilityFlags
{
/// <summary>
/// Used to indicate an erroneous ability flag. Should not be used.
/// </summary>
None = 0,
/// <summary>
/// Character will not be able to run.
/// </summary>
MustWalk = 0x1,
/// <summary>
/// Character is immune to pressure.
/// </summary>
ImmuneToPressure = 0x2,
/// <summary>
/// Character won't be targeted by enemy AI.
/// </summary>
IgnoredByEnemyAI = 0x4,
/// <summary>
/// Character can drag corpses without a movement speed penalty.
/// </summary>
MoveNormallyWhileDragging = 0x8,
/// <summary>
/// Character is able to tinker with items.
/// </summary>
CanTinker = 0x10,
/// <summary>
/// Character is able to tinker with fabricators and deconstructors.
/// </summary>
CanTinkerFabricatorsAndDeconstructors = 0x20,
/// <summary>
/// Allows items tinkered by the character to consume no power.
/// </summary>
TinkeringPowersDevices = 0x40,
/// <summary>
/// Allows the character to gain skills past 100.
/// </summary>
GainSkillPastMaximum = 0x80,
/// <summary>
/// Allows the character to retain experience when respawning as a new character.
/// </summary>
RetainExperienceForNewCharacter = 0x100,
/// <summary>
/// Allows CharacterAbilityApplyStatusEffectsToLastOrderedCharacter to affect the last 2 characters ordered.
/// </summary>
AllowSecondOrderedTarget = 0x200,
/// <summary>
/// Character will stay conscious even if their vitality drops below 0.
/// </summary>
AlwaysStayConscious = 0x400,
CanNotDieToAfflictions = 0x800,
/// <summary>
/// Prevents afflictions on the character from dropping the characters vitality below the kill threshold.
/// The character can still die from sources like getting crushed by pressure or if their head is severed.
/// </summary>
CanNotDieToAfflictions = 0x800
}
[Flags]
@@ -228,4 +585,4 @@ namespace Barotrauma
Local,
Radio
}
}
}
@@ -9,6 +9,8 @@ namespace Barotrauma
public event Action Finished;
protected bool isFinished;
public int RandomSeed;
protected readonly EventPrefab prefab;
public EventPrefab Prefab => prefab;
@@ -31,6 +31,8 @@ namespace Barotrauma
private bool isFinished;
private readonly Random random;
public MissionAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
if (MissionIdentifier.IsEmpty && MissionTag.IsEmpty)
@@ -42,6 +44,7 @@ namespace Barotrauma
DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": both MissionIdentifier or MissionTag have been configured. The tag will be ignored.");
}
LocationTypes = element.GetAttributeIdentifierArray("locationtype", Array.Empty<Identifier>()).ToImmutableArray();
random = new MTRandom(parentEvent.RandomSeed);
}
public override bool IsFinished(ref string goTo)
@@ -80,7 +83,7 @@ namespace Barotrauma
}
else if (!MissionTag.IsEmpty)
{
unlockedMission = unlockLocation.UnlockMissionByTag(MissionTag);
unlockedMission = unlockLocation.UnlockMissionByTag(MissionTag, random);
}
if (campaign is MultiPlayerCampaign mpCampaign)
{
@@ -31,7 +31,8 @@ namespace Barotrauma
if (Wait)
{
var gotoObjective = new AIObjectiveGoTo(npc, npc, humanAiController.ObjectiveManager, repeat: true)
var gotoObjective = new AIObjectiveGoTo(
AIObjectiveGoTo.GetTargetHull(npc) as ISpatialEntity ?? npc, npc, humanAiController.ObjectiveManager, repeat: true)
{
OverridePriority = 100.0f,
SourceEventAction = this
@@ -188,6 +188,10 @@ namespace Barotrauma
outPostInfo.AddOutpostNPCIdentifierOrTag(newCharacter, tag);
}
}
#if SERVER
newCharacter.LoadTalents();
GameMain.NetworkMember.CreateEntityEvent(newCharacter, new Character.UpdateTalentsEventData());
#endif
});
}
}
@@ -1,4 +1,5 @@
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
@@ -72,7 +73,7 @@ namespace Barotrauma
private readonly List<Event> activeEvents = new List<Event>();
private readonly HashSet<Identifier> finishedEvents = new HashSet<Identifier>();
private readonly HashSet<Event> finishedEvents = new HashSet<Event>();
private readonly HashSet<Identifier> nonRepeatableEvents = new HashSet<Identifier>();
private readonly HashSet<EventSet> usedUniqueSets = new HashSet<EventSet>();
@@ -123,7 +124,8 @@ namespace Barotrauma
public bool Enabled = true;
private MTRandom rand;
private MTRandom random;
private int randomSeed;
public void StartRound(Level level)
{
@@ -147,23 +149,22 @@ namespace Barotrauma
}
SelectSettings();
int seed = 0;
if (level != null)
{
seed = ToolBox.StringToInt(level.Seed);
randomSeed = ToolBox.StringToInt(level.Seed);
foreach (var previousEvent in level.LevelData.EventHistory)
{
seed ^= ToolBox.IdentifierToInt(previousEvent);
randomSeed ^= ToolBox.IdentifierToInt(previousEvent);
}
}
rand = new MTRandom(seed);
random = new MTRandom(randomSeed);
bool playingCampaign = GameMain.GameSession?.GameMode is CampaignMode;
EventSet initialEventSet = SelectRandomEvents(
EventSet.Prefabs.ToList(),
requireCampaignSet: playingCampaign,
random: rand);
random: random);
EventSet additiveSet = null;
if (initialEventSet != null && initialEventSet.Additive)
{
@@ -171,7 +172,7 @@ namespace Barotrauma
initialEventSet = SelectRandomEvents(
EventSet.Prefabs.Where(e => !e.Additive).ToList(),
requireCampaignSet: playingCampaign,
random: rand);
random: random);
}
if (initialEventSet != null)
{
@@ -366,20 +367,49 @@ namespace Barotrauma
/// <summary>
/// Registers the exhaustible events in the level as exhausted, and adds the current events to the event history
/// </summary>
public void RegisterEventHistory()
public void RegisterEventHistory(bool registerFinishedOnly = false)
{
if (level?.LevelData == null) { return; }
level.LevelData.EventsExhausted = !registerFinishedOnly;
if (level.LevelData.Type == LevelData.LevelType.Outpost)
{
level.LevelData.EventsExhausted = true;
level.LevelData.EventHistory.AddRange(selectedEvents.Values.SelectMany(v => v).Select(e => e.Prefab.Identifier).Where(e => !level.LevelData.EventHistory.Contains(e)));
if (registerFinishedOnly)
{
foreach (var finishedEvent in finishedEvents)
{
var key = finishedEvent.ParentSet;
if (key == null) { continue; }
if (level.LevelData.FinishedEvents.ContainsKey(key))
{
level.LevelData.FinishedEvents[key] += 1;
}
else
{
level.LevelData.FinishedEvents.Add(key, 1);
}
}
}
level.LevelData.EventHistory.AddRange(selectedEvents.Values
.SelectMany(v => v)
.Select(e => e.Prefab.Identifier)
.Where(eventId => Register(eventId) && !level.LevelData.EventHistory.Contains(eventId)));
if (level.LevelData.EventHistory.Count > MaxEventHistory)
{
level.LevelData.EventHistory.RemoveRange(0, level.LevelData.EventHistory.Count - MaxEventHistory);
}
}
level.LevelData.NonRepeatableEvents.AddRange(nonRepeatableEvents.Where(e => !level.LevelData.NonRepeatableEvents.Contains(e)));
level.LevelData.NonRepeatableEvents.AddRange(nonRepeatableEvents.Where(eventId => Register(eventId) && !level.LevelData.NonRepeatableEvents.Contains(eventId)));
if (!registerFinishedOnly)
{
level.LevelData.FinishedEvents.Clear();
}
bool Register(Identifier eventId) => !registerFinishedOnly || finishedEvents.Any(fe => fe.Prefab.Identifier == eventId);
}
public void SkipEventCooldown()
@@ -462,14 +492,14 @@ namespace Barotrauma
for (int j = 0; j < eventCount; j++)
{
if (unusedEvents.All(e => e.EventPrefabs.All(p => CalculateCommonness(p, e.Commonness) <= 0.0f))) { break; }
EventSet.SubEventPrefab subEventPrefab = ToolBox.SelectWeightedRandom(unusedEvents, e => e.EventPrefabs.Max(p => CalculateCommonness(p, e.Commonness)), rand);
EventSet.SubEventPrefab subEventPrefab = ToolBox.SelectWeightedRandom(unusedEvents, e => e.EventPrefabs.Max(p => CalculateCommonness(p, e.Commonness)), random);
(IEnumerable<EventPrefab> eventPrefabs, float commonness, float probability) = subEventPrefab;
if (eventPrefabs != null && rand.NextDouble() <= probability)
if (eventPrefabs != null && random.NextDouble() <= probability)
{
var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(isPrefabSuitable), e => e.Commonness, rand);
var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(isPrefabSuitable), e => e.Commonness, random);
var newEvent = eventPrefab.CreateInstance();
if (newEvent == null) { continue; }
newEvent.RandomSeed = randomSeed;
if (i < spawnPosFilter.Count) { newEvent.SpawnPosFilter = spawnPosFilter[i]; }
DebugConsole.NewMessage($"Initialized event {newEvent}", debugOnly: true);
if (!selectedEvents.ContainsKey(eventSet))
@@ -483,7 +513,7 @@ namespace Barotrauma
}
if (eventSet.ChildSets.Any())
{
var newEventSet = SelectRandomEvents(eventSet.ChildSets, random: rand);
var newEventSet = SelectRandomEvents(eventSet.ChildSets, random: random);
if (newEventSet != null)
{
CreateEvents(newEventSet);
@@ -494,9 +524,9 @@ namespace Barotrauma
{
foreach ((IEnumerable<EventPrefab> eventPrefabs, float commonness, float probability) in suitablePrefabSubsets)
{
if (rand.NextDouble() > probability) { continue; }
if (random.NextDouble() > probability) { continue; }
var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(isPrefabSuitable), e => e.Commonness, rand);
var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(isPrefabSuitable), e => e.Commonness, random);
var newEvent = eventPrefab.CreateInstance();
if (newEvent == null) { continue; }
if (!selectedEvents.ContainsKey(eventSet))
@@ -783,13 +813,13 @@ namespace Barotrauma
{
ev.Update(deltaTime);
}
else if (ev.Prefab != null && !finishedEvents.Contains(ev.Prefab.Identifier))
else if (ev.Prefab != null && !finishedEvents.Any(e => e.Prefab == ev.Prefab))
{
if (level?.LevelData != null && level.LevelData.Type == LevelData.LevelType.Outpost)
{
if (!level.LevelData.EventHistory.Contains(ev.Prefab.Identifier)) { level.LevelData.EventHistory.Add(ev.Prefab.Identifier); }
}
finishedEvents.Add(ev.Prefab.Identifier);
finishedEvents.Add(ev);
}
}
@@ -833,30 +863,44 @@ namespace Barotrauma
monsterStrength = 0;
foreach (Character character in Character.CharacterList)
{
if (character.IsIncapacitated || !character.Enabled || character.IsPet || CharacterParams.CompareGroup(CharacterPrefab.HumanSpeciesName, character.Group)) { continue; }
if (character.IsIncapacitated || character.IsArrested || !character.Enabled || character.IsPet) { continue; }
if (!(character.AIController is EnemyAIController enemyAI)) { continue; }
if (!enemyAI.AIParams.StayInAbyss)
if (character.AIController is EnemyAIController enemyAI)
{
// Ignore abyss monsters because they can stay active for quite great distances. They'll be taken into account when they target the sub.
monsterStrength += enemyAI.CombatStrength;
if (!enemyAI.AIParams.StayInAbyss)
{
// Ignore abyss monsters because they can stay active for quite great distances. They'll be taken into account when they target the sub.
monsterStrength += enemyAI.CombatStrength;
}
if (character.CurrentHull?.Submarine?.Info != null &&
(character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine)) &&
character.CurrentHull.Submarine.Info.Type == SubmarineType.Player)
{
// Enemy onboard -> Crawler inside the sub adds 0.2 to enemy danger, Mudraptor 0.42
enemyDanger += enemyAI.CombatStrength / 500.0f;
}
else if (enemyAI.SelectedAiTarget?.Entity?.Submarine != null)
{
// Enemy outside targeting the sub or something in it
// -> One Crawler adds 0.02, a Mudraptor 0.042, a Hammerhead 0.1, and a Moloch 0.25.
enemyDanger += enemyAI.CombatStrength / 5000.0f;
}
}
if (character.CurrentHull?.Submarine?.Info != null &&
(character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine)) &&
character.CurrentHull.Submarine.Info.Type == SubmarineType.Player)
else if (character.AIController is HumanAIController humanAi && !character.IsOnFriendlyTeam(CharacterTeamType.Team1))
{
// Enemy onboard -> Crawler inside the sub adds 0.2 to enemy danger, Mudraptor 0.42
enemyDanger += enemyAI.CombatStrength / 500.0f;
}
else if (enemyAI.SelectedAiTarget?.Entity?.Submarine != null)
{
// Enemy outside targeting the sub or something in it
// -> One Crawler adds 0.02, a Mudraptor 0.042, a Hammerhead 0.1, and a Moloch 0.25.
enemyDanger += enemyAI.CombatStrength / 5000.0f;
if (character.Submarine != null &&
character.Submarine.PhysicsBody is { BodyType: BodyType.Dynamic } &&
Vector2.DistanceSquared(character.Submarine.WorldPosition, Submarine.MainSub.WorldPosition) < Sonar.DefaultSonarRange * Sonar.DefaultSonarRange)
{
//we have no easy way to define the strength of a human enemy (depends more on the sub and it's state than the character),
//so let's just go with a fixed value.
//5 living enemy characters in an enemy sub in sonar range is enough to bump the intensity to max
enemyDanger += 0.2f;
}
}
}
// Add a portion of the total strength of active monsters to the enemy danger so that we don't spawn too many monsters around the sub.
// On top of the existing value, so if 10 crawlers are targeting the sub simultaneously from outside, the final value would be: 0.02 x 10 + 0.2 = 0.4.
// And if they get inside, we add 0.1 per crawler on that.
@@ -1,6 +1,7 @@
using System;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
namespace Barotrauma
{
@@ -396,8 +396,16 @@ namespace Barotrauma
public int GetEventCount(Level level)
{
if (level?.StartLocation == null || !overrideEventCount.TryGetValue(level.StartLocation.Type.Identifier, out int count)) { return eventCount; }
return count;
int finishedEventCount = 0;
if (level is not null)
{
level.LevelData.FinishedEvents.TryGetValue(this, out finishedEventCount);
}
if (level.StartLocation == null || !overrideEventCount.TryGetValue(level.StartLocation.Type.Identifier, out int count))
{
return eventCount - finishedEventCount;
}
return count - finishedEventCount;
}
public static List<string> GetDebugStatistics(int simulatedRoundCount = 100, Func<MonsterEvent, bool> filter = null, bool fullLog = false)
@@ -401,7 +401,7 @@ namespace Barotrauma
int reward = GetReward(sub);
IEnumerable<Character> crewCharacters = GameSession.GetSessionCrewCharacters(CharacterType.Both);
var missionMoneyGainMultiplier = new AbilityMissionMoneyGainMultiplier(this, 1f);
crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnGainMissionMoney, missionMoneyGainMultiplier));
CharacterTalent.CheckTalentsForCrew(crewCharacters, AbilityEffectType.OnGainMissionMoney, missionMoneyGainMultiplier);
crewCharacters.ForEach(c => missionMoneyGainMultiplier.Value += c.GetStatValue(StatTypes.MissionMoneyGainMultiplier));
finalReward = (int)(reward * missionMoneyGainMultiplier.Value);
}
@@ -380,11 +380,11 @@ namespace Barotrauma
{
if (fromType == "any" ||
fromType == from.Type.Identifier ||
(fromType == "anyoutpost" && from.HasOutpost()))
(fromType == "anyoutpost" && from.HasOutpost() && from.Type.Identifier != "abandoned"))
{
if (toType == "any" ||
toType == to.Type.Identifier ||
(toType == "anyoutpost" && to.HasOutpost()))
(toType == "anyoutpost" && to.HasOutpost() && to.Type.Identifier != "abandoned"))
{
return true;
}
@@ -260,9 +260,25 @@ namespace Barotrauma
int amount = Rand.Range(monster.Item2.X, monster.Item2.Y + 1);
for (int i = 0; i < amount; i++)
{
Character.Create(monster.Item1.Identifier, nestPosition + Rand.Vector(100.0f), ToolBox.RandomSeed(8), createNetworkEvent: true);
Vector2 offsetPosition;
int tries = 0;
do
{
offsetPosition = nestPosition + Rand.Vector(100.0f);
tries++;
if (tries > 10)
{
offsetPosition = nestPosition;
break;
}
} while (Level.Loaded.IsPositionInsideWall(offsetPosition));
Character.Create(monster.Item1.Identifier, offsetPosition, ToolBox.RandomSeed(8), createNetworkEvent: true);
}
}
if (Level.Loaded.IsPositionInsideWall(nestPosition))
{
DebugConsole.AddWarning($"Error in nest mission \"{Prefab.Identifier}\": nest position was inside a wall ({nestPosition}).");
}
monsterPrefabs.Clear();
break;
}
@@ -19,6 +19,8 @@ namespace Barotrauma
private float missionDifficulty;
private int alternateReward;
private Identifier factionIdentifier;
private Submarine enemySub;
private readonly List<Character> characters = new List<Character>();
private readonly Dictionary<Character, List<Item>> characterItems = new Dictionary<Character, List<Item>>();
@@ -140,8 +142,8 @@ namespace Barotrauma
missionDifficulty = level?.Difficulty ?? 0;
XElement submarineConfig = GetRandomDifficultyModifiedElement(submarineTypeConfig, missionDifficulty, ShipRandomnessModifier);
alternateReward = submarineConfig.GetAttributeInt("alternatereward", Reward);
factionIdentifier = submarineConfig.GetAttributeIdentifier("faction", Identifier.Empty);
string rewardText = $"‖color:gui.orange‖{string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:N0}", alternateReward)}‖end‖";
if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); }
@@ -170,11 +172,11 @@ namespace Barotrauma
submarineInfo = new SubmarineInfo(contentFile.Path.Value);
}
private float GetDifficultyModifiedValue(float preferredDifficulty, float levelDifficulty, float randomnessModifier, Random rand)
private static float GetDifficultyModifiedValue(float preferredDifficulty, float levelDifficulty, float randomnessModifier, Random rand)
{
return Math.Abs(levelDifficulty - preferredDifficulty + MathHelper.Lerp(-randomnessModifier, randomnessModifier, (float)rand.NextDouble()));
}
private int GetDifficultyModifiedAmount(int minAmount, int maxAmount, float levelDifficulty, Random rand)
private static int GetDifficultyModifiedAmount(int minAmount, int maxAmount, float levelDifficulty, Random rand)
{
return Math.Max((int)Math.Round(minAmount + (maxAmount - minAmount) * (levelDifficulty + MathHelper.Lerp(-RandomnessModifier, RandomnessModifier, (float)rand.NextDouble())) / MaxDifficulty), minAmount);
}
@@ -254,6 +256,7 @@ namespace Barotrauma
}
}
enemySub.ImmuneToBallastFlora = true;
enemySub.EnableFactionSpecificEntities(factionIdentifier);
}
private void InitPirates()
@@ -444,7 +447,7 @@ namespace Barotrauma
private bool CheckWinState() => !IsClient && characters.All(m => DeadOrCaptured(m));
private bool DeadOrCaptured(Character character)
private static bool DeadOrCaptured(Character character)
{
return character == null || character.Removed || character.Submarine == null || (character.LockHands && character.Submarine == Submarine.MainSub) || character.IsIncapacitated;
}
@@ -16,8 +16,8 @@ namespace Barotrauma
public Item Item;
/// <summary>
/// Note that the integer values matter here: the state of the target can't go back to a smaller value,
/// and a larger or equal value than the <see href="RequiredRetrievalState">RequiredRetrievalState</see> means the item counts as retrieved
/// Note that the integer values matter here:
/// a larger or equal value than the <see href="RequiredRetrievalState">RequiredRetrievalState</see> means the item counts as retrieved
/// (if the item needs to be picked up to be considered retrieved, it's also considered retrieved if it's in the sub)
/// </summary>
public enum RetrievalState
@@ -167,6 +167,8 @@ namespace Barotrauma
private readonly List<Target> targets = new List<Target>();
public bool AnyTargetNeedsToBeRetrievedToSub => targets.Any(t => t.RequiredRetrievalState == Target.RetrievalState.RetrievedToSub && !t.Retrieved);
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
{
get
@@ -382,29 +384,55 @@ namespace Barotrauma
switch (target.State)
{
case Target.RetrievalState.None:
if (target.Interacted)
{
TrySetRetrievalState(Target.RetrievalState.Interact);
}
var root = target.Item?.GetRootContainer() ?? target.Item;
if (root.ParentInventory?.Owner is Character character && character.TeamID == CharacterTeamType.Team1)
{
TrySetRetrievalState(Target.RetrievalState.PickedUp);
if (target.Interacted)
{
TrySetRetrievalState(Target.RetrievalState.Interact);
}
var root = target.Item?.GetRootContainer() ?? target.Item;
if (root.ParentInventory?.Owner is Character character && character.TeamID == CharacterTeamType.Team1)
{
TrySetRetrievalState(Target.RetrievalState.PickedUp);
}
}
break;
case Target.RetrievalState.PickedUp:
Submarine parentSub = target.Item.CurrentHull?.Submarine ?? target.Item.GetRootInventoryOwner()?.Submarine;
if (parentSub != null && parentSub.Info.Type == SubmarineType.Player)
case Target.RetrievalState.RetrievedToSub:
{
TrySetRetrievalState(Target.RetrievalState.RetrievedToSub);
Entity rootInventoryOwner = target.Item.GetRootInventoryOwner();
Submarine parentSub = target.Item.CurrentHull?.Submarine ?? rootInventoryOwner?.Submarine;
bool inPlayerSub = parentSub != null && parentSub.Info.Type == SubmarineType.Player;
bool inPlayerInventory = false;
bool playerInFriendlySub = false;
if (rootInventoryOwner is Character character && character.TeamID == CharacterTeamType.Team1)
{
inPlayerInventory = true;
if (character.Submarine != null)
{
playerInFriendlySub =
character.IsInFriendlySub ||
(character.Submarine == Level.Loaded?.StartOutpost && Level.IsLoadedFriendlyOutpost && GameMain.GameSession?.Campaign.CurrentLocation is not { IsFactionHostile: true });
}
}
if (inPlayerSub || (inPlayerInventory && playerInFriendlySub))
{
TrySetRetrievalState(Target.RetrievalState.RetrievedToSub);
}
else
{
target.State = Target.RetrievalState.PickedUp;
}
}
break;
}
void TrySetRetrievalState(Target.RetrievalState retrievalState)
{
if (retrievalState < target.State) { return; }
bool wasRetrieved = false;
if (retrievalState < target.State || target.State == retrievalState) { return; }
bool wasRetrieved = target.Retrieved;
target.State = retrievalState;
//increment the mission state if the target became retrieved
if (!wasRetrieved && target.Retrieved) { State = i + 1; }
@@ -244,7 +244,12 @@ namespace Barotrauma
float dist = Vector2.DistanceSquared(pos, refSub.WorldPosition);
foreach (Submarine sub in Submarine.Loaded)
{
if (sub.Info.Type != SubmarineType.Player && sub != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle) { continue; }
if (sub.Info.Type != SubmarineType.Player &&
sub.Info.Type != SubmarineType.EnemySubmarine &&
sub != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle)
{
continue;
}
float minDistToSub = GetMinDistanceToSub(sub);
if (dist < minDistToSub * minDistToSub) { continue; }
@@ -260,10 +260,12 @@ namespace Barotrauma
}
bool success = false;
bool isCampaign = GameMain.GameSession?.GameMode is CampaignMode;
float levelDifficulty = Level.Loaded?.Difficulty ?? 0.0f;
foreach (PreferredContainer preferredContainer in itemPrefab.PreferredContainers)
{
if (preferredContainer.CampaignOnly && !isCampaign) { continue; }
if (preferredContainer.NotCampaign && isCampaign) { continue; }
if (levelDifficulty < preferredContainer.MinLevelDifficulty || levelDifficulty > preferredContainer.MaxLevelDifficulty) { continue; }
if (preferredContainer.SpawnProbability <= 0.0f || preferredContainer.MaxAmount <= 0 && preferredContainer.Amount <= 0) { continue; }
validContainers = GetValidContainers(preferredContainer, containers, validContainers, primary: true);
if (validContainers.None())
@@ -420,20 +420,18 @@ namespace Barotrauma
{
List<Character> availableSpeakers = new List<Character>() { npc, player };
List<Identifier> dialogFlags = new List<Identifier>() { "OutpostNPC".ToIdentifier(), "EnterOutpost".ToIdentifier() };
if (npc.HumanPrefab != null)
{
foreach (var tag in npc.HumanPrefab.GetTags())
{
dialogFlags.Add(tag);
}
}
if (GameMain.GameSession?.GameMode is CampaignMode campaignMode)
{
if (campaignMode.Map?.CurrentLocation?.Type?.Identifier == "abandoned")
{
if (npc.TeamID == CharacterTeamType.None)
{
dialogFlags.Remove("OutpostNPC".ToIdentifier());
dialogFlags.Add("Bandit".ToIdentifier());
}
else if (npc.TeamID == CharacterTeamType.FriendlyNPC)
{
dialogFlags.Remove("OutpostNPC".ToIdentifier());
dialogFlags.Add("Hostage".ToIdentifier());
}
dialogFlags.Remove("OutpostNPC".ToIdentifier());
}
else if (campaignMode.Map?.CurrentLocation?.Reputation != null)
{
@@ -1,16 +1,26 @@
using Microsoft.Xna.Framework;
using System;
using System.Linq;
namespace Barotrauma
{
class Reputation
{
public const float HostileThreshold = 0.2f;
public const float ReputationLossPerNPCDamage = 0.05f;
public const float ReputationLossPerWallDamage = 0.05f;
public const float ReputationLossPerStolenItemPrice = 0.005f;
public const float MinReputationLossPerStolenItem = 0.05f;
public const float MaxReputationLossPerStolenItem = 1.0f;
public const float ReputationLossPerNPCDamage = 0.025f;
public const float ReputationLossPerWallDamage = 0.025f;
public const float ReputationLossPerStolenItemPrice = 0.0025f;
public const float MinReputationLossPerStolenItem = 0.025f;
public const float MaxReputationLossPerStolenItem = 0.5f;
/// <summary>
/// Maximum amount of reputation loss you can get from damaging outpost NPCs per round
/// </summary>
public const float MaxReputationLossFromNPCDamage = 20.0f;
/// <summary>
/// Maximum amount of reputation loss you can get from damaging outpost walls per round
/// </summary>
public const float MaxReputationLossFromWallDamage = 10.0f;
public Identifier Identifier { get; }
public int MinReputation { get; }
@@ -18,6 +28,8 @@ namespace Barotrauma
public int InitialReputation { get; }
public CampaignMetadata Metadata { get; }
public float ReputationAtRoundStart { get; set; }
private readonly Identifier metaDataIdentifier;
/// <summary>
@@ -60,32 +72,50 @@ namespace Barotrauma
public float GetReputationChangeMultiplier(float reputationChange)
{
if (reputationChange > 0f)
return reputationChange switch
{
float reputationGainMultiplier = 1f;
foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both))
{
reputationGainMultiplier *= 1f + character.GetStatValue(StatTypes.ReputationGainMultiplier, includeSaved: false);
reputationGainMultiplier *= 1f + character.Info?.GetSavedStatValue(StatTypes.ReputationGainMultiplier, Identifier) ?? 0;
}
return reputationGainMultiplier;
}
else if (reputationChange < 0f)
> 0f => GetMultiplierForStatType(StatTypes.ReputationGainMultiplier, Identifier),
< 0f => GetMultiplierForStatType(StatTypes.ReputationLossMultiplier, Identifier),
_ => 1.0f
};
static float GetMultiplierForStatType(StatTypes statTypes, Identifier identifier)
{
float reputationLossMultiplier = 1f;
foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both))
float multiplier = 1f;
var crew = GameSession.GetSessionCrewCharacters(CharacterType.Both);
if (crew != null && crew.Any())
{
reputationLossMultiplier *= 1f + character.GetStatValue(StatTypes.ReputationLossMultiplier, includeSaved: false);
reputationLossMultiplier *= 1f + character.Info?.GetSavedStatValue(StatTypes.ReputationLossMultiplier, Identifier) ?? 0;
multiplier *= 1f + crew.Max(c => c.GetStatValue(statTypes, includeSaved: false));
multiplier *= 1f + crew.Max(c => c.Info?.GetSavedStatValue(statTypes, identifier) ?? 0);
}
return reputationLossMultiplier;
return multiplier;
}
return 1.0f;
}
public void AddReputation(float reputationChange)
public void AddReputation(float reputationChange, float maxReputationChangePerRound = float.MaxValue)
{
Value += reputationChange * GetReputationChangeMultiplier(reputationChange);
float prevValue = Value;
//if we're already over the limit, do nothing (assuming the change is in the "same direction" that we've gone over the limit)
if (doesReputationChangeGoOverLimit(prevValue, reputationChange))
{
return;
}
float newValue = Value + reputationChange * GetReputationChangeMultiplier(reputationChange);
if (doesReputationChangeGoOverLimit(newValue, newValue - prevValue))
{
newValue = ReputationAtRoundStart + maxReputationChangePerRound * Math.Sign(reputationChange);
}
Value = newValue;
bool doesReputationChangeGoOverLimit(float newValue, float change)
{
float totalReputationChange = newValue - ReputationAtRoundStart;
return
Math.Abs(totalReputationChange) > maxReputationChangePerRound &&
Math.Sign(totalReputationChange) == Math.Sign(change);
}
}
public readonly NamedEvent<Reputation> OnReputationValueChanged = new NamedEvent<Reputation>();
@@ -114,6 +144,7 @@ namespace Barotrauma
metaDataIdentifier = $"reputation.{Identifier}".ToIdentifier();
MinReputation = minReputation;
MaxReputation = maxReputation;
ReputationAtRoundStart = initialReputation;
InitialReputation = initialReputation;
Faction = faction;
Location = location;
@@ -105,24 +105,30 @@ namespace Barotrauma
{
get
{
if (Map.CurrentLocation != null)
//map can be null if we're in the process of loading the save
if (Map != null)
{
foreach (Mission mission in map.CurrentLocation.SelectedMissions)
if (Map.CurrentLocation != null)
{
if (mission.Locations[0] == mission.Locations[1] ||
mission.Locations.Contains(Map.SelectedLocation))
foreach (Mission mission in map.CurrentLocation.SelectedMissions)
{
yield return mission;
if (mission.Locations[0] == mission.Locations[1] ||
mission.Locations.Contains(Map.SelectedLocation))
{
yield return mission;
}
}
}
}
foreach (Mission mission in extraMissions)
{
yield return mission;
foreach (Mission mission in extraMissions)
{
yield return mission;
}
}
}
}
public Location CurrentLocation => Map?.CurrentLocation;
public Wallet Bank;
public LevelData NextLevel
@@ -247,6 +253,11 @@ namespace Barotrauma
prevCampaignUIAutoOpenType = TransitionType.None;
#endif
foreach (var faction in factions)
{
faction.Reputation.ReputationAtRoundStart = faction.Reputation.Value;
}
if (PurchasedHullRepairsInLatestSave)
{
foreach (Structure wall in Structure.WallList)
@@ -907,6 +918,10 @@ namespace Barotrauma
{
location.TurnsInRadiation = 0;
}
foreach (var faction in Factions)
{
faction.Reputation.SetReputation(faction.Prefab.InitialReputation);
}
EndCampaignProjSpecific();
if (CampaignMetadata != null)
@@ -1154,15 +1169,12 @@ namespace Barotrauma
if (npc.Faction != null && Factions.FirstOrDefault(f => f.Prefab.Identifier == npc.Faction) is Faction faction)
{
faction.Reputation?.AddReputation(-attackResult.Damage * Reputation.ReputationLossPerNPCDamage);
faction.Reputation?.AddReputation(-attackResult.Damage * Reputation.ReputationLossPerNPCDamage, Reputation.MaxReputationLossFromNPCDamage);
}
else
{
Location location = Map?.CurrentLocation;
if (location != null)
{
location.Reputation?.AddReputation(-attackResult.Damage * Reputation.ReputationLossPerNPCDamage);
}
location?.Reputation?.AddReputation(-attackResult.Damage * Reputation.ReputationLossPerNPCDamage, Reputation.MaxReputationLossFromNPCDamage);
}
}
@@ -11,6 +11,9 @@ namespace Barotrauma
{
internal sealed partial class MedicalClinic
{
private const int RateLimitMaxRequests = 20,
RateLimitExpiry = 5;
public enum NetworkHeader
{
REQUEST_AFFLICTIONS,
@@ -24,7 +24,7 @@ namespace Barotrauma
}
public static readonly List<InvSlotType> anySlot = new List<InvSlotType>() { InvSlotType.Any };
public static readonly List<InvSlotType> AnySlot = new List<InvSlotType>() { InvSlotType.Any };
protected bool[] IsEquipped;
@@ -226,7 +226,7 @@ namespace Barotrauma
if (item.AllowedSlots.Contains(InvSlotType.Any))
{
var wearable = item.GetComponent<Wearable>();
if (wearable != null && !wearable.AutoEquipWhenFull && CheckIfAnySlotAvailable(item, false) == -1)
if (wearable != null && !wearable.AutoEquipWhenFull && !IsAnySlotAvailable(item))
{
return false;
}
@@ -336,7 +336,7 @@ namespace Barotrauma
//try to place the item in a LimbSlot.Any slot if that's allowed
if (allowedSlots.Contains(InvSlotType.Any) && item.AllowedSlots.Contains(InvSlotType.Any))
{
int freeIndex = CheckIfAnySlotAvailable(item, inWrongSlot);
int freeIndex = GetFreeAnySlot(item, inWrongSlot);
if (freeIndex > -1)
{
PutItem(item, freeIndex, user, true, createNetworkEvent);
@@ -393,7 +393,9 @@ namespace Barotrauma
return placedInSlot > -1;
}
public int CheckIfAnySlotAvailable(Item item, bool inWrongSlot)
public bool IsAnySlotAvailable(Item item) => GetFreeAnySlot(item, inWrongSlot: false) > -1;
private int GetFreeAnySlot(Item item, bool inWrongSlot)
{
//attempt to stack first
for (int i = 0; i < capacity; i++)
@@ -561,6 +561,12 @@ namespace Barotrauma.Items.Components
gap.ConnectedDoor = null;
}
}
if (OutsideSubmarineFixture != null)
{
OutsideSubmarineFixture.Body.Remove(OutsideSubmarineFixture);
OutsideSubmarineFixture = null;
}
//no need to remove the gap if we're unloading the whole submarine
//otherwise the gap will be removed twice and cause console warnings
@@ -600,7 +600,7 @@ namespace Barotrauma.Items.Components
public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null)
{
//no further data needed, the event just triggers the discharge
msg.WriteUInt16(user?.ID ?? Entity.NullEntityID);
}
}
}
@@ -700,7 +700,8 @@ namespace Barotrauma.Items.Components
}
Vector2 attachPos = GetAttachPosition(character, useWorldCoordinates: true);
Submarine attachSubmarine = Structure.GetAttachTarget(attachPos)?.Submarine ?? item.Submarine;
int maxAttachableCount = (int)character.Info.GetSavedStatValue(StatTypes.MaxAttachableCount, item.Prefab.Identifier);
int maxAttachableCount = (int)character.Info.GetSavedStatValueWithBotsInMp(StatTypes.MaxAttachableCount, item.Prefab.Identifier);
int currentlyAttachedCount = Item.ItemList.Count(
i => i.Submarine == attachSubmarine && i.GetComponent<Holdable>() is Holdable holdable && holdable.Attached && i.Prefab.Identifier == item.Prefab.Identifier);
if (maxAttachableCount == 0)
@@ -260,7 +260,7 @@ namespace Barotrauma.Items.Components
{
Vector2 barrelPos = TransformedBarrelPos + item.body.SimPosition;
float rotation = (Item.body.Dir == 1.0f) ? Item.body.Rotation : Item.body.Rotation - MathHelper.Pi;
float spread = GetSpread(character) * Projectile.GetSpreadFromPool(projectile.SpreadCounter);
float spread = GetSpread(character) * projectile.GetSpreadFromPool();
var lastProjectile = LastProjectile;
if (lastProjectile != projectile)
@@ -277,7 +277,7 @@ namespace Barotrauma.Items.Components
{
Item.body.ApplyLinearImpulse(new Vector2((float)Math.Cos(projectile.Item.body.Rotation), (float)Math.Sin(projectile.Item.body.Rotation)) * Item.body.Mass * -50.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
}
projectile.Item.body.ApplyTorque(projectile.Item.body.Mass * degreeOfFailure * 20.0f * Projectile.GetSpreadFromPool(projectile.SpreadCounter));
projectile.Item.body.ApplyTorque(projectile.Item.body.Mass * degreeOfFailure * 20.0f * projectile.GetSpreadFromPool());
}
Item.RemoveContained(projectile.Item);
}
@@ -84,6 +84,9 @@ namespace Barotrauma.Items.Components
[Serialize(false, IsPropertySaveable.No, description: "Can the item repair multiple things at once, or will it only affect the first thing the ray from the barrel hits.")]
public bool RepairMultiple { get; set; }
[Serialize(true, IsPropertySaveable.No, description: "Can the item repair multiple walls at once? Only relevant if RepairMultiple is true.")]
public bool RepairMultipleWalls { get; set; }
[Serialize(false, IsPropertySaveable.No, description: "Can the item repair things through holes in walls.")]
public bool RepairThroughHoles { get; set; }
@@ -383,6 +386,7 @@ namespace Barotrauma.Items.Components
//stop the ray if it already hit a door/wall and is now about to hit some other type of entity
if (lastHitType == typeof(Item) || lastHitType == typeof(Structure)) { break; }
}
if (!RepairMultipleWalls && (bodyType == typeof(Structure) || (body.UserData as Item)?.GetComponent<Door>() != null)) { break; }
Character hitCharacter = null;
if (body.UserData is Limb limb)
@@ -236,7 +236,7 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(0f, IsPropertySaveable.No, description: "How useful the item is in combat? Used by AI to decide which item it should use as a weapon. For the sake of clarity, use a value between 0 and 100 (not enforced). Note that there's also a generic BotPriority for all item prefabs.")]
[Serialize(0f, IsPropertySaveable.No, description: "How useful the item is in combat? Used by AI to decide which item it should use as a weapon. For the sake of clarity, use a value between 0 and 100 (not forced). Note that there's also a generic BotPriority for all item prefabs.")]
public float CombatPriority { get; private set; }
/// <summary>
@@ -687,9 +687,10 @@ namespace Barotrauma.Items.Components
public virtual void FlipY(bool relativeToSub) { }
public bool IsNotEmpty(Character user, bool checkContainedItems = true) =>
HasRequiredContainedItems(user, addMessage: false) &&
(!checkContainedItems || Item.OwnInventory == null || Item.OwnInventory.AllItems.Any(i => i.Condition > 0));
/// <summary>
/// Shorthand for !HasRequiredContainedItems()
/// </summary>
public bool IsEmpty(Character user) => !HasRequiredContainedItems(user, addMessage: false);
public bool HasRequiredContainedItems(Character user, bool addMessage, LocalizedString msg = null)
{
@@ -511,25 +511,28 @@ namespace Barotrauma.Items.Components
if (activeContainedItem.ExcludeFullCondition && contained.IsFullCondition) { continue; }
StatusEffect effect = activeContainedItem.StatusEffect;
targets.Clear();
bool wearing = item.GetComponent<Wearable>() is Wearable { IsActive: true };
if (effect.HasTargetType(StatusEffect.TargetType.This))
{
effect.Apply(ActionType.OnContaining, deltaTime, item, item.AllPropertyObjects);
targets.AddRange(item.AllPropertyObjects);
}
if (effect.HasTargetType(StatusEffect.TargetType.Contained))
{
effect.Apply(ActionType.OnContaining, deltaTime, item, contained.AllPropertyObjects);
targets.AddRange(contained.AllPropertyObjects);
}
if (effect.HasTargetType(StatusEffect.TargetType.Character) && item.ParentInventory?.Owner is Character character)
{
effect.Apply(ActionType.OnContaining, deltaTime, item, character);
targets.Add(character);
}
if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
targets.Clear();
effect.AddNearbyTargets(item.WorldPosition, targets);
effect.Apply(ActionType.OnActive, deltaTime, item, targets);
}
effect.Apply(ActionType.OnActive, deltaTime, item, targets);
effect.Apply(ActionType.OnContaining, deltaTime, item, targets);
if (wearing) { effect.Apply(ActionType.OnWearing, deltaTime, item, targets); }
}
}
@@ -267,7 +267,7 @@ namespace Barotrauma.Items.Components
itemList.Enabled = true;
if (amountInput != null)
{
amountInput.Enabled = true;
amountInput.Enabled = amountTextMax.Enabled;
}
RefreshActivateButtonText();
#endif
@@ -14,19 +14,18 @@ namespace Barotrauma.Items.Components
{
partial class Projectile : ItemComponent, IServerSerializable
{
const int SpreadCounterWrapAround = 256;
private static readonly ImmutableArray<float> spreadPool;
static Projectile()
{
MTRandom random = new MTRandom(0);
spreadPool = Enumerable.Range(0, SpreadCounterWrapAround).Select(f => (float)random.NextDouble() - 0.5f).ToImmutableArray();
spreadPool = Enumerable.Range(0, byte.MaxValue + 1).Select(f => (float)random.NextDouble() - 0.5f).ToImmutableArray();
}
public static float GetSpreadFromPool(int seed)
public static byte SpreadCounter { get; private set; }
public static void ResetSpreadCounter()
{
if (seed < 0) { seed = -seed; }
return spreadPool[seed % SpreadCounterWrapAround];
SpreadCounter = 0;
}
struct HitscanResult
@@ -63,7 +62,7 @@ namespace Barotrauma.Items.Components
private bool removePending;
public byte SpreadCounter { get; private set; }
private byte spreadIndex;
//continuous collision detection is used while the projectile is moving faster than this
const float ContinuousCollisionThreshold = 5.0f;
@@ -212,7 +211,7 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(false, IsPropertySaveable.No, description: "Override random spread with static spread; hitscan are launched with an equal amount of angle between them. Only applies when firing multiple hitscan.")]
[Serialize(false, IsPropertySaveable.No, description: "Override random spread with static spread; projectiles are launched with an equal amount of angle between them. Only applies when firing multiple projectiles.")]
public bool StaticSpread
{
get;
@@ -300,7 +299,8 @@ namespace Barotrauma.Items.Components
return;
}
SpreadCounter = (byte)(item.ID % SpreadCounterWrapAround);
spreadIndex = SpreadCounter;
SpreadCounter++;
InitProjSpecific(element);
}
@@ -329,6 +329,12 @@ namespace Barotrauma.Items.Components
originalCollisionTargets = item.body.CollidesWith;
}
public float GetSpreadFromPool()
{
spreadIndex = (byte)MathUtils.PositiveModulo(spreadIndex, spreadPool.Length);
return spreadPool[spreadIndex];
}
private void Launch(Character user, Vector2 simPosition, float rotation, float damageMultiplier = 1f, float launchImpulseModifier = 0f)
{
if (Item.body == null) { return; }
@@ -380,8 +386,8 @@ namespace Barotrauma.Items.Components
if (createNetworkEvent && !Item.Removed && GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
#if SERVER
launchRot = rotation;
Item.CreateServerEvent(this, new EventData(launch: true, spreadCounter: (byte)(SpreadCounter - 1)));
launchRot = rotation;
Item.CreateServerEvent(this, new EventData(launch: true, spreadCounter: (byte)(spreadIndex - 1)));
#endif
}
}
@@ -390,24 +396,22 @@ namespace Barotrauma.Items.Components
{
if (character != null && !characterUsable) { return false; }
if (item.body == null) { return false; }
//can't launch if already launched
if (StickTarget != null || IsActive) { return false; }
float initialRotation = item.body.Rotation;
for (int i = 0; i < HitScanCount; i++)
{
float launchAngle;
if (StaticSpread)
{
float staticSpread = Spread / (HitScanCount - 1);
// because the position of the item changes as hitscan are fired, we will set an
// initial offset on the first hitscan and then increase the item's angle by a set amount as hitscan are fired
float offset = i == 0 ? -staticSpread * (HitScanCount -1) : 0f;
launchAngle = item.body.Rotation + MathHelper.ToRadians(staticSpread + offset);
launchAngle = initialRotation + MathHelper.ToRadians(i - ((float)(HitScanCount - 1) / 2)) * Spread;
}
else
{
launchAngle = item.body.Rotation + MathHelper.ToRadians(Spread * GetSpreadFromPool(SpreadCounter));
launchAngle = initialRotation + MathHelper.ToRadians(Spread * GetSpreadFromPool());
}
SpreadCounter++;
spreadIndex++;
Vector2 launchDir = new Vector2((float)Math.Cos(launchAngle), (float)Math.Sin(launchAngle));
if (Hitscan)
@@ -243,14 +243,7 @@ namespace Barotrauma.Items.Components
for (int i = 0; i < labels.Length; i++)
{
labels[i] = i < newLabels.Length ? newLabels[i] : customInterfaceElementList[i].Label;
if (Screen.Selected != GameMain.SubEditorScreen)
{
customInterfaceElementList[i].Label = TextManager.Get(labels[i]).Fallback(labels[i]).Value;
}
else
{
customInterfaceElementList[i].Label = labels[i];
}
customInterfaceElementList[i].Label = labels[i];
}
UpdateLabelsProjSpecific();
}
@@ -230,7 +230,7 @@ namespace Barotrauma.Items.Components
Position = item.Position,
CastShadows = castShadows,
IsBackground = drawBehindSubs,
SpriteScale = Vector2.One * item.Scale,
SpriteScale = Vector2.One * item.Scale * LightSpriteScale,
Range = range
};
Light.LightSourceParams.Flicker = flicker;
@@ -309,13 +309,14 @@ namespace Barotrauma.Items.Components
#if CLIENT
Light.ParentSub = item.Submarine;
#endif
if (item.Container != null && item.GetRootInventoryOwner() is not Character)
var ownerCharacter = item.GetRootInventoryOwner() as Character;
if ((item.Container != null && ownerCharacter == null) ||
(ownerCharacter != null && ownerCharacter.InvisibleTimer > 0.0f))
{
lightBrightness = 0.0f;
SetLightSourceState(false, 0.0f);
return;
}
SetLightSourceTransformProjSpecific();
PhysicsBody body = ParentBody ?? item.body;
@@ -56,11 +56,9 @@ namespace Barotrauma.Items.Components
private float resetUserTimer;
private float aiTargetingGraceTimer;
private float aiFindTargetTimer;
private ISpatialEntity currentTarget;
private const float CrewAiFindTargetMaxInterval = 3.0f;
private const float CrewAiFindTargetMaxInterval = 1.0f;
private const float CrewAIFindTargetMinInverval = 0.2f;
private int currentLoaderIndex;
@@ -299,7 +297,7 @@ namespace Barotrauma.Items.Components
}
}
[Serialize(3000.0f, IsPropertySaveable.Yes, description: "How close to a target the turret has to be for an AI character to fire it.")]
[Serialize(3500.0f, IsPropertySaveable.Yes, description: "How close to a target the turret has to be for an AI character to fire it.")]
public float AIRange
{
get;
@@ -422,10 +420,7 @@ namespace Barotrauma.Items.Components
// Only make the Turret control the LightComponents that are it's children. So it'd be possible to for example have some extra lights on the turret that don't rotate with it.
if (lc?.Parent == this)
{
if (lightComponents == null)
{
lightComponents = new List<LightComponent>();
}
lightComponents ??= new List<LightComponent>();
lightComponents.Add(lc);
}
}
@@ -439,6 +434,8 @@ namespace Barotrauma.Items.Components
light.Parent = null;
light.Rotation = Rotation - item.RotationRad;
light.Light.Rotation = -rotation;
//turret lights are high-prio (don't want the lights to disappear when you're fighting something)
light.Light.PriorityMultiplier *= 10.0f;
}
}
#endif
@@ -590,10 +587,6 @@ namespace Barotrauma.Items.Components
angularVelocity *= -0.5f;
}
if (aiTargetingGraceTimer > 0f)
{
aiTargetingGraceTimer -= deltaTime;
}
if (aiFindTargetTimer > 0.0f)
{
aiFindTargetTimer -= deltaTime;
@@ -620,10 +613,18 @@ namespace Barotrauma.Items.Components
partial void UpdateProjSpecific(float deltaTime);
private bool isUseBeingCalled;
public override bool Use(float deltaTime, Character character = null)
{
if (!characterUsable && character != null) { return false; }
return TryLaunch(deltaTime, character);
//prevent an infinite loop if launching triggers a StatusEffect that Uses this item
if (isUseBeingCalled) { return false; }
isUseBeingCalled = true;
bool wasSuccessful = TryLaunch(deltaTime, character);
isUseBeingCalled = false;
return wasSuccessful;
}
public float GetPowerRequiredToShoot()
@@ -1015,6 +1016,7 @@ namespace Barotrauma.Items.Components
float dist = Vector2.DistanceSquared(item.WorldPosition, targetItem.WorldPosition);
if (dist > closestDist) { continue; }
if (dist > shootDistance * shootDistance) { continue; }
if (!IsTargetItemCloseEnough(targetItem, dist)) { continue; }
if (!CheckTurretAngle(targetItem.WorldPosition)) { continue; }
target = targetItem;
closestDist = dist / priority;
@@ -1130,9 +1132,12 @@ namespace Barotrauma.Items.Components
{
if (character.AIController.SelectedAiTarget?.Entity is Character previousTarget && previousTarget.IsDead)
{
character.Speak(TextManager.Get("DialogTurretTargetDead").Value,
identifier: $"killedtarget{previousTarget.ID}".ToIdentifier(),
minDurationBetweenSimilar: 10.0f);
if (previousTarget.LastAttacker == null || previousTarget.LastAttacker == character)
{
character.Speak(TextManager.Get("DialogTurretTargetDead").Value,
identifier: $"killedtarget{previousTarget.ID}".ToIdentifier(),
minDurationBetweenSimilar: 5.0f);
}
character.AIController.SelectTarget(null);
}
@@ -1265,18 +1270,27 @@ namespace Barotrauma.Items.Components
Vector2? targetPos = null;
float maxDistance = 10000;
float shootDistance = AIRange * item.OffsetOnSelectedMultiplier;
// use full range only if we're actively firing
if (aiTargetingGraceTimer <= 0f)
{
shootDistance *= 0.75f;
}
float closestDistance = maxDistance * maxDistance;
bool hadCurrentTarget = currentTarget != null;
if (hadCurrentTarget)
{
if (!IsValidTarget(currentTarget))
bool isValidTarget = IsValidTarget(currentTarget);
if (isValidTarget)
{
float dist = Vector2.DistanceSquared(item.WorldPosition, currentTarget.WorldPosition);
if (dist > closestDistance)
{
isValidTarget = false;
}
else if (currentTarget is Item targetItem)
{
if (!IsTargetItemCloseEnough(targetItem, dist))
{
isValidTarget = false;
}
}
}
if (!isValidTarget)
{
currentTarget = null;
aiFindTargetTimer = CrewAIFindTargetMinInverval;
@@ -1292,7 +1306,11 @@ namespace Barotrauma.Items.Components
if (character.Submarine != null)
{
if (enemy.Submarine == character.Submarine) { continue; }
if (enemy.Submarine != null && enemy.Submarine.TeamID == character.Submarine.TeamID) { continue; }
if (enemy.Submarine != null)
{
if (enemy.Submarine.TeamID == character.Submarine.TeamID) { continue; }
if (enemy.Submarine.Info.IsOutpost) { continue; }
}
}
// Don't aim monsters that are inside any submarine.
if (!enemy.IsHuman && enemy.CurrentHull != null) { continue; }
@@ -1318,6 +1336,7 @@ namespace Barotrauma.Items.Components
float dist = Vector2.DistanceSquared(item.WorldPosition, targetItem.WorldPosition);
if (dist > closestDistance) { continue; }
if (dist > shootDistance * shootDistance) { continue; }
if (!IsTargetItemCloseEnough(targetItem, dist)) { continue; }
if (!CheckTurretAngle(targetItem.WorldPosition)) { continue; }
targetPos = targetItem.WorldPosition;
closestDistance = dist / priority;
@@ -1325,14 +1344,7 @@ namespace Barotrauma.Items.Components
closestEnemy = null;
currentTarget = targetItem;
}
if (currentTarget == null)
{
aiFindTargetTimer = CrewAIFindTargetMinInverval;
}
else
{
aiFindTargetTimer = CrewAiFindTargetMaxInterval;
}
aiFindTargetTimer = currentTarget == null ? CrewAiFindTargetMaxInterval : CrewAIFindTargetMinInverval;
}
else if (currentTarget != null)
{
@@ -1346,6 +1358,10 @@ namespace Barotrauma.Items.Components
if (targetCharacter.Submarine != null && targetCharacter.CurrentHull != null && targetCharacter.Submarine != item.Submarine && !targetCharacter.CanSeeTarget(Item))
{
targetPos = targetCharacter.CurrentHull.WorldPosition;
if (closestDistance > maxDistance * maxDistance)
{
ResetTarget();
}
}
else
{
@@ -1365,12 +1381,17 @@ namespace Barotrauma.Items.Components
}
if (closestDist > shootDistance * shootDistance)
{
// Not close enough to shoot.
currentTarget = null;
closestEnemy = null;
targetPos = null;
aiFindTargetTimer = CrewAIFindTargetMinInverval;
ResetTarget();
}
}
void ResetTarget()
{
// Not close enough to shoot.
currentTarget = null;
closestEnemy = null;
targetPos = null;
}
}
else if (targetPos == null && item.Submarine != null && Level.Loaded != null)
{
@@ -1520,10 +1541,11 @@ namespace Barotrauma.Items.Components
}
character.SetInput(InputType.Shoot, true, true);
}
aiTargetingGraceTimer = 5f;
return false;
}
private bool IsTargetItemCloseEnough(Item target, float sqrDist) => float.IsPositiveInfinity(target.Prefab.AITurretTargetingMaxDistance) || sqrDist < MathUtils.Pow2(target.Prefab.AITurretTargetingMaxDistance);
/// <summary>
/// Turret doesn't consume grid power, directly takes from the batteries on its grid instead.
/// </summary>
@@ -1553,6 +1575,10 @@ namespace Barotrauma.Items.Components
{
return false;
}
if (targetItem.ParentInventory != null)
{
return false;
}
}
return true;
}
@@ -1568,6 +1594,7 @@ namespace Barotrauma.Items.Components
{
if (item.Submarine != null)
{
if (item.Submarine.Info.IsOutpost) { return false; }
// Check that the target is not in the friendly team, e.g. pirate or a hostile player sub (PvP).
return !target.IsOnFriendlyTeam(item.Submarine.TeamID) && TargetHumans;
}
@@ -1660,7 +1687,7 @@ namespace Barotrauma.Items.Components
return angle >= minRotation && angle <= maxRotation;
}
private bool CheckTurretAngle(Vector2 target) => CheckTurretAngle(-MathUtils.VectorToAngle(target - item.WorldPosition));
public bool CheckTurretAngle(Vector2 target) => CheckTurretAngle(-MathUtils.VectorToAngle(target - item.WorldPosition));
protected override void RemoveComponentSpecific()
{
@@ -288,7 +288,7 @@ namespace Barotrauma.Items.Components
public bool AutoEquipWhenFull { get; private set; }
public bool DisplayContainedStatus { get; private set; }
[Serialize(false, IsPropertySaveable.No, description: "Can the item be used (assuming it has components that are usable in some way) when worn."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)]
[Serialize(false, IsPropertySaveable.No, description: "Can the item be used (assuming it has components that are usable in some way) when worn.")]
public bool AllowUseWhenWorn { get; set; }
public readonly int Variants;
@@ -742,13 +742,13 @@ namespace Barotrauma
stackedItems.Distinct().All(stackedItem => TryPutItem(stackedItem, index, false, false, user, createNetworkEvent))
&&
(existingItems.All(existingItem => otherInventory.TryPutItem(existingItem, otherIndex, false, false, user, createNetworkEvent)) ||
existingItems.Count == 1 && otherInventory.TryPutItem(existingItems.First(), user, CharacterInventory.anySlot, createNetworkEvent));
existingItems.Count == 1 && otherInventory.TryPutItem(existingItems.First(), user, CharacterInventory.AnySlot, createNetworkEvent));
}
else
{
swapSuccessful =
(existingItems.All(existingItem => otherInventory.TryPutItem(existingItem, otherIndex, false, false, user, createNetworkEvent)) ||
existingItems.Count == 1 && otherInventory.TryPutItem(existingItems.First(), user, CharacterInventory.anySlot, createNetworkEvent))
existingItems.Count == 1 && otherInventory.TryPutItem(existingItems.First(), user, CharacterInventory.AnySlot, createNetworkEvent))
&&
stackedItems.Distinct().All(stackedItem => TryPutItem(stackedItem, index, false, false, user, createNetworkEvent));
@@ -651,11 +651,13 @@ namespace Barotrauma
}
}
[Serialize(true, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
public bool AllowStealing
private bool allowStealing;
[Serialize(true, IsPropertySaveable.Yes, alwaysUseInstanceValues: true,
description: $"Determined by where/how the item originally spawned. If ItemPrefab.AllowStealing is true, stealing the item is always allowed.")]
public bool AllowStealing
{
get;
set;
get { return allowStealing || Prefab.AllowStealingAlways; }
set { allowStealing = value; }
}
private string originalOutpost;
@@ -1585,6 +1587,17 @@ namespace Barotrauma
return tags.Contains(tag) || base.Prefab.Tags.Contains(tag);
}
public bool HasIdentifierOrTags(IEnumerable<Identifier> identifiersOrTags)
{
if (identifiersOrTags == null) { return false; }
if (identifiersOrTags.Contains(Prefab.Identifier)) { return true; }
foreach (Identifier tag in identifiersOrTags)
{
if (HasTag(tag)) { return true; }
}
return false;
}
public void ReplaceTag(string tag, string newTag)
{
ReplaceTag(tag.ToIdentifier(), newTag.ToIdentifier());
@@ -1756,7 +1769,7 @@ namespace Barotrauma
return new AttackResult(damageAmount, null);
}
private void SetCondition(float value, bool isNetworkEvent)
private void SetCondition(float value, bool isNetworkEvent, bool executeEffects = true)
{
if (!isNetworkEvent)
{
@@ -1779,16 +1792,22 @@ namespace Barotrauma
//Flag connections to be updated as device is broken
flagChangedConnections(connections);
#if CLIENT
foreach (ItemComponent ic in components)
{
ic.PlaySound(ActionType.OnBroken);
ic.StopSounds(ActionType.OnActive);
if (executeEffects)
{
foreach (ItemComponent ic in components)
{
ic.PlaySound(ActionType.OnBroken);
ic.StopSounds(ActionType.OnActive);
}
}
if (Screen.Selected == GameMain.SubEditorScreen) { return; }
#endif
// Have to set the previous condition here or OnBroken status effects that reduce the condition will keep triggering the status effects, resulting in a stack overflow.
SetPreviousCondition();
ApplyStatusEffects(ActionType.OnBroken, 1.0f, null);
if (executeEffects)
{
ApplyStatusEffects(ActionType.OnBroken, 1.0f, null);
}
}
else if (condition > 0.0f && prevCondition <= 0.0f)
{
@@ -1872,15 +1891,15 @@ namespace Barotrauma
if (!(GameMain.NetworkMember is { IsServer: true })) { return; }
if (!conditionUpdatePending) { return; }
CreateStatusEvent();
CreateStatusEvent(loadingRound: false);
lastSentCondition = condition;
sendConditionUpdateTimer = NetConfig.ItemConditionUpdateInterval;
conditionUpdatePending = false;
}
public void CreateStatusEvent()
public void CreateStatusEvent(bool loadingRound)
{
GameMain.NetworkMember.CreateEntityEvent(this, new ItemStatusEventData());
GameMain.NetworkMember.CreateEntityEvent(this, new ItemStatusEventData(loadingRound));
}
private bool isActive = true;
@@ -71,9 +71,9 @@ namespace Barotrauma
{
public EventType EventType => EventType.ItemStat;
public readonly Dictionary<ItemStatManager.TalentStatIdentifier, float> Stats;
public readonly Dictionary<TalentStatIdentifier, float> Stats;
public SetItemStatEventData(Dictionary<ItemStatManager.TalentStatIdentifier, float> stats)
public SetItemStatEventData(Dictionary<TalentStatIdentifier, float> stats)
{
Stats = stats;
}
@@ -82,6 +82,13 @@ namespace Barotrauma
private readonly struct ItemStatusEventData : IEventData
{
public EventType EventType => EventType.Status;
public readonly bool LoadingRound;
public ItemStatusEventData(bool loadingRound)
{
LoadingRound = loadingRound;
}
}
private readonly struct AssignCampaignInteractionEventData : IEventData
@@ -332,6 +332,8 @@ namespace Barotrauma
public readonly bool TransferOnlyOnePerContainer;
public readonly bool AllowTransfersHere = true;
public readonly float MinLevelDifficulty, MaxLevelDifficulty;
public PreferredContainer(XElement element)
{
Primary = XMLExtensions.GetAttributeIdentifierArray(element, "primary", Array.Empty<Identifier>()).ToImmutableHashSet();
@@ -347,6 +349,9 @@ namespace Barotrauma
TransferOnlyOnePerContainer = element.GetAttributeBool("TransferOnlyOnePerContainer", TransferOnlyOnePerContainer);
AllowTransfersHere = element.GetAttributeBool("AllowTransfersHere", AllowTransfersHere);
MinLevelDifficulty = element.GetAttributeFloat(nameof(MinLevelDifficulty), float.MinValue);
MaxLevelDifficulty = element.GetAttributeFloat(nameof(MaxLevelDifficulty), float.MaxValue);
if (element.GetAttribute("spawnprobability") == null)
{
//if spawn probability is not defined but amount is, assume the probability is 1
@@ -670,6 +675,9 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.No)]
public bool AllowSellingWhenBroken { get; private set; }
[Serialize(false, IsPropertySaveable.No)]
public bool AllowStealingAlways { get; private set; }
[Serialize(false, IsPropertySaveable.No)]
public bool Indestructible { get; private set; }
@@ -806,6 +814,9 @@ namespace Barotrauma
[Serialize(1.0f, IsPropertySaveable.No, description: "How much the bots prioritize shooting this item with slow turrets, like railguns? Defaults to 1. Not used if AITurretPriority is 0. Distance to the target affects the decision making.")]
public float AISlowTurretPriority { get; private set; }
[Serialize(float.PositiveInfinity, IsPropertySaveable.No, description: "The max distance at which the bots are allowed to target the items. Defaults to infinity.")]
public float AITurretTargetingMaxDistance { get; private set; }
protected override Identifier DetermineIdentifier(XElement element)
{
Identifier identifier = base.DetermineIdentifier(element);
@@ -5,29 +5,48 @@ using System.Collections.Generic;
namespace Barotrauma
{
[NetworkSerialize]
internal readonly record struct TalentStatIdentifier(ItemTalentStats Stat, Identifier TalentIdentifier, Option<UInt32> UniqueCharacterId) : INetSerializableStruct
{
/// <summary>
/// Stackable identifiers feature a unique ID to allow multiple stats applied by the same talent from different characters to coexist.
/// </summary>
public static TalentStatIdentifier CreateStackable(ItemTalentStats stat, Identifier talentIdentifier, UInt32 characterId)
=> new(stat, talentIdentifier, Option<UInt32>.Some(characterId));
/// <summary>
/// Unstackable identifiers do not have a unique ID causing them to be identical to other stats applied by the same talent from different characters and thus only one of them will be applied.
/// <see cref="ItemStatManager.ApplyStat"/> will always use the highest value for unstackable stats.
/// </summary>
public static TalentStatIdentifier CreateUnstackable(ItemTalentStats stat, Identifier talentIdentifier)
=> new(stat, talentIdentifier, Option.None);
}
internal sealed class ItemStatManager
{
private Item item;
public ItemStatManager(Item item)
{
this.item = item;
}
[NetworkSerialize]
public readonly record struct TalentStatIdentifier(ItemTalentStats Stat, Identifier TalentIdentifier, UInt32 CharacterID) : INetSerializableStruct;
private readonly Dictionary<TalentStatIdentifier, float> talentStats = new();
private readonly Item item;
public void ApplyStat(ItemTalentStats stat, float value, CharacterTalent talent)
public ItemStatManager(Item item) => this.item = item;
public void ApplyStat(ItemTalentStats stat, bool stackable, float value, CharacterTalent talent)
{
if (talent.Character?.ID is not { } characterId ||
talent.Prefab?.Identifier is not { } talentIdentifier)
talent.Prefab?.Identifier is not { } talentIdentifier) { return; }
var identifier = stackable
? TalentStatIdentifier.CreateStackable(stat, talentIdentifier, characterId)
: TalentStatIdentifier.CreateUnstackable(stat, talentIdentifier);
if (!stackable)
{
return;
if (talentStats.TryGetValue(identifier, out float existingValue))
{
// Always use the highest value for non-stackable stats
if (existingValue > value) { return; }
}
}
TalentStatIdentifier identifier = new TalentStatIdentifier(stat, talentIdentifier, characterId);
talentStats[identifier] = value;
#if SERVER
@@ -38,21 +57,19 @@ namespace Barotrauma
#endif
}
// Used for getting the value value from network packet
public void ApplyStat(TalentStatIdentifier identifier, float value)
{
talentStats[identifier] = value;
}
/// <summary>
/// Used for setting the value value from network packet; bypassing all validity checks.
/// </summary>
public void ApplyStatDirect(TalentStatIdentifier identifier, float value) => talentStats[identifier] = value;
public float GetAdjustedValue(ItemTalentStats stat, float originalValue)
{
float total = originalValue;
foreach (var (key, value) in talentStats)
{
if (key.Stat == stat)
{
total *= value;
}
if (key.Stat != stat) { continue; }
total *= value;
}
return total;
@@ -8,44 +8,85 @@ using Barotrauma.Extensions;
namespace Barotrauma
{
/// <summary>
/// Used by various features to define different kinds of relations between items:
/// for example, which item a character must have equipped to interact with some item in some way,
/// which items can go inside a container, or which kind of item the target of a status effect must have for the effect to execute.
/// </summary>
class RelatedItem
{
public enum RelationType
{
None,
/// <summary>
/// The item must be contained inside the item this relation is defined in.
/// Can for example by used to make an item usable only when there's a specific kind of item inside it.
/// </summary>
Contained,
/// <summary>
/// The user must have equipped the item (i.e. held or worn).
/// </summary>
Equipped,
/// <summary>
/// The user must have picked up the item (i.e. the item needs to be in the user's inventory).
/// </summary>
Picked,
/// <summary>
/// The item this relation is defined in must be inside a specific kind of container.
/// Can for example by used to make an item do something when it's inside some other type of item.
/// </summary>
Container
}
public bool IsOptional { get; set; }
/// <summary>
/// Should an empty inventory be considered valid? Can be used to, for example, make an item do something if there's a specific item, or nothing, inside it.
/// </summary>
public bool MatchOnEmpty { get; set; }
/// <summary>
/// Should only an empty inventory be considered valid? Can be used to, for example, make an item do something when there's nothing inside it.
/// </summary>
public bool RequireEmpty { get; set; }
/// <summary>
/// Only valid for the RequiredItems of an ItemComponent. Can be used to ignore the requirement in the submarine editor,
/// making it easier to for example make rewire things that require some special tool to rewire.
/// </summary>
public bool IgnoreInEditor { get; set; }
/// <summary>
/// Identifier(s) or tag(s) of the items that are NOT considered valid.
/// Can be used to, for example, exclude some specific items when using tags that apply to multiple items.
/// </summary>
public ImmutableHashSet<Identifier> ExcludedIdentifiers { get; private set; }
private RelationType type;
public List<StatusEffect> statusEffects;
/// <summary>
/// Only valid for the RequiredItems of an ItemComponent. A message displayed if the required item isn't found (e.g. a notification about lack of ammo or fuel).
/// </summary>
public LocalizedString Msg;
/// <summary>
/// Only valid for the RequiredItems of an ItemComponent. The localization tag of a message displayed if the required item isn't found (e.g. a notification about lack of ammo or fuel).
/// </summary>
public Identifier MsgTag;
/// <summary>
/// Should broken (0 condition) items be excluded
/// Should broken (0 condition) items be excluded?
/// </summary>
public bool ExcludeBroken { get; private set; }
/// <summary>
/// Should full condition (100%) items be excluded
/// Should full condition (100%) items be excluded?
/// </summary>
public bool ExcludeFullCondition { get; private set; }
/// <summary>
/// Are item variants considered valid?
/// </summary>
public bool AllowVariants { get; private set; } = true;
public RelationType Type
@@ -59,19 +100,34 @@ namespace Barotrauma
public int TargetSlot = -1;
/// <summary>
/// Overrides the position defined in ItemContainer.
/// Overrides the position defined in ItemContainer. Only valid when used in the Containable definitions of an ItemContainer.
/// </summary>
public Vector2? ItemPos;
/// <summary>
/// Only affects when ItemContainer.hideItems is false. Doesn't override the value.
/// Only valid when used in the Containable definitions of an ItemContainer.
/// Only affects when ItemContainer.hideItems is false. Doesn't override the value.
/// </summary>
public bool Hide;
/// <summary>
/// Only valid when used in the Containable definitions of an ItemContainer.
/// Can be used to override the rotation of specific items in the container.
/// </summary>
public float Rotation;
/// <summary>
/// Only valid when used in the Containable definitions of an ItemContainer.
/// Can be used to force specific items to stay active inside the container (such as flashlights attached to a gun).
/// </summary>
public bool SetActive;
/// <summary>
/// Only valid for the RequiredItems of an ItemComponent. Can be used to make the requirement optional,
/// meaning that you don't need to have the item to interact with something, but having it may still affect what the interaction does (such as using a crowbar on a door).
/// </summary>
public bool IsOptional { get; set; }
public string JoinedIdentifiers
{
get { return string.Join(",", Identifiers); }
@@ -83,6 +139,9 @@ namespace Barotrauma
}
}
/// <summary>
/// Identifier(s) or tag(s) of the items that are considered valid.
/// </summary>
public ImmutableHashSet<Identifier> Identifiers { get; private set; }
public string JoinedExcludedIdentifiers
@@ -290,6 +290,7 @@ namespace Barotrauma
dictionary.Clear();
Hull.EntityGrids.Clear();
Spawner?.Reset();
Items.Components.Projectile.ResetSpreadCounter();
}
/// <summary>
@@ -10,37 +10,169 @@ using System.Linq;
namespace Barotrauma
{
/// <summary>
/// Explosions are area of effect attacks that can damage characters, items and structures.
/// </summary>
/// <doc>
/// <Field Identifier="showEffects" Type="bool" DefaultValue="true">
/// Used to enable all particle effects without having to specify them one by one.
/// </Field>
/// </doc>
partial class Explosion
{
public readonly Attack Attack;
/// <summary>
/// How much force the explosion applies to the characters.
/// </summary>
private readonly float force;
private readonly float cameraShake, cameraShakeRange;
/// <summary>
/// Intensity of the screen shake effect.
/// </summary>
/// <doc>
/// <override type="DefaultValue">
/// 10% of the range if showEffects is true, 0 otherwise.
/// </override>
/// </doc>
private readonly float cameraShake;
/// <summary>
/// How far away does the camera shake effect reach.
/// </summary>
/// <doc>
/// <override type="DefaultValue">
/// Same as attack range if showEffects is true, 0 otherwise.
/// </override>
/// </doc>
private readonly float cameraShakeRange;
/// <summary>
/// Color tint to apply to the player's screen when in range of the explosion.
/// </summary>
private readonly Color screenColor;
private readonly float screenColorRange, screenColorDuration;
private bool sparks, shockwave, flames, smoke, flash, underwaterBubble;
/// <summary>
/// How far away can the screen color effect be seen.
/// </summary>
/// <doc>
/// <override type="DefaultValue">
/// 10% of the range if showEffects is true, 0 otherwise.
/// </override>
/// </doc>
private readonly float screenColorRange;
/// <summary>
/// How long the screen color effect lasts.
/// </summary>
private readonly float screenColorDuration;
/// <summary>
/// Whether a spark particle effect is created when the explosion happens.
/// </summary>
private bool sparks;
/// <summary>
/// Whether a shockwave particle effect is created when the explosion happens.
/// </summary>
private bool shockwave;
/// <summary>
/// Whether a flame particle effect is created when the explosion happens.
/// </summary>
private bool flames;
/// <summary>
/// Whether a smoke particle effect is created when the explosion happens.
/// </summary>
private bool smoke;
/// <summary>
/// Whether a flash effect is created when the explosion happens.
/// </summary>
private bool flash;
/// <summary>
/// Whether a underwater bubble particle effect is created when the explosion happens.
/// </summary>
private bool underwaterBubble;
/// <summary>
/// Color of the light source created by the explosion.
/// </summary>
private readonly Color flashColor;
/// <summary>
/// Whether the explosion plays a tinnitus sound to players who get hit by it.
/// </summary>
private readonly bool playTinnitus;
/// <summary>
/// Whether the explosion executes 'OnFire' status effects on the items it hits.
/// </summary>
/// <doc>
/// <override type="DefaultValue">
/// true if showEffects is true and flames haven't been explicitly set to false, false otherwise.
/// </override>
/// </doc>
private readonly bool applyFireEffects;
/// <summary>
/// List of item tags that the explosion ignores when applying fire effects.
/// </summary>
private readonly string[] ignoreFireEffectsForTags;
/// <summary>
/// When set to true, the explosion don't deal less damage when the target is behind a solid object.
/// </summary>
private readonly bool ignoreCover;
/// <summary>
/// How long the light source created by the explosion lasts.
/// </summary>
private readonly float flashDuration;
/// <summary>
/// How large the light source created by the explosion is.
/// </summary>
private readonly float? flashRange;
/// <summary>
/// Identifier of the decal the explosion creates on the background structure it explodes over.
/// Set to empty string to disable.
/// </summary>
private readonly string decal;
/// <summary>
/// Relative size of the decal created by the explosion.
/// </summary>
private readonly float decalSize;
private readonly bool applyToSelf;
public bool OnlyInside, OnlyOutside;
/// <summary>
/// Whether the explosion only affects characters inside a submarine.
/// </summary>
public bool OnlyInside;
/// <summary>
/// Whether the explosion only affects characters outside a submarine.
/// </summary>
public bool OnlyOutside;
/// <summary>
/// How much the explosion repairs items.
/// </summary>
private readonly float itemRepairStrength;
public readonly HashSet<Submarine> IgnoredSubmarines = new HashSet<Submarine>();
/// <summary>
/// Strength of the EMP effect created by the explosion.
/// </summary>
public float EmpStrength { get; set; }
/// <summary>
/// How much damage the explosion does to ballast flora.
/// </summary>
public float BallastFloraDamage { get; set; }
public Explosion(float range, float force, float damage, float structureDamage, float itemDamage, float empStrength = 0.0f, float ballastFloraStrength = 0.0f)
@@ -66,8 +198,6 @@ namespace Barotrauma
force = element.GetAttributeFloat("force", 0.0f);
applyToSelf = element.GetAttributeBool("applytoself", true);
//the "abilityexplosion" field is kept for backwards compatibility (basically the opposite of "showeffects")
bool showEffects = !element.GetAttributeBool("abilityexplosion", false) && element.GetAttributeBool("showeffects", true);
sparks = element.GetAttributeBool("sparks", showEffects);
@@ -231,7 +361,7 @@ namespace Barotrauma
return;
}
DamageCharacters(worldPosition, Attack, force, damageSource, attacker, applyToSelf);
DamageCharacters(worldPosition, Attack, force, damageSource, attacker);
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
{
@@ -284,8 +414,8 @@ namespace Barotrauma
}
partial void ExplodeProjSpecific(Vector2 worldPosition, Hull hull);
private void DamageCharacters(Vector2 worldPosition, Attack attack, float force, Entity damageSource, Character attacker, bool applyToSelf)
private void DamageCharacters(Vector2 worldPosition, Attack attack, float force, Entity damageSource, Character attacker)
{
if (attack.Range <= 0.0f) { return; }
@@ -300,7 +430,6 @@ namespace Barotrauma
{
continue;
}
//if (c == attacker && !applyToSelf) { continue; }
if (OnlyInside && c.Submarine == null)
{
@@ -513,21 +642,28 @@ namespace Barotrauma
for (int i = Level.Loaded.ExtraWalls.Count - 1; i >= 0; i--)
{
if (Level.Loaded.ExtraWalls[i] is not DestructibleLevelWall destructibleWall) { continue; }
bool inRange = false;
foreach (var cell in destructibleWall.Cells)
{
if (cell.IsPointInside(worldPosition))
{
destructibleWall.AddDamage(levelWallDamage, worldPosition);
continue;
inRange = true;
break;
}
foreach (var edge in cell.Edges)
{
if (MathUtils.LineSegmentToPointDistanceSquared((edge.Point1 + cell.Translation).ToPoint(), (edge.Point2 + cell.Translation).ToPoint(), worldPosition.ToPoint()) < worldRange * worldRange)
{
destructibleWall.AddDamage(levelWallDamage, worldPosition);
inRange = true;
break;
}
}
if (inRange) { break; }
}
if (inRange)
{
destructibleWall.AddDamage(levelWallDamage, worldPosition);
}
}
}
@@ -9,7 +9,7 @@ using System.Xml.Linq;
namespace Barotrauma
{
partial class Gap : MapEntity
partial class Gap : MapEntity, ISerializableEntity
{
public static List<Gap> GapList = new List<Gap>();
@@ -57,6 +57,8 @@ namespace Barotrauma
private Body outsideCollisionBlocker;
private float outsideColliderRaycastTimer;
private bool wasRoomToRoom;
public float Open
{
get { return open; }
@@ -153,12 +155,12 @@ namespace Barotrauma
}
}
public override string Name
public override string Name => "Gap";
public readonly Dictionary<Identifier, SerializableProperty> properties;
public Dictionary<Identifier, SerializableProperty> SerializableProperties
{
get
{
return "Gap";
}
get { return properties; }
}
public Gap(Rectangle rectangle)
@@ -185,6 +187,8 @@ namespace Barotrauma
IsDiagonal = isDiagonal;
open = 1.0f;
properties = SerializableProperty.GetProperties(this);
FindHulls();
GapList.Add(this);
InsertToList();
@@ -199,7 +203,10 @@ namespace Barotrauma
outsideCollisionBlocker.Enabled = false;
#if CLIENT
Resized += newRect => IsHorizontal = newRect.Width < newRect.Height;
#endif
# endif
wasRoomToRoom = IsRoomToRoom;
RefreshOutsideCollider();
DebugConsole.Log("Created gap (" + ID + ")");
}
@@ -332,9 +339,14 @@ namespace Barotrauma
public override void Update(float deltaTime, Camera cam)
{
flowForce = Vector2.Zero;
outsideColliderRaycastTimer -= deltaTime;
if (IsRoomToRoom != wasRoomToRoom)
{
RefreshOutsideCollider();
wasRoomToRoom = IsRoomToRoom;
}
if (open == 0.0f || linkedTo.Count == 0)
{
lerpedFlowForce = Vector2.Zero;
@@ -628,11 +640,16 @@ namespace Barotrauma
public bool RefreshOutsideCollider()
{
if (IsRoomToRoom || Submarine == null || open <= 0.0f || linkedTo.Count == 0 || !(linkedTo[0] is Hull)) return false;
if (outsideCollisionBlocker == null) { return false; }
if (IsRoomToRoom || Submarine == null || open <= 0.0f || linkedTo.Count == 0 || linkedTo[0] is not Hull)
{
outsideCollisionBlocker.Enabled = false;
return false;
}
if (outsideColliderRaycastTimer <= 0.0f)
{
UpdateOutsideColliderPos((Hull)linkedTo[0]);
UpdateOutsideColliderState((Hull)linkedTo[0]);
outsideColliderRaycastTimer = outsideCollisionBlocker.Enabled ?
OutsideColliderRaycastIntervalHighPrio :
OutsideColliderRaycastIntervalLowPrio;
@@ -641,7 +658,7 @@ namespace Barotrauma
return outsideCollisionBlocker.Enabled;
}
private void UpdateOutsideColliderPos(Hull hull)
private void UpdateOutsideColliderState(Hull hull)
{
if (Submarine == null || IsRoomToRoom || Level.Loaded == null) { return; }
@@ -678,7 +695,7 @@ namespace Barotrauma
if (blockingBody.UserData == Submarine) { return; }
outsideCollisionBlocker.Enabled = true;
Vector2 colliderPos = Submarine.LastPickedPosition - Submarine.SimPosition;
float colliderRotation = MathUtils.VectorToAngle(rayDir) - MathHelper.PiOver2;
float colliderRotation = MathUtils.VectorToAngle(Submarine.LastPickedNormal) - MathHelper.PiOver2;
outsideCollisionBlocker.SetTransformIgnoreContacts(ref colliderPos, colliderRotation);
}
else
@@ -775,8 +792,7 @@ namespace Barotrauma
public static Gap Load(ContentXElement element, Submarine submarine, IdRemap idRemap)
{
Rectangle rect = Rectangle.Empty;
Rectangle rect;
if (element.GetAttribute("rect") != null)
{
rect = element.GetAttributeRect("rect", Rectangle.Empty);

Some files were not shown because too many files have changed in this diff Show More