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

This commit is contained in:
EvilFactory
2024-06-18 12:19:13 -03:00
263 changed files with 7788 additions and 2849 deletions
@@ -184,6 +184,20 @@ namespace Barotrauma
}
}
/// <summary>
/// Is some condition met (e.g. entity null, indetectable, outside level) that prevents anyone from detecting the target?
/// </summary>
public bool ShouldBeIgnored()
{
if (InDetectable) { return true; }
if (Entity == null) { return true; }
if (Level.Loaded != null && WorldPosition.Y > Level.Loaded.Size.Y)
{
return true;
}
return false;
}
public AITarget(Entity e, XElement element) : this(e)
{
SightRange = element.GetAttributeFloat("sightrange", 0.0f);
@@ -216,7 +216,7 @@ namespace Barotrauma
private bool IsAttackingOwner(Character other) =>
PetBehavior != null && PetBehavior.Owner != null &&
!other.IsUnconscious && !other.IsArrested &&
!other.IsUnconscious && !other.IsHandcuffed &&
other.AIController is HumanAIController humanAI &&
humanAI.ObjectiveManager.CurrentObjective is AIObjectiveCombat combat &&
combat.Enemy != null && combat.Enemy == PetBehavior.Owner;
@@ -2694,13 +2694,8 @@ namespace Barotrauma
float maxModifier = 5;
foreach (AITarget aiTarget in AITarget.List)
{
if (aiTarget.InDetectable) { continue; }
if (aiTarget.Entity == null) { continue; }
if (aiTarget.ShouldBeIgnored()) { continue; }
if (ignoredTargets.Contains(aiTarget)) { continue; }
if (Level.Loaded != null && aiTarget.WorldPosition.Y > Level.Loaded.Size.Y)
{
continue;
}
if (aiTarget.Type == AITarget.TargetType.HumanOnly) { continue; }
if (!TargetOutposts)
{
@@ -67,6 +67,12 @@ namespace Barotrauma
private readonly float reportProblemsInterval = 1.0f;
private float reportProblemsTimer;
/// <summary>
/// Affects how far the character can hear sounds created by AI targets with the tag ProvocativeToHumanAI.
/// Used as a multiplier on the sound range of the target, e.g. a value of 0.5 would mean a target with a sound range of 1000 would need to be within 500 units for this character to hear it.
/// Only affects the "fight intruders" objective, which makes the character go and inspect noises.
/// </summary>
public float Hearing { get; set; } = 1.0f;
/// <summary>
/// How far other characters can hear reports done by this character (e.g. reports for fires, intruders). Defaults to infinity.
@@ -339,39 +345,8 @@ namespace Barotrauma
enemyCheckTimer -= deltaTime;
if (enemyCheckTimer < 0)
{
CheckEnemies();
enemyCheckTimer = enemyCheckInterval * Rand.Range(0.75f, 1.25f);
if (!objectiveManager.IsCurrentObjective<AIObjectiveCombat>())
{
float closestDistance = 0;
Character closestEnemy = null;
foreach (Character c in Character.CharacterList)
{
if (c.Submarine != Character.Submarine) { continue; }
if (c.Removed || c.IsDead || c.IsIncapacitated) { continue; }
if (IsFriendly(c)) { continue; }
Vector2 toTarget = c.WorldPosition - WorldPosition;
float dist = toTarget.LengthSquared();
float maxDistance = Character.Submarine == null ? enemySpotDistanceOutside : enemySpotDistanceInside;
if (dist > maxDistance * maxDistance) { continue; }
if (EnemyAIController.IsLatchedToSomeoneElse(c, Character)) { continue; }
var head = Character.AnimController.GetLimb(LimbType.Head);
if (head == null) { continue; }
float rotation = head.body.TransformedRotation;
Vector2 forward = VectorExtensions.Forward(rotation);
float angle = MathHelper.ToDegrees(VectorExtensions.Angle(toTarget, forward));
if (angle > 70) { continue; }
if (!Character.CanSeeTarget(c)) { continue; }
if (dist < closestDistance || closestEnemy == null)
{
closestEnemy = c;
closestDistance = dist;
}
}
if (closestEnemy != null)
{
AddCombatObjective(AIObjectiveCombat.CombatMode.Defensive, closestEnemy);
}
}
}
}
bool useInsideSteering = !isOutside || isBlocked || HasValidPath() || IsCloseEnoughToTarget(steeringBuffer);
@@ -586,6 +561,42 @@ namespace Barotrauma
ShipCommandManager?.Update(deltaTime);
}
private void CheckEnemies()
{
//already in combat, no need to check
if (objectiveManager.IsCurrentObjective<AIObjectiveCombat>()) { return; }
float closestDistance = 0;
Character closestEnemy = null;
foreach (Character c in Character.CharacterList)
{
if (c.Submarine != Character.Submarine) { continue; }
if (c.Removed || c.IsDead || c.IsIncapacitated) { continue; }
if (IsFriendly(c)) { continue; }
Vector2 toTarget = c.WorldPosition - WorldPosition;
float dist = toTarget.LengthSquared();
float maxDistance = Character.Submarine == null ? enemySpotDistanceOutside : enemySpotDistanceInside;
if (dist > maxDistance * maxDistance) { continue; }
if (EnemyAIController.IsLatchedToSomeoneElse(c, Character)) { continue; }
var head = Character.AnimController.GetLimb(LimbType.Head);
if (head == null) { continue; }
float rotation = head.body.TransformedRotation;
Vector2 forward = VectorExtensions.Forward(rotation);
float angle = MathHelper.ToDegrees(VectorExtensions.Angle(toTarget, forward));
if (angle > 70) { continue; }
if (!Character.CanSeeTarget(c)) { continue; }
if (dist < closestDistance || closestEnemy == null)
{
closestEnemy = c;
closestDistance = dist;
}
}
if (closestEnemy != null)
{
AddCombatObjective(AIObjectiveCombat.CombatMode.Defensive, closestEnemy);
}
}
private void UnequipUnnecessaryItems()
{
if (Character.LockHands) { return; }
@@ -632,7 +643,9 @@ namespace Barotrauma
isCurrentObjectiveFindSafety ||
Character.AnimController.InWater ||
Character.AnimController.HeadInWater ||
Character.IsClimbing ||
Character.Submarine == null ||
Character.Submarine.Info.HasTag(SubmarineTag.Shuttle) ||
(!Character.IsOnFriendlyTeam(Character.TeamID, Character.Submarine.TeamID) && !Character.IsEscorted) ||
ObjectiveManager.CurrentOrders.Any(o => o.Objective.KeepDivingGearOnAlsoWhenInactive) ||
ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn) ||
@@ -845,8 +858,9 @@ namespace Barotrauma
{
// In the campaign mode, undocking happens after leaving the outpost, so we can't use that.
campaign.BeforeLevelLoading += Relocate;
campaign.OnSaveAndQuit += Relocate;
campaign.ItemsRelocatedToMainSub = true;
}
campaign.ItemsRelocatedToMainSub = true;
#if CLIENT
HintManager.OnItemMarkedForRelocation();
#endif
@@ -1011,6 +1025,7 @@ namespace Barotrauma
{
Order newOrder = null;
Hull targetHull = null;
// for now, escorted characters use the report system to get targets but do not speak. escort-character specific dialogue could be implemented
bool speak = Character.SpeechImpediment < 100 && !Character.IsEscorted;
if (Character.CurrentHull != null)
@@ -1024,7 +1039,7 @@ namespace Barotrauma
if (target.CurrentHull != hull || !target.Enabled) { continue; }
if (AIObjectiveFightIntruders.IsValidTarget(target, Character, false))
{
if (!target.IsArrested && AddTargets<AIObjectiveFightIntruders, Character>(Character, target) && newOrder == null)
if (!target.IsHandcuffed && AddTargets<AIObjectiveFightIntruders, Character>(Character, target) && newOrder == null)
{
var orderPrefab = OrderPrefab.Prefabs["reportintruders"];
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
@@ -1436,10 +1451,7 @@ namespace Barotrauma
{
return AIObjectiveCombat.CombatMode.Offensive;
}
return
humanAI.ObjectiveManager.IsCurrentOrder<AIObjectiveFightIntruders>() ||
humanAI.ObjectiveManager.Objectives.Any(o => o is AIObjectiveFightIntruders) ?
AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Defensive;
return humanAI.ObjectiveManager.HasObjectiveOrOrder<AIObjectiveFightIntruders>() ? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Defensive;
}
else
{
@@ -1479,28 +1491,36 @@ namespace Barotrauma
{
// The guards don't react to player's aggressions when there's an instigator around
isAttackerFightingEnemy = true;
return c.IsSecurity ? AIObjectiveCombat.CombatMode.None : (instigator.CombatAction != null ? instigator.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.Retreat);
return c.IsSecurity ? AIObjectiveCombat.CombatMode.None : instigator.CombatAction?.WitnessReaction ?? AIObjectiveCombat.CombatMode.Retreat;
}
if (attacker.TeamID == CharacterTeamType.FriendlyNPC && !eitherIsMentallyUnstable)
{
if (c.IsSecurity)
{
return attacker.CombatAction != null ? attacker.CombatAction.GuardReaction : AIObjectiveCombat.CombatMode.Offensive;
return attacker.CombatAction?.GuardReaction ?? AIObjectiveCombat.CombatMode.Offensive;
}
else
{
return attacker.CombatAction != null ? attacker.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.Retreat;
return attacker.CombatAction?.WitnessReaction ?? AIObjectiveCombat.CombatMode.Retreat;
}
}
else
{
if (humanAI.ObjectiveManager.GetLastActiveObjective<AIObjectiveCombat>()?.Enemy == attacker)
if (humanAI.ObjectiveManager.GetLastActiveObjective<AIObjectiveCombat>() is AIObjectiveCombat currentCombatObjective && currentCombatObjective.Enemy == attacker)
{
// Already targeting the attacker -> treat as a more serious threat.
cumulativeDamage *= 2;
currentCombatObjective.AllowHoldFire = false;
c.IsCriminal = true;
}
if (c.IsCriminal)
{
// Always react if the attacker has been misbehaving earlier.
cumulativeDamage = Math.Max(cumulativeDamage, minorDamageThreshold);
}
if (cumulativeDamage > majorDamageThreshold)
{
c.IsCriminal = true;
if (c.IsSecurity)
{
return AIObjectiveCombat.CombatMode.Offensive;
@@ -1544,7 +1564,7 @@ namespace Barotrauma
}
}
public void AddCombatObjective(AIObjectiveCombat.CombatMode mode, Character target, float delay = 0, Func<AIObjective, bool> abortCondition = null, Action onAbort = null, Action onCompleted = null, bool allowHoldFire = false)
public void AddCombatObjective(AIObjectiveCombat.CombatMode mode, Character target, float delay = 0, Func<AIObjective, bool> abortCondition = null, Action onAbort = null, Action onCompleted = null, bool allowHoldFire = false, bool speakWarnings = false)
{
if (mode == AIObjectiveCombat.CombatMode.None) { return; }
if (Character.IsDead || Character.IsIncapacitated || Character.Removed) { return; }
@@ -1579,6 +1599,7 @@ namespace Barotrauma
HoldPosition = Character.Info?.Job?.Prefab.Identifier == "watchman",
AbortCondition = abortCondition,
AllowHoldFire = allowHoldFire,
SpeakWarnings = speakWarnings
};
if (onAbort != null)
{
@@ -1777,7 +1798,7 @@ namespace Barotrauma
}
if (!otherCharacter.CanSeeTarget(character, seeThroughWindows: true)) { continue; }
if (!otherHumanAI.structureDamageAccumulator.ContainsKey(character)) { otherHumanAI.structureDamageAccumulator.Add(character, 0.0f); }
otherHumanAI.structureDamageAccumulator.TryAdd(character, 0.0f);
float prevAccumulatedDamage = otherHumanAI.structureDamageAccumulator[character];
otherHumanAI.structureDamageAccumulator[character] += MathHelper.Clamp(damageAmount, -MaxDamagePerFrame, MaxDamagePerFrame);
float accumulatedDamage = Math.Max(otherHumanAI.structureDamageAccumulator[character], maxAccumulatedDamage);
@@ -1789,27 +1810,36 @@ namespace Barotrauma
GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.AddReputation(-reputationLoss, Reputation.MaxReputationLossFromWallDamage);
}
if (accumulatedDamage <= WarningThreshold) { return; }
if (accumulatedDamage > WarningThreshold && prevAccumulatedDamage <= WarningThreshold &&
!someoneSpoke && !character.IsIncapacitated && character.Stun <= 0.0f)
if (!character.IsCriminal)
{
//if the damage is still fairly low, wait and see if the character keeps damaging the walls to the point where we need to intervene
if (accumulatedDamage < ArrestThreshold)
if (accumulatedDamage <= WarningThreshold) { return; }
if (accumulatedDamage > WarningThreshold && prevAccumulatedDamage <= WarningThreshold &&
!someoneSpoke && !character.IsIncapacitated && character.Stun <= 0.0f)
{
if (otherHumanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveIdle>())
//if the damage is still fairly low, wait and see if the character keeps damaging the walls to the point where we need to intervene
if (accumulatedDamage < ArrestThreshold)
{
(otherHumanAI.ObjectiveManager.CurrentObjective as AIObjectiveIdle)?.FaceTargetAndWait(character, 5.0f);
if (otherHumanAI.ObjectiveManager.CurrentObjective is AIObjectiveIdle idleObjective)
{
idleObjective.FaceTargetAndWait(character, 5.0f);
}
}
otherCharacter.Speak(TextManager.Get("dialogdamagewallswarning").Value, null, Rand.Range(0.5f, 1.0f), "damageoutpostwalls".ToIdentifier(), 10.0f);
someoneSpoke = true;
}
otherCharacter.Speak(TextManager.Get("dialogdamagewallswarning").Value, null, Rand.Range(0.5f, 1.0f), "damageoutpostwalls".ToIdentifier(), 10.0f);
someoneSpoke = true;
}
// React if we are security
if ((accumulatedDamage > ArrestThreshold && prevAccumulatedDamage <= ArrestThreshold) ||
if (character.IsCriminal ||
(accumulatedDamage > ArrestThreshold && prevAccumulatedDamage <= ArrestThreshold) ||
(accumulatedDamage > KillThreshold && prevAccumulatedDamage <= KillThreshold))
{
var combatMode = accumulatedDamage > KillThreshold ? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Arrest;
if (combatMode == AIObjectiveCombat.CombatMode.Offensive)
{
character.IsCriminal = true;
}
if (!TriggerSecurity(otherHumanAI, combatMode))
{
// Else call the others
@@ -1830,17 +1860,18 @@ namespace Barotrauma
if (humanAI == null) { return false; }
if (!humanAI.Character.IsSecurity) { return false; }
if (humanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveCombat>()) { return false; }
humanAI.AddCombatObjective(combatMode, character, delay: GetReactionTime(), allowHoldFire: true, onCompleted: () =>
{
//if the target is arrested successfully, reset the damage accumulator
foreach (Character anyCharacter in Character.CharacterList)
{
if (anyCharacter.AIController is HumanAIController anyAI)
humanAI.AddCombatObjective(combatMode, character, delay: GetReactionTime(),
onCompleted: () =>
{
//if the target is arrested successfully, reset the damage accumulator
foreach (Character anyCharacter in Character.CharacterList)
{
anyAI.structureDamageAccumulator?.Remove(character);
if (anyCharacter.AIController is HumanAIController anyAI)
{
anyAI.structureDamageAccumulator?.Remove(character);
}
}
}
});
});
return true;
}
}
@@ -1848,11 +1879,14 @@ namespace Barotrauma
public static void ItemTaken(Item item, Character thief)
{
if (item == null || thief == null || item.GetComponent<LevelResource>() != null) { return; }
bool someoneSpoke = false;
bool stolenItemsInside = item.OwnInventory?.FindAllItems(it => it.SpawnedInCurrentOutpost && !it.AllowStealing, recursive: true).Any() ?? false;
if ((item.SpawnedInCurrentOutpost && !item.AllowStealing || stolenItemsInside) && thief.TeamID != CharacterTeamType.FriendlyNPC && !item.HasTag(Tags.HandLockerItem))
if (item.Illegitimate && item.GetRootInventoryOwner() is Character itemOwner && itemOwner != thief && itemOwner.TeamID == thief.TeamID)
{
// The player attempts to use a bot as a mule or get them arrested -> just arrest the player instead.
thief.IsCriminal = true;
}
bool foundIllegitimateItems = item.Illegitimate || item.OwnInventory?.FindItem(it => it.Illegitimate, recursive: true) != null;
if (foundIllegitimateItems && thief.TeamID != CharacterTeamType.FriendlyNPC)
{
foreach (Character otherCharacter in Character.CharacterList)
{
@@ -1872,19 +1906,24 @@ namespace Barotrauma
if (item.HasTag(Tags.FireExtinguisher) && connectedHulls.Any(h => h.FireSources.Any())) { continue; }
if (item.HasTag(Tags.DivingGear) && connectedHulls.Any(h => h.ConnectedGaps.Any(g => AIObjectiveFixLeaks.IsValidTarget(g, thief)))) { continue; }
}
if (!someoneSpoke)
if (item.HasTag(Tags.Handcuffs) && thief.HasEquippedItem(item))
{
if (!item.StolenDuringRound)
{
ApplyStealingReputationLoss(item);
item.StolenDuringRound = true;
}
otherCharacter.Speak(TextManager.Get("dialogstealwarning").Value, null, Rand.Range(0.5f, 1.0f), "thief".ToIdentifier(), 10.0f);
someoneSpoke = true;
// Handcuffed -> don't react.
continue;
}
if (!item.StolenDuringRound)
{
item.StolenDuringRound = true;
ApplyStealingReputationLoss(item);
#if CLIENT
HintManager.OnStoleItem(thief, item);
#endif
}
if (!someoneSpoke)
{
otherCharacter.Speak(TextManager.Get("dialogstealwarning").Value, null, Rand.Range(0.5f, 1.0f), "thief".ToIdentifier(), 10.0f);
someoneSpoke = true;
}
// React if we are security
if (!TriggerSecurity(otherHumanAI))
{
@@ -1900,7 +1939,7 @@ namespace Barotrauma
}
}
}
else if (item.OwnInventory?.FindItem(it => it.SpawnedInCurrentOutpost && !item.AllowStealing, true) is { } foundItem)
else if (item.OwnInventory?.FindItem(it => it.Illegitimate, true) is { } foundItem)
{
ItemTaken(foundItem, thief);
}
@@ -1914,11 +1953,12 @@ namespace Barotrauma
{
findThieves.InspectEveryone();
}
bool isCriminal = thief.IsCriminal;
humanAI.AddCombatObjective(AIObjectiveCombat.CombatMode.Arrest, thief, delay: GetReactionTime(),
abortCondition: obj => thief.Inventory.FindItem(it => it != null && it.StolenDuringRound, true) == null,
abortCondition: obj => !isCriminal && thief.Inventory.FindItem(it => it.Illegitimate, recursive: true) == null,
onAbort: () =>
{
if (item != null && !item.Removed && humanAI != null && !humanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveGetItem>())
if (!item.Removed && !humanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveGetItem>())
{
humanAI.ObjectiveManager.AddObjective(new AIObjectiveGetItem(humanAI.Character, item, humanAI.ObjectiveManager, equip: false)
{
@@ -1926,7 +1966,8 @@ namespace Barotrauma
});
}
},
allowHoldFire: true);
allowHoldFire: !isCriminal,
speakWarnings: !isCriminal);
return true;
}
}
@@ -2084,7 +2125,7 @@ namespace Barotrauma
}
bool ignoreFire = objectiveManager.CurrentOrder is AIObjectiveExtinguishFires extinguishOrder && extinguishOrder.Priority > 0 || objectiveManager.HasActiveObjective<AIObjectiveExtinguishFire>();
bool ignoreOxygen = HasDivingGear(character);
bool ignoreEnemies = ObjectiveManager.IsCurrentOrder<AIObjectiveFightIntruders>() || ObjectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>();
bool ignoreEnemies = ObjectiveManager.HasObjectiveOrOrder<AIObjectiveFightIntruders>();
float safety = CalculateHullSafety(hull, visibleHulls, character, ignoreWater: false, ignoreOxygen, ignoreFire, ignoreEnemies);
if (isCurrentHull)
{
@@ -2146,7 +2187,7 @@ namespace Barotrauma
{
if (!visibleHulls.Contains(c.CurrentHull)) { continue; }
}
if (IsActive(c) && !IsFriendly(character, c) && !c.IsArrested)
if (IsActive(c) && !IsFriendly(character, c) && !c.IsHandcuffed)
{
enemyCount++;
}
@@ -492,14 +492,20 @@ namespace Barotrauma
var door = currentPath.CurrentNode.ConnectedDoor;
float margin = MathHelper.Lerp(1, 10, MathHelper.Clamp(Math.Abs(velocity.X) / 5, 0, 1));
float colliderHeight = collider.Height / 2 + collider.Radius;
float heightDiff = currentPath.CurrentNode.SimPosition.Y - collider.SimPosition.Y;
if (heightDiff < colliderHeight)
if (currentPath.CurrentNode.Stairs == null)
{
//the waypoint is between the top and bottom of the collider, no need to move vertically.
diff.Y = 0.0f;
float heightDiff = currentPath.CurrentNode.SimPosition.Y - collider.SimPosition.Y;
if (heightDiff < colliderHeight)
{
// Original comment:
//the waypoint is between the top and bottom of the collider, no need to move vertically.
// Note that the waypoint can be below collider too! This might be incorrect.
diff.Y = 0.0f;
}
}
if (currentPath.CurrentNode.Stairs != null)
else
{
// In stairs
bool isNextNodeInSameStairs = currentPath.NextNode?.Stairs == currentPath.CurrentNode.Stairs;
if (!isNextNodeInSameStairs)
{
@@ -883,7 +889,18 @@ namespace Barotrauma
//steer away from edges of the hull
bool wander = false;
bool inWater = character.AnimController.InWater;
var currentHull = character.CurrentHull;
Hull currentHull = character.CurrentHull;
// TODO: disabled for now, because seems to cause bots to walk towards walls/doors in some places. In some places it's because how the hulls are defined, but there is probably something else too, is it seems to happen also elsewhere.
// if (!inWater)
// {
// Vector2 colliderBottomPos = ConvertUnits.ToDisplayUnits(character.AnimController.GetColliderBottom());
// if (Hull.FindHull(colliderBottomPos, guess: currentHull, useWorldCoordinates: false) is Hull lowestHull)
// {
// // Use the hull found at the collider bottom, if found.
// // Makes difference in some rooms that have multiple hulls, of which the lowest hull where the feet are might not be the same as where the center position of the main collider is.
// currentHull = lowestHull;
// }
// }
if (currentHull != null && !inWater)
{
float roomWidth = currentHull.Rect.Width;
@@ -388,6 +388,7 @@ namespace Barotrauma
character.TeleportTo(ConvertUnits.ToDisplayUnits(forceColliderSimPosition.Value));
}
// TODO: Shouldn't multiply by LimbScale here, because it's already applied in attachLimb.Scale!
Vector2 transformedLocalAttachPos = localAttachPos * attachLimb.Scale * attachLimb.Params.Ragdoll.LimbScale;
if (jointDir < 0.0f)
{
@@ -15,7 +15,10 @@ namespace Barotrauma
public virtual string DebugTag => Identifier.Value;
public virtual bool ForceRun => false;
public virtual bool IgnoreUnsafeHulls => false;
public virtual bool AbandonWhenCannotCompleteSubjectives => true;
public virtual bool AbandonWhenCannotCompleteSubObjectives => true;
/// <summary>
/// Should subobjectives be sorted according to their priority?
/// </summary>
public virtual bool AllowSubObjectiveSorting => false;
public virtual bool PrioritizeIfSubObjectivesActive => false;
@@ -28,8 +31,7 @@ namespace Barotrauma
/// Run the main objective with all subobjectives concurrently?
/// If false, the main objective will continue only when all the subobjectives have been removed (done).
/// </summary>
public virtual bool ConcurrentObjectives => false;
protected virtual bool ConcurrentObjectives => false;
public virtual bool KeepDivingGearOn => false;
public virtual bool KeepDivingGearOnAlsoWhenInactive => false;
@@ -37,10 +39,36 @@ namespace Barotrauma
/// There's a separate property for diving suit and mask: KeepDivingGearOn.
/// </summary>
public virtual bool AllowAutomaticItemUnequipping => false;
public virtual bool AllowOutsideSubmarine => false;
public virtual bool AllowInFriendlySubs => false;
public virtual bool AllowInAnySub => false;
public virtual bool AllowWhileHandcuffed => true;
// These booleans are used for defining whether the objective is allowed in different contexts. E.g. AllowOutsideSubmarine needs to be true or the objective cannot be active when the bot is swimming outside.
protected virtual bool AllowOutsideSubmarine => false;
/// <summary>
/// When true, the objective is allowed in the player subs (when in the same team) and on friendly outposts (regardless of the alignment).
/// Note: ignored when <see cref="AllowInAnySub"/> is true.
/// </summary>
protected virtual bool AllowInFriendlySubs => false;
protected virtual bool AllowInAnySub => false;
protected virtual bool AllowWhileHandcuffed => true;
/// <summary>
/// Should the objective abandon when it's not allowed in the current context or should it just stay inactive with 0 priority?
/// Abandoned automatic objectives are removed and recreated automatically (when new orders are assigned or after a cooldown period).
/// Abandoned orders are removed, but the most recent order can be reissued by clicking the small order icon with the arrow in the crew manager panel.
/// </summary>
protected virtual bool AbandonIfDisallowed => true;
public virtual bool CanBeCompleted => !Abandon;
protected virtual float MaxDevotion => 10;
/// <summary>
/// Which event action (if any) created this objective
/// </summary>
public EventAction SourceEventAction;
/// <summary>
/// Which objective (if any) created this objective. When this is a subobjective, the parent objective is used by default.
/// </summary>
public AIObjective SourceObjective;
protected readonly List<AIObjective> subObjectives = new List<AIObjective>();
private float _cumulatedDevotion;
@@ -50,8 +78,6 @@ namespace Barotrauma
set { _cumulatedDevotion = MathHelper.Clamp(value, 0, MaxDevotion); }
}
protected virtual float MaxDevotion => 10;
/// <summary>
/// Final priority value after all calculations.
/// </summary>
@@ -100,17 +126,13 @@ namespace Barotrauma
}
}
}
public virtual bool CanBeCompleted => !Abandon;
/// <summary>
/// When true, the objective is never completed, unless CanBeCompleted returns false.
/// </summary>
public virtual bool IsLoop { get; set; }
public IEnumerable<AIObjective> SubObjectives => subObjectives;
public AIObjective CurrentSubObjective => subObjectives.FirstOrDefault();
private readonly List<AIObjective> all = new List<AIObjective>();
public IEnumerable<AIObjective> GetSubObjectivesRecursive(bool includingSelf = false)
{
all.Clear();
@@ -124,13 +146,11 @@ namespace Barotrauma
}
return all;
}
#pragma warning disable CS0649
/// <summary>
/// Aborts the objective when this condition is true.
/// </summary>
public Func<AIObjective, bool> AbortCondition;
#pragma warning restore CS0649
/// <summary>
/// A single shot event. Automatically cleared after launching. Use OnCompleted method for implementing (internal) persistent behavior.
@@ -186,6 +206,7 @@ namespace Barotrauma
public void AddSubObjective(AIObjective objective, bool addFirst = false)
{
var type = objective.GetType();
objective.SourceObjective = this;
subObjectives.RemoveAll(o => o.GetType() == type);
if (addFirst)
{
@@ -259,17 +280,21 @@ namespace Barotrauma
return character.Submarine.Info.IsOutpost && character.Submarine.TeamID == CharacterTeamType.FriendlyNPC;
}
protected void HandleNonAllowed()
protected void HandleDisallowed()
{
Priority = 0;
Abandon = !IsIgnoredAtOutpost();
if (AbandonIfDisallowed && !IsIgnoredAtOutpost())
{
// Never abandon objectives inside a friendly outpost, because otherwise we'd have to reassign most orders every round.
Abandon = true;
}
}
protected virtual float GetPriority()
{
if (!IsAllowed)
{
HandleNonAllowed();
HandleDisallowed();
return Priority;
}
if (objectiveManager.IsOrder(this))
@@ -360,7 +385,7 @@ namespace Barotrauma
/// <summary>
/// Checks if the subobjectives in the given collection are removed from the subobjectives. And if so, removes it also from the dictionary.
/// </summary>
protected void SyncRemovedObjectives<T1, T2>(Dictionary<T1, T2> dictionary, IEnumerable<T1> collection) where T2 : AIObjective
protected virtual void SyncRemovedObjectives<T1, T2>(Dictionary<T1, T2> dictionary, IEnumerable<T1> collection) where T2 : AIObjective
{
foreach (T1 key in collection)
{
@@ -398,6 +423,7 @@ namespace Barotrauma
{
if (objective.AllowMultipleInstances)
{
objective.SourceObjective = this;
subObjectives.Add(objective);
}
else
@@ -482,6 +508,9 @@ namespace Barotrauma
}
}
/// <summary>
/// Check whether the objective should be aborted (and abandon if it should), and return whether the objective is completed or not.
/// </summary>
private bool Check()
{
if (AbortCondition != null && AbortCondition(this))
@@ -492,6 +521,9 @@ namespace Barotrauma
return CheckObjectiveSpecific();
}
/// <summary>
/// Should return whether the objective is completed or not.
/// </summary>
protected abstract bool CheckObjectiveSpecific();
private bool CheckState()
@@ -527,7 +559,7 @@ namespace Barotrauma
DebugConsole.NewMessage($"{character.Name}: Removing SUBobjective {subObjective.DebugTag} of {DebugTag}, because it cannot be completed.", Color.Red);
#endif
subObjectives.Remove(subObjective);
if (AbandonWhenCannotCompleteSubjectives)
if (AbandonWhenCannotCompleteSubObjectives)
{
if (objectiveManager.IsOrder(this))
{
@@ -16,7 +16,7 @@ namespace Barotrauma
public AIObjectiveChargeBatteries(Character character, AIObjectiveManager objectiveManager, Identifier option, float priorityModifier)
: base(character, objectiveManager, priorityModifier, option) { }
protected override bool Filter(PowerContainer battery)
protected override bool IsValidTarget(PowerContainer battery)
{
if (battery == null) { return false; }
if (battery.OutputDisabled) { return false; }
@@ -37,7 +37,7 @@ namespace Barotrauma
return true;
}
protected override float TargetEvaluation()
protected override float GetTargetPriority()
{
if (Targets.None()) { return 0; }
if (Option == "charge")
@@ -80,7 +80,6 @@ namespace Barotrauma
protected override AIObjective ObjectiveConstructor(PowerContainer battery) =>
new AIObjectiveOperateItem(battery, character, objectiveManager, Option, false, priorityModifier: PriorityModifier)
{
IsLoop = false,
Override = !character.IsDismissed,
completionCondition = () => IsReady(battery)
};
@@ -8,8 +8,8 @@ namespace Barotrauma
class AIObjectiveCheckStolenItems : AIObjective
{
public override Identifier Identifier { get; set; } = "check stolen items".ToIdentifier();
public override bool AllowOutsideSubmarine => false;
public override bool AllowInAnySub => false;
protected override bool AllowOutsideSubmarine => false;
protected override bool AllowInAnySub => false;
public float FindStolenItemsProbability = 1.0f;
@@ -21,36 +21,38 @@ namespace Barotrauma
Done
}
private float inspectDelay;
private float warnDelay;
private const float InspectTime = 5.0f;
private const float NormalWarnDelay = 5.0f;
private const float CriminalWarnDelay = 3.0f;
private float inspectTimer;
private float warnTimer;
private float currentWarnDelay;
private State currentState;
public readonly Character TargetCharacter;
public readonly Character Target;
private AIObjectiveGoTo? goToObjective;
private readonly List<Item> stolenItems = new List<Item>();
public AIObjectiveCheckStolenItems(Character character, Character targetCharacter, AIObjectiveManager objectiveManager, float priorityModifier = 1) :
public AIObjectiveCheckStolenItems(Character character, Character target, AIObjectiveManager objectiveManager, float priorityModifier = 1) :
base(character, objectiveManager, priorityModifier)
{
TargetCharacter = targetCharacter;
inspectDelay = 5.0f;
warnDelay = 5.0f;
}
public override bool IsLoop
{
get => false;
set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace.CleanupStackTrace());
Target = target;
InitTimers();
}
protected override bool CheckObjectiveSpecific() => false;
protected override float GetPriority()
{
if (!Abandon && !IsCompleted && objectiveManager.IsOrder(this))
if (character.IsClimbing)
{
// Target is climbing -> stop following the objective (soft abandon, without ignoring the target).
Priority = 0;
}
else if (!Abandon && !IsCompleted && objectiveManager.IsOrder(this))
{
Priority = objectiveManager.GetOrderPriority(this);
}
@@ -70,22 +72,32 @@ namespace Barotrauma
{
switch (currentState)
{
case State.Done:
IsCompleted = true;
break;
case State.GotoTarget:
TryAddSubObjective(ref goToObjective,
constructor: () =>
constructor: () => new AIObjectiveGoTo(Target, character, objectiveManager, repeat: false)
{
return new AIObjectiveGoTo(TargetCharacter, character, objectiveManager, repeat: false)
{
SpeakIfFails = false
};
SpeakIfFails = false
},
onCompleted: () =>
{
RemoveSubObjective(ref goToObjective);
currentState = State.Inspect;
stolenItems.Clear();
TargetCharacter.Inventory.FindAllItems(it => it.SpawnedInCurrentOutpost && !it.AllowStealing, recursive: true, stolenItems);
character.Speak(TextManager.Get("dialogcheckstolenitems").Value);
if (character.IsClimbing)
{
// Shouldn't start inspecting characters when they climb, nor get here, because the priority should be 0,
// but if this still happens, we'll have to abandon the objective
// because it's not currently possible to hold to characters and ladders at the same time.
Abandon = true;
}
else
{
currentState = State.Inspect;
stolenItems.Clear();
Target.Inventory.FindAllItems(it => it.Illegitimate, recursive: true, stolenItems);
character.Speak(TextManager.Get(Target.IsCriminal ? "dialogcheckstolenitems.criminal" : "dialogcheckstolenitems").Value);
}
},
onAbandon: () =>
{
@@ -103,10 +115,23 @@ namespace Barotrauma
private void Inspect(float deltaTime)
{
if (inspectDelay > 0.0f)
if (inspectTimer > 0.0f)
{
character.SelectCharacter(TargetCharacter);
inspectDelay -= deltaTime;
character.SelectCharacter(Target);
inspectTimer -= deltaTime;
if (inspectTimer < InspectTime - 1)
{
if (Target.AnimController.IsMovingFast)
{
ArrestFleeing();
}
else if (Math.Abs(Target.AnimController.TargetMovement.X) > 1.0f)
{
// If the target moves, reset the inspect timer and tell to hold still
character.Speak(TextManager.Get("dialogcheckstolenitems.holdstill").Value, identifier: "holdstill".ToIdentifier(), minDurationBetweenSimilar: 3f);
inspectTimer = InspectTime;
}
}
return;
}
@@ -118,7 +143,7 @@ namespace Barotrauma
}
else
{
character.Speak(TextManager.Get("dialogcheckstolenitems.nostolenitems").Value);
character.Speak(TextManager.Get(Target.IsCriminal ? "dialogcheckstolenitems.nostolenitems.criminal" : "dialogcheckstolenitems.nostolenitems").Value);
currentState = State.Done;
IsCompleted = true;
}
@@ -127,16 +152,23 @@ namespace Barotrauma
private void Warn(float deltaTime)
{
if (warnDelay > 0.0f)
if (warnTimer > 0.0f)
{
warnDelay -= deltaTime;
warnTimer -= deltaTime;
if (warnTimer < currentWarnDelay - 1)
{
if (Target.AnimController.IsMovingFast)
{
ArrestFleeing();
}
}
return;
}
var stolenItemsOnCharacter = stolenItems.Where(it => it.GetRootInventoryOwner() == TargetCharacter);
var stolenItemsOnCharacter = stolenItems.Where(it => it.GetRootInventoryOwner() == Target);
if (stolenItemsOnCharacter.Any())
{
character.Speak(TextManager.Get("dialogcheckstolenitems.arrest").Value);
HumanAIController.AddCombatObjective(AIObjectiveCombat.CombatMode.Arrest, TargetCharacter);
character.Speak(TextManager.Get(character.IsCriminal ? "dialogcheckstolenitems.arrest.criminal" : "dialogcheckstolenitems.arrest").Value);
Arrest(abortWhenItemsDropped: true, allowHoldFire: true);
foreach (var stolenItem in stolenItemsOnCharacter)
{
HumanAIController.ApplyStealingReputationLoss(stolenItem);
@@ -156,5 +188,44 @@ namespace Barotrauma
currentState = State.Done;
IsCompleted = true;
}
private void ArrestFleeing()
{
character.Speak(TextManager.Get("dialogcheckstolenitems.arrest").Value);
currentState = State.Done;
IsCompleted = true;
Arrest(abortWhenItemsDropped: false, allowHoldFire: false);
}
private void Arrest(bool abortWhenItemsDropped, bool allowHoldFire)
{
bool isCriminal = Target.IsCriminal;
Func<AIObjective, bool>? abortCondition = null;
if (abortWhenItemsDropped && !isCriminal)
{
abortCondition = obj => Target.Inventory.FindItem(it => it.Illegitimate, recursive: true) == null;
}
HumanAIController.AddCombatObjective(AIObjectiveCombat.CombatMode.Arrest, Target, allowHoldFire: allowHoldFire && !isCriminal, speakWarnings: !isCriminal, abortCondition: abortCondition);
}
public override void OnDeselected()
{
base.OnDeselected();
character.DeselectCharacter();
}
public override void Reset()
{
base.Reset();
currentState = State.GotoTarget;
InitTimers();
}
private void InitTimers()
{
inspectTimer = InspectTime;
currentWarnDelay = Target.IsCriminal ? CriminalWarnDelay : NormalWarnDelay;
warnTimer = currentWarnDelay;
}
}
}
@@ -12,7 +12,7 @@ namespace Barotrauma
public override Identifier Identifier { get; set; } = "cleanup item".ToIdentifier();
public override bool KeepDivingGearOn => true;
public override bool AllowAutomaticItemUnequipping => false;
public override bool AllowWhileHandcuffed => false;
protected override bool AllowWhileHandcuffed => false;
public readonly Item item;
public bool IsPriority { get; set; }
@@ -24,7 +24,7 @@ namespace Barotrauma
/// <summary>
/// Allows decontainObjective to be interrupted if this objective gets abandoned (e.g. due to the item no longer being eligible for cleanup)
/// </summary>
public override bool ConcurrentObjectives => true;
protected override bool ConcurrentObjectives => true;
public AIObjectiveCleanupItem(Item item, Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
@@ -36,7 +36,7 @@ namespace Barotrauma
{
if (!IsAllowed)
{
HandleNonAllowed();
HandleDisallowed();
return Priority;
}
else
@@ -31,7 +31,7 @@ namespace Barotrauma
this.prioritizedItems.AddRange(prioritizedItems.Where(i => i != null));
}
protected override float TargetEvaluation()
protected override float GetTargetPriority()
{
if (Targets.None()) { return 0; }
if (objectiveManager.IsOrder(this))
@@ -47,7 +47,7 @@ namespace Barotrauma
return AIObjectiveManager.RunPriority - 0.5f;
}
protected override bool Filter(Item target)
protected override bool IsValidTarget(Item target)
{
System.Diagnostics.Debug.Assert(target.GetComponent<Pickable>() is { } pickable && !pickable.IsAttached, "Invalid target in AIObjectiveCleanUpItems - the the objective should only be checking pickable, non-attached items.");
System.Diagnostics.Debug.Assert(target.Prefab.PreferredContainers.Any(), "Invalid target in AIObjectiveCleanUpItems - the the objective should only be checking items that have preferred containers defined.");
@@ -100,7 +100,7 @@ namespace Barotrauma
{
if (item == null) { return false; }
if (item.DontCleanUp) { return false; }
if ((item.SpawnedInCurrentOutpost && !item.AllowStealing) == character.IsOnPlayerTeam) { return false; }
if (item.Illegitimate == character.IsOnPlayerTeam) { return false; }
if (item.ParentInventory != null)
{
if (item.Container == null)
@@ -16,23 +16,23 @@ namespace Barotrauma
public override bool KeepDivingGearOn => true;
public override bool IgnoreUnsafeHulls => true;
public override bool AllowOutsideSubmarine => true;
public override bool AllowInAnySub => true;
protected override bool AllowOutsideSubmarine => true;
protected override bool AllowInAnySub => true;
private readonly CombatMode initialMode;
private float checkWeaponsTimer;
private const float checkWeaponsInterval = 1;
private const float CheckWeaponsInterval = 1;
private float ignoreWeaponTimer;
private const float ignoredWeaponsClearTime = 10;
private const float IgnoredWeaponsClearTime = 10;
private const float goodWeaponPriority = 30;
private const float arrestHoldFireTime = 8;
private const float GoodWeaponPriority = 30;
private float holdFireTimer;
private bool hasAimed;
private bool isLethalWeapon;
private bool AllowCoolDown => !IsOffensiveOrArrest || Mode != initialMode || character.TeamID == Enemy.TeamID;
private bool AllowCoolDown => allowCooldown || !IsOffensiveOrArrest || Mode != initialMode || character.TeamID == Enemy.TeamID;
private bool allowCooldown;
public Character Enemy { get; private set; }
public bool HoldPosition { get; set; }
@@ -45,7 +45,6 @@ namespace Barotrauma
{
_weapon = value;
_weaponComponent = null;
hasAimed = false;
}
}
private ItemComponent _weaponComponent;
@@ -58,8 +57,8 @@ namespace Barotrauma
}
}
public override bool ConcurrentObjectives => true;
public override bool AbandonWhenCannotCompleteSubjectives => false;
protected override bool ConcurrentObjectives => true;
public override bool AbandonWhenCannotCompleteSubObjectives => false;
private readonly AIObjectiveFindSafety findSafety;
private readonly HashSet<ItemComponent> weapons = new HashSet<ItemComponent>();
@@ -72,6 +71,9 @@ namespace Barotrauma
private Hull retreatTarget;
private float coolDownTimer;
private float pathBackTimer;
private const float DefaultCoolDown = 10.0f;
private const float PathBackCheckTime = 1.0f;
private IEnumerable<Body> myBodies;
private float aimTimer;
private float reloadTimer;
@@ -79,17 +81,25 @@ namespace Barotrauma
private bool canSeeTarget;
private float visibilityCheckTimer;
private const float visibilityCheckInterval = 0.2f;
private const float VisibilityCheckInterval = 0.2f;
private float sqrDistance;
private const float maxDistance = 2000;
private const float distanceCheckInterval = 0.2f;
private const float MaxDistance = 2000;
private const float DistanceCheckInterval = 0.2f;
private float distanceTimer;
private const float closeDistanceThreshold = 300;
private const float floorHeightApproximate = 100;
private const float CloseDistanceThreshold = 300;
private const float FloorHeightApproximate = 100;
public bool AllowHoldFire;
public bool SpeakWarnings;
private bool firstWarningTriggered;
private bool lastWarningTriggered;
public float ArrestHoldFireTime { get; init; } = 10;
private const float ArrestTargetDistance = 100;
private bool arrestingRegistered;
/// <summary>
/// Don't start using a weapon if this condition is true
@@ -123,7 +133,7 @@ namespace Barotrauma
public CombatMode Mode { get; private set; }
private bool IsOffensiveOrArrest => initialMode is CombatMode.Offensive or CombatMode.Arrest;
private bool TargetEliminated => IsEnemyDisabled || Enemy.IsUnconscious && Enemy.Params.Health.ConstantHealthRegeneration <= 0.0f || Enemy.IsArrested && !character.IsInstigator;
private bool TargetEliminated => IsEnemyDisabled || (Enemy.IsUnconscious && Enemy.Params.Health.ConstantHealthRegeneration <= 0.0f) || (!character.IsInstigator && Enemy.IsHandcuffed && Enemy.IsKnockedDown);
private bool IsEnemyDisabled => Enemy == null || Enemy.Removed || Enemy.IsDead;
private float AimSpeed => HumanAIController.AimSpeed;
@@ -141,7 +151,7 @@ namespace Barotrauma
if (character.CurrentHull != null && Enemy.CurrentHull != null && character.CurrentHull != Enemy.CurrentHull)
{
// Inside, not in the same hull with the enemy
if (Math.Abs(toEnemy.Y) > floorHeightApproximate)
if (Math.Abs(toEnemy.Y) > FloorHeightApproximate)
{
// Different floor
return false;
@@ -156,7 +166,7 @@ namespace Barotrauma
return Vector2.DistanceSquared(character.WorldPosition, Enemy.WorldPosition) < margin * margin;
}
public AIObjectiveCombat(Character character, Character enemy, CombatMode mode, AIObjectiveManager objectiveManager, float priorityModifier = 1, float coolDown = 10.0f)
public AIObjectiveCombat(Character character, Character enemy, CombatMode mode, AIObjectiveManager objectiveManager, float priorityModifier = 1, float coolDown = DefaultCoolDown)
: base(character, objectiveManager, priorityModifier)
{
if (mode == CombatMode.None)
@@ -187,48 +197,31 @@ namespace Barotrauma
protected override float GetPriority()
{
if (Enemy == null || Enemy.Removed)
{
Priority = 0;
Abandon = true;
return Priority;
}
if (character.TeamID == CharacterTeamType.FriendlyNPC)
{
if (Enemy.Submarine == null || (Enemy.Submarine.TeamID != character.TeamID && Enemy.Submarine != character.Submarine))
{
Priority = 0;
Abandon = true;
return Priority;
}
}
if (TargetEliminated)
{
Priority = 0;
return Priority;
}
else
// 91-100
const float minPriority = AIObjectiveManager.EmergencyObjectivePriority + 1;
const float maxPriority = AIObjectiveManager.MaxObjectivePriority;
const float priorityScale = maxPriority - minPriority;
float xDist = Math.Abs(character.WorldPosition.X - Enemy.WorldPosition.X);
float yDist = Math.Abs(character.WorldPosition.Y - Enemy.WorldPosition.Y);
if (HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull))
{
// 91-100
const float minPriority = AIObjectiveManager.EmergencyObjectivePriority + 1;
const float maxPriority = AIObjectiveManager.MaxObjectivePriority;
const float priorityScale = maxPriority - minPriority;
float xDist = Math.Abs(character.WorldPosition.X - Enemy.WorldPosition.X);
float yDist = Math.Abs(character.WorldPosition.Y - Enemy.WorldPosition.Y);
if (HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull))
xDist /= 2;
yDist /= 2;
}
float distanceFactor = MathUtils.InverseLerp(3000, 0, xDist + yDist * 5);
float devotion = CumulatedDevotion / 100;
float additionalPriority = MathHelper.Lerp(0, priorityScale, Math.Clamp(devotion + distanceFactor, 0, 1));
Priority = Math.Min((minPriority + additionalPriority) * PriorityModifier, maxPriority);
if (Priority > 0)
{
if (EnemyAIController.IsLatchedToSomeoneElse(Enemy, character))
{
xDist /= 2;
yDist /= 2;
}
float distanceFactor = MathUtils.InverseLerp(3000, 0, xDist + yDist * 5);
float devotion = CumulatedDevotion / 100;
float additionalPriority = MathHelper.Lerp(0, priorityScale, Math.Clamp(devotion + distanceFactor, 0, 1));
Priority = Math.Min((minPriority + additionalPriority) * PriorityModifier, maxPriority);
if (Priority > 0)
{
if (EnemyAIController.IsLatchedToSomeoneElse(Enemy, character))
{
Priority = 0;
}
Priority = 0;
}
}
return Priority;
@@ -246,7 +239,7 @@ namespace Barotrauma
if (ignoreWeaponTimer < 0)
{
ignoredWeapons.Clear();
ignoreWeaponTimer = ignoredWeaponsClearTime;
ignoreWeaponTimer = IgnoredWeaponsClearTime;
}
bool isFightingIntruders = objectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>();
if (findSafety != null && isFightingIntruders)
@@ -258,7 +251,7 @@ namespace Barotrauma
distanceTimer -= deltaTime;
if (distanceTimer < 0)
{
distanceTimer = distanceCheckInterval;
distanceTimer = DistanceCheckInterval;
sqrDistance = Vector2.DistanceSquared(character.WorldPosition, Enemy.WorldPosition);
}
}
@@ -266,16 +259,62 @@ namespace Barotrauma
protected override bool CheckObjectiveSpecific()
{
if (character.Submarine is not { TeamID: CharacterTeamType.FriendlyNPC })
if (character.Submarine is { TeamID: CharacterTeamType.FriendlyNPC } && character.Submarine == Enemy.Submarine)
{
// Can't lose the target in friendly outposts.
if (sqrDistance > maxDistance * maxDistance)
// Target still in the outpost
if (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsSecurity)
{
// The target escaped from us.
return true;
// Outpost guards shouldn't lose the target in friendly outposts,
// However, if we are not a guard, let's ensure that we allow the cooldown.
allowCooldown = true;
}
}
return IsEnemyDisabled || (AllowCoolDown && coolDownTimer <= 0);
else
{
if ((Enemy.Submarine == null && character.Submarine != null) || sqrDistance > MaxDistance * MaxDistance)
{
// The target escaped from us.
Abandon = true;
if (character.TeamID == CharacterTeamType.FriendlyNPC && IsOffensiveOrArrest)
{
Enemy.IsCriminal = true;
}
return false;
}
if (Enemy.Submarine != null && character.Submarine != null && character.TeamID == CharacterTeamType.FriendlyNPC)
{
if (Enemy.Submarine.TeamID != character.TeamID)
{
allowCooldown = true;
// Target not in the outpost anymore.
if (character.CanSeeTarget(Enemy))
{
allowCooldown = false;
coolDownTimer = DefaultCoolDown;
}
else if (pathBackTimer <= 0)
{
// Check once per sec during the cooldown whether we can find a path back to the docking port
pathBackTimer = PathBackCheckTime;
foreach ((Submarine sub, DockingPort dockingPort) in character.Submarine.ConnectedDockingPorts)
{
if (sub.TeamID != character.TeamID) { continue; }
var path = PathSteering.PathFinder.FindPath(character.SimPosition, character.GetRelativeSimPosition(dockingPort.Item), character.Submarine, nodeFilter: node => node.Waypoint.CurrentHull != null);
if (path.Unreachable)
{
allowCooldown = false;
coolDownTimer = DefaultCoolDown;
}
}
}
if (IsOffensiveOrArrest)
{
Enemy.IsCriminal = true;
}
}
}
}
return TargetEliminated || (AllowCoolDown && coolDownTimer <= 0);
}
protected override void Act(float deltaTime)
@@ -288,6 +327,10 @@ namespace Barotrauma
if (AllowCoolDown)
{
coolDownTimer -= deltaTime;
if (pathBackTimer > 0)
{
pathBackTimer -= deltaTime;
}
}
if (seekAmmunitionObjective == null && seekWeaponObjective == null)
{
@@ -303,27 +346,6 @@ namespace Barotrauma
{
Move(deltaTime);
}
switch (Mode)
{
case CombatMode.Offensive:
if (TargetEliminated && objectiveManager.IsCurrentOrder<AIObjectiveFightIntruders>())
{
character.Speak(TextManager.Get("DialogTargetDown").Value, null, 3.0f, "targetdown".ToIdentifier(), 30.0f);
}
break;
case CombatMode.Arrest:
if (HumanAIController.HasItem(Enemy, Tags.HandLockerItem, out _, requireEquipped: true))
{
IsCompleted = true;
}
else if (Enemy.IsKnockedDown &&
!objectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>() &&
!HumanAIController.HasItem(character, Tags.HandLockerItem, out _, requireEquipped: false))
{
IsCompleted = true;
}
break;
}
}
}
@@ -389,7 +411,7 @@ namespace Barotrauma
&& !character.IsInstigator); // Instigators (= aggressive NPCs spawned with events) shouldn't seek new weapons, because we don't want them to grab e.g. an smg, if they spawn with a wrench or something.
if (checkWeaponsTimer < 0)
{
checkWeaponsTimer = checkWeaponsInterval;
checkWeaponsTimer = CheckWeaponsInterval;
// First go through all weapons and try to reload without seeking ammunition
HashSet<ItemComponent> allWeapons = FindWeaponsFromInventory();
while (allWeapons.Any())
@@ -412,7 +434,7 @@ namespace Barotrauma
// All good, the weapon is loaded
break;
}
bool seekAmmo = isAllowedToSeekWeapons && seekAmmunitionObjective == null && !IsEnemyClose(closeDistanceThreshold);
bool seekAmmo = isAllowedToSeekWeapons && seekAmmunitionObjective == null && !IsEnemyClose(CloseDistanceThreshold);
if (Reload(seekAmmo: seekAmmo))
{
// All good, we can use the weapon.
@@ -458,7 +480,7 @@ namespace Barotrauma
Mode = CombatMode.Retreat;
}
}
else if (seekAmmunitionObjective == null && (WeaponComponent == null || (WeaponComponent.CombatPriority < goodWeaponPriority && !IsEnemyClose(closeDistanceThreshold))))
else if (seekAmmunitionObjective == null && (WeaponComponent == null || (WeaponComponent.CombatPriority < GoodWeaponPriority && !IsEnemyClose(CloseDistanceThreshold))))
{
// No weapon or only a poor weapon equipped -> try to find better.
RemoveSubObjective(ref retreatObjective);
@@ -485,7 +507,7 @@ namespace Barotrauma
if (range is > 0 and < float.PositiveInfinity)
{
// Y distance is irrelevant when we are on the same floor. If we are on a different floor, let's double it.
float yDiff = Math.Abs(toItem.Y) > floorHeightApproximate ? toItem.Y * 2 : 0;
float yDiff = Math.Abs(toItem.Y) > FloorHeightApproximate ? toItem.Y * 2 : 0;
Vector2 adjustedDiff = new Vector2(toItem.X, yDiff);
if (adjustedDiff.LengthSquared() > MathUtils.Pow2(range))
{
@@ -501,7 +523,7 @@ namespace Barotrauma
}
if (i.CurrentHull != null && !HumanAIController.VisibleHulls.Contains(i.CurrentHull))
{
if (Math.Abs(toItem.Y) > floorHeightApproximate && Math.Abs(toEnemy.Y) > floorHeightApproximate)
if (Math.Abs(toItem.Y) > FloorHeightApproximate && Math.Abs(toEnemy.Y) > FloorHeightApproximate)
{
if (Math.Sign(toItem.Y) == Math.Sign(toEnemy.Y))
{
@@ -522,7 +544,7 @@ namespace Barotrauma
SpeakNoWeapons();
Mode = CombatMode.Retreat;
}
else if (!objectiveManager.HasActiveObjective<AIObjectiveFightIntruders>())
else if (!objectiveManager.HasObjectiveOrOrder<AIObjectiveFightIntruders>())
{
// Poor weapon equipped
Mode = CombatMode.Defensive;
@@ -642,7 +664,7 @@ namespace Barotrauma
priority /= 2;
}
}
else if (Enemy.IsKnockedDown)
else if (Enemy.IsKnockedDown && Mode != CombatMode.Arrest)
{
// Enemy is stunned, reduce the priority of stunner weapons.
Attack attack = GetAttackDefinition(weapon);
@@ -775,7 +797,7 @@ namespace Barotrauma
float bestPriority = 0;
float lethalDmg = -1;
bool prioritizeMelee = IsEnemyClose(50) || EnemyAIController.IsLatchedTo(Enemy, character);
bool isCloseToEnemy = prioritizeMelee || IsEnemyClose(closeDistanceThreshold);
bool isCloseToEnemy = prioritizeMelee || IsEnemyClose(CloseDistanceThreshold);
foreach (var weapon in weaponList)
{
float priority = GetWeaponPriority(weapon, prioritizeMelee, canSeekAmmo: !isCloseToEnemy, out lethalDmg);
@@ -801,9 +823,28 @@ namespace Barotrauma
}
isLethalWeapon = lethalDmg > 1;
}
if (AllowHoldFire && !hasAimed && holdFireTimer <= 0)
if (AllowHoldFire)
{
holdFireTimer = arrestHoldFireTime * Rand.Range(0.75f, 1.25f);
if (!hasAimed && holdFireTimer <= 0)
{
holdFireTimer = ArrestHoldFireTime * Rand.Range(0.9f, 1.1f);
}
else
{
if (SpeakWarnings)
{
if (!lastWarningTriggered && holdFireTimer < ArrestHoldFireTime * 0.3f)
{
FriendlyGuardSpeak("dialogarrest.lastwarning".ToIdentifier(), delay: 0, minDurationBetweenSimilar: 0f);
lastWarningTriggered = true;
}
else if (!firstWarningTriggered && holdFireTimer < ArrestHoldFireTime * 0.8f)
{
FriendlyGuardSpeak("dialogarrest.firstwarning".ToIdentifier(), delay: 0, minDurationBetweenSimilar: 0f);
firstWarningTriggered = true;
}
}
}
}
}
return weaponComponent.Item;
@@ -909,9 +950,10 @@ namespace Barotrauma
private void Retreat(float deltaTime)
{
if (!Enemy.IsHuman)
if (!Enemy.IsHuman && !character.IsInFriendlySub)
{
SpeakRetreating();
// Only relevant when we are retreating from monsters and are not inside a friendly sub.
PlayerCrewSpeak("dialogcombatretreating".ToIdentifier(), delay: Rand.Range(0f, 1f), minDurationBetweenSimilar: 20);
}
RemoveFollowTarget();
RemoveSubObjective(ref seekAmmunitionObjective);
@@ -931,7 +973,7 @@ namespace Barotrauma
{
RemoveSubObjective(ref retreatObjective);
}
if (character.Submarine == null && sqrDistance < MathUtils.Pow2(maxDistance))
if (character.Submarine == null && sqrDistance < MathUtils.Pow2(MaxDistance))
{
// Swim away
SteeringManager.Reset();
@@ -1017,6 +1059,16 @@ namespace Barotrauma
}
return;
}
if (character.TeamID == CharacterTeamType.FriendlyNPC && character.Submarine != null && character.Submarine.TeamID != character.TeamID)
{
// An outpost guard following the target (possibly a player) to another sub -> don't go further, unless can see the enemy.
if (!character.IsClimbing && !character.CanSeeTarget(Enemy))
{
SteeringManager.Reset();
RemoveFollowTarget();
return;
}
}
if (followTargetObjective != null && followTargetObjective.Target != Enemy)
{
RemoveFollowTarget();
@@ -1045,32 +1097,27 @@ namespace Barotrauma
}
});
if (followTargetObjective == null) { return; }
if (Mode == CombatMode.Arrest && Enemy.IsKnockedDown)
if (Mode == CombatMode.Arrest && Enemy.IsKnockedDown && !arrestingRegistered)
{
if (HumanAIController.HasItem(character, Tags.HandLockerItem, out _))
bool hasHandCuffs = HumanAIController.HasItem(character, Tags.HandLockerItem, out _);
if (!hasHandCuffs && character.TeamID == CharacterTeamType.FriendlyNPC)
{
if (!arrestingRegistered)
// Spawn handcuffs
ItemPrefab prefab = ItemPrefab.Find(null, "handcuffs".ToIdentifier());
if (prefab != null)
{
arrestingRegistered = true;
followTargetObjective.Completed += OnArrestTargetReached;
followTargetObjective.CloseEnough = 100;
}
}
else
{
if (character.TeamID == CharacterTeamType.FriendlyNPC)
{
ItemPrefab prefab = ItemPrefab.Find(null, "handcuffs".ToIdentifier());
if (prefab != null)
Entity.Spawner.AddItemToSpawnQueue(prefab, character.Inventory, onSpawned: i =>
{
Entity.Spawner.AddItemToSpawnQueue(prefab, character.Inventory, onSpawned: (Item i) => i.SpawnedInCurrentOutpost = true);
}
i.SpawnedInCurrentOutpost = true;
i.AllowStealing = false;
});
}
RemoveFollowTarget();
SteeringManager.Reset();
}
arrestingRegistered = true;
followTargetObjective.Completed += OnArrestTargetReached;
followTargetObjective.CloseEnough = ArrestTargetDistance;
}
if (!arrestingRegistered && followTargetObjective != null)
if (!arrestingRegistered)
{
followTargetObjective.CloseEnough =
WeaponComponent switch
@@ -1083,8 +1130,6 @@ namespace Barotrauma
}
}
private bool arrestingRegistered;
private void RemoveFollowTarget()
{
if (followTargetObjective != null)
@@ -1110,9 +1155,9 @@ namespace Barotrauma
// Confiscate stolen goods and all weapons
foreach (var item in Enemy.Inventory.AllItemsMod)
{
if (character.TeamID == CharacterTeamType.FriendlyNPC && item.StolenDuringRound ||
item.HasTag(Tags.Weapon) || item.HasTag(Tags.Poison) ||
GetWeaponComponent(item) is { CombatPriority: > 0 })
// Ignore handcuffs already on the target.
if (item.HasTag(Tags.HandLockerItem) && Enemy.HasEquippedItem(item)) { continue; }
if (item.Illegitimate || item.HasTag(Tags.Weapon) || item.HasTag(Tags.Poison) || GetWeaponComponent(item) is { CombatPriority: > 0 })
{
item.Drop(character);
character.Inventory.TryPutItem(item, character, CharacterInventory.AnySlot);
@@ -1255,7 +1300,7 @@ namespace Barotrauma
if (visibilityCheckTimer <= 0.0f)
{
canSeeTarget = character.CanSeeTarget(Enemy);
visibilityCheckTimer = visibilityCheckInterval;
visibilityCheckTimer = VisibilityCheckInterval;
}
if (!canSeeTarget)
{
@@ -1267,7 +1312,7 @@ namespace Barotrauma
character.SetInput(InputType.Aim, hit: false, held: true);
}
hasAimed = true;
if (holdFireTimer > 0)
if (AllowHoldFire && holdFireTimer > 0)
{
holdFireTimer -= deltaTime;
return;
@@ -1280,7 +1325,7 @@ namespace Barotrauma
if (reloadTimer > 0) { return; }
if (holdFireCondition != null && holdFireCondition()) { return; }
sqrDistance = Vector2.DistanceSquared(character.WorldPosition, Enemy.WorldPosition);
distanceTimer = distanceCheckInterval;
distanceTimer = DistanceCheckInterval;
if (WeaponComponent is MeleeWeapon meleeWeapon)
{
bool closeEnough = true;
@@ -1354,8 +1399,8 @@ namespace Barotrauma
private void UseWeapon(float deltaTime)
{
// Never allow to attack characters with deadly weapons while trying to arrest.
if (Mode == CombatMode.Arrest && isLethalWeapon) { return; }
// Never allow friendly crew (bots) to attack with deadly weapons.
if (Mode == CombatMode.Arrest && isLethalWeapon && character.IsOnPlayerTeam && Enemy.IsOnPlayerTeam) { return; }
character.SetInput(InputType.Shoot, hit: false, held: true);
Weapon.Use(deltaTime, user: character);
SetReloadTime(WeaponComponent);
@@ -1408,6 +1453,36 @@ namespace Barotrauma
protected override void OnCompleted()
{
base.OnCompleted();
if (Enemy != null)
{
switch (Mode)
{
case CombatMode.Offensive when Enemy.IsUnconscious && objectiveManager.HasObjectiveOrOrder<AIObjectiveFightIntruders>():
character.Speak(TextManager.Get("DialogTargetDown").Value, null, 3.0f, "targetdown".ToIdentifier(), 30.0f);
break;
case CombatMode.Arrest when IsCompleted:
if (!HumanAIController.IsTrueForAnyBotInTheCrew(bot =>
(bot != HumanAIController && bot.ObjectiveManager.CurrentObjective is AIObjectiveCombat { Mode: CombatMode.Arrest } combatObj && combatObj.Enemy == Enemy) ||
bot.ObjectiveManager.CurrentObjective is AIObjectiveGoTo { SourceObjective: AIObjectiveCombat combatObjective } && combatObjective.Enemy == Enemy))
{
// Go to the target and confiscate any stolen items, unless someone is already on it.
// Added on the root level, because the lifetime of the new objective exceeds the lifetime of this objective.
RemoveFollowTarget();
var approachArrestTarget = new AIObjectiveGoTo(Enemy, character, objectiveManager, repeat: false, getDivingGearIfNeeded: false, closeEnough: ArrestTargetDistance)
{
UsePathingOutside = false,
IgnoreIfTargetDead = true,
TargetName = Enemy.DisplayName,
AlwaysUseEuclideanDistance = false,
SpeakIfFails = false,
SourceObjective = this
};
approachArrestTarget.Completed += OnArrestTargetReached;
objectiveManager.AddObjective(approachArrestTarget);
}
break;
}
}
if (ShouldUnequipWeapon)
{
Unequip();
@@ -1424,11 +1499,23 @@ namespace Barotrauma
}
SteeringManager?.Reset();
}
public override void OnDeselected()
{
base.OnDeselected();
if (character.TeamID == CharacterTeamType.FriendlyNPC && IsOffensiveOrArrest && (!AllowHoldFire || (hasAimed && holdFireTimer <= 0)))
{
// Remember that the target resisted or acted offensively (we've aimed or tried to arrest/attack)
Enemy.IsCriminal = true;
}
}
public override void Reset()
{
base.Reset();
hasAimed = false;
holdFireTimer = 0;
pathBackTimer = 0;
isLethalWeapon = false;
canSeeTarget = false;
seekWeaponObjective = null;
@@ -1436,20 +1523,43 @@ namespace Barotrauma
retreatObjective = null;
followTargetObjective = null;
retreatTarget = null;
firstWarningTriggered = false;
lastWarningTriggered = false;
}
private void SpeakNoWeapons() => Speak("dialogcombatnoweapons".ToIdentifier(), delay: 0, minDuration: 30);
private void SpeakRetreating() => Speak("dialogcombatretreating".ToIdentifier(), delay: Rand.Range(0f, 1f), minDuration: 20);
private void Speak(Identifier textIdentifier, float delay, float minDuration)
/// <summary>
/// Speak that we don't have weapons. But only outside of friendly subs (not that relevant there, reduces spam).
/// </summary>
private void SpeakNoWeapons()
{
if (character.IsOnPlayerTeam && !character.IsInFriendlySub)
if (!character.IsInFriendlySub)
{
LocalizedString msg = TextManager.Get(textIdentifier);
if (!msg.IsNullOrEmpty())
{
character.Speak(msg.Value, identifier: textIdentifier, delay: delay, minDurationBetweenSimilar: minDuration);
}
PlayerCrewSpeak("dialogcombatnoweapons".ToIdentifier(), delay: 0, minDurationBetweenSimilar: 30);
}
}
private void PlayerCrewSpeak(Identifier textIdentifier, float delay, float minDurationBetweenSimilar)
{
if (character.IsOnPlayerTeam)
{
Speak(textIdentifier, delay, minDurationBetweenSimilar);
}
}
private void FriendlyGuardSpeak(Identifier textIdentifier, float delay, float minDurationBetweenSimilar)
{
if (character.TeamID == CharacterTeamType.FriendlyNPC && character.IsSecurity)
{
Speak(textIdentifier, delay, minDurationBetweenSimilar);
}
}
private void Speak(Identifier textIdentifier, float delay, float minDurationBetweenSimilar)
{
LocalizedString msg = TextManager.Get(textIdentifier);
if (!msg.IsNullOrEmpty())
{
character.Speak(msg.Value, identifier: textIdentifier, delay: delay, minDurationBetweenSimilar: minDurationBetweenSimilar);
}
}
@@ -11,7 +11,7 @@ namespace Barotrauma
class AIObjectiveContainItem: AIObjective
{
public override Identifier Identifier { get; set; } = "contain item".ToIdentifier();
public override bool AllowWhileHandcuffed => false;
protected override bool AllowWhileHandcuffed => false;
public Func<Item, float> GetItemPriority;
@@ -6,9 +6,9 @@ namespace Barotrauma
class AIObjectiveDeconstructItem : AIObjective
{
public override Identifier Identifier { get; set; } = "deconstruct item".ToIdentifier();
public override bool AllowWhileHandcuffed => false;
protected override bool AllowWhileHandcuffed => false;
public override bool AllowInFriendlySubs => true;
protected override bool AllowInFriendlySubs => true;
public readonly Item Item;
@@ -11,7 +11,7 @@ namespace Barotrauma
//Clear periodically, because we may ending up ignoring items when all deconstructors are full
protected override float IgnoreListClearInterval => 30;
public override bool AllowInFriendlySubs => true;
protected override bool AllowInFriendlySubs => true;
protected override int MaxTargets => 10;
@@ -47,7 +47,7 @@ namespace Barotrauma
checkedDeconstructorExists = false;
}
protected override float TargetEvaluation()
protected override float GetTargetPriority()
{
if (Targets.None()) { return 0; }
if (objectiveManager.IsOrder(this))
@@ -57,7 +57,7 @@ namespace Barotrauma
return AIObjectiveManager.RunPriority - 0.5f;
}
protected override bool Filter(Item target)
protected override bool IsValidTarget(Item target)
{
// If the target was selected as a valid target, we'll have to accept it so that the objective can be completed.
// The validity changes when a character picks the item up.
@@ -9,7 +9,7 @@ namespace Barotrauma
class AIObjectiveDecontainItem : AIObjective
{
public override Identifier Identifier { get; set; } = "decontain item".ToIdentifier();
public override bool AllowWhileHandcuffed => false;
protected override bool AllowWhileHandcuffed => false;
public Func<Item, float> GetItemPriority;
@@ -8,8 +8,8 @@ namespace Barotrauma
// Used for prisoner escorts to allow them to escape their binds
public override Identifier Identifier { get; set; } = "escape handcuffs".ToIdentifier();
public override bool AllowAutomaticItemUnequipping => true;
public override bool AllowOutsideSubmarine => true;
public override bool AllowInAnySub => true;
protected override bool AllowOutsideSubmarine => true;
protected override bool AllowInAnySub => true;
private int escapeProgress;
private bool isBeingWatched;
@@ -28,7 +28,6 @@ namespace Barotrauma
}
public override bool CanBeCompleted => true;
public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace.CleanupStackTrace()); }
protected override bool CheckObjectiveSpecific() => false;
// escape timer is set to 60 by default to allow players to locate prisoners in time
@@ -10,12 +10,10 @@ namespace Barotrauma
{
public override Identifier Identifier { get; set; } = "extinguish fire".ToIdentifier();
public override bool ForceRun => true;
public override bool ConcurrentObjectives => true;
protected override bool ConcurrentObjectives => true;
public override bool KeepDivingGearOn => true;
public override bool AllowInAnySub => true;
public override bool AllowWhileHandcuffed => false;
protected override bool AllowInAnySub => true;
protected override bool AllowWhileHandcuffed => false;
private readonly Hull targetHull;
@@ -32,7 +30,7 @@ namespace Barotrauma
{
if (!IsAllowed)
{
HandleNonAllowed();
HandleDisallowed();
return Priority;
}
bool isOrder = objectiveManager.HasOrder<AIObjectiveExtinguishFires>();
@@ -10,13 +10,13 @@ namespace Barotrauma
{
public override Identifier Identifier { get; set; } = "extinguish fires".ToIdentifier();
public override bool ForceRun => true;
public override bool AllowInAnySub => true;
protected override bool AllowInAnySub => true;
public AIObjectiveExtinguishFires(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { }
protected override bool Filter(Hull hull) => IsValidTarget(hull, character);
protected override bool IsValidTarget(Hull hull) => IsValidTarget(hull, character);
protected override float TargetEvaluation() =>
protected override float GetTargetPriority() =>
// If any target is visible -> 100 priority
Targets.Any(t => t == character.CurrentHull || HumanAIController.VisibleHulls.Contains(t)) ? 100 :
// Else based on the fire severity
@@ -1,6 +1,6 @@
using System.Collections.Generic;
using Barotrauma.Extensions;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
@@ -9,19 +9,17 @@ namespace Barotrauma
public override Identifier Identifier { get; set; } = "fight intruders".ToIdentifier();
protected override float IgnoreListClearInterval => 30;
public override bool IgnoreUnsafeHulls => true;
protected override float TargetUpdateTimeMultiplier => 0.2f;
public bool TargetCharactersInOtherSubs { get; init; }
public AIObjectiveFightIntruders(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
public AIObjectiveFightIntruders(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier) { }
protected override bool Filter(Character target) => IsValidTarget(target, character, TargetCharactersInOtherSubs);
protected override bool IsValidTarget(Character target) => IsValidTarget(target, character, TargetCharactersInOtherSubs);
protected override IEnumerable<Character> GetList() => Character.CharacterList;
protected override float TargetEvaluation()
protected override float GetTargetPriority()
{
if (Targets.None()) { return 0; }
if (!character.IsOnPlayerTeam && !character.IsOriginallyOnPlayerTeam) { return 100; }
@@ -68,14 +66,14 @@ namespace Barotrauma
if (HumanAIController.IsFriendly(character, target)) { return false; }
if (!character.Submarine.IsConnectedTo(target.Submarine)) { return false; }
if (!targetCharactersInOtherSubs)
{
{
if (character.Submarine.TeamID != target.Submarine.TeamID && character.OriginalTeamID != target.Submarine.TeamID)
{
return false;
}
}
if (target.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI)) { return false; }
if (target.IsArrested) { return false; }
if (target.IsHandcuffed && target.IsKnockedDown) { return false; }
if (EnemyAIController.IsLatchedToSomeoneElse(target, character)) { return false; }
return true;
}
@@ -11,8 +11,8 @@ namespace Barotrauma
public override string DebugTag => $"{Identifier} ({gearTag})";
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
public override bool AbandonWhenCannotCompleteSubjectives => false;
public override bool AllowWhileHandcuffed => false;
public override bool AbandonWhenCannotCompleteSubObjectives => false;
protected override bool AllowWhileHandcuffed => false;
private readonly Identifier gearTag;
@@ -13,18 +13,16 @@ namespace Barotrauma
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
public override bool IgnoreUnsafeHulls => true;
public override bool ConcurrentObjectives => true;
public override bool AllowOutsideSubmarine => true;
public override bool AllowInAnySub => true;
public override bool AbandonWhenCannotCompleteSubjectives => false;
public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace.CleanupStackTrace()); }
protected override bool ConcurrentObjectives => true;
protected override bool AllowOutsideSubmarine => true;
protected override bool AllowInAnySub => true;
public override bool AbandonWhenCannotCompleteSubObjectives => false;
// TODO: expose?
const float priorityIncrease = 100;
const float priorityDecrease = 10;
const float SearchHullInterval = 3.0f;
private const float PriorityIncrease = 100;
private const float PriorityDecrease = 10;
private const float SearchHullInterval = 3.0f;
private float currenthullSafety;
private float currentHullSafety;
private float searchHullTimer;
@@ -111,15 +109,15 @@ namespace Barotrauma
}
if (character.CurrentHull == null)
{
currenthullSafety = 0;
currentHullSafety = 0;
}
else
{
currenthullSafety = HumanAIController.CurrentHullSafety;
if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD)
currentHullSafety = HumanAIController.CurrentHullSafety;
if (currentHullSafety > HumanAIController.HULL_SAFETY_THRESHOLD)
{
Priority -= priorityDecrease * deltaTime;
if (currenthullSafety >= 100 && !character.IsLowInOxygen)
Priority -= PriorityDecrease * deltaTime;
if (currentHullSafety >= 100 && !character.IsLowInOxygen)
{
// Reduce the priority to zero so that the bot can get switch to other objectives immediately, e.g. when entering the airlock.
Priority = 0;
@@ -127,8 +125,8 @@ namespace Barotrauma
}
else
{
float dangerFactor = (100 - currenthullSafety) / 100;
Priority += dangerFactor * priorityIncrease * deltaTime;
float dangerFactor = (100 - currentHullSafety) / 100;
Priority += dangerFactor * PriorityIncrease * deltaTime;
}
Priority = MathHelper.Clamp(Priority, 0, AIObjectiveManager.MaxObjectivePriority);
}
@@ -192,7 +190,7 @@ namespace Barotrauma
}
if (divingGearObjective == null || !divingGearObjective.CanBeCompleted)
{
if (currenthullSafety < HumanAIController.HULL_SAFETY_THRESHOLD)
if (currentHullSafety < HumanAIController.HULL_SAFETY_THRESHOLD)
{
searchHullTimer = Math.Min(1, searchHullTimer);
}
@@ -231,7 +229,7 @@ namespace Barotrauma
},
onCompleted: () =>
{
if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD ||
if (currentHullSafety > HumanAIController.HULL_SAFETY_THRESHOLD ||
HumanAIController.NeedsDivingGear(currentHull, out bool needsSuit) && (needsSuit ? HumanAIController.HasDivingSuit(character) : HumanAIController.HasDivingMask(character)))
{
resetPriority = true;
@@ -299,7 +297,7 @@ namespace Barotrauma
}
foreach (Character enemy in Character.CharacterList)
{
if (!HumanAIController.IsActive(enemy) || HumanAIController.IsFriendly(enemy) || enemy.IsArrested) { continue; }
if (!HumanAIController.IsActive(enemy) || HumanAIController.IsFriendly(enemy) || enemy.IsHandcuffed) { continue; }
if (HumanAIController.VisibleHulls.Contains(enemy.CurrentHull))
{
Vector2 dir = character.Position - enemy.Position;
@@ -2,6 +2,7 @@
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Items.Components;
namespace Barotrauma
{
@@ -13,17 +14,33 @@ namespace Barotrauma
protected override float TargetUpdateTimeMultiplier => 1.0f;
const float DefaultInspectDistance = 200.0f;
/// <summary>
/// How long the round must have ran before NPCs can start doing inspections
/// (prevents "unfair" inspections you have no chance to react to if you happen to spawn right next to a security NPC with stolen items on you)
/// </summary>
private const float DelayOnRoundStart = 30.0f;
private const float DefaultInspectDistance = 200.0f;
/// <summary>
/// Used when something is stolen and when the guards decide to inspect everyone.
/// </summary>
private const float ExtendedInspectDistance = 400.0f;
/// <summary>
/// Used when the target is tagged as a criminal (= suspective).
/// </summary>
private const float CriminalInspectDistance = 500.0f;
private const float CriminalInspectProbability = 1.0f;
/// <summary>
/// How close the NPC must be to the target to the inspect them? You can use high values to make the NPC
/// systematically go through targets no matter where they are, and low values to check targets they happen to come across.
/// </summary>
public float InspectDistance = DefaultInspectDistance;
private float inspectDistance = DefaultInspectDistance;
private float? overrideInspectProbability;
/// <summary>
/// Chance of inspecting a valid target. The NPC won't try to inspect that target again for <see cref="inspectionInterval"/>
/// Chance of inspecting a valid target. The NPC won't try to inspect that target again for <see cref="NormalInspectionInterval"/>
/// regardless if the target is inspected or not.
/// </summary>
public float InspectProbability
@@ -53,18 +70,25 @@ namespace Barotrauma
/// When did the character last inspect whether some other character has stolen items on them?
/// </summary>
private static readonly Dictionary<Character, double> lastInspectionTimes = new Dictionary<Character, double>();
private readonly float inspectionInterval = 120.0f;
private const float NormalInspectionInterval = 120.0f;
private const float CriminalInspectionInterval = 30.0f;
public AIObjectiveFindThieves(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier) { }
protected override bool Filter(Character target)
protected override bool IsValidTarget(Character target)
{
if (GameMain.GameSession is not { RoundDuration: > DelayOnRoundStart })
{
return false;
}
if (!IsValidTarget(target, character)) { return false; }
if (Vector2.DistanceSquared(target.WorldPosition, character.WorldPosition) > InspectDistance * InspectDistance) { return false; }
float inspectDist = target.IsCriminal ? CriminalInspectDistance : inspectDistance;
if (Vector2.DistanceSquared(target.WorldPosition, character.WorldPosition) > inspectDist * inspectDist) { return false; }
if (lastInspectionTimes.TryGetValue(target, out double lastInspectionTime))
{
float inspectionInterval = target.IsCriminal ? CriminalInspectionInterval : NormalInspectionInterval;
if (Timing.TotalTime < lastInspectionTime + inspectionInterval)
{
return false;
@@ -75,8 +99,14 @@ namespace Barotrauma
protected override IEnumerable<Character> GetList() => Character.CharacterList;
protected override float TargetEvaluation()
protected override float GetTargetPriority()
{
if (character.IsClimbing)
{
// Don't inspect while climbing, because cannot grab while holding the ladders.
// Can lead to abandoning the objective when we need to climb the ladders to get to the target, but I think that's acceptable.
return 0;
}
return subObjectives.Any() ? 50 : 0;
}
@@ -84,13 +114,14 @@ namespace Barotrauma
{
lastInspectionTimes.Clear();
overrideInspectProbability = 1.0f;
InspectDistance = DefaultInspectDistance * 2;
inspectDistance = ExtendedInspectDistance;
}
protected override AIObjective ObjectiveConstructor(Character target)
{
var checkStolenItemsObjective = new AIObjectiveCheckStolenItems(character, target, objectiveManager);
if (Rand.Range(0.0f, 1.0f, Rand.RandSync.Unsynced) >= InspectProbability)
float probabity = target.IsCriminal ? CriminalInspectProbability : InspectProbability;
if (Rand.Range(0.0f, 1.0f, Rand.RandSync.Unsynced) >= probabity)
{
checkStolenItemsObjective.ForceComplete();
lastInspectionTimes[target] = Timing.TotalTime;
@@ -104,26 +135,30 @@ namespace Barotrauma
public override void Update(float deltaTime)
{
base.Update(deltaTime);
if (checkVisibleStolenItemsTimer > 0.0f)
if (checkVisibleStolenItemsTimer > 0.0f || character.IsClimbing)
{
checkVisibleStolenItemsTimer -= deltaTime;
return;
}
if (character.SelectedSecondaryItem?.GetComponent<Controller>() != null)
{
// Might be e.g. sitting on a chair.
character.SelectedSecondaryItem = null;
}
foreach (var target in Character.CharacterList)
{
if (!IsValidTarget(target, character)) { continue; }
//if we spot someone wearing or holding stolen items, immediately check them (with 100% chance of spotting the stolen items)
if (target.Inventory.AllItems.Any(it => it.SpawnedInCurrentOutpost && !it.AllowStealing && target.HasEquippedItem(it)) &&
if (target.Inventory.AllItems.Any(it => it.Illegitimate && target.HasEquippedItem(it)) &&
character.CanSeeTarget(target, seeThroughWindows: true))
{
AIObjectiveCheckStolenItems? existingObjective =
objectiveManager.GetActiveObjectives<AIObjectiveCheckStolenItems>().FirstOrDefault(o => o.TargetCharacter == target);
objectiveManager.GetActiveObjectives<AIObjectiveCheckStolenItems>().FirstOrDefault(o => o.Target == target);
if (existingObjective == null)
{
objectiveManager.AddObjective(new AIObjectiveCheckStolenItems(character, target, objectiveManager));
lastInspectionTimes[target] = Timing.TotalTime;
}
}
}
checkVisibleStolenItemsTimer = CheckVisibleStolenItemsInterval;
@@ -140,7 +175,17 @@ namespace Barotrauma
if (target.Submarine != character.Submarine) { return false; }
//only player's crew can steal, ignore other teams
if (!target.IsOnPlayerTeam) { return false; }
if (target.IsArrested) { return false; }
if (target.IsHandcuffed) { return false; }
// Ignore targets that are climbing, because might need to use ladders to get to them.
if (target.IsClimbing) { return false; }
if (HumanAIController.IsTrueForAnyBotInTheCrew(bot =>
bot != HumanAIController &&
((bot.ObjectiveManager.GetActiveObjective() is AIObjectiveCheckStolenItems checkObj && checkObj.Target == target) ||
(bot.ObjectiveManager.GetActiveObjective() is AIObjectiveCombat combatObj && combatObj.Enemy == target))))
{
// Already being inspected by someone or fighting with someone in our team.
return false;
}
return true;
}
@@ -148,5 +193,11 @@ namespace Barotrauma
{
lastInspectionTimes[target] = Timing.TotalTime;
}
public override void OnDeselected()
{
base.OnDeselected();
character.DeselectCharacter();
}
}
}
@@ -12,9 +12,9 @@ namespace Barotrauma
public override Identifier Identifier { get; set; } = "fix leak".ToIdentifier();
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
public override bool AllowInFriendlySubs => true;
public override bool AllowInAnySub => true;
public override bool AllowWhileHandcuffed => false;
protected override bool AllowInFriendlySubs => true;
protected override bool AllowInAnySub => true;
protected override bool AllowWhileHandcuffed => false;
public Gap Leak { get; private set; }
@@ -37,7 +37,7 @@ namespace Barotrauma
{
if (!IsAllowed)
{
HandleNonAllowed();
HandleDisallowed();
return Priority;
}
float coopMultiplier = 1;
@@ -9,7 +9,7 @@ namespace Barotrauma
public override Identifier Identifier { get; set; } = "fix leaks".ToIdentifier();
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
public override bool AllowInFriendlySubs => true;
protected override bool AllowInFriendlySubs => true;
private Hull PrioritizedHull { get; set; }
@@ -18,7 +18,7 @@ namespace Barotrauma
PrioritizedHull = prioritizedHull;
}
protected override bool Filter(Gap gap) => IsValidTarget(gap, character);
protected override bool IsValidTarget(Gap gap) => IsValidTarget(gap, character);
public static float GetLeakSeverity(Gap leak)
{
@@ -37,7 +37,7 @@ namespace Barotrauma
}
}
protected override float TargetEvaluation()
protected override float GetTargetPriority()
{
int totalLeaks = Targets.Count;
if (totalLeaks == 0) { return 0; }
@@ -13,9 +13,9 @@ namespace Barotrauma
{
public override Identifier Identifier { get; set; } = "get item".ToIdentifier();
public override bool AbandonWhenCannotCompleteSubjectives => false;
public override bool AbandonWhenCannotCompleteSubObjectives => false;
public override bool AllowMultipleInstances => true;
public override bool AllowWhileHandcuffed => false;
protected override bool AllowWhileHandcuffed => false;
public HashSet<Item> ignoredItems = new HashSet<Item>();
@@ -444,7 +444,7 @@ namespace Barotrauma
}
if (!AllowStealing && character.IsOnPlayerTeam)
{
if (item.SpawnedInCurrentOutpost && !item.AllowStealing) { continue; }
if (item.Illegitimate) { continue; }
}
if (!CheckItem(item)) { continue; }
if (item.Container != null)
@@ -13,7 +13,7 @@ namespace Barotrauma
public override string DebugTag => $"{Identifier}";
public override bool KeepDivingGearOn => true;
public override bool AllowMultipleInstances => true;
public override bool AllowWhileHandcuffed => false;
protected override bool AllowWhileHandcuffed => false;
public bool AllowStealing { get; set; }
public bool TakeWholeStack { get; set; }
@@ -16,7 +16,7 @@ namespace Barotrauma
private readonly bool repeat;
//how long until the path to the target is declared unreachable
private float waitUntilPathUnreachable;
private bool getDivingGearIfNeeded;
private readonly bool getDivingGearIfNeeded;
/// <summary>
/// Doesn't allow the objective to complete if this condition is false
@@ -34,11 +34,6 @@ namespace Barotrauma
public bool DebugLogWhenFails { get; set; } = true;
public bool UsePathingOutside { get; set; } = true;
/// <summary>
/// Which event action created this objective (if any)
/// </summary>
public EventAction SourceEventAction;
public float ExtraDistanceWhileSwimming;
public float ExtraDistanceOutsideSub;
private float _closeEnoughMultiplier = 1;
@@ -94,10 +89,10 @@ namespace Barotrauma
/// </summary>
public bool UseDistanceRelativeToAimSourcePos { get; set; } = false;
public override bool AbandonWhenCannotCompleteSubjectives => false;
public override bool AbandonWhenCannotCompleteSubObjectives => false;
public override bool AllowOutsideSubmarine => AllowGoingOutside;
public override bool AllowInAnySub => true;
protected override bool AllowOutsideSubmarine => AllowGoingOutside;
protected override bool AllowInAnySub => true;
public Identifier DialogueIdentifier { get; set; } = "dialogcannotreachtarget".ToIdentifier();
public LocalizedString TargetName { get; set; }
@@ -287,10 +282,12 @@ namespace Barotrauma
if (waitUntilPathUnreachable < 0)
{
waitUntilPathUnreachable = pathWaitingTime;
if (repeat)
if (repeat && !IsCompleted)
{
SpeakCannotReach();
return;
if (!IsDoneFollowing())
{
SpeakCannotReach();
}
}
else
{
@@ -374,16 +371,10 @@ namespace Barotrauma
return;
}
}
if (repeat && IsCloseEnough)
if (IsDoneFollowing())
{
if (requiredCondition == null || requiredCondition())
{
if (character.CanSeeTarget(Target) && (!character.IsClimbing || IsFollowOrder))
{
OnCompleted();
return;
}
}
OnCompleted();
return;
}
float maxGapDistance = 500;
Character targetCharacter = Target as Character;
@@ -653,6 +644,21 @@ namespace Barotrauma
character.SetInput(InputType.Aim, false, true);
character.SetInput(InputType.Shoot, false, true);
}
bool IsDoneFollowing()
{
if (repeat && IsCloseEnough)
{
if (requiredCondition == null || requiredCondition())
{
if (character.CanSeeTarget(Target) && (!character.IsClimbing || IsFollowOrder))
{
return true;
}
}
}
return false;
}
}
private bool useScooter;
@@ -12,7 +12,7 @@ namespace Barotrauma
{
public override Identifier Identifier { get; set; } = "idle".ToIdentifier();
public override bool AllowAutomaticItemUnequipping => true;
public override bool AllowInAnySub => true;
protected override bool AllowInAnySub => true;
private BehaviorType behavior;
public BehaviorType Behavior
@@ -91,8 +91,6 @@ namespace Barotrauma
protected override bool CheckObjectiveSpecific() => false;
public override bool CanBeCompleted => true;
public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace.CleanupStackTrace()); }
public readonly HashSet<Identifier> PreferredOutpostModuleTypes = new HashSet<Identifier>();
public void CalculatePriority(float max = 0)
@@ -266,7 +264,7 @@ namespace Barotrauma
if (node.Waypoint.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(node.Waypoint.CurrentHull)) { return false; }
return true;
//don't stop at ladders when idling
}, endNodeFilter: node => node.Waypoint.Ladders == null && (!isCurrentHullAllowed || !IsForbidden(node.Waypoint.CurrentHull)));
}, endNodeFilter: node => node.Waypoint.Stairs == null && node.Waypoint.Ladders == null && (!isCurrentHullAllowed || !IsForbidden(node.Waypoint.CurrentHull)));
if (path.Unreachable)
{
//can't go to this room, remove it from the list and try another room
@@ -299,7 +297,7 @@ namespace Barotrauma
{
PathSteering.SteeringSeek(character.GetRelativeSimPosition(currentTarget), weight: 1,
nodeFilter: node => node.Waypoint.CurrentHull != null,
endNodeFilter: node => node.Waypoint.Ladders == null);
endNodeFilter: node => node.Waypoint.Ladders == null && node.Waypoint.Stairs == null);
}
else
{
@@ -0,0 +1,126 @@
using System;
namespace Barotrauma
{
class AIObjectiveInspectNoises : AIObjective
{
public override Identifier Identifier { get; set; } = "inspect noises".ToIdentifier();
private AIObjectiveGoTo inspectNoiseObjective;
/// <summary>
/// Initial priority of the objective to check noises made by enemies
/// </summary>
const float InspectNoisePriority = 10.0f;
/// <summary>
/// How much the priority of the objective to check noises made by enemies increases per noise
/// </summary>
const float InspectNoisePriorityIncrease = 10.0f;
private const float InspectNoiseInterval = 1.0f;
private float inspectNoiseTimer;
/// <summary>
/// If the character is not currently inspecting the noise (= if some other objective is taking priority)
/// it forgets about it after this delay runs out. Otherwise they might unnecessarily go and inspect some
/// noise that was emitted a long time ago once done with the higher-prio objective.
/// </summary>
private const float InspectNoiseExpirationDelay = 60.0f;
private float inspectNoiseExpirationTimer = 0.0f;
protected override float GetPriority() => inspectNoiseObjective?.Priority ?? 0.0f;
public AIObjectiveInspectNoises(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
inspectNoiseTimer = Rand.Range(0.0f, InspectNoiseInterval);
}
public override void Update(float deltaTime)
{
base.Update(deltaTime);
inspectNoiseTimer -= deltaTime;
if (inspectNoiseTimer <= 0.0f)
{
CheckEnemyNoises();
inspectNoiseTimer = InspectNoiseInterval;
}
//if we're not currently inspecting the noise (something else taking priority), forget about it after a while
if (inspectNoiseObjective != null && objectiveManager.GetActiveObjective() != inspectNoiseObjective)
{
inspectNoiseExpirationTimer += deltaTime;
if (inspectNoiseExpirationTimer > InspectNoiseExpirationDelay)
{
inspectNoiseObjective.Abandon = true;
}
}
}
/// <summary>
/// Check if there's any loud provocative items used by enemies nearby (= if someone fired a gun somewhere), and go inspect them
/// </summary>
private void CheckEnemyNoises()
{
if (character.CurrentHull == null) { return; }
//forget about inspecting if we're doing another subobjective (= fighting something)
if (inspectNoiseObjective != null &&
CurrentSubObjective != inspectNoiseObjective)
{
inspectNoiseObjective.Abandon = true;
}
foreach (var aiTarget in AITarget.List)
{
if (aiTarget.ShouldBeIgnored()) { continue; }
if (!aiTarget.IsWithinSector(character.WorldPosition)) { continue; }
if (aiTarget.Entity is not Item item) { continue; }
if (!item.HasTag(Tags.ProvocativeToHumanAI)) { continue; }
if (item.GetRootInventoryOwner() is Character targetCharacter &&
AIObjectiveFightIntruders.IsValidTarget(targetCharacter, character, targetCharactersInOtherSubs: false))
{
float dist = character.CurrentHull.GetApproximateDistance(character.Position, targetCharacter.Position, targetCharacter.CurrentHull, aiTarget.SoundRange, distanceMultiplierPerClosedDoor: 2);
if (dist * HumanAIController.Hearing > aiTarget.SoundRange) { continue; }
character.Speak(TextManager.Get("dialogheardenemy").Value, identifier: "heardenemy".ToIdentifier(), minDurationBetweenSimilar: 30.0f);
if (inspectNoiseObjective != null && subObjectives.Contains(inspectNoiseObjective))
{
//priority of inspecting noises increases with each noise
//but orders still remain a higher priority
inspectNoiseObjective.Priority = Math.Min(inspectNoiseObjective.Priority + InspectNoisePriorityIncrease, AIObjectiveManager.LowestOrderPriority - 1);
//only refresh the target if the character hasn't yet started inspecting the noise
//(if it has, it should not switch the target, otherwise you could e.g. bounce an NPC back and forth by firing guns at different sides of an outpost)
if (objectiveManager.GetActiveObjective() != inspectNoiseObjective &&
inspectNoiseObjective.Target != targetCharacter.CurrentHull)
{
CreateInspectNoiseObjective(targetCharacter.CurrentHull, priority: inspectNoiseObjective.Priority);
}
}
else
{
CreateInspectNoiseObjective(targetCharacter.CurrentHull, priority: InspectNoisePriority);
}
}
}
void CreateInspectNoiseObjective(ISpatialEntity target, float priority)
{
RemoveSubObjective(ref inspectNoiseObjective);
inspectNoiseObjective = new AIObjectiveGoTo(target, character, objectiveManager)
{
Priority = priority,
SourceObjective = this
};
inspectNoiseObjective.Completed += () => { inspectNoiseObjective = null; inspectNoiseExpirationTimer = 0.0f; };
inspectNoiseObjective.Abandoned += () => { inspectNoiseObjective = null; inspectNoiseExpirationTimer = 0.0f; };
AddSubObjective(inspectNoiseObjective);
}
}
protected override void Act(float deltaTime)
{
}
protected override bool CheckObjectiveSpecific() => false;
}
}
@@ -1,4 +1,4 @@
using Barotrauma.Extensions;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
@@ -11,13 +11,8 @@ namespace Barotrauma
class AIObjectiveLoadItem : AIObjective
{
public override Identifier Identifier { get; set; } = "load item".ToIdentifier();
public override bool IsLoop
{
get => true;
set => throw new Exception("Trying to set the value for AIObjectiveLoadItem.IsLoop from: " + Environment.StackTrace.CleanupStackTrace());
}
public override bool AllowWhileHandcuffed => false;
protected override bool AllowWhileHandcuffed => false;
private AIObjectiveLoadItems.ItemCondition TargetItemCondition { get; }
private Item Container { get; }
@@ -163,7 +158,7 @@ namespace Barotrauma
{
if (!IsAllowed)
{
HandleNonAllowed();
HandleDisallowed();
return Priority;
}
else if (!AIObjectiveLoadItems.IsValidTarget(Container, character, targetCondition: TargetItemCondition))
@@ -298,12 +293,14 @@ namespace Barotrauma
if (item.Removed) { return false; }
if (!ValidContainableItemIdentifiers.Contains(item.Prefab.Identifier)) { return false; }
if (ignoredItems.Contains(item)) { return false; }
if ((item.Illegitimate) == character.IsOnPlayerTeam) { return false; }
if ((item.SpawnedInCurrentOutpost && !item.AllowStealing) == character.IsOnPlayerTeam) { return false; }
var rootInventoryOwner = item.GetRootInventoryOwner();
if (rootInventoryOwner is Character owner && owner != character) { return false; }
if (rootInventoryOwner is Item parentItem)
if (item.GetRootInventoryOwner() is Character owner && owner != character) { return false; }
Item parentItem = item.Container;
while (parentItem != null)
{
if (parentItem.HasTag(Tags.DontTakeItems)) { return false; }
parentItem = parentItem.Container;
}
if (!item.HasAccess(character)) { return false; }
if (!character.HasItem(item) && !CanEquip(item, allowWearing: false)) { return false; }
@@ -50,7 +50,7 @@ namespace Barotrauma
TargetCondition = option == "turretammo" ? ItemCondition.Empty : ItemCondition.Full;
}
protected override bool Filter(Item target)
protected override bool IsValidTarget(Item target)
{
//don't pass TargetContainerTags to the method (no need to filter by tags anymore, it's already done when populating TargetContainers)
if (!IsValidTarget(target, character, null, TargetCondition)) { return false; }
@@ -104,7 +104,7 @@ namespace Barotrauma
protected override void OnObjectiveCompleted(AIObjective objective, Item target)
=> HumanAIController.RemoveTargets<AIObjectiveLoadItems, Item>(character, target);
protected override float TargetEvaluation()
protected override float GetTargetPriority()
{
if (Targets.None()) { return 0; }
if (objectiveManager.IsOrder(this))
@@ -4,21 +4,34 @@ using Microsoft.Xna.Framework;
namespace Barotrauma
{
/// <summary>
/// An objective that creates specific kinds of subobjectives for specific types of targets, and loops through those targets.
/// For example, a cleanup objective that loops through items that need to be cleaned up, or a "fix leaks" objective that loops through leaks that need welding.
/// </summary>
abstract class AIObjectiveLoop<T> : AIObjective
{
public HashSet<T> Targets { get; private set; } = new HashSet<T>();
public Dictionary<T, AIObjective> Objectives { get; private set; } = new Dictionary<T, AIObjective>();
protected HashSet<T> ignoreList = new HashSet<T>();
private float ignoreListTimer;
private float ignoreListClearTimer;
protected float targetUpdateTimer;
protected virtual float TargetUpdateTimeMultiplier { get; } = 1;
/// <summary>
/// How often are the subobjectives synced based on the available targets?
/// </summary>
private float syncTimer;
private readonly float syncTime = 1;
// By default, doesn't clear the list automatically
/// <summary>
/// By default, doesn't clear the list automatically
/// </summary>
protected virtual float IgnoreListClearInterval => 0;
/// <summary>
/// Contains targets that anyone in the same crew has reported about. Used for automatic the target has to be reported before it can be can be targeted, so characters don't magically know where e.g. enemies are.
/// Ignored on orders: a bot explicitly ordered to repair leaks or fight intruders can find targets that haven't been reported.
/// </summary>
public HashSet<T> ReportedTargets { get; private set; } = new HashSet<T>();
public bool AddTarget(T target)
@@ -28,7 +41,7 @@ namespace Barotrauma
{
return false;
}
if (Filter(target))
if (IsValidTarget(target))
{
ReportedTargets.Add(target);
return true;
@@ -42,24 +55,27 @@ namespace Barotrauma
protected override void Act(float deltaTime) { }
protected override bool CheckObjectiveSpecific() => false;
public override bool CanBeCompleted => true;
public override bool AbandonWhenCannotCompleteSubjectives => false;
public override bool AbandonWhenCannotCompleteSubObjectives => false;
public override bool AllowSubObjectiveSorting => true;
public override bool AllowWhileHandcuffed => false;
protected override bool AllowWhileHandcuffed => false;
protected override bool AbandonIfDisallowed => false;
public virtual bool InverseTargetEvaluation => false;
/// <summary>
/// Makes the priority inversely proportional to the value returned by <see cref="GetTargetPriority"/>.
/// In other words, gives this objective a high priority when priority of the targets is low.
/// </summary>
public virtual bool InverseTargetPriority => false;
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)
{
base.Update(deltaTime);
if (IgnoreListClearInterval > 0)
{
if (ignoreListTimer > IgnoreListClearInterval)
if (ignoreListClearTimer > IgnoreListClearInterval)
{
if (ResetWhenClearingIgnoreList)
{
@@ -68,12 +84,12 @@ namespace Barotrauma
else
{
ignoreList.Clear();
ignoreListTimer = 0;
ignoreListClearTimer = 0;
}
}
else
{
ignoreListTimer += deltaTime;
ignoreListClearTimer += deltaTime;
}
}
if (targetUpdateTimer <= 0)
@@ -104,14 +120,17 @@ namespace Barotrauma
}
}
// the timer is set between 1 and 10 seconds, depending on the priority modifier and a random +-25%
//
/// <summary>
/// The timer is set between 1 and 10 seconds, depending on the priority modifier and a random +-25%
/// </summary>
private float CalculateTargetUpdateTimer() => targetUpdateTimer = 1 / MathHelper.Clamp(PriorityModifier * Rand.Range(0.75f, 1.25f), 0.1f, 1) * TargetUpdateTimeMultiplier;
public override void Reset()
{
base.Reset();
ignoreList.Clear();
ignoreListTimer = 0;
ignoreListClearTimer = 0;
UpdateTargets();
}
@@ -119,25 +138,25 @@ namespace Barotrauma
{
if (!IsAllowed)
{
HandleNonAllowed();
HandleDisallowed();
return Priority;
}
// Allow the target value to be more than 100.
float targetValue = TargetEvaluation();
if (InverseTargetEvaluation)
float targetPriority = GetTargetPriority();
if (InverseTargetPriority)
{
targetValue = 100 - targetValue;
targetPriority = 100 - targetPriority;
}
var currentSubObjective = CurrentSubObjective;
if (currentSubObjective != null && currentSubObjective.Priority > targetValue)
if (currentSubObjective != null && currentSubObjective.Priority > targetPriority)
{
// If the priority is higher than the target value, let's just use it.
// The priority calculation is more precise, but it takes into account things like distances,
// so it's better not to use it if it's lower than the rougher targetValue.
targetValue = currentSubObjective.Priority;
targetPriority = currentSubObjective.Priority;
}
// If the target value is less than 1% of the max value, let's just treat it as zero.
if (targetValue < 1)
if (targetPriority < 1)
{
Priority = 0;
}
@@ -145,7 +164,7 @@ namespace Barotrauma
{
if (objectiveManager.IsOrder(this))
{
Priority = ForceOrderPriority ? objectiveManager.GetOrderPriority(this) : targetValue;
Priority = ForceOrderPriority ? objectiveManager.GetOrderPriority(this) : targetPriority;
}
else
{
@@ -155,7 +174,7 @@ namespace Barotrauma
// Allow higher prio
max = AIObjectiveManager.EmergencyObjectivePriority;
}
float value = MathHelper.Clamp((CumulatedDevotion + (targetValue * PriorityModifier)) / 100, 0, 1);
float value = MathHelper.Clamp((CumulatedDevotion + (targetPriority * PriorityModifier)) / 100, 0, 1);
Priority = MathHelper.Lerp(0, max, value);
}
}
@@ -181,7 +200,7 @@ namespace Barotrauma
bool ignore = this is AIObjectiveChargeBatteries || this is AIObjectivePumpWater || this is AIObjectiveFindThieves;
if (!ignore && !ReportedTargets.Contains(target)) { continue; }
}
if (!Filter(target)) { continue; }
if (!IsValidTarget(target)) { continue; }
if (!ignoreList.Contains(target))
{
Targets.Add(target);
@@ -228,9 +247,13 @@ namespace Barotrauma
/// </summary>
protected abstract IEnumerable<T> GetList();
protected abstract float TargetEvaluation();
/// <summary>
/// Returns a priority value based on the current targets (e.g. high prio when there's lots of severe fires or leaks).
/// The priority of this objective is based on the target priority.
/// </summary>
protected abstract float GetTargetPriority();
protected abstract AIObjective ObjectiveConstructor(T target);
protected abstract bool Filter(T target);
protected abstract bool IsValidTarget(T target);
}
}
@@ -20,10 +20,27 @@ namespace Barotrauma
MaxValue = 2
}
/// <summary>
/// Highest possible priority for any objective. Used in certain cases where the character needs to react immediately to survive,
/// such as finding a suit when under pressure or getting out of a burning room.
/// </summary>
public const float MaxObjectivePriority = 100;
/// <summary>
/// Priority of objectives such as finding safety, rescuing someone in a critical state or defending against an attacker
/// (= objectives that are critical for saving the character's or someone else's life)
/// </summary>
public const float EmergencyObjectivePriority = 90;
/// <summary>
/// Maximum priority of an order given to the character (forced order, or the leftmost order in the crew list)
/// </summary>
public const float HighestOrderPriority = 70;
/// <summary>
/// Maximum priority of an order given to the character (rightmost order in the crew list)
/// </summary>
public const float LowestOrderPriority = 60;
/// <summary>
/// Objectives with a priority equal to or higher than this make the character run.
/// </summary>
public const float RunPriority = 50;
// Constantly increases the priority of the selected objective, unless overridden
public const float baseDevotion = 5;
@@ -138,6 +155,7 @@ namespace Barotrauma
return;
#endif
}
foreach (var delayedObjective in DelayedObjectives)
{
CoroutineManager.StopCoroutines(delayedObjective.Value);
@@ -159,36 +177,46 @@ namespace Barotrauma
AddObjective(newIdleObjective);
int objectiveCount = Objectives.Count;
foreach (var autonomousObjective in character.Info.Job.Prefab.AutonomousObjectives)
if (character.Info?.Job != null)
{
var orderPrefab = OrderPrefab.Prefabs[autonomousObjective.Identifier];
if (orderPrefab == null) { throw new Exception($"Could not find a matching prefab by the identifier: '{autonomousObjective.Identifier}'"); }
Item item = null;
if (orderPrefab.MustSetTarget)
foreach (var autonomousObjective in character.Info.Job.Prefab.AutonomousObjectives)
{
item = orderPrefab.GetMatchingItems(character.Submarine, mustBelongToPlayerSub: false, requiredTeam: character.Info.TeamID, interactableFor: character)?.GetRandomUnsynced();
}
var order = new Order(orderPrefab, autonomousObjective.Option, item ?? character.CurrentHull as Entity, orderPrefab.GetTargetItemComponent(item), orderGiver: character);
if (order == null) { continue; }
if ((order.IgnoreAtOutpost || autonomousObjective.IgnoreAtOutpost) &&
Level.IsLoadedFriendlyOutpost && character.TeamID != CharacterTeamType.FriendlyNPC && !character.IsFriendlyNPCTurnedHostile)
{
if (Submarine.MainSub != null && Submarine.MainSub.DockedTo.None(s => s.TeamID != CharacterTeamType.FriendlyNPC && s.TeamID != character.TeamID))
var orderPrefab = OrderPrefab.Prefabs[autonomousObjective.Identifier] ?? throw new Exception($"Could not find a matching prefab by the identifier: '{autonomousObjective.Identifier}'");
Item item = null;
if (orderPrefab.MustSetTarget)
{
item = orderPrefab.GetMatchingItems(character.Submarine, mustBelongToPlayerSub: false, requiredTeam: character.Info.TeamID, interactableFor: character)?.GetRandomUnsynced();
}
var order = new Order(orderPrefab, autonomousObjective.Option, item ?? character.CurrentHull as Entity, orderPrefab.GetTargetItemComponent(item), orderGiver: character);
if (order == null) { continue; }
if ((order.IgnoreAtOutpost || autonomousObjective.IgnoreAtOutpost) &&
Level.IsLoadedFriendlyOutpost && character.TeamID != CharacterTeamType.FriendlyNPC && !character.IsFriendlyNPCTurnedHostile)
{
if (Submarine.MainSub != null && Submarine.MainSub.DockedTo.None(s => s.TeamID != CharacterTeamType.FriendlyNPC && s.TeamID != character.TeamID))
{
continue;
}
}
if (autonomousObjective.IgnoreAtNonOutpost && !Level.IsLoadedFriendlyOutpost)
{
continue;
}
}
if (autonomousObjective.IgnoreAtNonOutpost && !Level.IsLoadedFriendlyOutpost)
{
continue;
}
var objective = CreateObjective(order, autonomousObjective.PriorityModifier);
if (objective != null && objective.CanBeCompleted)
{
AddObjective(objective, delay: Rand.Value() / 2);
objectiveCount++;
}
}
var objective = CreateObjective(order, autonomousObjective.PriorityModifier);
if (objective != null && objective.CanBeCompleted)
{
AddObjective(objective, delay: Rand.Value() / 2);
objectiveCount++;
}
}
}
else
{
string warningMsg = character.Info == null ?
$"The character {character.DisplayName} has been set to use human ai, but has no {nameof(CharacterInfo)}. This may cause issues with the AI. Consider adding {nameof(CharacterPrefab.HasCharacterInfo)}=\"True\" to the character config." :
$"The character {character.DisplayName} has been set to use human ai, but has no job. This may cause issues with the AI. Consider configuring some jobs for the character type.";
DebugConsole.AddWarning(warningMsg, character.Prefab.ContentPackage);
}
_waitTimer = Math.Max(_waitTimer, Rand.Range(0.5f, 1f) * objectiveCount);
}
@@ -501,7 +529,6 @@ namespace Barotrauma
if (!order.TargetItemComponent.Item.IsInteractable(character)) { return null; }
newObjective = new AIObjectiveOperateItem(targetPump, character, this, order.Option, false, priorityModifier: priorityModifier)
{
IsLoop = false,
Override = order.OrderGiver is { IsCommanding: true }
};
newObjective.Completed += () => DismissSelf(order);
@@ -531,7 +558,7 @@ namespace Barotrauma
newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, order.Option,
requireEquip: false, useController: order.UseController, controller: order.ConnectedController, priorityModifier: priorityModifier)
{
IsLoop = true,
Repeat = true,
// Don't override unless it's an order by a player
Override = order.OrderGiver != null && order.OrderGiver.IsCommanding
};
@@ -539,7 +566,6 @@ namespace Barotrauma
case "setchargepct":
newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, order.Option, false, priorityModifier: priorityModifier)
{
IsLoop = false,
Override = !character.IsDismissed,
completionCondition = () =>
{
@@ -603,7 +629,6 @@ namespace Barotrauma
{
prepareObjective = new AIObjectivePrepare(character, this, order.GetTargetItems(order.Option), order.RequireItems)
{
KeepActiveWhenReady = false,
CheckInventory = false,
EvaluateCombatPriority = true,
FindAllItems = false,
@@ -621,13 +646,16 @@ namespace Barotrauma
case "deconstructitems":
newObjective = new AIObjectiveDeconstructItems(character, this, priorityModifier);
break;
case "inspectnoises":
newObjective = new AIObjectiveInspectNoises(character, this, priorityModifier);
break;
default:
if (order.TargetItemComponent == null) { return null; }
if (!order.TargetItemComponent.Item.IsInteractable(character)) { return null; }
newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, order.Option,
requireEquip: false, useController: order.UseController, controller: order.ConnectedController, priorityModifier: priorityModifier)
{
IsLoop = true,
Repeat = true,
// Don't override unless it's an order by a player
Override = order.OrderGiver != null && order.OrderGiver.IsCommanding
};
@@ -689,10 +717,16 @@ namespace Barotrauma
/// Only checks the current order. Deprecated, use pattern matching instead.
/// </summary>
public bool IsCurrentOrder<T>() where T : AIObjective => CurrentOrder is T;
/// <summary>
/// Checks the current objective (which can be an order too). Deprecated, use pattern matching instead.
/// </summary>
public bool IsCurrentObjective<T>() where T : AIObjective => CurrentObjective is T;
/// <summary>
/// Checks if any objectives or orders are of the specified type. Regardless of whether the objective is active or inactive.
/// </summary>
public bool HasObjectiveOrOrder<T>() where T : AIObjective => Objectives.Any(o => o is T) || HasOrder<T>();
public AIObjective GetActiveObjective() => CurrentObjective?.GetActiveObjective();
@@ -705,10 +739,23 @@ namespace Barotrauma
/// Return the first order with the specified objective. Can return null.
/// </summary>
public Order GetOrder(AIObjective objective) => CurrentOrders.FirstOrDefault(o => o.Objective == objective);
/// <summary>
/// Returns the last active objective of the specified objective type.
/// Should generally be used to get the active objective (or subobjective) of objectives that don't sort their subobjectives by priority (see <see cref="AIObjective.AllowSubObjectiveSorting"/>.
/// </summary>
/// <returns>The last active objective of the specified type if found.
/// </returns>
public T GetLastActiveObjective<T>() where T : AIObjective
=> CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).LastOrDefault(so => so is T) as T;
/// <summary>
/// Returns the first active objective of the specified objective type.
/// Should generally be used to get the active objective (or subobjective) of objectives that sort their subobjectives by priority, such as those that inherit <see cref="AIObjectiveLoop"/>.
/// </summary>
/// <returns>
/// The first active objective of the specified type if found.
/// </returns>
public T GetFirstActiveObjective<T>() where T : AIObjective
=> CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).FirstOrDefault(so => so is T) as T;
@@ -13,8 +13,8 @@ namespace Barotrauma
public override bool AllowAutomaticItemUnequipping => true;
public override bool AllowMultipleInstances => true;
public override bool AllowInAnySub => true;
public override bool AllowWhileHandcuffed => false;
protected override bool AllowInAnySub => true;
protected override bool AllowWhileHandcuffed => false;
public override bool PrioritizeIfSubObjectivesActive => component != null && (component is Reactor || component is Turret);
private readonly ItemComponent component, controller;
@@ -29,7 +29,12 @@ namespace Barotrauma
/// </summary>
public Func<PathNode, bool> EndNodeFilter;
public bool Override { get; set; } = true;
public bool Override { get; init; } = true;
/// <summary>
/// When true, the operate objective is never completed, unless it's abandoned.
/// </summary>
public bool Repeat { get; init; }
public override bool CanBeCompleted => base.CanBeCompleted && (!useController || controller != null);
@@ -50,7 +55,7 @@ namespace Barotrauma
bool isOrder = objectiveManager.IsOrder(this);
if (!IsAllowed)
{
HandleNonAllowed();
HandleDisallowed();
return Priority;
}
if (!isOrder && component.Item.ConditionPercentage <= 0)
@@ -307,7 +312,7 @@ namespace Barotrauma
}
}
protected override bool CheckObjectiveSpecific() => isDoneOperating && !IsLoop;
protected override bool CheckObjectiveSpecific() => isDoneOperating && !Repeat;
public override void Reset()
{
@@ -13,7 +13,7 @@ namespace Barotrauma
public override bool KeepDivingGearOn => true;
public override bool KeepDivingGearOnAlsoWhenInactive => true;
public override bool PrioritizeIfSubObjectivesActive => true;
public override bool AllowWhileHandcuffed => false;
protected override bool AllowWhileHandcuffed => false;
private AIObjectiveGetItem getSingleItemObjective;
private AIObjectiveGetItems getAllItemsObjective;
@@ -22,7 +22,6 @@ namespace Barotrauma
private readonly Item targetItem;
private readonly ImmutableArray<Identifier> requiredItems;
private readonly ImmutableArray<Identifier> optionalItems;
private readonly HashSet<Item> items = new HashSet<Item>();
public bool KeepActiveWhenReady { get; set; }
public bool CheckInventory { get; set; }
public bool FindAllItems { get; set; }
@@ -61,12 +60,12 @@ namespace Barotrauma
{
if (!IsAllowed)
{
HandleNonAllowed();
HandleDisallowed();
return Priority;
}
Priority = objectiveManager.GetOrderPriority(this);
var subObjective = GetSubObjective();
if (subObjective != null && subObjective.IsCompleted)
if (subObjective is { IsCompleted: true })
{
Priority = 0;
}
@@ -113,20 +112,7 @@ namespace Barotrauma
},
onCompleted: () =>
{
if (KeepActiveWhenReady)
{
if (objectiveReference != null)
{
foreach (var item in objectiveReference.achievedItems)
{
if (item?.IsOwnedBy(character) != null)
{
items.Add(item);
}
}
}
}
else
if (!KeepActiveWhenReady)
{
IsCompleted = true;
}
@@ -165,22 +151,11 @@ namespace Barotrauma
if (!TryAddSubObjective(ref getSingleItemObjective, getItemConstructor,
onCompleted: () =>
{
if (KeepActiveWhenReady)
{
if (getSingleItemObjective != null)
{
var item = getSingleItemObjective?.TargetItem;
if (item?.IsOwnedBy(character) != null)
{
items.Add(item);
}
}
}
else
if (!KeepActiveWhenReady)
{
IsCompleted = true;
}
},
},
onAbandon: () => Abandon = true))
{
Abandon = true;
@@ -193,7 +168,6 @@ namespace Barotrauma
public override void Reset()
{
base.Reset();
items.Clear();
subObjectivesCreated = false;
getMultipleItemsObjective = null;
getSingleItemObjective = null;
@@ -12,7 +12,7 @@ namespace Barotrauma
public override Identifier Identifier { get; set; } = "pump water".ToIdentifier();
public override bool KeepDivingGearOn => true;
public override bool AllowAutomaticItemUnequipping => true;
public override bool AllowWhileHandcuffed => false;
protected override bool AllowWhileHandcuffed => false;
private List<Pump> pumpList;
@@ -25,7 +25,7 @@ namespace Barotrauma
base.FindTargets();
}
protected override bool Filter(Pump pump)
protected override bool IsValidTarget(Pump pump)
{
if (pump?.Item == null || pump.Item.Removed) { return false; }
if (pump.Item.IgnoreByAI(character)) { return false; }
@@ -62,7 +62,7 @@ namespace Barotrauma
return pumpList;
}
protected override float TargetEvaluation()
protected override float GetTargetPriority()
{
if (Targets.None()) { return 0; }
if (Option == "stoppumping")
@@ -90,7 +90,6 @@ namespace Barotrauma
protected override AIObjective ObjectiveConstructor(Pump pump)
=> new AIObjectiveOperateItem(pump, character, objectiveManager, Option, false)
{
IsLoop = false,
completionCondition = () => IsReady(pump)
};
@@ -10,9 +10,9 @@ namespace Barotrauma
{
public override Identifier Identifier { get; set; } = "repair item".ToIdentifier();
public override bool AllowInFriendlySubs => true;
protected override bool AllowInFriendlySubs => true;
public override bool KeepDivingGearOn => Item?.CurrentHull == null;
public override bool AllowWhileHandcuffed => false;
protected override bool AllowWhileHandcuffed => false;
public Item Item { get; private set; }
@@ -37,7 +37,7 @@ namespace Barotrauma
protected override float GetPriority()
{
if (!IsAllowed) { HandleNonAllowed(); }
if (!IsAllowed) { HandleDisallowed(); }
if (Item.IgnoreByAI(character))
{
Abandon = true;
@@ -19,9 +19,9 @@ namespace Barotrauma
public Item PrioritizedItem { get; private set; }
public override bool AllowMultipleInstances => true;
public override bool AllowInFriendlySubs => true;
protected override bool AllowInFriendlySubs => true;
public readonly static float RequiredSuccessFactor = 0.4f;
public const float RequiredSuccessFactor = 0.4f;
public override bool IsDuplicate<T>(T otherObjective) => otherObjective is AIObjectiveRepairItems repairObjective && objectiveManager.IsOrder(repairObjective) == objectiveManager.IsOrder(this);
@@ -62,7 +62,7 @@ namespace Barotrauma
}
}
protected override bool Filter(Item item)
protected override bool IsValidTarget(Item item)
{
if (!ViableForRepair(item, character, HumanAIController)) { return false; };
if (!Objectives.ContainsKey(item))
@@ -94,7 +94,7 @@ namespace Barotrauma
return item.Repairables.All(r => !r.IsBelowRepairThreshold);
}
protected override float TargetEvaluation()
protected override float GetTargetPriority()
{
var selectedItem = character.SelectedItem;
if (selectedItem != null && AIObjectiveRepairItem.IsRepairing(character, selectedItem) && selectedItem.ConditionPercentage < 100)
@@ -13,10 +13,9 @@ namespace Barotrauma
public override Identifier Identifier { get; set; } = "rescue".ToIdentifier();
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
public override bool AllowOutsideSubmarine => true;
public override bool AllowInAnySub => true;
public override bool AllowWhileHandcuffed => false;
protected override bool AllowOutsideSubmarine => true;
protected override bool AllowInAnySub => true;
protected override bool AllowWhileHandcuffed => false;
const float TreatmentDelay = 0.5f;
@@ -484,7 +483,7 @@ namespace Barotrauma
protected override float GetPriority()
{
if (Target == null) { Abandon = true; }
if (!IsAllowed) { HandleNonAllowed(); }
if (!IsAllowed) { HandleDisallowed(); }
if (Abandon)
{
return Priority;
@@ -531,8 +530,8 @@ namespace Barotrauma
public override void OnDeselected()
{
character.SelectedCharacter = null;
base.OnDeselected();
character.DeselectCharacter();
}
}
}
@@ -9,9 +9,9 @@ namespace Barotrauma
{
public override Identifier Identifier { get; set; } = "rescue all".ToIdentifier();
public override bool ForceRun => true;
public override bool InverseTargetEvaluation => true;
public override bool AllowOutsideSubmarine => true;
public override bool AllowInAnySub => true;
public override bool InverseTargetPriority => true;
protected override bool AllowOutsideSubmarine => true;
protected override bool AllowInAnySub => true;
private readonly HashSet<Character> charactersWithMinorInjuries = new HashSet<Character>();
@@ -32,7 +32,7 @@ namespace Barotrauma
public AIObjectiveRescueAll(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier) { }
protected override bool Filter(Character target)
protected override bool IsValidTarget(Character target)
{
if (!IsValidTarget(target, character, out bool ignoredasMinorWounds))
{
@@ -61,7 +61,7 @@ namespace Barotrauma
protected override IEnumerable<Character> GetList() => Character.CharacterList;
protected override float TargetEvaluation()
protected override float GetTargetPriority()
{
if (Targets.None()) { return 100; }
if (!objectiveManager.IsOrder(this))
@@ -12,8 +12,8 @@ namespace Barotrauma
private AIObjectiveGoTo moveInsideObjective, moveOutsideObjective;
private bool usingEscapeBehavior, isSteeringThroughGap;
public override bool AllowOutsideSubmarine => true;
public override bool AllowInAnySub => true;
protected override bool AllowOutsideSubmarine => true;
protected override bool AllowInAnySub => true;
public AIObjectiveReturn(Character character, Character orderGiver, AIObjectiveManager objectiveManager, float priorityModifier = 1.0f) : base(character, objectiveManager, priorityModifier)
{
@@ -178,7 +178,6 @@ namespace Barotrauma
public PetBehavior(XElement element, EnemyAIController aiController)
{
AIController = aiController;
AIController.Character.CanBeDragged = true;
MaxHappiness = element.GetAttributeFloat(nameof(MaxHappiness), 100.0f);
UnhappyThreshold = element.GetAttributeFloat(nameof(UnhappyThreshold), MaxHappiness * 0.25f);
@@ -75,7 +75,7 @@ namespace Barotrauma
public void Update(float deltaTime)
{
if (!Active || character.IsArrested) { return; }
if (!Active || character.IsHandcuffed) { return; }
decisionTimer -= deltaTime;
if (decisionTimer <= 0.0f)
{
@@ -1,4 +1,4 @@
using Barotrauma.Extensions;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
@@ -109,7 +109,7 @@ namespace Barotrauma
private static IEnumerable<MapEntity> GetThalamusEntities(Submarine wreck, Identifier tag) => MapEntity.MapEntityList.Where(e => e.Submarine == wreck && e.Prefab != null && IsThalamus(e.Prefab, tag));
private static bool IsThalamus(MapEntityPrefab entityPrefab, Identifier tag) => entityPrefab.HasSubCategory("thalamus") || entityPrefab.Tags.Contains(tag);
public static bool IsThalamus(MapEntityPrefab entityPrefab, Identifier tag) => entityPrefab.HasSubCategory("thalamus") || entityPrefab.Tags.Contains(tag);
public static WreckAI Create(Submarine wreck)
{
@@ -213,7 +213,7 @@ namespace Barotrauma
foreach (Item item in thalamusItems)
{
// Ensure that thalamus items are visible
item.HiddenInGame = false;
item.IsLayerHidden = false;
if (item.HasTag(Config.Spawner))
{
if (!spawnOrgans.Contains(item))
@@ -154,7 +154,10 @@ namespace Barotrauma
Collider.SetTransformIgnoreContacts(mainLimb.SimPosition, mainLimb.Rotation);
//reset pull joints to prevent the character from "hanging" mid-air if pull joints had been active when the character was still moving
//(except when dragging, then we need the pull joints)
if (!character.CanBeDragged || character.SelectedBy == null) { ResetPullJoints(); }
if (!Draggable || character.SelectedBy == null)
{
ResetPullJoints();
}
}
if (character.IsDead && deathAnimTimer < deathAnimDuration)
{
@@ -309,7 +309,10 @@ namespace Barotrauma
Collider.SetTransformIgnoreContacts(MainLimb.SimPosition, MainLimb.Rotation);
//reset pull joints to prevent the character from "hanging" mid-air if pull joints had been active when the character was still moving
//(except when dragging, then we need the pull joints)
if (!character.CanBeDragged || character.SelectedBy == null) { ResetPullJoints(); }
if (!Draggable || character.SelectedBy == null)
{
ResetPullJoints();
}
}
return;
}
@@ -1104,8 +1104,8 @@ namespace Barotrauma
if (newHull?.Submarine == null && currentHull?.Submarine != null)
{
//don't teleport out yet if the character is going through a gap
if (Gap.FindAdjacent(Gap.GapList.Where(g => g.Submarine == currentHull.Submarine), findPos, 150.0f) != null) { return; }
if (Limbs.Any(l => Gap.FindAdjacent(currentHull.ConnectedGaps, l.WorldPosition, ConvertUnits.ToDisplayUnits(l.body.GetSize().Combine())) != null)) { return; }
if (Gap.FindAdjacent(Gap.GapList.Where(g => g.Submarine == currentHull.Submarine), findPos, 150.0f, allowRoomToRoom: true) != null) { return; }
if (Limbs.Any(l => Gap.FindAdjacent(currentHull.ConnectedGaps, l.WorldPosition, ConvertUnits.ToDisplayUnits(l.body.GetSize().Combine()), allowRoomToRoom: true) != null)) { return; }
character.MemLocalState?.Clear();
Teleport(ConvertUnits.ToSimUnits(currentHull.Submarine.Position), currentHull.Submarine.Velocity);
}
@@ -1,17 +1,17 @@
using Barotrauma.Networking;
using Barotrauma.Abilities;
using Barotrauma.Extensions;
using Barotrauma.IO;
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using FarseerPhysics;
using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
using System;
using Barotrauma.IO;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Items.Components;
using FarseerPhysics.Dynamics;
using Barotrauma.Extensions;
using System.Collections.Immutable;
using Barotrauma.Abilities;
using System.Diagnostics;
#if SERVER
using System.Text;
#endif
@@ -354,6 +354,15 @@ namespace Barotrauma
public bool IsInstigator => CombatAction is { IsInstigator: true };
/// <summary>
/// Do the outpost security officers treat the character as a criminal?
/// Triggers when the character has either committed a major crime or resisted being arrested (or fled).
/// Only affects the reactions of friendly NPCs in the outposts.
/// The NPCs still don't react immediately to "criminals", but take this into account when the character next time does something wrong.
/// The consequences are that the guards will not hold fire and will not give more warnings before attacking.
/// </summary>
public bool IsCriminal;
/// <summary>
/// Set true only, if the character is turned hostile from an escort mission (See <see cref="EscortMission"/>).
/// </summary>
@@ -798,15 +807,27 @@ namespace Barotrauma
}
private float obstructVisionAmount;
public float ObstructVisionAmount
{
get { return obstructVisionAmount; }
set
{
obstructVisionAmount = MathHelper.Clamp(value, 0.0f, 1.0f);
}
}
/// <summary>
/// Provided for backwards compatibility: use <see cref="ObstructVisionAmount"/> instead.
/// </summary>
public bool ObstructVision
{
get
{
return obstructVisionAmount > 0.5f;
return obstructVisionAmount > 0.01f;
}
set
{
obstructVisionAmount = value ? 1.0f : 0.0f;
obstructVisionAmount = value ? 0.5f : 0.0f;
}
}
@@ -860,7 +881,7 @@ namespace Barotrauma
get { return CharacterHealth.IsUnconscious; }
}
public bool IsArrested
public bool IsHandcuffed
{
get { return IsHuman && HasEquippedItem(Tags.HandLockerItem); }
}
@@ -958,6 +979,18 @@ namespace Barotrauma
}
}
private float textChatVolume;
/// <summary>
/// How "loud" the player is when they use text chat.
/// When the user speaks in text chat this gets set to 1 and then slowly decreases back to 0 over 5 seconds.
/// </summary>
public float TextChatVolume
{
get => textChatVolume;
set => textChatVolume = MathHelper.Clamp(value, 0.0f, 1.0f);
}
public float PressureTimer
{
get;
@@ -1108,37 +1141,8 @@ namespace Barotrauma
return !Removed;
}
}
private bool canBeDragged = true;
public bool CanBeDragged
{
get
{
if (!canBeDragged) { return false; }
if (Removed || !AnimController.Draggable) { return false; }
return IsKnockedDown || LockHands || IsPet || CanInventoryBeAccessed;
}
set { canBeDragged = value; }
}
//can other characters access the inventory of this character
private bool canInventoryBeAccessed = true;
public bool CanInventoryBeAccessed
{
get
{
if (!canInventoryBeAccessed || Removed || Inventory == null) { return false; }
if (!Inventory.AccessibleWhenAlive)
{
return IsDead;
}
else
{
return IsKnockedDown || LockHands || IsBot && IsOnPlayerTeam;
}
}
set { canInventoryBeAccessed = value; }
}
public bool IsDraggable => !Removed || AnimController.Draggable;
public bool CanAim
{
@@ -1328,6 +1332,10 @@ namespace Barotrauma
: base(null, id)
{
wallet = new Wallet(Option<Character>.Some(this));
if (GameMain.GameSession?.Campaign?.Bank is { } bank)
{
wallet.SetRewardDistribution(bank.RewardDistribution);
}
this.Seed = seed;
this.Prefab = prefab;
@@ -1363,6 +1371,8 @@ namespace Barotrauma
if (Info != null)
{
teamID = Info.TeamID;
//no longer a new hire after spawning (only displayed as a new hire at the end of the outpost round, when the character hasn't spawned yet)
Info.IsNewHire = false;
}
keys = new Key[Enum.GetNames(typeof(InputType)).Length];
for (int i = 0; i < Enum.GetNames(typeof(InputType)).Length; i++)
@@ -1880,7 +1890,7 @@ namespace Barotrauma
public bool CanRunWhileDragging()
{
if (selectedCharacter == null || !selectedCharacter.CanBeDragged) { return true; }
if (selectedCharacter is not { IsDraggable: true }) { return true; }
//if the dragged character is conscious, don't allow running (the dragged character won't keep up, and the dragging gets interrupted)
if (!selectedCharacter.IsIncapacitated && selectedCharacter.Stun <= 0.0f) { return false; }
return HasAbilityFlag(AbilityFlags.MoveNormallyWhileDragging);
@@ -2581,18 +2591,15 @@ namespace Barotrauma
return null;
}
public bool CanAccessInventory(Inventory inventory)
public bool CanAccessInventory(Inventory inventory, CharacterInventory.AccessLevel accessLevel = CharacterInventory.AccessLevel.Limited)
{
if (!CanInteract || inventory.Locked) { return false; }
//the inventory belongs to some other character
if (inventory.Owner is Character character && inventory.Owner != this)
if (inventory.Owner is Character inventoryOwner)
{
var owner = character;
//can only be accessed if the character is incapacitated and has been selected
return SelectedCharacter == owner && owner.CanInventoryBeAccessed;
return inventoryOwner.IsInventoryAccessibleTo(this, accessLevel) && (inventoryOwner == this || CanInteractWith(inventoryOwner));
}
if (inventory.Owner is Item item)
{
if (!CanInteractWith(item))
@@ -2613,7 +2620,47 @@ namespace Barotrauma
}
return true;
}
public bool CanBeHealedBy(Character character, bool checkFriendlyTeam = true) =>
!character.IsClimbing && !DisableHealthWindow &&
UseHealthWindow && character.CanInteract &&
(!checkFriendlyTeam || IsFriendly(character) || CanBeDraggedBy(character)) &&
character.CanInteractWith(this, 160f, false);
public bool CanBeDraggedBy(Character character)
{
if (!IsDraggable) { return false; }
return IsKnockedDown || LockHands || IsPet || (IsBot && character.TeamID == TeamID);
}
/// <summary>
/// Is the inventory accessible to the character? Doesn't check if the character can actually interact with it (distance checks etc).
/// </summary>
public bool IsInventoryAccessibleTo(Character character, CharacterInventory.AccessLevel accessLevel = CharacterInventory.AccessLevel.Limited)
{
if (Removed || Inventory == null) { return false; }
if (!Inventory.AccessibleWhenAlive && !IsDead)
{
if (character == this)
{
return Inventory.AccessibleByOwner;
}
return false;
}
if (character == this) { return true; }
if (IsKnockedDown || LockHands) { return true; }
return accessLevel switch
{
CharacterInventory.AccessLevel.Restricted => false,
CharacterInventory.AccessLevel.Limited => (IsBot && IsOnSameTeam()) || IsFriendlyPet(),
CharacterInventory.AccessLevel.Allowed => IsOnSameTeam() || IsFriendlyPet(),
_ => throw new NotImplementedException()
};
bool IsOnSameTeam() => character.TeamID == teamID;
bool IsFriendlyPet() => IsPet && character.IsFriendly(this);
}
private Stopwatch sw;
private Stopwatch StopWatch => sw ??= new Stopwatch();
private float _selectedItemPriority;
@@ -2701,7 +2748,7 @@ namespace Barotrauma
public bool CanInteractWith(Character c, float maxDist = 200.0f, bool checkVisibility = true, bool skipDistanceCheck = false)
{
if (c == this || Removed || !c.Enabled || !c.CanBeSelected || c.InvisibleTimer > 0.0f) { return false; }
if (!c.CharacterHealth.UseHealthWindow && !c.CanBeDragged && (c.onCustomInteract == null || !c.AllowCustomInteract)) { return false; }
if (!c.CharacterHealth.UseHealthWindow && !c.IsDraggable && (c.onCustomInteract == null || !c.AllowCustomInteract)) { return false; }
if (!skipDistanceCheck)
{
@@ -2725,7 +2772,7 @@ namespace Barotrauma
{
distanceToItem = -1.0f;
bool hidden = item.HiddenInGame;
bool hidden = item.IsHidden;
#if CLIENT
if (Screen.Selected == GameMain.SubEditorScreen) { hidden = false; }
#endif
@@ -2834,23 +2881,7 @@ namespace Barotrauma
}
if (distanceToItem > interactDistance && item.InteractDistance > 0.0f) { return false; }
Vector2 itemPosition = item.SimPosition;
if (Submarine == null && item.Submarine != null)
{
//character is outside, item inside
itemPosition += item.Submarine.SimPosition;
}
else if (Submarine != null && item.Submarine == null)
{
//character is inside, item outside
itemPosition -= Submarine.SimPosition;
}
else if (Submarine != item.Submarine)
{
//character and the item are inside different subs
itemPosition += item.Submarine.SimPosition;
itemPosition -= Submarine.SimPosition;
}
Vector2 itemPosition = GetPosition(Submarine, item, item.SimPosition);
if (SelectedSecondaryItem != null && !item.IsSecondaryItem)
{
@@ -2871,21 +2902,79 @@ namespace Barotrauma
if (!item.Prefab.InteractThroughWalls && Screen.Selected != GameMain.SubEditorScreen && !insideTrigger)
{
var body = Submarine.CheckVisibility(SimPosition, itemPosition, ignoreLevel: true);
if (body != null)
bool itemCenterVisible = CheckBody(body, item);
if (!itemCenterVisible && item.Prefab.RequireCursorInsideTrigger)
{
var otherItem = body.UserData as Item ?? (body.UserData as ItemComponent)?.Item;
if (otherItem != item &&
(body.UserData as ItemComponent)?.Item != item &&
/*allow interacting through open doors (e.g. duct blocks' colliders stay active despite being open)*/
otherItem?.GetComponent<Door>() is not { IsOpen: true } &&
Submarine.LastPickedFixture?.UserData as Item != item)
{
return false;
foreach (Rectangle trigger in item.Prefab.Triggers)
{
Rectangle transformTrigger = item.TransformTrigger(trigger, world: false);
RectangleF simRect = new RectangleF(
x: ConvertUnits.ToSimUnits(transformTrigger.X),
y: ConvertUnits.ToSimUnits(transformTrigger.Y - transformTrigger.Height),
width: ConvertUnits.ToSimUnits(transformTrigger.Width),
height: ConvertUnits.ToSimUnits(transformTrigger.Height));
simRect.Location = GetPosition(Submarine, item, simRect.Location);
Vector2 closest = ToolBox.GetClosestPointOnRectangle(simRect, SimPosition);
var triggerBody = Submarine.CheckVisibility(SimPosition, closest, ignoreLevel: true);
if (CheckBody(triggerBody, item)) { return true; }
}
}
else
{
return itemCenterVisible;
}
}
return true;
static bool CheckBody(Body body, Item item)
{
if (body is null) { return true; }
var otherItem = body.UserData as Item ?? (body.UserData as ItemComponent)?.Item;
if (otherItem != item &&
(body.UserData as ItemComponent)?.Item != item &&
/*allow interacting through open doors (e.g. duct blocks' colliders stay active despite being open)*/
otherItem?.GetComponent<Door>() is not { IsOpen: true } &&
Submarine.LastPickedFixture?.UserData as Item != item)
{
return false;
}
return true;
}
static Vector2 GetPosition(Submarine submarine, Item item, Vector2 simPosition)
{
Vector2 position = simPosition;
Vector2 itemSubPos = item.Submarine?.SimPosition ?? Vector2.Zero;
Vector2 subPos = submarine?.SimPosition ?? Vector2.Zero;
if (submarine == null && item.Submarine != null)
{
//character is outside, item inside
position += itemSubPos;
}
else if (submarine != null && item.Submarine == null)
{
//character is inside, item outside
position -= subPos;
}
else if (submarine != item.Submarine && submarine != null)
{
//character and the item are inside different subs
position += itemSubPos;
position -= subPos;
}
return position;
}
}
/// <summary>
@@ -3058,15 +3147,15 @@ namespace Barotrauma
{
DeselectCharacter();
}
else if (FocusedCharacter != null && IsKeyHit(InputType.Grab) && FocusedCharacter.CanBeDragged && (CanInteract || FocusedCharacter.IsDead && CanEat))
else if (FocusedCharacter != null && IsKeyHit(InputType.Grab) && FocusedCharacter.CanBeDraggedBy(this) && (CanInteract || FocusedCharacter.IsDead && CanEat))
{
SelectCharacter(FocusedCharacter);
}
else if (FocusedCharacter != null && !FocusedCharacter.IsIncapacitated && IsKeyHit(InputType.Use) && FocusedCharacter.IsPet && CanInteract)
else if (FocusedCharacter is { IsIncapacitated: false } && IsKeyHit(InputType.Use) && FocusedCharacter.IsPet && CanInteract)
{
(FocusedCharacter.AIController as EnemyAIController).PetBehavior.Play(this);
}
else if (FocusedCharacter != null && IsKeyHit(InputType.Health) && FocusedCharacter.CharacterHealth.UseHealthWindow && CanInteract && CanInteractWith(FocusedCharacter, 160f, false))
else if (FocusedCharacter != null && IsKeyHit(InputType.Health) && FocusedCharacter.CanBeHealedBy(this))
{
if (FocusedCharacter == SelectedCharacter)
{
@@ -3078,12 +3167,14 @@ namespace Barotrauma
}
#endif
}
else if (!IsClimbing)
else
{
SelectCharacter(FocusedCharacter);
#if CLIENT
if (Controlled == this)
{
HealingCooldown.PutOnCooldown();
CharacterHealth.OpenHealthWindow = FocusedCharacter.CharacterHealth;
}
#elif SERVER
if (GameMain.Server?.ConnectedClients is { } clients)
@@ -3096,13 +3187,6 @@ namespace Barotrauma
break;
}
}
#endif
SelectCharacter(FocusedCharacter);
#if CLIENT
if (Controlled == this)
{
CharacterHealth.OpenHealthWindow = FocusedCharacter.CharacterHealth;
}
#endif
}
}
@@ -3251,6 +3335,11 @@ namespace Barotrauma
{
UpdateProjSpecific(deltaTime, cam);
if (TextChatVolume > 0)
{
TextChatVolume -= 0.2f * deltaTime;
}
if (InvisibleTimer > 0.0f)
{
if (Controlled == null || Controlled == this || (Controlled.CharacterHealth.GetAffliction("psychosis")?.Strength ?? 0.0f) <= 0.0f)
@@ -3480,16 +3569,33 @@ namespace Barotrauma
DoInteractionUpdate(deltaTime, mouseSimPos);
}
if (SelectedItem != null && !CanInteractWith(SelectedItem))
if (MustDeselect(SelectedItem))
{
SelectedItem = null;
}
if (SelectedSecondaryItem != null && !CanInteractWith(SelectedSecondaryItem))
if (MustDeselect(SelectedSecondaryItem))
{
ReleaseSecondaryItem();
}
if (!IsDead) { LockHands = false; }
bool MustDeselect(Item item)
{
if (item == null) { return false; }
if (!CanInteractWith(item)) { return true; }
bool hasSelectableComponent = false;
foreach (var component in item.Components)
{
//the "selectability" of an item can change e.g. if the player unequips another item that's required to access it
if (component.CanBeSelected && component.HasRequiredItems(this, addMessage: false))
{
hasSelectableComponent = true;
break;
}
}
return !hasSelectableComponent;
}
}
partial void UpdateControlled(float deltaTime, Camera cam);
@@ -3805,6 +3911,9 @@ namespace Barotrauma
private void UpdateSoundRange(float deltaTime)
{
const float textChatVolumeMultiplier = 0.5f;
const float voiceChatVolumeMultiplier = 1.5f;
if (aiTarget == null) { return; }
if (IsDead)
{
@@ -3814,8 +3923,37 @@ namespace Barotrauma
{
float massFactor = (float)Math.Sqrt(Mass / 10);
float targetRange = Math.Min(massFactor * AnimController.Collider.LinearVelocity.Length() * 2 * Noise, maxAIRange);
float speechImpedimentMultiplier = 1.0f - SpeechImpediment / 100.0f;
if (TextChatVolume > 0)
{
targetRange = Math.Max(targetRange, TextChatVolume * textChatVolumeMultiplier * ChatMessage.SpeakRange * speechImpedimentMultiplier);
}
if (IsPlayer)
{
float voipAmplitude = 0.0f;
#if SERVER
foreach (var c in GameMain.Server.ConnectedClients)
{
if (c.Character != this) { continue; }
voipAmplitude = c.VoipServerDecoder.Amplitude;
break;
}
#elif CLIENT && DEBUG
if (Controlled == this && GameMain.Client != null)
{
voipAmplitude = GameMain.Client.DebugServerVoipAmplitude;
}
#endif
targetRange = Math.Max(targetRange, voipAmplitude * voiceChatVolumeMultiplier * ChatMessage.SpeakRange * speechImpedimentMultiplier);
}
targetRange *= 1.0f + GetStatValue(StatTypes.SoundRangeMultiplier);
targetRange = Math.Min(targetRange, maxAIRange);
float newRange = MathHelper.SmoothStep(aiTarget.SoundRange, targetRange, deltaTime * aiTargetChangeSpeed);
newRange *= 1.0f + GetStatValue(StatTypes.SoundRangeMultiplier);
if (!float.IsNaN(newRange))
{
aiTarget.SoundRange = newRange;
@@ -4712,7 +4850,31 @@ namespace Barotrauma
}
#endif
isDead = true;
ApplyStatusEffects(ActionType.OnDeath, 1.0f);
AnimController.Frozen = false;
Character killer = causeOfDeathAffliction?.Source;
if (IsBot)
{
foreach (var item in Inventory.AllItems)
{
if (item.Equipper is { IsPlayer: true } &&
item.GetComponents<ItemContainer>().Any(ic => ic.BlameEquipperForDeath()))
{
killer = item.Equipper;
if (AIController is HumanAIController humanAi)
{
humanAi.OnAttacked(killer, new AttackResult(damage: MaxVitality));
}
break;
}
}
}
CauseOfDeath = new CauseOfDeath(
causeOfDeath, causeOfDeathAffliction?.Prefab,
killer, LastDamageSource);
// Save these resistances in the CharacterInfo object so that if they
// are needed for respawning, they will be available (because there
@@ -4723,13 +4885,25 @@ namespace Barotrauma
info.LastResistanceMultiplierSkillLossRespawn = GetAbilityResistance(Tags.SkillLossRespawnResistance);
}
ApplyStatusEffects(ActionType.OnDeath, 1.0f);
isDead = true;
AnimController.Frozen = false;
#if CLIENT
// Keep permadeath status in sync (to show it correctly in the UI, the server takes care of the actual logic)
// NOTE: The opposite is done in Revive
if (GameMain.NetworkMember is { ServerSettings.RespawnMode: RespawnMode.Permadeath } &&
GameMain.Client.Character == this &&
GameMain.Client.CharacterInfo is CharacterInfo characterInfo)
{
characterInfo.PermanentlyDead = true;
}
#endif
CauseOfDeath = new CauseOfDeath(
causeOfDeath, causeOfDeathAffliction?.Prefab,
causeOfDeathAffliction?.Source, LastDamageSource);
#if SERVER
if (Info is not null)
{
Info.LastRewardDistribution = Option.Some(Wallet.RewardDistribution);
}
#endif
if (GameAnalyticsManager.SendUserStatistics && Prefab?.ContentPackage == ContentPackageManager.VanillaCorePackage)
{
@@ -4843,6 +5017,15 @@ namespace Barotrauma
if (info != null)
{
info.CauseOfDeath = null;
// Keep permadeath status in sync (to show it correctly in the UI, the server takes care of the actual logic)
// NOTE: The opposite is done in Kill
// FYI: In case you're wondering, it's alright to revive a "permanently" dead character here, because if
// this gets called, the character wasn't actually dead anyway (eg. returning to lobby without saving)
if (GameMain.NetworkMember is { ServerSettings.RespawnMode: RespawnMode.Permadeath })
{
info.PermanentlyDead = false;
}
}
foreach (LimbJoint joint in AnimController.LimbJoints)
@@ -5470,7 +5653,14 @@ namespace Barotrauma
{
statValue += wearableValue;
}
foreach (var heldItem in HeldItems)
{
if (heldItem.GetComponent<Holdable>() is Holdable holdable &&
holdable.HoldableStatValues.TryGetValue(statType, out float holdableValue))
{
statValue += holdableValue;
}
}
return statValue;
}
@@ -1,4 +1,4 @@
using Barotrauma.Extensions;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
@@ -44,8 +44,13 @@ namespace Barotrauma
public readonly Identifier MenuCategoryVar;
public readonly Identifier Pronouns;
public CharacterInfoPrefab(ContentXElement headsElement, XElement varsElement, XElement menuCategoryElement, XElement pronounsElement)
public CharacterInfoPrefab(CharacterPrefab characterPrefab, ContentXElement headsElement, XElement varsElement, XElement menuCategoryElement, XElement pronounsElement)
{
if (headsElement == null)
{
throw new Exception($"No heads configured for the character \"{characterPrefab.Identifier}\". Characters with CharacterInfo must have head sprites. Please add a <Heads> element to the character's config.");
}
Heads = headsElement.Elements().Select(e => new CharacterInfo.HeadPreset(this, e)).ToImmutableArray();
if (varsElement != null)
{
@@ -82,6 +87,10 @@ namespace Barotrauma
}
}
/// <summary>
/// Stores information about the Character that is needed between rounds in the
/// menu etc., whereas Character itself is the object actually spawned in-game.
/// </summary>
partial class CharacterInfo
{
public class HeadInfo
@@ -289,7 +298,9 @@ namespace Barotrauma
public XElement HealthData;
public XElement OrderData;
private static ushort idCounter;
public bool PermanentlyDead;
private static ushort idCounter = 1;
private const string disguiseName = "???";
public bool HasNickname => Name != OriginalName;
@@ -492,6 +503,9 @@ namespace Barotrauma
public bool StartItemsGiven;
/// <summary>
/// Newly hired bot that hasn't spawned yet
/// </summary>
public bool IsNewHire;
public CauseOfDeath CauseOfDeath;
@@ -642,6 +656,15 @@ namespace Barotrauma
=> element.GetAttributeBool("specifiertags",
element.GetAttributeBool("genders",
element.GetAttributeBool("races", false)));
/// <summary>
/// Keeps track of the last reward distribution that was set on the character's wallet.
/// Is used to keep salary when the character respawns since CharacterInfo is preserved between deaths.
/// </summary>
/// <remarks>
/// None means the salary has not been set yet, which is not always 0 if default salary is set.
/// </remarks>
public Option<int> LastRewardDistribution = Option.None;
// Used for creating the data
public CharacterInfo(
@@ -662,6 +685,7 @@ namespace Barotrauma
}
ID = idCounter;
idCounter++;
if (idCounter == 0) { idCounter++; }
SpeciesName = speciesName;
SpriteTags = new List<Identifier>();
CharacterConfigElement = CharacterPrefab.FindBySpeciesName(SpeciesName)?.ConfigElement;
@@ -699,6 +723,12 @@ namespace Barotrauma
Salary = CalculateSalary();
}
OriginalName = !string.IsNullOrEmpty(originalName) ? originalName : Name;
int loadedLastRewardDistribution = CharacterConfigElement.GetAttributeInt("lastrewarddistribution", -1);
if (loadedLastRewardDistribution >= 0)
{
LastRewardDistribution = Option.Some(loadedLastRewardDistribution);
}
}
private void SetPersonalityTrait()
@@ -771,6 +801,7 @@ namespace Barotrauma
HashSet<Identifier> tags = infoElement.GetAttributeIdentifierArray("tags", Array.Empty<Identifier>()).ToHashSet();
LoadTagsBackwardsCompatibility(infoElement, tags);
SpeciesName = infoElement.GetAttributeIdentifier("speciesname", "");
PermanentlyDead = infoElement.GetAttributeBool("permanentlydead", false);
ContentXElement element;
if (!SpeciesName.IsEmpty)
{
@@ -951,7 +982,7 @@ namespace Barotrauma
}
/// <summary>
/// Returns a presumably (not guaranteed) unique hash using the (current) Name, appearence, and job.
/// Returns a presumably (not guaranteed) unique and persistent hash using the (current) Name, appearence, and job.
/// So unless there's another character with the exactly same name, job, and appearance, the hash should be unique.
/// </summary>
public int GetIdentifier()
@@ -960,7 +991,7 @@ namespace Barotrauma
}
/// <summary>
/// Returns a presumably (not guaranteed) unique hash using the OriginalName, appearence, and job.
/// Returns a presumably (not guaranteed) unique hash and persistent using the OriginalName, appearence, and job.
/// So unless there's another character with the exactly same name, job, and appearance, the hash should be unique.
/// </summary>
public int GetIdentifierUsingOriginalName()
@@ -1466,7 +1497,10 @@ namespace Barotrauma
new XAttribute("haircolor", XMLExtensions.ColorToString(Head.HairColor)),
new XAttribute("facialhaircolor", XMLExtensions.ColorToString(Head.FacialHairColor)),
new XAttribute("startitemsgiven", StartItemsGiven),
new XAttribute("personality", PersonalityTrait?.Identifier ?? Identifier.Empty));
new XAttribute("personality", PersonalityTrait?.Identifier ?? Identifier.Empty),
new XAttribute("lastrewarddistribution", LastRewardDistribution.Match(some: value => value, none: () => -1).ToString()),
new XAttribute("permanentlydead", PermanentlyDead)
);
if (HumanPrefabIds != default)
{
@@ -32,6 +32,8 @@ namespace Barotrauma
return speciesName;
}
public bool HasCharacterInfo { get; private set; }
public void InheritFrom(CharacterPrefab parent)
{
ConfigElement = CharacterParams.CreateVariantXml(originalElement, parent.ConfigElement).FromPackage(ConfigElement.ContentPackage);
@@ -45,9 +47,10 @@ namespace Barotrauma
var menuCategoryElement = ConfigElement.GetChildElement("MenuCategory");
var pronounsElement = ConfigElement.GetChildElement("Pronouns");
if (headsElement != null)
HasCharacterInfo = headsElement != null || ConfigElement.GetAttributeBool(nameof(HasCharacterInfo), false);
if (HasCharacterInfo)
{
CharacterInfoPrefab = new CharacterInfoPrefab(headsElement, varsElement, menuCategoryElement, pronounsElement);
CharacterInfoPrefab = new CharacterInfoPrefab(this, headsElement, varsElement, menuCategoryElement, pronounsElement);
}
}
@@ -100,6 +100,12 @@ namespace Barotrauma
[Serialize(AIObjectiveIdle.BehaviorType.Passive, IsPropertySaveable.No)]
public AIObjectiveIdle.BehaviorType Behavior { get; protected set; }
[Serialize(1.0f, IsPropertySaveable.No, description:
"Affects how far the character can hear sounds created by AI targets with the tag ProvocativeToHumanAI. "+
"Used as a multiplier on the sound range of the target, e.g. a value of 0.5 would mean a target with a sound range of 1000 would need to be within 500 units for this character to hear it. "+
"Only affects the \"fight intruders\" objective, which makes the character go and inspect noises.")]
public float Hearing { get; set; } = 1.0f;
[Serialize(float.PositiveInfinity, IsPropertySaveable.No)]
public float ReportRange { get; protected set; }
@@ -174,6 +180,7 @@ namespace Barotrauma
idleObjective.PreferredOutpostModuleTypes.Add(moduleType);
}
}
humanAI.ReportRange = Hearing;
humanAI.ReportRange = ReportRange;
humanAI.FindWeaponsRange = FindWeaponsRange;
humanAI.AimSpeed = AimSpeed;
@@ -10,15 +10,26 @@ namespace Barotrauma
private float level;
/// <summary>
/// The highest skill level during the round (before any death penalties were applied)
/// </summary>
public float HighestLevelDuringRound { get; private set; }
public float Level
{
get { return level; }
set { level = value; }
set
{
HighestLevelDuringRound = MathHelper.Max(value, HighestLevelDuringRound);
level = value;
}
}
public LocalizedString DisplayName { get; private set; }
public void IncreaseSkill(float value, bool increasePastMax)
{
level = MathHelper.Clamp(level + value, 0.0f, increasePastMax ? SkillSettings.Current.MaximumSkillWithTalents : MaximumSkill);
Level = MathHelper.Clamp(level + value, 0.0f, increasePastMax ? SkillSettings.Current.MaximumSkillWithTalents : MaximumSkill);
}
private readonly Identifier iconJobId;
@@ -32,16 +43,18 @@ namespace Barotrauma
public Skill(SkillPrefab prefab, Rand.RandSync randSync)
{
Identifier = prefab.Identifier;
level = Rand.Range(prefab.LevelRange.Start, prefab.LevelRange.End, randSync);
Level = Rand.Range(prefab.LevelRange.Start, prefab.LevelRange.End, randSync);
iconJobId = GetIconJobId();
PriceMultiplier = prefab.PriceMultiplier;
DisplayName = TextManager.Get("SkillName." + Identifier);
}
public Skill(Identifier identifier, float level)
{
Identifier = identifier;
this.level = level;
Level = level;
iconJobId = GetIconJobId();
DisplayName = TextManager.Get("SkillName." + Identifier);
}
private Identifier GetIconJobId()
@@ -22,6 +22,7 @@ namespace Barotrauma
abstract class GroundedMovementParams : AnimationParams
{
[Header("Legs")]
[Serialize("1.0, 1.0", IsPropertySaveable.Yes, description: "How big steps the character takes."), Editable(DecimalCount = 2, ValueStep = 0.01f)]
public Vector2 StepSize
{
@@ -29,12 +30,14 @@ namespace Barotrauma
set;
}
[Header("Standing")]
[Serialize(0f, IsPropertySaveable.Yes, description: "How high above the ground the character's head is positioned."), Editable(DecimalCount = 2, ValueStep = 0.1f)]
public float HeadPosition { get; set; }
[Serialize(0f, IsPropertySaveable.Yes, description: "How high above the ground the character's torso is positioned."), Editable(DecimalCount = 2, ValueStep = 0.1f)]
public float TorsoPosition { get; set; }
[Header("Step lift")]
[Serialize(1f, IsPropertySaveable.Yes, description: "Separate multiplier for the head lift"), Editable(MinValueFloat = 0, MaxValueFloat = 2, ValueStep = 0.1f)]
public float StepLiftHeadMultiplier { get; set; }
@@ -50,6 +53,7 @@ namespace Barotrauma
[Serialize(2f, IsPropertySaveable.Yes, description: "How frequently the body raises when taking a step. The default is 2 (after every step)."), Editable(MinValueFloat = 0, MaxValueFloat = 10, ValueStep = 0.1f)]
public float StepLiftFrequency { get; set; }
[Header("Movement")]
[Serialize(0.75f, IsPropertySaveable.Yes, description: "The character's movement speed is multiplied with this value when moving backwards."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 0.99f, DecimalCount = 2)]
public float BackwardsMovementMultiplier { get; set; }
}
@@ -69,11 +73,15 @@ namespace Barotrauma
public bool IsGroundedAnimation => AnimationType is AnimationType.Walk or AnimationType.Run or AnimationType.Crouch;
public bool IsSwimAnimation => AnimationType is AnimationType.SwimSlow or AnimationType.SwimFast;
[Header("General")]
[Serialize(AnimationType.NotDefined, IsPropertySaveable.Yes), Editable]
public virtual AnimationType AnimationType { get; protected set; }
/// <summary>
/// The cached animations of all the characters that have been loaded.
/// </summary>
private static readonly Dictionary<Identifier, Dictionary<string, AnimationParams>> allAnimations = new Dictionary<Identifier, Dictionary<string, AnimationParams>>();
[Header("Movement")]
[Serialize(1.0f, IsPropertySaveable.Yes), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = Ragdoll.MAX_SPEED, ValueStep = 0.1f)]
public float MovementSpeed { get; set; }
@@ -84,6 +92,7 @@ namespace Barotrauma
/// <summary>
/// In degrees.
/// </summary>
[Header("Standing")]
[Serialize(float.NaN, IsPropertySaveable.Yes), Editable(-360f, 360f)]
public float HeadAngle
{
@@ -122,12 +131,11 @@ namespace Barotrauma
[Serialize(50.0f, IsPropertySaveable.Yes, description: "How much torque is used to rotate the torso to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)]
public float TorsoTorque { get; set; }
[Header("Legs")]
[Serialize(25.0f, IsPropertySaveable.Yes, description: "How much torque is used to rotate the feet to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)]
public float FootTorque { get; set; }
[Serialize(AnimationType.NotDefined, IsPropertySaveable.Yes), Editable]
public virtual AnimationType AnimationType { get; protected set; }
[Header("Arms")]
[Serialize(1f, IsPropertySaveable.Yes, description: "How much force is used to rotate the arms to the IK position."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
public float ArmIKStrength { get; set; }
@@ -69,21 +69,13 @@ namespace Barotrauma
abstract class HumanSwimParams : SwimParams, IHumanAnimation
{
[Header("Legs")]
[Serialize(0.5f, IsPropertySaveable.Yes), Editable(DecimalCount = 2)]
public float LegMoveAmount { get; set; }
[Serialize(5.0f, IsPropertySaveable.Yes), Editable]
public float LegCycleLength { get; set; }
[Serialize("0.5, 0.1", IsPropertySaveable.Yes), Editable(DecimalCount = 2)]
public Vector2 HandMoveAmount { get; set; }
[Serialize(5.0f, IsPropertySaveable.Yes), Editable]
public float HandCycleSpeed { get; set; }
[Serialize("0.0, 0.0", IsPropertySaveable.Yes), Editable(DecimalCount = 2)]
public Vector2 HandMoveOffset { get; set; }
/// <summary>
/// In degrees.
/// </summary>
@@ -96,20 +88,33 @@ namespace Barotrauma
FootAngleInRadians = MathHelper.ToRadians(value);
}
}
public float FootAngleInRadians { get; private set; }
[Header("Arms")]
[Serialize("0.5, 0.1", IsPropertySaveable.Yes), Editable(DecimalCount = 2)]
public Vector2 HandMoveAmount { get; set; }
[Serialize(5.0f, IsPropertySaveable.Yes), Editable]
public float HandCycleSpeed { get; set; }
[Serialize("0.0, 0.0", IsPropertySaveable.Yes), Editable(DecimalCount = 2)]
public Vector2 HandMoveOffset { get; set; }
[Serialize(1f, IsPropertySaveable.Yes, description: "How much force is used to move the arms."), Editable(MinValueFloat = 0, MaxValueFloat = 20, DecimalCount = 2)]
public float ArmMoveStrength { get; set; }
[Serialize(1f, IsPropertySaveable.Yes, description: "How much force is used to move the hands."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
public float HandMoveStrength { get; set; }
[Header("Other")]
[Serialize(true, IsPropertySaveable.Yes, description: "Is the head angle fixed or does the angle follow the mouse position?"), Editable]
public bool FixedHeadAngle { get; set; }
}
abstract class HumanGroundedParams : GroundedMovementParams, IHumanAnimation
{
[Header("Standing")]
[Serialize(0.3f, IsPropertySaveable.Yes, description: "How much force is used to force the character upright."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)]
public float GetUpForce { get; set; }
@@ -119,6 +124,7 @@ namespace Barotrauma
[Serialize(0.25f, IsPropertySaveable.Yes, description: "How much the character's torso leans forwards when moving."), Editable(DecimalCount = 2)]
public float TorsoLeanAmount { get; set; }
[Header("Legs")]
[Serialize(15.0f, IsPropertySaveable.Yes, description: "How much force is used to move the feet to the correct position."), Editable(MinValueFloat = 0, MaxValueFloat = 100)]
public float FootMoveStrength { get; set; }
@@ -152,6 +158,7 @@ namespace Barotrauma
[Serialize(10.0f, IsPropertySaveable.Yes, description: "How much torque is used to bend the characters legs when taking a step."), Editable(MinValueFloat = 0, MaxValueFloat = 100)]
public float LegBendTorque { get; set; }
[Header("Arms")]
[Serialize("0.4, 0.15", IsPropertySaveable.Yes, description: "How much the hands move along each axis."), Editable(DecimalCount = 2)]
public Vector2 HandMoveAmount { get; set; }
@@ -167,6 +174,7 @@ namespace Barotrauma
[Serialize(1f, IsPropertySaveable.Yes, description: "How much force is used to move the hands."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
public float HandMoveStrength { get; set; }
[Header("Other")]
[Serialize(true, IsPropertySaveable.Yes, description: "Is the head angle fixed or does the angle follow the mouse position?"), Editable]
public bool FixedHeadAngle { get; set; }
}
@@ -87,9 +87,20 @@ namespace Barotrauma
set { jointScale = MathHelper.Clamp(value, MIN_SCALE, MAX_SCALE); }
}
// Don't show in the editor, because shouldn't be edited in runtime. Requires that the limb scale and the collider sizes are adjusted. TODO: automatize?
/// <summary>
/// Can be used for scaling the textures without having to readjust the entire ragdoll.
/// Note that we'll still have to readjust the source rects and the colliders sizes, unless we also adjust <see cref="SourceRectScale"/>.
/// E.g. for upscaling the textures 2x, set <see cref="TextureScale"/> to 0.5 and <see cref="SourceRectScale"/> to 2.
/// </summary>
[Serialize(1f, IsPropertySaveable.No)]
public float TextureScale { get; set; }
/// <summary>
/// Multiplies both the position and the size of the source rects.
/// Used for scaling the textures when we cannot/don't want to touch the source rect definitions (e.g. on variants).
/// </summary>
[Serialize(1f, IsPropertySaveable.No)]
public float SourceRectScale { get; set; }
[Serialize(45f, IsPropertySaveable.Yes, description: "How high from the ground the main collider levitates when the character is standing? Doesn't affect swimming."), Editable(0f, 1000f)]
public float ColliderHeightFromFloor { get; set; }
@@ -491,6 +502,18 @@ namespace Barotrauma
float scaleMultiplier = ragdollElement.GetAttributeFloat("scalemultiplier", 1f);
JointScale *= scaleMultiplier;
LimbScale *= scaleMultiplier;
float textureScale = ragdollElement.GetAttributeFloat(nameof(TextureScale), 0f);
if (textureScale > 0)
{
// Override, if defined.
TextureScale = textureScale;
}
float sourceRectScale = ragdollElement.GetAttributeFloat(nameof(SourceRectScale), 0f);
if (sourceRectScale > 0)
{
// Override, if defined.
SourceRectScale = sourceRectScale;
}
}
}
isVariantScaleApplied = true;