Merge https://github.com/Regalis11/Barotrauma into develop
This commit is contained in:
@@ -95,10 +95,7 @@ namespace Barotrauma
|
||||
{
|
||||
get
|
||||
{
|
||||
if (visibleHulls == null)
|
||||
{
|
||||
visibleHulls = Character.GetVisibleHulls();
|
||||
}
|
||||
visibleHulls ??= Character.GetVisibleHulls();
|
||||
return visibleHulls;
|
||||
}
|
||||
private set
|
||||
@@ -425,14 +422,9 @@ namespace Barotrauma
|
||||
var door = gap.ConnectedDoor;
|
||||
if (door != null)
|
||||
{
|
||||
if (!door.CanBeTraversed)
|
||||
if (!pathSteering.CanAccessDoor(door))
|
||||
{
|
||||
if (!door.HasAccess(Character))
|
||||
{
|
||||
if (!canAttackDoors) { continue; }
|
||||
// Treat doors that don't have access to like they were farther, because it will take time to break them.
|
||||
multiplier = 5;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -473,7 +465,7 @@ 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 || IsCurrentPathUnreachable || 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();
|
||||
|
||||
@@ -369,7 +369,13 @@ namespace Barotrauma
|
||||
}
|
||||
else if (targetCharacter.AIController is EnemyAIController enemy)
|
||||
{
|
||||
if (targetCharacter.IsHusk && AIParams.HasTag("husk"))
|
||||
if (enemy.PetBehavior != null && (PetBehavior != null || AIParams.HasTag("pet")))
|
||||
{
|
||||
// Pets see other pets as pets by default.
|
||||
// Monsters see them only as pet only when they have a matching ai target. Otherwise they use the other tags, specified below.
|
||||
targetingTag = "pet";
|
||||
}
|
||||
else if (targetCharacter.IsHusk && AIParams.HasTag("husk"))
|
||||
{
|
||||
targetingTag = "husk";
|
||||
}
|
||||
@@ -695,6 +701,9 @@ namespace Barotrauma
|
||||
// Can't target characters of same species/group because that would make us hostile to all friendly characters in the same species/group.
|
||||
if (Character.IsSameSpeciesOrGroup(c)) { return false; }
|
||||
if (targetCharacter.IsSameSpeciesOrGroup(c)) { return false; }
|
||||
//don't try to attack targets in a sub that belongs to a different team
|
||||
//(for example, targets in an outpost if we're in the main sub)
|
||||
if (c.Submarine?.TeamID != Character.Submarine?.TeamID) { return false; }
|
||||
if (c.IsPlayer || Character.IsOnFriendlyTeam(c))
|
||||
{
|
||||
return a.Damage >= selectedTargetingParams.Threshold;
|
||||
@@ -894,7 +903,7 @@ namespace Barotrauma
|
||||
_previousAttackLimb?.attack is Attack previousAttack && (previousAttack.AfterAttack != AIBehaviorAfterAttack.FallBack || previousAttack.CoolDownTimer <= 0)))
|
||||
{
|
||||
// Keep heading to the last known position of the target
|
||||
var memory = GetTargetMemory(target, false);
|
||||
var memory = GetTargetMemory(target);
|
||||
if (memory != null)
|
||||
{
|
||||
var location = memory.Location;
|
||||
@@ -981,7 +990,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
PathSteering.SetPath(path);
|
||||
PathSteering.SetPath(patrolTarget.SimPosition, path);
|
||||
patrolTimerMargin = 0;
|
||||
newPatrolTargetTimer = newPatrolTargetIntervalMax * Rand.Range(0.5f, 1.5f);
|
||||
searchingNewHull = false;
|
||||
@@ -1088,13 +1097,13 @@ namespace Barotrauma
|
||||
Character owner = GetOwner(item);
|
||||
if (owner != null)
|
||||
{
|
||||
if (Character.IsFriendly(owner))
|
||||
if (Character.IsFriendly(owner) || owner.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI))
|
||||
{
|
||||
ResetAITarget();
|
||||
State = AIState.Idle;
|
||||
return;
|
||||
}
|
||||
else if (!owner.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI))
|
||||
else
|
||||
{
|
||||
SelectedAiTarget = owner.AiTarget;
|
||||
}
|
||||
@@ -2186,7 +2195,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
AITargetMemory targetMemory = GetTargetMemory(attacker.AiTarget, addIfNotFound: true);
|
||||
AITargetMemory targetMemory = GetTargetMemory(attacker.AiTarget, addIfNotFound: true, keepAlive: true);
|
||||
targetMemory.Priority += GetRelativeDamage(attackResult.Damage, Character.Vitality) * AIParams.AggressionHurt;
|
||||
|
||||
// Only allow to react once. Otherwise would attack the target with only a fraction of a cooldown
|
||||
@@ -2531,8 +2540,10 @@ namespace Barotrauma
|
||||
if (Math.Abs(limbDiff.X) < itemBodyExtent &&
|
||||
Math.Abs(limbDiff.Y) < Character.AnimController.Collider.GetMaxExtent() + Character.AnimController.ColliderHeightFromFloor)
|
||||
{
|
||||
Vector2 velocity = limbDiff;
|
||||
if (limbDiff.LengthSquared() > 0.01f) { velocity = Vector2.Normalize(velocity); }
|
||||
item.body.LinearVelocity *= 0.9f;
|
||||
item.body.LinearVelocity -= limbDiff * 0.25f;
|
||||
item.body.LinearVelocity -= velocity * 0.25f;
|
||||
bool wasBroken = item.Condition <= 0.0f;
|
||||
item.AddDamage(Character, item.WorldPosition, new Attack(0.0f, 0.0f, 0.0f, 0.0f, 0.02f * Character.Params.EatingSpeed), deltaTime);
|
||||
Character.ApplyStatusEffects(ActionType.OnEating, deltaTime);
|
||||
@@ -2924,7 +2935,8 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
if (targetParams.State == AIState.Eat && Character.Params.Health.HealthRegenerationWhenEating > 0)
|
||||
//no need to eat if the character is already in full health (except if it's a pet - pets actually need to eat to stay alive, not just to regain health)
|
||||
if (targetParams.State == AIState.Eat && Character.Params.Health.HealthRegenerationWhenEating > 0 && !Character.IsPet)
|
||||
{
|
||||
valueModifier *= MathHelper.Lerp(1f, 0.1f, Character.HealthPercentage / 100f);
|
||||
}
|
||||
@@ -3021,7 +3033,7 @@ namespace Barotrauma
|
||||
//if the target is very close, the distance doesn't make much difference
|
||||
// -> just ignore the distance and target whatever has the highest priority
|
||||
dist = Math.Max(dist, 100.0f);
|
||||
AITargetMemory targetMemory = GetTargetMemory(aiTarget, addIfNotFound: true);
|
||||
AITargetMemory targetMemory = GetTargetMemory(aiTarget, addIfNotFound: true, keepAlive: SelectedAiTarget != aiTarget);
|
||||
if (Character.Submarine != null && !Character.Submarine.Info.IsRuin && Character.CurrentHull != null)
|
||||
{
|
||||
float diff = Math.Abs(toTarget.Y) - Character.CurrentHull.Size.Y;
|
||||
@@ -3090,12 +3102,20 @@ namespace Barotrauma
|
||||
if (aiTarget.Entity is Item i)
|
||||
{
|
||||
Character owner = GetOwner(i);
|
||||
// Don't target items that we own.
|
||||
// This is a rare case, and almost entirely related to Humanhusks, so let's check it last to reduce unnecessary checks (although the check shouldn't be expensive)
|
||||
if (owner == Character) { continue; }
|
||||
if (owner != null && (Character.IsFriendly(owner) || owner.AiTarget != null && ignoredTargets.Contains(owner.AiTarget)))
|
||||
if (owner != null)
|
||||
{
|
||||
continue;
|
||||
if (owner.AiTarget != null && ignoredTargets.Contains(owner.AiTarget)) { continue; }
|
||||
if (Character.IsFriendly(owner))
|
||||
{
|
||||
// Don't target items that we own. This is a rare case, and almost entirely related to Humanhusks (in the vanilla game).
|
||||
continue;
|
||||
}
|
||||
if (owner.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI))
|
||||
{
|
||||
// ignore if owner is tagged to be explicitly ignored (Feign Death)
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (targetCharacter != null)
|
||||
@@ -3418,7 +3438,7 @@ namespace Barotrauma
|
||||
return false;
|
||||
}
|
||||
|
||||
private AITargetMemory GetTargetMemory(AITarget target, bool addIfNotFound)
|
||||
private AITargetMemory GetTargetMemory(AITarget target, bool addIfNotFound = false, bool keepAlive = false)
|
||||
{
|
||||
if (!targetMemories.TryGetValue(target, out AITargetMemory memory))
|
||||
{
|
||||
@@ -3428,9 +3448,8 @@ namespace Barotrauma
|
||||
targetMemories.Add(target, memory);
|
||||
}
|
||||
}
|
||||
if (addIfNotFound)
|
||||
if (keepAlive)
|
||||
{
|
||||
// Keep the memory alive.
|
||||
memory.Priority = Math.Max(memory.Priority, minPriority);
|
||||
}
|
||||
return memory;
|
||||
@@ -3446,7 +3465,7 @@ namespace Barotrauma
|
||||
}
|
||||
else if (CanPerceive(_selectedAiTarget, checkVisibility: false))
|
||||
{
|
||||
var memory = GetTargetMemory(_selectedAiTarget, false);
|
||||
var memory = GetTargetMemory(_selectedAiTarget);
|
||||
if (memory != null)
|
||||
{
|
||||
memory.Location = _selectedAiTarget.WorldPosition;
|
||||
@@ -3504,10 +3523,10 @@ namespace Barotrauma
|
||||
private readonly float stateResetCooldown = 10;
|
||||
private float stateResetTimer;
|
||||
private bool isStateChanged;
|
||||
private readonly Dictionary<AITrigger, CharacterParams.TargetParams> activeTriggers = new Dictionary<AITrigger, CharacterParams.TargetParams>();
|
||||
private readonly HashSet<AITrigger> inactiveTriggers = new HashSet<AITrigger>();
|
||||
private readonly Dictionary<StatusEffect.AITrigger, CharacterParams.TargetParams> activeTriggers = new Dictionary<StatusEffect.AITrigger, CharacterParams.TargetParams>();
|
||||
private readonly HashSet<StatusEffect.AITrigger> inactiveTriggers = new HashSet<StatusEffect.AITrigger>();
|
||||
|
||||
public void LaunchTrigger(AITrigger trigger)
|
||||
public void LaunchTrigger(StatusEffect.AITrigger trigger)
|
||||
{
|
||||
if (trigger.IsTriggered) { return; }
|
||||
if (activeTriggers.ContainsKey(trigger)) { return; }
|
||||
@@ -3527,7 +3546,7 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (var triggerObject in activeTriggers)
|
||||
{
|
||||
AITrigger trigger = triggerObject.Key;
|
||||
StatusEffect.AITrigger trigger = triggerObject.Key;
|
||||
if (trigger.IsPermanent) { continue; }
|
||||
trigger.UpdateTimer(deltaTime);
|
||||
if (!trigger.IsActive)
|
||||
@@ -3537,7 +3556,7 @@ namespace Barotrauma
|
||||
inactiveTriggers.Add(trigger);
|
||||
}
|
||||
}
|
||||
foreach (AITrigger trigger in inactiveTriggers)
|
||||
foreach (StatusEffect.AITrigger trigger in inactiveTriggers)
|
||||
{
|
||||
activeTriggers.Remove(trigger);
|
||||
}
|
||||
@@ -3643,7 +3662,11 @@ namespace Barotrauma
|
||||
{
|
||||
isStateChanged = true;
|
||||
SetStateResetTimer();
|
||||
ChangeParams(target.SpeciesName, state, priority, ignoreAttacksIfNotInSameSub: !target.IsHuman);
|
||||
if (!Character.IsPet || !target.IsHuman)
|
||||
{
|
||||
//don't turn pets hostile to all humans when attacked by one
|
||||
ChangeParams(target.SpeciesName, state, priority, ignoreAttacksIfNotInSameSub: !target.IsHuman);
|
||||
}
|
||||
if (target.IsHuman)
|
||||
{
|
||||
priority = GetTargetParams("human")?.Priority;
|
||||
|
||||
@@ -42,6 +42,8 @@ namespace Barotrauma
|
||||
public readonly HashSet<Hull> UnsafeHulls = new HashSet<Hull>();
|
||||
public readonly List<Item> IgnoredItems = new List<Item>();
|
||||
|
||||
private readonly HashSet<Hull> dirtyHullSafetyCalculations = new HashSet<Hull>();
|
||||
|
||||
private float respondToAttackTimer;
|
||||
private const float RespondToAttackInterval = 1.0f;
|
||||
private bool wasConscious;
|
||||
@@ -436,6 +438,7 @@ namespace Barotrauma
|
||||
foreach (Hull h in VisibleHulls)
|
||||
{
|
||||
PropagateHullSafety(Character, h);
|
||||
dirtyHullSafetyCalculations.Remove(h);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -443,9 +446,15 @@ namespace Barotrauma
|
||||
foreach (Hull h in VisibleHulls)
|
||||
{
|
||||
RefreshHullSafety(h);
|
||||
dirtyHullSafetyCalculations.Remove(h);
|
||||
}
|
||||
}
|
||||
foreach (Hull h in dirtyHullSafetyCalculations)
|
||||
{
|
||||
RefreshHullSafety(h);
|
||||
}
|
||||
}
|
||||
dirtyHullSafetyCalculations.Clear();
|
||||
if (reportProblemsTimer <= 0.0f)
|
||||
{
|
||||
if (Character.Submarine != null && (Character.Submarine.TeamID == Character.TeamID || Character.Submarine.TeamID == Character.OriginalTeamID || Character.IsEscorted) && !Character.Submarine.Info.IsWreck)
|
||||
@@ -615,7 +624,7 @@ namespace Barotrauma
|
||||
ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn) ||
|
||||
Character.CurrentHull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 10 ||
|
||||
Character.CurrentHull.IsWetRoom;
|
||||
bool IsOrderedToWait() => Character.IsOnPlayerTeam && ObjectiveManager.CurrentOrder is AIObjectiveGoTo goTo && goTo.Target == Character;
|
||||
bool IsOrderedToWait() => Character.IsOnPlayerTeam && ObjectiveManager.CurrentOrder is AIObjectiveGoTo { IsWaitOrder: true };
|
||||
bool removeDivingSuit = !shouldKeepTheGearOn && !IsOrderedToWait();
|
||||
if (shouldActOnSuffocation && Character.CurrentHull.Oxygen > 0 && (!isCurrentObjectiveFindSafety || Character.OxygenAvailable < 1))
|
||||
{
|
||||
@@ -900,7 +909,7 @@ namespace Barotrauma
|
||||
var container = i.GetComponent<ItemContainer>();
|
||||
if (container == null) { return 0; }
|
||||
if (!container.Inventory.CanBePut(containableItem)) { return 0; }
|
||||
var rootContainer = container.Item.GetRootContainer() ?? container.Item;
|
||||
var rootContainer = container.Item.RootContainer ?? container.Item;
|
||||
if (rootContainer.GetComponent<Fabricator>() != null || rootContainer.GetComponent<Deconstructor>() != null) { return 0; }
|
||||
if (container.ShouldBeContained(containableItem, out bool isRestrictionsDefined))
|
||||
{
|
||||
@@ -1145,7 +1154,7 @@ namespace Barotrauma
|
||||
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 && !Character.IsMedic)
|
||||
if (Character.Bleeding > AfflictionPrefab.Bleeding.TreatmentThreshold && !Character.IsMedic)
|
||||
{
|
||||
string msgId = "DialogBleeding";
|
||||
Character.Speak(TextManager.Get(msgId).Value, delay: Rand.Range(minDelay, maxDelay), identifier: msgId.ToIdentifier(), minDurationBetweenSimilar: 30.0f);
|
||||
@@ -1658,7 +1667,7 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public static bool HasDivingSuit(Character character, float conditionPercentage = 0, bool requireOxygenTank = true)
|
||||
=> HasItem(character, AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR, out _, requireOxygenTank ? AIObjectiveFindDivingGear.OXYGEN_SOURCE : Identifier.Empty, conditionPercentage, requireEquipped: true,
|
||||
predicate: (Item item) => character.HasEquippedItem(item, InvSlotType.OuterClothes));
|
||||
predicate: (Item item) => character.HasEquippedItem(item, InvSlotType.OuterClothes | InvSlotType.InnerClothes));
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the character has a diving mask in usable condition plus some oxygen.
|
||||
@@ -1891,7 +1900,7 @@ namespace Barotrauma
|
||||
private static float GetReactionTime() => reactionTime * Rand.Range(0.75f, 1.25f);
|
||||
|
||||
/// <summary>
|
||||
/// Updates the hull safety for all ai characters in the team. The idea is that the crew communicates (magically) via radio about the threads.
|
||||
/// Updates the hull safety for all ai characters in the team. The idea is that the crew communicates (magically) via radio about the threats.
|
||||
/// The safety levels need to be calculated for each bot individually, because the formula takes into account things like current orders.
|
||||
/// There's now a cached value per each hull, which should prevent too frequent calculations.
|
||||
/// </summary>
|
||||
@@ -1900,9 +1909,13 @@ namespace Barotrauma
|
||||
DoForEachBot(character, (humanAi) => humanAi.RefreshHullSafety(hull));
|
||||
}
|
||||
|
||||
public void AskToRecalculateHullSafety(Hull hull) => dirtyHullSafetyCalculations.Add(hull);
|
||||
|
||||
private void RefreshHullSafety(Hull hull)
|
||||
{
|
||||
if (GetHullSafety(hull, Character, VisibleHulls) > HULL_SAFETY_THRESHOLD)
|
||||
var visibleHulls = dirtyHullSafetyCalculations.Contains(hull) ? hull.GetConnectedHulls(includingThis: true, searchDepth: 1) : VisibleHulls;
|
||||
float hullSafety = GetHullSafety(hull, Character, visibleHulls);
|
||||
if (hullSafety > HULL_SAFETY_THRESHOLD)
|
||||
{
|
||||
UnsafeHulls.Remove(hull);
|
||||
}
|
||||
|
||||
+123
-115
@@ -22,7 +22,10 @@ namespace Barotrauma
|
||||
|
||||
private readonly Character character;
|
||||
|
||||
private Vector2 currentTarget;
|
||||
/// <summary>
|
||||
/// In sim units.
|
||||
/// </summary>
|
||||
private Vector2 currentTargetPos;
|
||||
|
||||
private float findPathTimer;
|
||||
|
||||
@@ -40,11 +43,6 @@ namespace Barotrauma
|
||||
get { return pathFinder; }
|
||||
}
|
||||
|
||||
public Vector2 CurrentTarget
|
||||
{
|
||||
get { return currentTarget; }
|
||||
}
|
||||
|
||||
public bool IsPathDirty
|
||||
{
|
||||
get;
|
||||
@@ -54,9 +52,9 @@ namespace Barotrauma
|
||||
/// <summary>
|
||||
/// Returns true if any node in the path is in stairs
|
||||
/// </summary>
|
||||
public bool InStairs => currentPath != null && currentPath.Nodes.Any(n => n.Stairs != null);
|
||||
public bool PathHasStairs => currentPath != null && currentPath.Nodes.Any(n => n.Stairs != null);
|
||||
|
||||
public bool IsCurrentNodeLadder => currentPath?.CurrentNode?.Ladders != null && currentPath.CurrentNode.Ladders.Item.IsInteractable(character);
|
||||
public bool IsCurrentNodeLadder => GetCurrentLadder() != null;
|
||||
|
||||
public bool IsNextNodeLadder => GetNextLadder() != null;
|
||||
|
||||
@@ -64,14 +62,9 @@ namespace Barotrauma
|
||||
{
|
||||
get
|
||||
{
|
||||
if (currentPath == null) { return false; }
|
||||
if (currentPath.CurrentNode == null) { return false; }
|
||||
if (currentPath.NextNode == null) { return false; }
|
||||
var currentLadder = currentPath.CurrentNode.Ladders;
|
||||
var currentLadder = GetCurrentLadder();
|
||||
if (currentLadder == null) { return false; }
|
||||
if (!currentLadder.Item.IsInteractable(character)) { return false; }
|
||||
var nextLadder = GetNextLadder();
|
||||
return nextLadder != null && nextLadder == currentLadder;
|
||||
return currentLadder == GetNextLadder();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,13 +100,10 @@ namespace Barotrauma
|
||||
findPathTimer -= step;
|
||||
}
|
||||
|
||||
public void SetPath(SteeringPath path)
|
||||
public void SetPath(Vector2 targetPos, SteeringPath path)
|
||||
{
|
||||
currentTargetPos = targetPos;
|
||||
currentPath = path;
|
||||
if (path.Nodes.Any())
|
||||
{
|
||||
currentTarget = path.Nodes[path.Nodes.Count - 1].SimPosition;
|
||||
}
|
||||
findPathTimer = Math.Min(findPathTimer, 1.0f);
|
||||
IsPathDirty = false;
|
||||
}
|
||||
@@ -136,46 +126,17 @@ namespace Barotrauma
|
||||
steering += addition;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Seeks the ladder from the next and next + 1 nodes.
|
||||
/// </summary>
|
||||
public Ladder GetNextLadder()
|
||||
{
|
||||
if (currentPath == null) { return null; }
|
||||
if (currentPath.NextNode == null) { return null; }
|
||||
if (currentPath.NextNode.Ladders != null && currentPath.NextNode.Ladders.Item.IsInteractable(character))
|
||||
{
|
||||
return currentPath.NextNode.Ladders;
|
||||
}
|
||||
else
|
||||
{
|
||||
int index = currentPath.CurrentIndex + 2;
|
||||
if (currentPath.Nodes.Count > index)
|
||||
{
|
||||
var node = currentPath.Nodes[index];
|
||||
if (node == null) { return null; }
|
||||
if (node.Ladders != null && node.Ladders.Item.IsInteractable(character))
|
||||
{
|
||||
return node.Ladders;
|
||||
}
|
||||
//if the next node is a hatch, check if the node after that is a ladder
|
||||
else if (node.ConnectedDoor != null && node.ConnectedDoor.IsHorizontal)
|
||||
{
|
||||
index++;
|
||||
if (currentPath.Nodes.Count > index)
|
||||
{
|
||||
node = currentPath.Nodes[index];
|
||||
if (node == null) { return null; }
|
||||
if (node.Ladders != null && node.Ladders.Item.IsInteractable(character))
|
||||
{
|
||||
return node.Ladders;
|
||||
}
|
||||
}
|
||||
}
|
||||
public Ladder GetCurrentLadder() => GetLadder(currentPath?.CurrentNode);
|
||||
|
||||
}
|
||||
return null;
|
||||
public Ladder GetNextLadder() => GetLadder(currentPath?.NextNode);
|
||||
|
||||
private Ladder GetLadder(WayPoint wp)
|
||||
{
|
||||
if (wp?.Ladders?.Item is Item item && item.IsInteractable(character))
|
||||
{
|
||||
return wp.Ladders;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -183,19 +144,10 @@ namespace Barotrauma
|
||||
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;
|
||||
if (currentPath != null && currentPath.Nodes.Any() && character.Submarine != null)
|
||||
{
|
||||
//target in a different sub than where the character is now
|
||||
//take that into account when calculating if the target has moved
|
||||
Submarine currentPathSub = currentPath?.CurrentNode?.Submarine;
|
||||
if (currentPathSub == character.Submarine) { currentPathSub = currentPath?.Nodes.LastOrDefault()?.Submarine; }
|
||||
if (currentPathSub != character.Submarine && targetDiff.LengthSquared() > 1 && currentPathSub != null)
|
||||
{
|
||||
Vector2 subDiff = character.Submarine.SimPosition - currentPathSub.SimPosition;
|
||||
targetDiff += subDiff;
|
||||
}
|
||||
}
|
||||
// If the target has moved, we need a new path.
|
||||
// Different subs are already taken into account before setting the target.
|
||||
// Triggers when either the target or we have changed subs, but only once (until the new path has been accepted).
|
||||
Vector2 targetDiff = target - currentTargetPos;
|
||||
if (targetDiff.LengthSquared() > 1)
|
||||
{
|
||||
needsNewPath = true;
|
||||
@@ -205,14 +157,14 @@ namespace Barotrauma
|
||||
if (needsNewPath || findPathTimer < -1.0f)
|
||||
{
|
||||
IsPathDirty = true;
|
||||
if (!needsNewPath && findPathTimer < -1)
|
||||
if (!needsNewPath && currentPath?.CurrentNode is WayPoint wp)
|
||||
{
|
||||
if (character.Submarine != null && Math.Abs(character.AnimController.TargetMovement.Combine()) <= 0)
|
||||
if (character.Submarine != null && wp.Ladders == null && wp.ConnectedDoor == 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)
|
||||
if (character.Submarine == null && 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)
|
||||
@@ -226,7 +178,7 @@ namespace Barotrauma
|
||||
if (findPathTimer < 0)
|
||||
{
|
||||
SkipCurrentPathNodes();
|
||||
currentTarget = target;
|
||||
currentTargetPos = target;
|
||||
Vector2 currentPos = host.SimPosition;
|
||||
pathFinder.InsideSubmarine = character.Submarine != null && !character.Submarine.Info.IsRuin;
|
||||
pathFinder.ApplyPenaltyToOutsideNodes = character.Submarine != null && !character.IsProtectedFromPressure;
|
||||
@@ -252,6 +204,14 @@ namespace Barotrauma
|
||||
useNewPath = Vector2.DistanceSquared(character.WorldPosition, currentPath.CurrentNode.WorldPosition) > Math.Pow(Vector2.Distance(character.WorldPosition, newPath.Nodes.First().WorldPosition) * 3, 2);
|
||||
}
|
||||
}
|
||||
if (!useNewPath && !character.CanSeeTarget(currentPath.CurrentNode))
|
||||
{
|
||||
// If we are set to disregard the new path, ensure that we can actually see the current node of the old path,
|
||||
// because it's possible that there's e.g. a closed door between us and the current node,
|
||||
// and in that case we'd want to use the new path instead of the old.
|
||||
// There's visibility checks in the pathfinder calls, so the new path should always be ok.
|
||||
useNewPath = true;
|
||||
}
|
||||
|
||||
bool IsIdenticalPath()
|
||||
{
|
||||
@@ -330,6 +290,7 @@ namespace Barotrauma
|
||||
//if not in water and the waypoint is between the top and bottom of the collider, no need to move vertically
|
||||
if (canClimb && !character.AnimController.InWater && !character.IsClimbing && diff.Y < collider.Height / 2 + collider.Radius)
|
||||
{
|
||||
// TODO: might cause some edge cases -> do we need this?
|
||||
diff.Y = 0.0f;
|
||||
}
|
||||
if (diff == Vector2.Zero) { return Vector2.Zero; }
|
||||
@@ -346,12 +307,12 @@ namespace Barotrauma
|
||||
}
|
||||
if (currentPath.Finished)
|
||||
{
|
||||
Vector2 pos2 = host.SimPosition;
|
||||
Vector2 hostPosition = host.SimPosition;
|
||||
if (character != null && character.Submarine == null && CurrentPath.Nodes.Count > 0 && CurrentPath.Nodes.Last().Submarine != null)
|
||||
{
|
||||
pos2 -= CurrentPath.Nodes.Last().Submarine.SimPosition;
|
||||
hostPosition -= CurrentPath.Nodes.Last().Submarine.SimPosition;
|
||||
}
|
||||
return currentTarget - pos2;
|
||||
return currentTargetPos - hostPosition;
|
||||
}
|
||||
bool doorsChecked = false;
|
||||
checkDoorsTimer = Math.Min(checkDoorsTimer, GetDoorCheckTime());
|
||||
@@ -371,14 +332,46 @@ namespace Barotrauma
|
||||
bool isDiving = character.AnimController.InWater && character.AnimController.HeadInWater;
|
||||
// Only humanoids can climb ladders
|
||||
bool canClimb = character.AnimController is HumanoidAnimController && !character.LockHands;
|
||||
Ladder currentLadder = currentPath.CurrentNode.Ladders;
|
||||
if (currentLadder != null && !currentLadder.Item.IsInteractable(character))
|
||||
{
|
||||
currentLadder = null;
|
||||
}
|
||||
Ladder currentLadder = GetCurrentLadder();
|
||||
Ladder nextLadder = GetNextLadder();
|
||||
var ladders = currentLadder ?? nextLadder;
|
||||
bool useLadders = canClimb && ladders != null && steering.LengthSquared() > 0.1f && (!isDiving || steering.Y > 1);
|
||||
bool useLadders = canClimb && ladders != null;
|
||||
var collider = character.AnimController.Collider;
|
||||
Vector2 colliderSize = collider.GetSize();
|
||||
if (useLadders)
|
||||
{
|
||||
if (character.IsClimbing && Math.Abs(diff.X) - ConvertUnits.ToDisplayUnits(colliderSize.X) > Math.Abs(diff.Y))
|
||||
{
|
||||
// If the current node is horizontally farther from us than vertically, we don't want to keep climbing the ladders.
|
||||
useLadders = false;
|
||||
}
|
||||
else if (!character.IsClimbing && currentPath.NextNode != null && nextLadder == null)
|
||||
{
|
||||
Vector2 diffToNextNode = currentPath.NextNode.WorldPosition - pos;
|
||||
if (Math.Abs(diffToNextNode.X) > Math.Abs(diffToNextNode.Y))
|
||||
{
|
||||
// If the next node is horizontally farther from us than vertically, we don't want to start climbing.
|
||||
useLadders = false;
|
||||
}
|
||||
}
|
||||
else if (isDiving && steering.Y < 1)
|
||||
{
|
||||
// When diving, only use ladders to get upwards (towards the surface), otherwise we can just ignore them.
|
||||
useLadders = false;
|
||||
}
|
||||
}
|
||||
if (character.IsClimbing && !useLadders)
|
||||
{
|
||||
if (currentPath.IsAtEndNode && canClimb && ladders != null)
|
||||
{
|
||||
// Don't release the ladders when ending a path in ladders.
|
||||
useLadders = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
character.StopClimbing();
|
||||
}
|
||||
}
|
||||
if (useLadders && character.SelectedSecondaryItem != ladders.Item)
|
||||
{
|
||||
if (character.CanInteractWith(ladders.Item))
|
||||
@@ -398,40 +391,28 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
var collider = character.AnimController.Collider;
|
||||
if (character.IsClimbing && !useLadders)
|
||||
{
|
||||
character.StopClimbing();
|
||||
}
|
||||
if (character.IsClimbing && useLadders)
|
||||
{
|
||||
if (currentLadder == null && nextLadder != null)
|
||||
if (currentLadder == null && nextLadder != null && character.SelectedSecondaryItem == nextLadder.Item)
|
||||
{
|
||||
// Climbing a ladder but the path is still on the node next to the ladder -> Skip the node.
|
||||
NextNode(!doorsChecked);
|
||||
}
|
||||
else
|
||||
{
|
||||
bool nextLadderSameAsCurrent = IsNextLadderSameAsCurrent;
|
||||
if (nextLadderSameAsCurrent || currentLadder != null && nextLadder != null && Math.Abs(currentLadder.Item.Position.X - nextLadder.Item.Position.X) < 50)
|
||||
bool nextLadderSameAsCurrent = currentLadder == nextLadder;
|
||||
if (currentLadder != null && nextLadder != null)
|
||||
{
|
||||
//climbing ladders -> don't move horizontally
|
||||
diff.X = 0.0f;
|
||||
}
|
||||
//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)
|
||||
float colliderHeight = collider.Height / 2 + collider.Radius;
|
||||
float distanceMargin = ConvertUnits.ToDisplayUnits(colliderSize.X);
|
||||
if (heightDiff < colliderHeight * 1.25f)
|
||||
{
|
||||
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))
|
||||
{
|
||||
character.StopClimbing();
|
||||
}
|
||||
else if (nextLadder != null && !nextLadderSameAsCurrent)
|
||||
if (nextLadder != null && !nextLadderSameAsCurrent)
|
||||
{
|
||||
// Try to change the ladder (hatches between two submarines)
|
||||
if (character.SelectedSecondaryItem != nextLadder.Item && character.CanInteractWith(nextLadder.Item))
|
||||
@@ -442,12 +423,36 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isAboveFloor || nextLadderSameAsCurrent || nextLadder == null && Math.Abs(diff.Y) < 10)
|
||||
bool isAboveFloor;
|
||||
if (diff.Y < 0)
|
||||
{
|
||||
NextNode(!doorsChecked);
|
||||
// When climbing down, let's use the collider bottom to prevent getting stuck at the bottom of the ladders.
|
||||
float colliderBottom = character.AnimController.Collider.SimPosition.Y;
|
||||
float floorY = character.AnimController.FloorY;
|
||||
isAboveFloor = colliderBottom > floorY;
|
||||
}
|
||||
else
|
||||
{
|
||||
// When climbing up, let's use the lowest collider (feet).
|
||||
// We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative,
|
||||
// when a foot is still below the platform.
|
||||
float heightFromFloor = character.AnimController.GetHeightFromFloor();
|
||||
isAboveFloor = heightFromFloor > -0.1f;
|
||||
}
|
||||
if (isAboveFloor)
|
||||
{
|
||||
if (Math.Abs(diff.Y) < distanceMargin)
|
||||
{
|
||||
NextNode(!doorsChecked);
|
||||
}
|
||||
else if (!currentPath.IsAtEndNode && (nextLadder == null || (currentLadder != null && Math.Abs(currentLadder.Item.WorldPosition.X - nextLadder.Item.WorldPosition.X) > distanceMargin)))
|
||||
{
|
||||
// Can't skip the node -> Release the ladders, because the next node is not on a ladder or it's horizontally too far.
|
||||
character.StopClimbing();
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (nextLadder != null)
|
||||
else if (currentLadder != null && currentPath.NextNode != null)
|
||||
{
|
||||
if (Math.Sign(currentPath.CurrentNode.WorldPosition.Y - character.WorldPosition.Y) != Math.Sign(currentPath.NextNode.WorldPosition.Y - character.WorldPosition.Y))
|
||||
{
|
||||
@@ -466,7 +471,6 @@ namespace Barotrauma
|
||||
if (door == null || door.CanBeTraversed)
|
||||
{
|
||||
float margin = MathHelper.Lerp(1, 5, MathHelper.Clamp(collider.LinearVelocity.Length() / 10, 0, 1));
|
||||
Vector2 colliderSize = collider.GetSize();
|
||||
float targetDistance = Math.Max(Math.Max(colliderSize.X, colliderSize.Y) / 2 * margin, 0.5f);
|
||||
float horizontalDistance = Math.Abs(character.WorldPosition.X - currentPath.CurrentNode.WorldPosition.X);
|
||||
float verticalDistance = Math.Abs(character.WorldPosition.Y - currentPath.CurrentNode.WorldPosition.Y);
|
||||
@@ -485,7 +489,6 @@ namespace Barotrauma
|
||||
{
|
||||
// Walking horizontally
|
||||
Vector2 colliderBottom = character.AnimController.GetColliderBottom();
|
||||
Vector2 colliderSize = collider.GetSize();
|
||||
Vector2 velocity = collider.LinearVelocity;
|
||||
// If the character is very short, it would fail to use the waypoint nodes because they are always too high.
|
||||
// If the character is very thin, it would often fail to reach the waypoints, because the horizontal distance is too small.
|
||||
@@ -512,9 +515,12 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
float targetDistance = Math.Max(colliderSize.X / 2 * margin, minWidth / 2);
|
||||
if (horizontalDistance < targetDistance && !isTargetTooHigh && !isTargetTooLow && currentLadder == null && (door == null || door.CanBeTraversed))
|
||||
if (horizontalDistance < targetDistance && !isTargetTooHigh && !isTargetTooLow)
|
||||
{
|
||||
NextNode(!doorsChecked);
|
||||
if (door is not { CanBeTraversed: false } && (currentLadder == null || nextLadder == null))
|
||||
{
|
||||
NextNode(!doorsChecked);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (currentPath.CurrentNode == null)
|
||||
@@ -533,9 +539,9 @@ namespace Barotrauma
|
||||
currentPath.SkipToNextNode();
|
||||
}
|
||||
|
||||
private bool CanAccessDoor(Door door, Func<Controller, bool> buttonFilter = null)
|
||||
public bool CanAccessDoor(Door door, Func<Controller, bool> buttonFilter = null)
|
||||
{
|
||||
if (door.IsBroken) { return true; }
|
||||
if (door.CanBeTraversed) { return true; }
|
||||
if (door.IsClosed)
|
||||
{
|
||||
if (!door.Item.IsInteractable(character)) { return false; }
|
||||
@@ -631,10 +637,12 @@ namespace Barotrauma
|
||||
{
|
||||
//the node we're heading towards is the last one in the path, and at a door
|
||||
//the door needs to be open for the character to reach the node
|
||||
if (currentWaypoint.ConnectedDoor.LinkedGap != null)
|
||||
if (currentWaypoint.ConnectedDoor.LinkedGap is Gap linkedGap)
|
||||
{
|
||||
// Keep the airlock doors closed, but not in ruins/wrecks
|
||||
if (currentWaypoint.ConnectedDoor.LinkedGap.IsRoomToRoom && currentWaypoint.CurrentHull is { IsWetRoom: false } || currentWaypoint.Submarine == null || currentWaypoint.Submarine.Info.IsRuin || currentWaypoint.Submarine.Info.IsWreck)
|
||||
if (currentWaypoint.Submarine == null ||
|
||||
currentWaypoint.Submarine.Info is { IsPlayer: false } ||
|
||||
!linkedGap.IsRoomToRoom ||
|
||||
(linkedGap.IsRoomToRoom && currentWaypoint.CurrentHull is { IsWetRoom: false }))
|
||||
{
|
||||
shouldBeOpen = true;
|
||||
door = currentWaypoint.ConnectedDoor;
|
||||
|
||||
@@ -213,7 +213,7 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (Voronoi2.GraphEdge edge in cell.Edges)
|
||||
{
|
||||
if (MathUtils.GetLineIntersection(edge.Point1, edge.Point2, character.WorldPosition, cell.Center, out Vector2 intersection))
|
||||
if (MathUtils.GetLineSegmentIntersection(edge.Point1, edge.Point2, character.WorldPosition, cell.Center, out Vector2 intersection))
|
||||
{
|
||||
Vector2 potentialAttachPos = ConvertUnits.ToSimUnits(intersection);
|
||||
float distSqr = Vector2.DistanceSquared(character.SimPosition, potentialAttachPos);
|
||||
|
||||
@@ -506,6 +506,8 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void SpeakAfterOrderReceived() { }
|
||||
|
||||
protected static bool CanEquip(Character character, Item item, bool allowWearing)
|
||||
{
|
||||
if (item == null) { return false; }
|
||||
|
||||
+6
-2
@@ -14,6 +14,10 @@ namespace Barotrauma
|
||||
|
||||
public readonly List<Item> prioritizedItems = new List<Item>();
|
||||
|
||||
public static readonly Identifier AllowCleanupTag = "allowcleanup".ToIdentifier();
|
||||
|
||||
protected override int MaxTargets => 100;
|
||||
|
||||
public AIObjectiveCleanupItems(Character character, AIObjectiveManager objectiveManager, Item prioritizedItem = null, float priorityModifier = 1)
|
||||
: base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
@@ -81,8 +85,8 @@ namespace Barotrauma
|
||||
|
||||
public static bool IsValidContainer(Item container, Character character, bool allowUnloading = true) =>
|
||||
allowUnloading &&
|
||||
container.HasTag(AllowCleanupTag) &&
|
||||
container.HasAccess(character) &&
|
||||
container.HasTag("allowcleanup") &&
|
||||
container.ParentInventory == null && container.OwnInventory != null && container.OwnInventory.AllItems.Any() &&
|
||||
container.GetComponent<ItemContainer>() != null &&
|
||||
IsItemInsideValidSubmarine(container, character) &&
|
||||
@@ -91,7 +95,6 @@ namespace Barotrauma
|
||||
public static bool IsValidTarget(Item item, Character character, bool checkInventory, bool allowUnloading = true)
|
||||
{
|
||||
if (item == null) { return false; }
|
||||
if (!item.HasAccess(character)) { return false; }
|
||||
if ((item.SpawnedInCurrentOutpost && !item.AllowStealing) == character.IsOnPlayerTeam) { return false; }
|
||||
if (item.ParentInventory != null)
|
||||
{
|
||||
@@ -102,6 +105,7 @@ namespace Barotrauma
|
||||
}
|
||||
if (!IsValidContainer(item.Container, character, allowUnloading)) { return false; }
|
||||
}
|
||||
if (!item.HasAccess(character)) { return false; }
|
||||
if (character != null && !IsItemInsideValidSubmarine(item, character)) { return false; }
|
||||
if (item.HasBallastFloraInHull) { return false; }
|
||||
var wire = item.GetComponent<Wire>();
|
||||
|
||||
+10
-2
@@ -995,10 +995,18 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
if (HumanAIController.HasItem(character, "handlocker".ToIdentifier(), out IEnumerable<Item> matchingItems) && !Enemy.IsUnconscious && Enemy.IsKnockedDown && character.CanInteractWith(Enemy))
|
||||
|
||||
//prefer using handcuffs already on the enemy's inventory
|
||||
if (!HumanAIController.HasItem(Enemy, "handlocker".ToIdentifier(), out IEnumerable<Item> matchingItems))
|
||||
{
|
||||
HumanAIController.HasItem(character, "handlocker".ToIdentifier(), out matchingItems);
|
||||
}
|
||||
|
||||
if (matchingItems.Any() &&
|
||||
!Enemy.IsUnconscious && Enemy.IsKnockedDown && character.CanInteractWith(Enemy) && !Enemy.LockHands)
|
||||
{
|
||||
var handCuffs = matchingItems.First();
|
||||
if (!HumanAIController.TakeItem(handCuffs, Enemy.Inventory, equip: true))
|
||||
if (!HumanAIController.TakeItem(handCuffs, Enemy.Inventory, equip: true, wear: true))
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.NewMessage($"{character.Name}: Failed to handcuff the target.", Color.Red);
|
||||
|
||||
+1
-1
@@ -198,7 +198,7 @@ namespace Barotrauma
|
||||
TargetName = container.Item.Name,
|
||||
AbortCondition = obj =>
|
||||
container?.Item == null || container.Item.Removed || !container.Item.HasAccess(character) ||
|
||||
(container.Item.GetRootContainer()?.OwnInventory?.Locked ?? false) ||
|
||||
(container.Item.RootContainer?.OwnInventory?.Locked ?? false) ||
|
||||
ItemToContain == null || ItemToContain.Removed ||
|
||||
!ItemToContain.IsOwnedBy(character) || container.Item.GetRootInventoryOwner() is Character c && c != character,
|
||||
SpeakIfFails = !objectiveManager.IsCurrentOrder<AIObjectiveCleanupItems>(),
|
||||
|
||||
+4
-3
@@ -30,7 +30,8 @@ namespace Barotrauma
|
||||
public static readonly Identifier DIVING_GEAR_WEARABLE_INDOORS = "divinggear_wearableindoors".ToIdentifier();
|
||||
public static readonly Identifier OXYGEN_SOURCE = "oxygensource".ToIdentifier();
|
||||
|
||||
protected override bool CheckObjectiveSpecific() => targetItem != null && character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.Head);
|
||||
protected override bool CheckObjectiveSpecific() =>
|
||||
targetItem != null && character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head);
|
||||
|
||||
public AIObjectiveFindDivingGear(Character character, bool needsDivingSuit, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
@@ -51,7 +52,7 @@ namespace Barotrauma
|
||||
TrySetTargetItem(character.Inventory.FindItemByTag(HEAVY_DIVING_GEAR, true));
|
||||
}
|
||||
if (targetItem == null ||
|
||||
!character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.Head | InvSlotType.InnerClothes) &&
|
||||
!character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head) &&
|
||||
targetItem.ContainedItems.Any(it => IsSuitableContainedOxygenSource(it)))
|
||||
{
|
||||
TryAddSubObjective(ref getDivingGear, () =>
|
||||
@@ -65,7 +66,7 @@ namespace Barotrauma
|
||||
AllowStealing = HumanAIController.NeedsDivingGear(character.CurrentHull, out _),
|
||||
AllowToFindDivingGear = false,
|
||||
AllowDangerousPressure = true,
|
||||
EquipSlotType = InvSlotType.OuterClothes | InvSlotType.Head | InvSlotType.InnerClothes,
|
||||
EquipSlotType = InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head,
|
||||
Wear = true
|
||||
};
|
||||
},
|
||||
|
||||
+1
-1
@@ -406,7 +406,7 @@ namespace Barotrauma
|
||||
if (allowChangingSubmarine || !potentialHull.OutpostModuleTags.Any(t => t == "airlock"))
|
||||
{
|
||||
// Don't allow to go outside if not already outside.
|
||||
var path = PathSteering.PathFinder.FindPath(character.SimPosition, potentialHull.SimPosition, character.Submarine, nodeFilter: node => node.Waypoint.CurrentHull != null);
|
||||
var path = PathSteering.PathFinder.FindPath(character.SimPosition, character.GetRelativeSimPosition(potentialHull), character.Submarine, nodeFilter: node => node.Waypoint.CurrentHull != null);
|
||||
if (path.Unreachable)
|
||||
{
|
||||
hullSafety = 0;
|
||||
|
||||
+2
-2
@@ -376,7 +376,7 @@ namespace Barotrauma
|
||||
// Otherwise it will take some time for us to find a valid item when there are multiple items that we can't reach and some that we can.
|
||||
// This is relatively expensive, so let's do this only when it significantly improves the behavior.
|
||||
// Only allow one path find call per frame.
|
||||
CheckPathForEachItem = priority >= AIObjectiveManager.LowestOrderPriority && (objectiveManager.IsCurrentOrder<AIObjectiveFixLeaks>() || objectiveManager.CurrentOrder is AIObjectiveGoTo gotoOrder && gotoOrder.IsFollowOrderObjective);
|
||||
CheckPathForEachItem = priority >= AIObjectiveManager.LowestOrderPriority && (objectiveManager.IsCurrentOrder<AIObjectiveFixLeaks>() || objectiveManager.CurrentOrder is AIObjectiveGoTo gotoOrder && gotoOrder.IsFollowOrder);
|
||||
}
|
||||
bool checkPath = CheckPathForEachItem;
|
||||
// Reset if the character has switched subs.
|
||||
@@ -536,7 +536,7 @@ namespace Barotrauma
|
||||
{
|
||||
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);
|
||||
var path = PathSteering.PathFinder.FindPath(character.SimPosition, character.GetRelativeSimPosition(itemCandidate.item), 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.
|
||||
|
||||
+303
-274
@@ -26,7 +26,8 @@ namespace Barotrauma
|
||||
|
||||
public Func<float> PriorityGetter;
|
||||
|
||||
public bool IsFollowOrderObjective;
|
||||
public bool IsFollowOrder;
|
||||
public bool IsWaitOrder;
|
||||
public bool Mimic;
|
||||
|
||||
public bool SpeakIfFails { get; set; } = true;
|
||||
@@ -59,7 +60,7 @@ namespace Barotrauma
|
||||
{
|
||||
get
|
||||
{
|
||||
if (IsFollowOrderObjective && Target is Character targetCharacter && (targetCharacter.CurrentHull == null) != (character.CurrentHull == null))
|
||||
if (IsFollowOrder && Target is Character targetCharacter && (targetCharacter.CurrentHull == null) != (character.CurrentHull == null))
|
||||
{
|
||||
// Keep close when the target is going inside/outside
|
||||
return minDistance;
|
||||
@@ -220,15 +221,45 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
Hull targetHull = GetTargetHull();
|
||||
if (!IsFollowOrderObjective)
|
||||
if (!IsFollowOrder)
|
||||
{
|
||||
// Abandon if going through unsafe paths. Note ignores unsafe nodes when following an order or when the objective is set to ignore unsafe hulls.
|
||||
bool containsUnsafeNodes = character.IsDismissed && !HumanAIController.ObjectiveManager.CurrentObjective.IgnoreUnsafeHulls
|
||||
&& PathSteering != null && PathSteering.CurrentPath != null
|
||||
&& PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull));
|
||||
if (containsUnsafeNodes || HumanAIController.UnreachableHulls.Contains(targetHull))
|
||||
bool isUnreachable = HumanAIController.UnreachableHulls.Contains(targetHull);
|
||||
if (!objectiveManager.CurrentObjective.IgnoreUnsafeHulls)
|
||||
{
|
||||
Abandon = true;
|
||||
if (HumanAIController.UnsafeHulls.Contains(targetHull))
|
||||
{
|
||||
isUnreachable = true;
|
||||
HumanAIController.AskToRecalculateHullSafety(targetHull);
|
||||
}
|
||||
else if (PathSteering?.CurrentPath != null)
|
||||
{
|
||||
foreach (WayPoint wp in PathSteering.CurrentPath.Nodes)
|
||||
{
|
||||
if (wp.CurrentHull == null) { continue; }
|
||||
if (HumanAIController.UnsafeHulls.Contains(wp.CurrentHull))
|
||||
{
|
||||
isUnreachable = true;
|
||||
HumanAIController.AskToRecalculateHullSafety(wp.CurrentHull);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isUnreachable)
|
||||
{
|
||||
SteeringManager.Reset();
|
||||
if (PathSteering?.CurrentPath != null)
|
||||
{
|
||||
PathSteering.CurrentPath.Unreachable = true;
|
||||
}
|
||||
if (repeat)
|
||||
{
|
||||
SpeakCannotReach();
|
||||
}
|
||||
else
|
||||
{
|
||||
Abandon = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
bool insideSteering = SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.IsPathDirty;
|
||||
@@ -250,6 +281,7 @@ namespace Barotrauma
|
||||
if (repeat)
|
||||
{
|
||||
SpeakCannotReach();
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -262,310 +294,285 @@ namespace Barotrauma
|
||||
waitUntilPathUnreachable = pathWaitingTime;
|
||||
}
|
||||
}
|
||||
if (!Abandon)
|
||||
if (Abandon) { return; }
|
||||
if (getDivingGearIfNeeded)
|
||||
{
|
||||
if (getDivingGearIfNeeded)
|
||||
Character followTarget = Target as Character;
|
||||
bool needsDivingSuit = (!isInside || hasOutdoorNodes) && !character.IsImmuneToPressure;
|
||||
bool tryToGetDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit);
|
||||
bool tryToGetDivingSuit = needsDivingSuit;
|
||||
if (Mimic && !character.IsImmuneToPressure)
|
||||
{
|
||||
Character followTarget = Target as Character;
|
||||
bool needsDivingSuit = (!isInside || hasOutdoorNodes) && !character.IsImmuneToPressure;
|
||||
bool tryToGetDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit);
|
||||
bool tryToGetDivingSuit = needsDivingSuit;
|
||||
if (Mimic && !character.IsImmuneToPressure)
|
||||
if (HumanAIController.HasDivingSuit(followTarget))
|
||||
{
|
||||
if (HumanAIController.HasDivingSuit(followTarget))
|
||||
tryToGetDivingGear = true;
|
||||
tryToGetDivingSuit = true;
|
||||
}
|
||||
else if (HumanAIController.HasDivingMask(followTarget) && character.CharacterHealth.OxygenLowResistance < 1)
|
||||
{
|
||||
tryToGetDivingGear = true;
|
||||
}
|
||||
}
|
||||
bool needsEquipment = false;
|
||||
float minOxygen = AIObjectiveFindDivingGear.GetMinOxygen(character);
|
||||
if (tryToGetDivingSuit)
|
||||
{
|
||||
needsEquipment = !HumanAIController.HasDivingSuit(character, minOxygen);
|
||||
}
|
||||
else if (tryToGetDivingGear)
|
||||
{
|
||||
needsEquipment = !HumanAIController.HasDivingGear(character, minOxygen);
|
||||
}
|
||||
if (character.LockHands)
|
||||
{
|
||||
cantFindDivingGear = true;
|
||||
}
|
||||
if (cantFindDivingGear && needsDivingSuit)
|
||||
{
|
||||
// Don't try to reach the target without a suit because it's lethal.
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
if (needsEquipment && !cantFindDivingGear)
|
||||
{
|
||||
SteeringManager.Reset();
|
||||
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: tryToGetDivingSuit, objectiveManager),
|
||||
onAbandon: () =>
|
||||
{
|
||||
tryToGetDivingGear = true;
|
||||
tryToGetDivingSuit = true;
|
||||
}
|
||||
else if (HumanAIController.HasDivingMask(followTarget) && character.CharacterHealth.OxygenLowResistance < 1)
|
||||
{
|
||||
tryToGetDivingGear = true;
|
||||
}
|
||||
}
|
||||
bool needsEquipment = false;
|
||||
float minOxygen = AIObjectiveFindDivingGear.GetMinOxygen(character);
|
||||
if (tryToGetDivingSuit)
|
||||
{
|
||||
needsEquipment = !HumanAIController.HasDivingSuit(character, minOxygen);
|
||||
}
|
||||
else if (tryToGetDivingGear)
|
||||
{
|
||||
needsEquipment = !HumanAIController.HasDivingGear(character, minOxygen);
|
||||
}
|
||||
if (character.LockHands)
|
||||
{
|
||||
cantFindDivingGear = true;
|
||||
}
|
||||
if (cantFindDivingGear && needsDivingSuit)
|
||||
{
|
||||
// Don't try to reach the target without a suit because it's lethal.
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
if (needsEquipment && !cantFindDivingGear)
|
||||
{
|
||||
SteeringManager.Reset();
|
||||
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: tryToGetDivingSuit, objectiveManager),
|
||||
onAbandon: () =>
|
||||
{
|
||||
cantFindDivingGear = true;
|
||||
if (needsDivingSuit)
|
||||
{
|
||||
// Shouldn't try to reach the target without a suit, because it's lethal.
|
||||
Abandon = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Try again without requiring the diving suit
|
||||
RemoveSubObjective(ref findDivingGear);
|
||||
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager),
|
||||
onAbandon: () =>
|
||||
{
|
||||
Abandon = character.CurrentHull != null && (objectiveManager.CurrentOrder != this || Target.Submarine == null);
|
||||
RemoveSubObjective(ref findDivingGear);
|
||||
},
|
||||
onCompleted: () =>
|
||||
{
|
||||
RemoveSubObjective(ref findDivingGear);
|
||||
});
|
||||
}
|
||||
},
|
||||
onCompleted: () => RemoveSubObjective(ref findDivingGear));
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (repeat)
|
||||
{
|
||||
if (IsCloseEnough)
|
||||
{
|
||||
if (requiredCondition == null || requiredCondition())
|
||||
if (needsDivingSuit)
|
||||
{
|
||||
if (character.CanSeeTarget(Target))
|
||||
{
|
||||
OnCompleted();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
float maxGapDistance = 500;
|
||||
Character targetCharacter = Target as Character;
|
||||
if (character.AnimController.InWater)
|
||||
{
|
||||
if (character.CurrentHull == null ||
|
||||
IsFollowOrderObjective &&
|
||||
targetCharacter != null && (targetCharacter.CurrentHull == null) != (character.CurrentHull == null) &&
|
||||
Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition) < maxGapDistance * maxGapDistance)
|
||||
{
|
||||
if (seekGapsTimer > 0)
|
||||
{
|
||||
seekGapsTimer -= deltaTime;
|
||||
// Shouldn't try to reach the target without a suit, because it's lethal.
|
||||
Abandon = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
bool isRuins = character.Submarine?.Info.IsRuin != null || Target.Submarine?.Info.IsRuin != null;
|
||||
bool isEitherOneInside = isInside || Target.Submarine != null;
|
||||
if (isEitherOneInside && (!isRuins || !HumanAIController.HasValidPath()))
|
||||
{
|
||||
SeekGaps(maxGapDistance);
|
||||
seekGapsTimer = seekGapsInterval * Rand.Range(0.1f, 1.1f);
|
||||
if (TargetGap != null)
|
||||
// Try again without requiring the diving suit
|
||||
RemoveSubObjective(ref findDivingGear);
|
||||
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager),
|
||||
onAbandon: () =>
|
||||
{
|
||||
// Check that nothing is blocking the way
|
||||
Vector2 rayStart = character.SimPosition;
|
||||
Vector2 rayEnd = TargetGap.SimPosition;
|
||||
if (TargetGap.Submarine != null && character.Submarine == null)
|
||||
{
|
||||
rayStart -= TargetGap.Submarine.SimPosition;
|
||||
}
|
||||
else if (TargetGap.Submarine == null && character.Submarine != null)
|
||||
{
|
||||
rayEnd -= character.Submarine.SimPosition;
|
||||
}
|
||||
var closestBody = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true);
|
||||
if (closestBody != null)
|
||||
{
|
||||
TargetGap = null;
|
||||
}
|
||||
}
|
||||
Abandon = character.CurrentHull != null && (objectiveManager.CurrentOrder != this || Target.Submarine == null);
|
||||
RemoveSubObjective(ref findDivingGear);
|
||||
},
|
||||
onCompleted: () =>
|
||||
{
|
||||
RemoveSubObjective(ref findDivingGear);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
TargetGap = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
onCompleted: () => RemoveSubObjective(ref findDivingGear));
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (repeat && IsCloseEnough)
|
||||
{
|
||||
if (requiredCondition == null || requiredCondition())
|
||||
{
|
||||
if (character.CanSeeTarget(Target) && (!character.IsClimbing || IsFollowOrder))
|
||||
{
|
||||
OnCompleted();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
float maxGapDistance = 500;
|
||||
Character targetCharacter = Target as Character;
|
||||
if (character.AnimController.InWater)
|
||||
{
|
||||
if (character.CurrentHull == null ||
|
||||
IsFollowOrder &&
|
||||
targetCharacter != null && (targetCharacter.CurrentHull == null) != (character.CurrentHull == null) &&
|
||||
Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition) < maxGapDistance * maxGapDistance)
|
||||
{
|
||||
if (seekGapsTimer > 0)
|
||||
{
|
||||
seekGapsTimer -= deltaTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
TargetGap = null;
|
||||
}
|
||||
if (TargetGap != null)
|
||||
{
|
||||
if (TargetGap.FlowTargetHull != null && HumanAIController.SteerThroughGap(TargetGap, IsFollowOrderObjective ? Target.WorldPosition : TargetGap.FlowTargetHull.WorldPosition, deltaTime))
|
||||
bool isRuins = character.Submarine?.Info.IsRuin != null || Target.Submarine?.Info.IsRuin != null;
|
||||
bool isEitherOneInside = isInside || Target.Submarine != null;
|
||||
if (isEitherOneInside && (!isRuins || !HumanAIController.HasValidPath()))
|
||||
{
|
||||
SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 1);
|
||||
return;
|
||||
SeekGaps(maxGapDistance);
|
||||
seekGapsTimer = seekGapsInterval * Rand.Range(0.1f, 1.1f);
|
||||
if (TargetGap != null)
|
||||
{
|
||||
// Check that nothing is blocking the way
|
||||
Vector2 rayStart = character.SimPosition;
|
||||
Vector2 rayEnd = TargetGap.SimPosition;
|
||||
if (TargetGap.Submarine != null && character.Submarine == null)
|
||||
{
|
||||
rayStart -= TargetGap.Submarine.SimPosition;
|
||||
}
|
||||
else if (TargetGap.Submarine == null && character.Submarine != null)
|
||||
{
|
||||
rayEnd -= character.Submarine.SimPosition;
|
||||
}
|
||||
var closestBody = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true);
|
||||
if (closestBody != null)
|
||||
{
|
||||
TargetGap = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
TargetGap = null;
|
||||
}
|
||||
}
|
||||
if (checkScooterTimer <= 0)
|
||||
{
|
||||
useScooter = false;
|
||||
checkScooterTimer = checkScooterTime * Rand.Range(0.75f, 1.25f);
|
||||
Identifier scooterTag = "scooter".ToIdentifier();
|
||||
Identifier batteryTag = "mobilebattery".ToIdentifier();
|
||||
Item scooter = null;
|
||||
bool shouldUseScooter = Mimic && targetCharacter != null && targetCharacter.HasEquippedItem(scooterTag, allowBroken: false);
|
||||
if (!shouldUseScooter)
|
||||
{
|
||||
float threshold = 500;
|
||||
if (isInside)
|
||||
{
|
||||
Vector2 diff = Target.WorldPosition - character.WorldPosition;
|
||||
shouldUseScooter = Math.Abs(diff.X) > threshold || Math.Abs(diff.Y) > 150;
|
||||
}
|
||||
else
|
||||
{
|
||||
shouldUseScooter = Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition) > threshold * threshold;
|
||||
}
|
||||
}
|
||||
if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> equippedScooters, recursive: false, requireEquipped: true))
|
||||
{
|
||||
// Currently equipped scooter
|
||||
scooter = equippedScooters.FirstOrDefault();
|
||||
}
|
||||
else if (shouldUseScooter)
|
||||
{
|
||||
var leftHandItem = character.GetEquippedItem(slotType: InvSlotType.LeftHand);
|
||||
var rightHandItem = character.GetEquippedItem(slotType: InvSlotType.RightHand);
|
||||
bool handsFull =
|
||||
(leftHandItem != null && !character.Inventory.IsAnySlotAvailable(leftHandItem)) ||
|
||||
(rightHandItem != null && !character.Inventory.IsAnySlotAvailable(rightHandItem));
|
||||
if (!handsFull)
|
||||
{
|
||||
bool hasBattery = false;
|
||||
if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> nonEquippedScooters, containedTag: batteryTag, conditionPercentage: 1, requireEquipped: false))
|
||||
{
|
||||
// Non-equipped scooter with a battery
|
||||
scooter = nonEquippedScooters.FirstOrDefault();
|
||||
hasBattery = true;
|
||||
}
|
||||
else if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> _nonEquippedScooters, requireEquipped: false))
|
||||
{
|
||||
// Non-equipped scooter without a battery
|
||||
scooter = _nonEquippedScooters.FirstOrDefault();
|
||||
// Non-recursive so that the bots won't take batteries from other items. Also means that they can't find batteries inside containers. Not sure how to solve this.
|
||||
hasBattery = HumanAIController.HasItem(character, batteryTag, out _, requireEquipped: false, conditionPercentage: 1, recursive: false);
|
||||
}
|
||||
if (scooter != null && hasBattery)
|
||||
{
|
||||
// Equip only if we have a battery available
|
||||
HumanAIController.TakeItem(scooter, character.Inventory, equip: true, dropOtherIfCannotMove: false, allowSwapping: true, storeUnequipped: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (scooter != null && character.HasEquippedItem(scooter))
|
||||
{
|
||||
if (shouldUseScooter)
|
||||
{
|
||||
useScooter = true;
|
||||
// Check the battery
|
||||
if (scooter.ContainedItems.None(i => i.Condition > 0))
|
||||
{
|
||||
// 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));
|
||||
if (!scooter.Combine(batteries.OrderByDescending(b => b.Condition).First(), character))
|
||||
{
|
||||
useScooter = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
useScooter = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!useScooter)
|
||||
{
|
||||
// Unequip
|
||||
character.Inventory.TryPutItem(scooter, character, CharacterInventory.AnySlot);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
checkScooterTimer -= deltaTime;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
TargetGap = null;
|
||||
useScooter = false;
|
||||
checkScooterTimer = 0;
|
||||
}
|
||||
if (SteeringManager == PathSteering)
|
||||
if (TargetGap != null)
|
||||
{
|
||||
Vector2 targetPos = character.GetRelativeSimPosition(Target);
|
||||
Func<PathNode, bool> nodeFilter = null;
|
||||
if (isInside && !AllowGoingOutside)
|
||||
if (TargetGap.FlowTargetHull != null && HumanAIController.SteerThroughGap(TargetGap, IsFollowOrder ? Target.WorldPosition : TargetGap.FlowTargetHull.WorldPosition, deltaTime))
|
||||
{
|
||||
nodeFilter = n => n.Waypoint.CurrentHull != null;
|
||||
}
|
||||
else if (!isInside)
|
||||
{
|
||||
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)
|
||||
{
|
||||
SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 15);
|
||||
}
|
||||
SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 1);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
PathSteering.SteeringSeek(targetPos, weight: 1,
|
||||
startNodeFilter: n => (n.Waypoint.CurrentHull == null) == (character.CurrentHull == null),
|
||||
endNodeFilter: endNodeFilter,
|
||||
nodeFilter: nodeFilter,
|
||||
checkVisiblity: Target is Item || Target is Character);
|
||||
TargetGap = null;
|
||||
}
|
||||
if (!isInside && (PathSteering.CurrentPath == null || PathSteering.IsPathDirty || PathSteering.CurrentPath.Unreachable))
|
||||
}
|
||||
if (checkScooterTimer <= 0)
|
||||
{
|
||||
useScooter = false;
|
||||
checkScooterTimer = checkScooterTime * Rand.Range(0.75f, 1.25f);
|
||||
Identifier scooterTag = "scooter".ToIdentifier();
|
||||
Identifier batteryTag = "mobilebattery".ToIdentifier();
|
||||
Item scooter = null;
|
||||
bool shouldUseScooter = Mimic && targetCharacter != null && targetCharacter.HasEquippedItem(scooterTag, allowBroken: false);
|
||||
if (!shouldUseScooter)
|
||||
{
|
||||
if (useScooter)
|
||||
float threshold = 500;
|
||||
if (isInside)
|
||||
{
|
||||
UseScooter(Target.WorldPosition);
|
||||
Vector2 diff = Target.WorldPosition - character.WorldPosition;
|
||||
shouldUseScooter = Math.Abs(diff.X) > threshold || Math.Abs(diff.Y) > 150;
|
||||
}
|
||||
else
|
||||
{
|
||||
character.ReleaseSecondaryItem();
|
||||
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(Target.WorldPosition - character.WorldPosition));
|
||||
if (character.AnimController.InWater)
|
||||
shouldUseScooter = Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition) > threshold * threshold;
|
||||
}
|
||||
}
|
||||
if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> equippedScooters, recursive: false, requireEquipped: true))
|
||||
{
|
||||
// Currently equipped scooter
|
||||
scooter = equippedScooters.FirstOrDefault();
|
||||
}
|
||||
else if (shouldUseScooter)
|
||||
{
|
||||
var leftHandItem = character.GetEquippedItem(slotType: InvSlotType.LeftHand);
|
||||
var rightHandItem = character.GetEquippedItem(slotType: InvSlotType.RightHand);
|
||||
bool handsFull =
|
||||
(leftHandItem != null && !character.Inventory.IsAnySlotAvailable(leftHandItem)) ||
|
||||
(rightHandItem != null && !character.Inventory.IsAnySlotAvailable(rightHandItem));
|
||||
if (!handsFull)
|
||||
{
|
||||
bool hasBattery = false;
|
||||
if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> nonEquippedScooters, containedTag: batteryTag, conditionPercentage: 1, requireEquipped: false))
|
||||
{
|
||||
SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 2);
|
||||
// Non-equipped scooter with a battery
|
||||
scooter = nonEquippedScooters.FirstOrDefault();
|
||||
hasBattery = true;
|
||||
}
|
||||
else if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> _nonEquippedScooters, requireEquipped: false))
|
||||
{
|
||||
// Non-equipped scooter without a battery
|
||||
scooter = _nonEquippedScooters.FirstOrDefault();
|
||||
// Non-recursive so that the bots won't take batteries from other items. Also means that they can't find batteries inside containers. Not sure how to solve this.
|
||||
hasBattery = HumanAIController.HasItem(character, batteryTag, out _, requireEquipped: false, conditionPercentage: 1, recursive: false);
|
||||
}
|
||||
if (scooter != null && hasBattery)
|
||||
{
|
||||
// Equip only if we have a battery available
|
||||
HumanAIController.TakeItem(scooter, character.Inventory, equip: true, dropOtherIfCannotMove: false, allowSwapping: true, storeUnequipped: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (useScooter && PathSteering.CurrentPath?.CurrentNode != null)
|
||||
if (scooter != null && character.HasEquippedItem(scooter))
|
||||
{
|
||||
UseScooter(PathSteering.CurrentPath.CurrentNode.WorldPosition);
|
||||
if (shouldUseScooter)
|
||||
{
|
||||
useScooter = true;
|
||||
// Check the battery
|
||||
if (scooter.ContainedItems.None(i => i.Condition > 0))
|
||||
{
|
||||
// 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));
|
||||
if (!scooter.Combine(batteries.OrderByDescending(b => b.Condition).First(), character))
|
||||
{
|
||||
useScooter = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
useScooter = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!useScooter)
|
||||
{
|
||||
// Unequip
|
||||
character.Inventory.TryPutItem(scooter, character, CharacterInventory.AnySlot);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
checkScooterTimer -= deltaTime;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
TargetGap = null;
|
||||
useScooter = false;
|
||||
checkScooterTimer = 0;
|
||||
}
|
||||
if (SteeringManager == PathSteering)
|
||||
{
|
||||
Vector2 targetPos = character.GetRelativeSimPosition(Target);
|
||||
Func<PathNode, bool> nodeFilter = null;
|
||||
if (isInside && !AllowGoingOutside)
|
||||
{
|
||||
nodeFilter = n => n.Waypoint.CurrentHull != null;
|
||||
}
|
||||
else if (!isInside)
|
||||
{
|
||||
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)
|
||||
{
|
||||
SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 15);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
PathSteering.SteeringSeek(targetPos, weight: 1,
|
||||
startNodeFilter: n => (n.Waypoint.CurrentHull == null) == (character.CurrentHull == null),
|
||||
endNodeFilter: endNodeFilter,
|
||||
nodeFilter: nodeFilter,
|
||||
checkVisiblity: Target is Item || Target is Character);
|
||||
}
|
||||
if (!isInside && (PathSteering.CurrentPath == null || PathSteering.IsPathDirty || PathSteering.CurrentPath.Unreachable))
|
||||
{
|
||||
if (useScooter)
|
||||
{
|
||||
@@ -574,13 +581,33 @@ namespace Barotrauma
|
||||
else
|
||||
{
|
||||
character.ReleaseSecondaryItem();
|
||||
SteeringManager.SteeringSeek(character.GetRelativeSimPosition(Target), 10);
|
||||
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(Target.WorldPosition - character.WorldPosition));
|
||||
if (character.AnimController.InWater)
|
||||
{
|
||||
SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 15);
|
||||
SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (useScooter && PathSteering.CurrentPath?.CurrentNode != null)
|
||||
{
|
||||
UseScooter(PathSteering.CurrentPath.CurrentNode.WorldPosition);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (useScooter)
|
||||
{
|
||||
UseScooter(Target.WorldPosition);
|
||||
}
|
||||
else
|
||||
{
|
||||
character.ReleaseSecondaryItem();
|
||||
SteeringManager.SteeringSeek(character.GetRelativeSimPosition(Target), 10);
|
||||
if (character.AnimController.InWater)
|
||||
{
|
||||
SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 15);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UseScooter(Vector2 targetWorldPos)
|
||||
@@ -595,7 +622,7 @@ namespace Barotrauma
|
||||
}
|
||||
Vector2 diff = character.CursorPosition - character.Position;
|
||||
Vector2 dir = Vector2.Normalize(diff);
|
||||
if (character.CurrentHull == null && IsFollowOrderObjective)
|
||||
if (character.CurrentHull == null && IsFollowOrder)
|
||||
{
|
||||
float sqrDist = diff.LengthSquared();
|
||||
if (sqrDist > MathUtils.Pow2(CloseEnough * 1.5f))
|
||||
@@ -674,7 +701,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (gap.Open < 1) { continue; }
|
||||
if (gap.Submarine == null) { continue; }
|
||||
if (!IsFollowOrderObjective)
|
||||
if (!IsFollowOrder)
|
||||
{
|
||||
if (gap.FlowTargetHull == null) { continue; }
|
||||
if (gap.Submarine != Target.Submarine) { continue; }
|
||||
@@ -792,8 +819,10 @@ namespace Barotrauma
|
||||
// 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();
|
||||
if (character.IsClimbing && character.AnimController.IsAboveFloor)
|
||||
{
|
||||
character.StopClimbing();
|
||||
}
|
||||
}
|
||||
base.OnCompleted();
|
||||
}
|
||||
|
||||
+5
-4
@@ -168,7 +168,7 @@ namespace Barotrauma
|
||||
CleanupItems(deltaTime);
|
||||
}
|
||||
|
||||
if (behavior == BehaviorType.StayInHull && TargetHull == null && character.CurrentHull != null)
|
||||
if (behavior == BehaviorType.StayInHull && TargetHull == null && character.CurrentHull != null && !IsForbidden(character.CurrentHull))
|
||||
{
|
||||
TargetHull = character.CurrentHull;
|
||||
}
|
||||
@@ -178,7 +178,7 @@ namespace Barotrauma
|
||||
IsForbidden(currentTarget) ||
|
||||
(PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull)));
|
||||
|
||||
if (behavior == BehaviorType.StayInHull && !currentTargetIsInvalid && !HumanAIController.UnsafeHulls.Contains(TargetHull))
|
||||
if (behavior == BehaviorType.StayInHull && TargetHull != null && !IsForbidden(TargetHull) && !currentTargetIsInvalid && !HumanAIController.UnsafeHulls.Contains(TargetHull))
|
||||
{
|
||||
currentTarget = TargetHull;
|
||||
bool stayInHull = character.CurrentHull == currentTarget && IsSteeringFinished() && !character.IsClimbing;
|
||||
@@ -258,7 +258,8 @@ namespace Barotrauma
|
||||
currentTarget = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced);
|
||||
bool isInWrongSub = (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted) && character.Submarine.TeamID != character.TeamID;
|
||||
bool isCurrentHullAllowed = !isInWrongSub && !IsForbidden(character.CurrentHull);
|
||||
var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, character.Submarine, nodeFilter: node =>
|
||||
Vector2 targetPos = character.GetRelativeSimPosition(currentTarget);
|
||||
var path = PathSteering.PathFinder.FindPath(character.SimPosition, targetPos, character.Submarine, nodeFilter: node =>
|
||||
{
|
||||
if (node.Waypoint.CurrentHull == null) { return false; }
|
||||
// Check that there is no unsafe hulls on the way to the target
|
||||
@@ -278,7 +279,7 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
character.AIController.SelectTarget(currentTarget.AiTarget);
|
||||
PathSteering.SetPath(path);
|
||||
PathSteering.SetPath(targetPos, path);
|
||||
SetTargetTimerNormal();
|
||||
searchingNewHull = false;
|
||||
}
|
||||
|
||||
+1
-1
@@ -98,7 +98,7 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (var item in itemContainer.ContainableItems)
|
||||
{
|
||||
if (CheckStatusEffects(item.statusEffects) == CheckStatus.Finished)
|
||||
if (CheckStatusEffects(item.StatusEffects) == CheckStatus.Finished)
|
||||
{
|
||||
return CheckStatus.Finished;
|
||||
}
|
||||
|
||||
@@ -48,6 +48,8 @@ namespace Barotrauma
|
||||
protected virtual bool ResetWhenClearingIgnoreList => true;
|
||||
protected virtual bool ForceOrderPriority => true;
|
||||
|
||||
protected virtual int MaxTargets => int.MaxValue;
|
||||
|
||||
public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace.CleanupStackTrace()); }
|
||||
|
||||
public override void Update(float deltaTime)
|
||||
@@ -188,6 +190,10 @@ namespace Barotrauma
|
||||
if (!ignoreList.Contains(target))
|
||||
{
|
||||
Targets.Add(target);
|
||||
if (Targets.Count > MaxTargets)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-2
@@ -436,7 +436,7 @@ namespace Barotrauma
|
||||
ExtraDistanceWhileSwimming = 100,
|
||||
AllowGoingOutside = true,
|
||||
IgnoreIfTargetDead = true,
|
||||
IsFollowOrderObjective = true,
|
||||
IsFollowOrder = true,
|
||||
Mimic = character.IsOnPlayerTeam,
|
||||
DialogueIdentifier = "dialogcannotreachplace".ToIdentifier()
|
||||
};
|
||||
@@ -444,7 +444,11 @@ namespace Barotrauma
|
||||
case "wait":
|
||||
newObjective = new AIObjectiveGoTo(order.TargetSpatialEntity ?? character, character, this, repeat: true, priorityModifier: priorityModifier)
|
||||
{
|
||||
AllowGoingOutside = true
|
||||
AllowGoingOutside = true,
|
||||
IsWaitOrder = true,
|
||||
DebugLogWhenFails = false,
|
||||
SpeakIfFails = false,
|
||||
CloseEnough = 100
|
||||
};
|
||||
break;
|
||||
case "return":
|
||||
|
||||
+48
-12
@@ -13,6 +13,8 @@ namespace Barotrauma
|
||||
public override bool AllowOutsideSubmarine => true;
|
||||
public override bool AllowInAnySub => true;
|
||||
|
||||
private readonly HashSet<Character> charactersWithMinorInjuries = new HashSet<Character>();
|
||||
|
||||
private const float vitalityThreshold = 75;
|
||||
private const float vitalityThresholdForOrders = 90;
|
||||
public static float GetVitalityThreshold(AIObjectiveManager manager, Character character, Character target)
|
||||
@@ -23,17 +25,34 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
// When targeting player characters, always treat them when ordered, else use the threshold so that minor/non-severe damage is ignored.
|
||||
// If we ignore any damage when the player orders a bot to do healings, it's observed to cause confusion among the players.
|
||||
// On the other hand, if the bots too eagerly heal characters when it's not necessary, it's inefficient and can feel frustrating, because it can't be controlled.
|
||||
return character == target || manager.HasOrder<AIObjectiveRescueAll>() ? (target.IsPlayer && target.HealthPercentage < 100 ? 100 : vitalityThresholdForOrders) : vitalityThreshold;
|
||||
return character == target || manager.HasOrder<AIObjectiveRescueAll>() ? vitalityThresholdForOrders : vitalityThreshold;
|
||||
}
|
||||
}
|
||||
|
||||
public AIObjectiveRescueAll(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
|
||||
|
||||
public AIObjectiveRescueAll(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
|
||||
: base(character, objectiveManager, priorityModifier) { }
|
||||
|
||||
protected override bool Filter(Character target) => IsValidTarget(target, character);
|
||||
protected override bool Filter(Character target)
|
||||
{
|
||||
if (!IsValidTarget(target, character, requireTreatableAfflictions: false)) { return false; }
|
||||
if (GetTreatableAfflictions(target).Any())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
//the target might be at a low enough health to be considered a valid target,
|
||||
//but if all afflictions are below treatment thresholds, the bot won't (and shouldn't) treat them
|
||||
// -> make the bot speak to make it clear the bot intentionally ignores very minor injuries
|
||||
if (!charactersWithMinorInjuries.Contains(character))
|
||||
{
|
||||
character.Speak(TextManager.GetWithVariable("dialogignoreminorinjuries", "[targetname]", target.Name).Value,
|
||||
null, 1.0f, $"notreatableafflictions{target.Name}".ToIdentifier(), 10.0f);
|
||||
charactersWithMinorInjuries.Add(character);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected override IEnumerable<Character> GetList() => Character.CharacterList;
|
||||
|
||||
@@ -68,7 +87,7 @@ namespace Barotrauma
|
||||
float vitality = 100;
|
||||
vitality -= character.Bleeding * 2;
|
||||
vitality += Math.Min(character.Oxygen, 0);
|
||||
foreach (Affliction affliction in GetTreatableAfflictions(character))
|
||||
foreach (Affliction affliction in GetTreatableAfflictions(character, ignoreTreatmentThreshold: true))
|
||||
{
|
||||
float strength = character.CharacterHealth.GetPredictedStrength(affliction, predictFutureDuration: 10.0f);
|
||||
vitality -= affliction.GetVitalityDecrease(character.CharacterHealth, strength) / character.MaxVitality * 100;
|
||||
@@ -84,12 +103,19 @@ namespace Barotrauma
|
||||
return Math.Clamp(vitality, 0, 100);
|
||||
}
|
||||
|
||||
public static IEnumerable<Affliction> GetTreatableAfflictions(Character character)
|
||||
public static IEnumerable<Affliction> GetTreatableAfflictions(Character character, bool ignoreTreatmentThreshold = false)
|
||||
{
|
||||
var allAfflictions = character.CharacterHealth.GetAllAfflictions();
|
||||
foreach (Affliction affliction in allAfflictions)
|
||||
{
|
||||
if (affliction.Prefab.IsBuff || affliction.Strength < affliction.Prefab.TreatmentThreshold) { continue; }
|
||||
if (affliction.Prefab.IsBuff) { continue; }
|
||||
if (!ignoreTreatmentThreshold)
|
||||
{
|
||||
//other afflictions of the same type increase the "treatability"
|
||||
// e.g. we might want to ignore burns below 5%, but not if the character has them on all limbs
|
||||
float totalAfflictionStrength = character.CharacterHealth.GetTotalAdjustedAfflictionStrength(affliction);
|
||||
if (totalAfflictionStrength < affliction.Prefab.TreatmentThreshold) { continue; }
|
||||
}
|
||||
if (affliction.Prefab.TreatmentSuitability.None(kvp => kvp.Value > 0)) { continue; }
|
||||
if (allAfflictions.Any(otherAffliction => affliction.Prefab.IgnoreTreatmentIfAfflictedBy.Contains(otherAffliction.Identifier))) { continue; }
|
||||
yield return affliction;
|
||||
@@ -102,7 +128,7 @@ namespace Barotrauma
|
||||
protected override void OnObjectiveCompleted(AIObjective objective, Character target)
|
||||
=> HumanAIController.RemoveTargets<AIObjectiveRescueAll, Character>(character, target);
|
||||
|
||||
public static bool IsValidTarget(Character target, Character character)
|
||||
public static bool IsValidTarget(Character target, Character character, bool requireTreatableAfflictions = true)
|
||||
{
|
||||
if (target == null || target.IsDead || target.Removed) { return false; }
|
||||
if (target.IsInstigator) { return false; }
|
||||
@@ -112,7 +138,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (GetVitalityFactor(target) >= GetVitalityThreshold(humanAI.ObjectiveManager, character, target))
|
||||
{
|
||||
return false;
|
||||
return false;
|
||||
}
|
||||
if (!humanAI.ObjectiveManager.HasOrder<AIObjectiveRescueAll>())
|
||||
{
|
||||
@@ -127,6 +153,10 @@ namespace Barotrauma
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (requireTreatableAfflictions && GetTreatableAfflictions(target).None())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -159,5 +189,11 @@ namespace Barotrauma
|
||||
}
|
||||
return character.GetDamageDoneByAttacker(target) <= 0;
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
base.Reset();
|
||||
charactersWithMinorInjuries.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsRemotePlayer && !(AIController is HumanAIController))
|
||||
if (!IsRemotePlayer && AIController is not HumanAIController)
|
||||
{
|
||||
float characterDistSqr = GetDistanceSqrToClosestPlayer();
|
||||
if (characterDistSqr > MathUtils.Pow2(Params.DisableDistance * 0.5f))
|
||||
@@ -63,6 +63,10 @@ namespace Barotrauma
|
||||
AnimController.SimplePhysicsEnabled = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AnimController.SimplePhysicsEnabled = false;
|
||||
}
|
||||
|
||||
if (GameMain.NetworkMember != null && !GameMain.NetworkMember.IsServer) { return; }
|
||||
if (Controlled == this) { return; }
|
||||
|
||||
+13
-10
@@ -1,10 +1,10 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.Networking;
|
||||
using FarseerPhysics;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Networking;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -1550,6 +1550,10 @@ namespace Barotrauma
|
||||
target.AnimController.ResetPullJoints();
|
||||
}
|
||||
|
||||
bool targetPoseControlled =
|
||||
target.SelectedItem?.GetComponent<Controller>() is { ControlCharacterPose: true } ||
|
||||
target.SelectedSecondaryItem?.GetComponent<Controller>() is { ControlCharacterPose: true };
|
||||
|
||||
if (IsClimbing)
|
||||
{
|
||||
//cannot drag up ladders if the character is conscious
|
||||
@@ -1725,13 +1729,12 @@ namespace Barotrauma
|
||||
targetForce = 5000.0f;
|
||||
}
|
||||
|
||||
targetLimb.PullJointEnabled = true;
|
||||
targetLimb.PullJointMaxForce = targetForce;
|
||||
targetLimb.PullJointWorldAnchorB = targetAnchor;
|
||||
targetLimb.Disabled = true;
|
||||
|
||||
if (diff.LengthSquared() > 0.1f)
|
||||
if (!targetPoseControlled)
|
||||
{
|
||||
targetLimb.PullJointEnabled = true;
|
||||
targetLimb.PullJointMaxForce = targetForce;
|
||||
targetLimb.PullJointWorldAnchorB = targetAnchor;
|
||||
targetLimb.Disabled = true;
|
||||
target.AnimController.movement = -diff;
|
||||
}
|
||||
}
|
||||
@@ -1757,7 +1760,7 @@ namespace Barotrauma
|
||||
target.AnimController.IgnorePlatforms = IgnorePlatforms;
|
||||
target.AnimController.TargetMovement = TargetMovement;
|
||||
}
|
||||
else if (target is AICharacter && target != Character.Controlled)
|
||||
else if (target is AICharacter && target != Character.Controlled && !targetPoseControlled)
|
||||
{
|
||||
if (target.AnimController.Dir > 0 == WorldPosition.X > target.WorldPosition.X)
|
||||
{
|
||||
|
||||
@@ -1205,7 +1205,7 @@ namespace Barotrauma
|
||||
{
|
||||
inWater = false;
|
||||
headInWater = false;
|
||||
RefreshFloorY(ignoreStairs: Stairs == null);
|
||||
RefreshFloorY(deltaTime, ignoreStairs: Stairs == null);
|
||||
}
|
||||
//ragdoll isn't in any room -> it's in the water
|
||||
else if (currentHull == null)
|
||||
@@ -1217,7 +1217,7 @@ namespace Barotrauma
|
||||
{
|
||||
headInWater = false;
|
||||
inWater = false;
|
||||
RefreshFloorY(ignoreStairs: Stairs == null);
|
||||
RefreshFloorY(deltaTime, ignoreStairs: Stairs == null);
|
||||
if (currentHull.WaterPercentage > 0.001f)
|
||||
{
|
||||
(float waterSurfaceDisplayUnits, float ceilingDisplayUnits) = GetWaterSurfaceAndCeilingY();
|
||||
@@ -1562,15 +1562,24 @@ namespace Barotrauma
|
||||
lastFloorCheckPos = Vector2.Zero;
|
||||
}
|
||||
|
||||
private void RefreshFloorY(Limb refLimb = null, bool ignoreStairs = false)
|
||||
// Force check floor y at least once a second so that we'll drop through gaps that we are standing upon.
|
||||
private const float FloorYStaleTime = 1;
|
||||
private float floorYCheckTimer;
|
||||
private void RefreshFloorY(float deltaTime, Limb refLimb = null, bool ignoreStairs = false)
|
||||
{
|
||||
floorYCheckTimer -= deltaTime;
|
||||
PhysicsBody refBody = refLimb == null ? Collider : refLimb.body;
|
||||
if (Vector2.DistanceSquared(lastFloorCheckPos, refBody.SimPosition) > 0.1f * 0.1f || lastFloorCheckIgnoreStairs != ignoreStairs || lastFloorCheckIgnorePlatforms != IgnorePlatforms)
|
||||
if (floorYCheckTimer < 0 ||
|
||||
lastFloorCheckIgnoreStairs != ignoreStairs ||
|
||||
lastFloorCheckIgnorePlatforms != IgnorePlatforms ||
|
||||
Vector2.DistanceSquared(lastFloorCheckPos, refBody.SimPosition) > 0.1f * 0.1f)
|
||||
{
|
||||
floorY = GetFloorY(refBody.SimPosition, ignoreStairs);
|
||||
lastFloorCheckPos = refBody.SimPosition;
|
||||
lastFloorCheckIgnoreStairs = ignoreStairs;
|
||||
lastFloorCheckIgnorePlatforms = IgnorePlatforms;
|
||||
// Add some randomness to prevent all stationary characters doing the checks at the same frame.
|
||||
floorYCheckTimer = FloorYStaleTime * Rand.Range(0.9f, 1.1f);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1854,6 +1863,7 @@ namespace Barotrauma
|
||||
|
||||
|
||||
private bool collisionsDisabled;
|
||||
private double lastObstacleRayCastTime;
|
||||
|
||||
protected void CheckDistFromCollider()
|
||||
{
|
||||
@@ -1861,15 +1871,28 @@ namespace Barotrauma
|
||||
allowedDist = Math.Max(allowedDist, 1.0f);
|
||||
float resetDist = allowedDist * 5.0f;
|
||||
|
||||
float obstacleCheckDist = 0.3f;
|
||||
|
||||
Vector2 diff = Collider.SimPosition - MainLimb.SimPosition;
|
||||
float distSqrd = diff.LengthSquared();
|
||||
|
||||
if (distSqrd > resetDist * resetDist)
|
||||
bool shouldReset = distSqrd > resetDist * resetDist;
|
||||
if (!shouldReset && distSqrd > obstacleCheckDist * obstacleCheckDist)
|
||||
{
|
||||
if (Timing.TotalTime > lastObstacleRayCastTime + 1 &&
|
||||
Submarine.PickBody(Collider.SimPosition, MainLimb.SimPosition, collisionCategory: Physics.CollisionWall) != null)
|
||||
{
|
||||
shouldReset = true;
|
||||
lastObstacleRayCastTime = Timing.TotalTime;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldReset)
|
||||
{
|
||||
//ragdoll way too far, reset position
|
||||
SetPosition(Collider.SimPosition, lerp: true, forceMainLimbToCollider: true);
|
||||
}
|
||||
if (distSqrd > allowedDist * allowedDist)
|
||||
else if (distSqrd > allowedDist * allowedDist)
|
||||
{
|
||||
//ragdoll too far from the collider, disable collisions until it's close enough
|
||||
//(in case the ragdoll has gotten stuck somewhere)
|
||||
@@ -1891,7 +1914,7 @@ namespace Barotrauma
|
||||
collisionsDisabled = false;
|
||||
//force collision categories to be updated
|
||||
prevCollisionCategory = Category.None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
partial void UpdateNetPlayerPositionProjSpecific(float deltaTime, float lowestSubPos);
|
||||
|
||||
@@ -1068,7 +1068,7 @@ namespace Barotrauma
|
||||
{
|
||||
get
|
||||
{
|
||||
return SelectedItem == null || (SelectedItem.GetComponent<Controller>()?.AllowAiming ?? false);
|
||||
return (SelectedItem == null || SelectedItem.GetComponent<Controller>() is { AllowAiming: true }) && !IsIncapacitated && !IsRagdolled;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1131,6 +1131,7 @@ namespace Barotrauma
|
||||
public HashSet<Identifier> MarkedAsLooted = new();
|
||||
|
||||
public bool IsInFriendlySub => Submarine != null && Submarine.TeamID == TeamID;
|
||||
public bool IsInPlayerSub => Submarine != null && Submarine.Info.IsPlayer;
|
||||
|
||||
public float AITurretPriority
|
||||
{
|
||||
@@ -2710,27 +2711,6 @@ namespace Barotrauma
|
||||
CustomInteractHUDText = hudText;
|
||||
}
|
||||
|
||||
private void TransformCursorPos()
|
||||
{
|
||||
if (Submarine == null)
|
||||
{
|
||||
//character is outside but cursor position inside
|
||||
if (cursorPosition.Y > Level.Loaded.Size.Y)
|
||||
{
|
||||
var sub = Submarine.FindContaining(cursorPosition);
|
||||
if (sub != null) cursorPosition += sub.Position;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//character is inside but cursor position is outside
|
||||
if (cursorPosition.Y < Level.Loaded.Size.Y)
|
||||
{
|
||||
cursorPosition -= Submarine.Position;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SelectCharacter(Character character)
|
||||
{
|
||||
if (character == null || character == this) { return; }
|
||||
@@ -4335,7 +4315,7 @@ namespace Barotrauma
|
||||
if (statusEffect.type == ActionType.OnDamaged)
|
||||
{
|
||||
if (!statusEffect.HasRequiredAfflictions(LastDamage)) { continue; }
|
||||
if (statusEffect.OnlyPlayerTriggered)
|
||||
if (statusEffect.OnlyWhenDamagedByPlayer)
|
||||
{
|
||||
if (LastAttacker == null || !LastAttacker.IsPlayer)
|
||||
{
|
||||
@@ -4393,6 +4373,10 @@ namespace Barotrauma
|
||||
{
|
||||
statusEffect.Apply(actionType, deltaTime, this, this);
|
||||
}
|
||||
if (statusEffect.HasTargetType(StatusEffect.TargetType.Hull) && CurrentHull != null)
|
||||
{
|
||||
statusEffect.Apply(actionType, deltaTime, this, CurrentHull);
|
||||
}
|
||||
}
|
||||
if (actionType != ActionType.OnDamaged && actionType != ActionType.OnSevered)
|
||||
{
|
||||
@@ -4670,6 +4654,13 @@ namespace Barotrauma
|
||||
|
||||
CharacterList.Remove(this);
|
||||
|
||||
foreach (var attachedProjectile in AttachedProjectiles.ToList())
|
||||
{
|
||||
attachedProjectile.Unstick();
|
||||
}
|
||||
Latchers.ForEachMod(l => l?.DeattachFromBody(reset: true));
|
||||
Latchers.Clear();
|
||||
|
||||
if (Inventory != null)
|
||||
{
|
||||
foreach (Item item in Inventory.AllItems)
|
||||
@@ -4756,6 +4747,8 @@ namespace Barotrauma
|
||||
}
|
||||
#if SERVER
|
||||
newItem.GetComponent<Terminal>()?.SyncHistory();
|
||||
if (newItem.GetComponent<WifiComponent>() is WifiComponent wifiComponent) { newItem.CreateServerEvent(wifiComponent); }
|
||||
if (newItem.GetComponent<GeneticMaterial>() is GeneticMaterial geneticMaterial) { newItem.CreateServerEvent(geneticMaterial); }
|
||||
#endif
|
||||
int[] slotIndices = itemElement.GetAttributeIntArray("i", new int[] { 0 });
|
||||
if (!slotIndices.Any())
|
||||
|
||||
@@ -386,6 +386,8 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (AfflictionPrefab.PeriodicEffect periodicEffect in Prefab.PeriodicEffects)
|
||||
{
|
||||
if (Strength <= periodicEffect.MinStrength) { continue; }
|
||||
if (periodicEffect.MaxStrength > 0 && Strength > periodicEffect.MaxStrength) { continue; }
|
||||
PeriodicEffectTimers[periodicEffect] -= deltaTime;
|
||||
if (PeriodicEffectTimers[periodicEffect] <= 0.0f)
|
||||
{
|
||||
@@ -498,6 +500,13 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public void SetStrength(float strength)
|
||||
{
|
||||
if (!MathUtils.IsValid(strength))
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.ThrowError($"Attempted to set an affliction to an invalid strength ({strength})\n" + Environment.StackTrace.CleanupStackTrace());
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
_nonClampedStrength = strength;
|
||||
_strength = _nonClampedStrength;
|
||||
activeEffectDirty |= !MathUtils.NearlyEqual(_strength, prevActiveEffectStrength);
|
||||
|
||||
+4
-4
@@ -730,13 +730,13 @@ namespace Barotrauma
|
||||
/// <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;
|
||||
public readonly float ShowInHealthScannerThreshold;
|
||||
|
||||
/// <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;
|
||||
public readonly float TreatmentThreshold;
|
||||
|
||||
/// <summary>
|
||||
/// Bots will not try to treat the affliction if the character has any of these afflictions
|
||||
@@ -847,7 +847,7 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (var itemPrefab in ItemPrefab.Prefabs)
|
||||
{
|
||||
float suitability = Math.Max(itemPrefab.GetTreatmentSuitability(Identifier), itemPrefab.GetTreatmentSuitability(AfflictionType));
|
||||
float suitability = itemPrefab.GetTreatmentSuitability(Identifier) + itemPrefab.GetTreatmentSuitability(AfflictionType);
|
||||
if (!MathUtils.NearlyEqual(suitability, 0.0f))
|
||||
{
|
||||
yield return new KeyValuePair<Identifier, float>(itemPrefab.Identifier, suitability);
|
||||
@@ -915,7 +915,7 @@ namespace Barotrauma
|
||||
|
||||
ShowInHealthScannerThreshold = element.GetAttributeFloat(nameof(ShowInHealthScannerThreshold),
|
||||
Math.Max(ActivationThreshold, AfflictionType == "talentbuff" ? float.MaxValue : ShowIconToOthersThreshold));
|
||||
TreatmentThreshold = element.GetAttributeFloat(nameof(TreatmentThreshold), Math.Max(ActivationThreshold, 5.0f));
|
||||
TreatmentThreshold = element.GetAttributeFloat(nameof(TreatmentThreshold), Math.Max(ActivationThreshold, 10.0f));
|
||||
|
||||
DamageOverlayAlpha = element.GetAttributeFloat(nameof(DamageOverlayAlpha), 0.0f);
|
||||
BurnOverlayAlpha = element.GetAttributeFloat(nameof(BurnOverlayAlpha), 0.0f);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Abilities;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Networking;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
@@ -422,9 +423,13 @@ namespace Barotrauma
|
||||
return strength;
|
||||
}
|
||||
|
||||
public void ApplyAffliction(Limb targetLimb, Affliction affliction, bool allowStacking = true)
|
||||
public void ApplyAffliction(Limb targetLimb, Affliction affliction, bool allowStacking = true, bool ignoreUnkillability = false)
|
||||
{
|
||||
if (!affliction.Prefab.IsBuff && Unkillable || Character.GodMode) { return; }
|
||||
if (Character.GodMode) { return; }
|
||||
if (!ignoreUnkillability)
|
||||
{
|
||||
if (!affliction.Prefab.IsBuff && Unkillable) { return; }
|
||||
}
|
||||
if (affliction.Prefab.LimbSpecific)
|
||||
{
|
||||
if (targetLimb == null)
|
||||
@@ -455,11 +460,7 @@ namespace Barotrauma
|
||||
var affliction = kvp.Key;
|
||||
resistance += affliction.GetResistance(afflictionPrefab.Identifier);
|
||||
}
|
||||
|
||||
resistance = 1 - ((1 - resistance) * Character.GetAbilityResistance(afflictionPrefab));
|
||||
if (resistance > 1f) { resistance = 1f; }
|
||||
|
||||
return resistance;
|
||||
return 1 - ((1 - resistance) * Character.GetAbilityResistance(afflictionPrefab));
|
||||
}
|
||||
|
||||
public float GetStatValue(StatTypes statType)
|
||||
@@ -990,7 +991,7 @@ namespace Barotrauma
|
||||
#endif
|
||||
}
|
||||
|
||||
private float GetVitalityMultiplier(Affliction affliction, LimbHealth limbHealth)
|
||||
private static float GetVitalityMultiplier(Affliction affliction, LimbHealth limbHealth)
|
||||
{
|
||||
float multiplier = 1.0f;
|
||||
if (limbHealth.VitalityMultipliers.TryGetValue(affliction.Prefab.Identifier, out float vitalityMultiplier))
|
||||
@@ -1132,7 +1133,11 @@ namespace Barotrauma
|
||||
strength = GetPredictedStrength(affliction, predictFutureDuration, limb);
|
||||
}
|
||||
|
||||
if (strength <= affliction.Prefab.TreatmentThreshold) { continue; }
|
||||
//other afflictions of the same type increase the "treatability"
|
||||
// e.g. we might want to ignore burns below 5%, but not if the character has them on all limbs
|
||||
float totalAfflictionStrength = strength + GetTotalAdjustedAfflictionStrength(affliction, includeSameAffliction: false);
|
||||
if (totalAfflictionStrength < affliction.Prefab.TreatmentThreshold) { continue; }
|
||||
|
||||
if (afflictions.Any(otherAffliction => affliction.Prefab.IgnoreTreatmentIfAfflictedBy.Contains(otherAffliction.Key.Identifier))) { continue; }
|
||||
|
||||
if (ignoreHiddenAfflictions)
|
||||
@@ -1149,13 +1154,20 @@ namespace Barotrauma
|
||||
|
||||
foreach (KeyValuePair<Identifier, float> treatment in affliction.Prefab.TreatmentSuitability)
|
||||
{
|
||||
float suitability = treatment.Value * strength;
|
||||
if (treatment.Value > strength)
|
||||
{
|
||||
//avoid using very effective meds on small injuries
|
||||
float overtreatmentFactor = MathHelper.Clamp(treatment.Value / strength, 1.0f, 10.0f);
|
||||
suitability /= overtreatmentFactor;
|
||||
}
|
||||
if (!treatmentSuitability.ContainsKey(treatment.Key))
|
||||
{
|
||||
treatmentSuitability[treatment.Key] = treatment.Value * strength;
|
||||
treatmentSuitability[treatment.Key] = suitability;
|
||||
}
|
||||
else
|
||||
{
|
||||
treatmentSuitability[treatment.Key] += treatment.Value * strength;
|
||||
treatmentSuitability[treatment.Key] += suitability;
|
||||
}
|
||||
minSuitability = Math.Min(treatmentSuitability[treatment.Key], minSuitability);
|
||||
maxSuitability = Math.Max(treatmentSuitability[treatment.Key], maxSuitability);
|
||||
@@ -1171,6 +1183,28 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the total strength of instances of the same affliction on all the characters limbs,
|
||||
/// with a smaller weight given to the other afflictions on other limbs
|
||||
/// </summary>
|
||||
/// <param name="otherAfflictionMultiplier">Multiplier on the strengths of the afflictions on other limbs.</param>
|
||||
/// <param name="includeSameAffliction">Should the strength of the provided affliction be included too?</param>
|
||||
public float GetTotalAdjustedAfflictionStrength(Affliction affliction, float otherAfflictionMultiplier = 0.3f, bool includeSameAffliction = true)
|
||||
{
|
||||
float totalAfflictionStrength = includeSameAffliction ? affliction.Strength : 0;
|
||||
if (affliction.Prefab.LimbSpecific)
|
||||
{
|
||||
foreach (Affliction otherAffliction in afflictions.Keys)
|
||||
{
|
||||
if (affliction.Prefab == otherAffliction.Prefab && affliction != otherAffliction)
|
||||
{
|
||||
totalAfflictionStrength += otherAffliction.Strength * otherAfflictionMultiplier;
|
||||
}
|
||||
}
|
||||
}
|
||||
return totalAfflictionStrength;
|
||||
}
|
||||
|
||||
private readonly HashSet<Identifier> afflictionTags = new HashSet<Identifier>();
|
||||
public IEnumerable<Identifier> GetActiveAfflictionTags()
|
||||
{
|
||||
|
||||
@@ -182,7 +182,9 @@ namespace Barotrauma
|
||||
{
|
||||
humanAI.ObjectiveManager.SetForcedOrder(new AIObjectiveGoTo(positionToStayIn, npc, humanAI.ObjectiveManager, repeat: true, getDivingGearIfNeeded: false, closeEnough: 200)
|
||||
{
|
||||
DebugLogWhenFails = false
|
||||
DebugLogWhenFails = false,
|
||||
IsWaitOrder = true,
|
||||
CloseEnough = 100
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1226,7 +1226,7 @@ namespace Barotrauma
|
||||
if (statusEffect.type == ActionType.OnDamaged)
|
||||
{
|
||||
if (!statusEffect.HasRequiredAfflictions(character.LastDamage)) { continue; }
|
||||
if (statusEffect.OnlyPlayerTriggered)
|
||||
if (statusEffect.OnlyWhenDamagedByPlayer)
|
||||
{
|
||||
if (character.LastAttacker == null || !character.LastAttacker.IsPlayer)
|
||||
{
|
||||
|
||||
@@ -231,12 +231,17 @@ namespace Barotrauma
|
||||
protected void CreateSubParams()
|
||||
{
|
||||
SubParams.Clear();
|
||||
var health = MainElement.GetChildElement("health");
|
||||
if (health != null)
|
||||
var healthElement = MainElement.GetChildElement("health");
|
||||
if (healthElement != null)
|
||||
{
|
||||
Health = new HealthParams(health, this);
|
||||
SubParams.Add(Health);
|
||||
Health = new HealthParams(healthElement, this);
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.ThrowError($"No health parameters defined for character \"{(SpeciesName)}\".");
|
||||
Health = new HealthParams(null, this);
|
||||
}
|
||||
SubParams.Add(Health);
|
||||
// TODO: support for multiple ai elements?
|
||||
var ai = MainElement.GetChildElement("ai");
|
||||
if (ai != null)
|
||||
|
||||
+7
-2
@@ -29,7 +29,7 @@ namespace Barotrauma.Abilities
|
||||
nearbyCharactersAppliesToEnemies = abilityElement.GetAttributeBool("nearbycharactersappliestoenemies", true);
|
||||
}
|
||||
|
||||
protected void ApplyEffectSpecific(Character targetCharacter)
|
||||
protected void ApplyEffectSpecific(Character targetCharacter, Limb targetLimb = null)
|
||||
{
|
||||
//prevent an infinite loop if an effect triggers itself
|
||||
//(e.g. a talent that triggers when an affliction is applied, and applies that same affliction)
|
||||
@@ -66,6 +66,11 @@ namespace Barotrauma.Abilities
|
||||
statusEffect.SetUser(Character);
|
||||
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, targetCharacter, targets);
|
||||
}
|
||||
else if (statusEffect.HasTargetType(StatusEffect.TargetType.Limb) && targetLimb != null)
|
||||
{
|
||||
statusEffect.SetUser(Character);
|
||||
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, targetLimb);
|
||||
}
|
||||
else if (statusEffect.HasTargetType(StatusEffect.TargetType.Character))
|
||||
{
|
||||
statusEffect.SetUser(Character);
|
||||
@@ -99,7 +104,7 @@ namespace Barotrauma.Abilities
|
||||
{
|
||||
if ((abilityObject as IAbilityCharacter)?.Character is Character targetCharacter && !applyToSelf)
|
||||
{
|
||||
ApplyEffectSpecific(targetCharacter);
|
||||
ApplyEffectSpecific(targetCharacter, targetLimb: (abilityObject as AbilityApplyTreatment)?.TargetLimb);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
+5
-7
@@ -22,15 +22,13 @@
|
||||
{
|
||||
foreach (Identifier afflictionIdentifier in afflictionIdentifiers)
|
||||
{
|
||||
if (affliction.Identifier != afflictionIdentifier) { continue; }
|
||||
affliction.Strength *= 1 + addedMultiplier;
|
||||
if (affliction.Identifier != afflictionIdentifier) { continue; }
|
||||
AfflictionPrefab afflictionPrefab = affliction.Prefab;
|
||||
if (!replaceWith.IsEmpty)
|
||||
{
|
||||
if (AfflictionPrefab.Prefabs.TryGet(replaceWith, out AfflictionPrefab afflictionPrefab))
|
||||
{
|
||||
abilityAffliction.Affliction = new Affliction(afflictionPrefab, abilityAffliction.Affliction.Strength);
|
||||
}
|
||||
}
|
||||
AfflictionPrefab.Prefabs.TryGet(replaceWith, out afflictionPrefab);
|
||||
}
|
||||
abilityAffliction.Affliction = new Affliction(afflictionPrefab, affliction.Strength * (1 + addedMultiplier));
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
Reference in New Issue
Block a user