This commit is contained in:
Evil Factory
2022-02-24 14:30:39 -03:00
364 changed files with 10838 additions and 3966 deletions
@@ -150,7 +150,7 @@ namespace Barotrauma
private CoroutineHandle disableTailCoroutine;
private readonly IEnumerable<Body> myBodies;
private readonly List<Body> myBodies;
public LatchOntoAI LatchOntoAI { get; private set; }
public SwarmBehavior SwarmBehavior { get; private set; }
@@ -207,8 +207,10 @@ namespace Barotrauma
} = new HashSet<Submarine>();
public bool IsTargetingPlayerTeam => IsTargetInPlayerTeam(SelectedAiTarget);
public bool IsBeingChasedBy(Character c) => c.AIController is EnemyAIController enemyAI && enemyAI.SelectedAiTarget?.Entity is Character && (enemyAI.State == AIState.Aggressive || enemyAI.State == AIState.Attack);
private bool IsBeingChased => SelectedAiTarget?.Entity is Character targetCharacter && IsBeingChasedBy(targetCharacter);
public static bool IsTargetBeingChasedBy(Character target, Character character)
=> character?.AIController is EnemyAIController enemyAI && enemyAI.SelectedAiTarget?.Entity == target && (enemyAI.State == AIState.Attack || enemyAI.State == AIState.Aggressive);
public bool IsBeingChasedBy(Character c) => IsTargetBeingChasedBy(Character, c);
private bool IsBeingChased => IsBeingChasedBy(SelectedAiTarget?.Entity as Character);
private bool IsTargetInPlayerTeam(AITarget target) => target?.Entity?.Submarine != null && target.Entity.Submarine.Info.IsPlayer || target?.Entity is Character targetCharacter && targetCharacter.IsOnPlayerTeam;
@@ -306,7 +308,8 @@ namespace Barotrauma
requiredHoleCount = (int)Math.Ceiling(ConvertUnits.ToDisplayUnits(colliderWidth) / Structure.WallSectionSize);
myBodies = Character.AnimController.Limbs.Select(l => l.body.FarseerBody);
myBodies = Character.AnimController.Limbs.Select(l => l.body.FarseerBody).ToList();
myBodies.Add(Character.AnimController.Collider.FarseerBody);
}
private CharacterParams.AIParams _aiParams;
@@ -339,7 +342,7 @@ namespace Barotrauma
{
targetingTag = "dead";
}
else if (AIParams.TryGetTarget(targetCharacter.CharacterHealth.GetActiveAfflictionTags(), out CharacterParams.TargetParams tp) && tp.Threshold > Character.GetDamageDoneByAttacker(targetCharacter))
else if (AIParams.TryGetTarget(targetCharacter.CharacterHealth.GetActiveAfflictionTags(), out CharacterParams.TargetParams tp) && tp.Threshold >= Character.GetDamageDoneByAttacker(targetCharacter))
{
targetingTag = tp.Tag;
}
@@ -530,8 +533,7 @@ namespace Barotrauma
selectedTargetingParams = targetingParams;
State = targetingParams.State;
}
if (SelectedAiTarget?.Entity != null &&
(LatchOntoAI == null || !LatchOntoAI.IsAttached || wallTarget != null) &&
if ((LatchOntoAI == null || !LatchOntoAI.IsAttached || wallTarget != null) &&
(State == AIState.Attack || State == AIState.Aggressive || State == AIState.PassiveAggressive))
{
UpdateWallTarget(requiredHoleCount);
@@ -645,7 +647,7 @@ namespace Barotrauma
}
else
{
run = isBeingChased ? true : squaredDistance < Math.Pow(halfReactDistance, 2);
run = isBeingChased || squaredDistance < Math.Pow(halfReactDistance, 2);
State = AIState.Escape;
avoidTimer = AIParams.AvoidTime * 0.5f * Rand.Range(0.75f, 1.25f);
}
@@ -673,15 +675,21 @@ namespace Barotrauma
Character c = a.Character;
if (c.IsDead || c.Removed) { return false; }
if (!Character.IsFriendly(c)) { return true; }
// Only apply the threshold to friendly characters
if (!c.IsPlayer) { return false; }
// Only apply the threshold to players
return a.Damage >= selectedTargetingParams.Threshold;
}
Character attacker = targetCharacter.LastAttackers.LastOrDefault(IsValid)?.Character;
if (attacker != null)
//if the attacker has the same targeting tag as the character we're protecting, we can't change the TargetState
//otherwise e.g. a pet that's set to follow humans would start attacking all humans (and other pets, since they're considered part of the same group) when a hostile human attacks it
//TODO: a way for pets to differentiate hostile and friendly humans?
if (attacker?.AiTarget != null && !targetCharacter.SpeciesName.Equals(GetTargetingTag(attacker.AiTarget), StringComparison.OrdinalIgnoreCase))
{
// Attack the character that attacked the target we are protecting
ChangeTargetState(attacker, AIState.Attack, selectedTargetingParams.Priority * 2);
SelectTarget(attacker.AiTarget);
State = AIState.Attack;
UpdateWallTarget(requiredHoleCount);
return;
}
}
@@ -1316,7 +1324,7 @@ namespace Barotrauma
Vector2 rayEnd = rayStart + dir.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 2);
Body closestBody = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true);
if (Submarine.LastPickedFraction != 1.0f && closestBody != null &&
(!AIParams.TargetOuterWalls || !canAttackWalls && closestBody.UserData is Structure s && s.Submarine != null || !canAttackDoors && closestBody.UserData is Item i && i.Submarine != null && i.GetComponent<Door>() != null))
((!AIParams.TargetOuterWalls || !canAttackWalls) && closestBody.UserData is Structure s && s.Submarine != null || !canAttackDoors && closestBody.UserData is Item i && i.Submarine != null && i.GetComponent<Door>() != null))
{
// Target is unreachable, there's a door or wall ahead
State = AIState.Idle;
@@ -1597,7 +1605,7 @@ namespace Barotrauma
}
else
{
sweepTimer = Rand.Range(-1000, 1000) * selectedTargetingParams.SweepSpeed;
sweepTimer = Rand.Range(-1000f, 1000f) * selectedTargetingParams.SweepSpeed;
}
}
break;
@@ -1837,7 +1845,7 @@ namespace Barotrauma
if (!attack.IsValidTarget(target)) { return false; }
if (target is ISerializableEntity se && target is Character)
{
if (attack.Conditionals.Any(c => !c.Matches(se))) { return false; }
if (attack.Conditionals.Any(c => !c.TargetSelf && !c.Matches(se))) { return false; }
}
if (attack.Conditionals.Any(c => c.TargetSelf && !c.Matches(Character))) { return false; }
if (attack.Ranged)
@@ -2182,10 +2190,22 @@ namespace Barotrauma
float margin = MathHelper.PiOver4 * distanceFactor;
if (angle < margin)
{
var collisionCategories = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel;
var pickedBody = Submarine.PickBody(weapon.SimPosition, target.SimPosition, myBodies, collisionCategories, allowInsideFixture: true);
var collisionCategories = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel;
var pickedBody = Submarine.PickBody(weapon.SimPosition, Character.GetRelativeSimPosition(target), myBodies, collisionCategories, allowInsideFixture: true);
if (pickedBody != null)
{
if (target is MapEntity)
{
if (pickedBody.UserData is Submarine sub && sub == target.Submarine)
{
return true;
}
else if (target == pickedBody.UserData)
{
return true;
}
}
Character t = null;
if (pickedBody.UserData is Character c)
{
@@ -2254,6 +2274,10 @@ namespace Barotrauma
if (SelectedAiTarget == null || SelectedAiTarget.Entity == null || SelectedAiTarget.Entity.Removed)
{
State = AIState.Idle;
if (Character.SelectedCharacter != null)
{
Character.DeselectCharacter();
}
return;
}
if (SelectedAiTarget.Entity is Character || SelectedAiTarget.Entity is Item)
@@ -2269,7 +2293,16 @@ namespace Barotrauma
Vector2 attackSimPosition = Character.GetRelativeSimPosition(SelectedAiTarget.Entity);
Vector2 limbDiff = attackSimPosition - mouthPos;
float extent = Math.Max(mouthLimb.body.GetMaxExtent(), 2);
if (limbDiff.LengthSquared() < extent * extent)
bool tooFar = Character.InWater ? limbDiff.LengthSquared() > extent * extent : limbDiff.X > extent;
if (tooFar)
{
steeringManager.SteeringSeek(attackSimPosition - (mouthPos - SimPosition), 2);
if (Character.InWater)
{
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15);
}
}
else
{
if (SelectedAiTarget.Entity is Character targetCharacter)
{
@@ -2285,14 +2318,12 @@ namespace Barotrauma
{
item.body.LinearVelocity *= 0.9f;
item.body.LinearVelocity -= limbDiff * 0.25f;
bool wasBroken = item.Condition <= 0.0f;
item.AddDamage(Character, item.WorldPosition, new Attack(0.0f, 0.0f, 0.0f, 0.0f, 0.1f), deltaTime);
item.AddDamage(Character, item.WorldPosition, new Attack(0.0f, 0.0f, 0.0f, 0.0f, 0.02f * Character.Params.EatingSpeed), deltaTime);
Character.ApplyStatusEffects(ActionType.OnEating, deltaTime);
if (item.Condition <= 0.0f)
{
if (!wasBroken) { PetBehavior?.OnEat(item.GetTags(), 1.0f); }
if (!wasBroken) { PetBehavior?.OnEat(item); }
Entity.Spawner.AddToRemoveQueue(item);
}
}
@@ -2301,14 +2332,6 @@ namespace Barotrauma
steeringManager.SteeringManual(deltaTime, Vector2.Normalize(limbDiff) * 3);
Character.AnimController.Collider.ApplyForce(limbDiff * mouthLimb.Mass * 50.0f, mouthPos);
}
else
{
steeringManager.SteeringSeek(attackSimPosition - (mouthPos - SimPosition), 2);
if (Character.AnimController.InWater)
{
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15);
}
}
}
else
{
@@ -2364,6 +2387,24 @@ namespace Barotrauma
}
#region Targeting
public static bool IsLatchedTo(Character target, Character character)
{
if (target.AIController is EnemyAIController enemyAI && enemyAI.LatchOntoAI != null)
{
return enemyAI.LatchOntoAI.IsAttached && enemyAI.LatchOntoAI.TargetCharacter == character;
}
return false;
}
public static bool IsLatchedToSomeoneElse(Character target, Character character)
{
if (target.AIController is EnemyAIController enemyAI && enemyAI.LatchOntoAI != null)
{
return enemyAI.LatchOntoAI.IsAttached && enemyAI.LatchOntoAI.TargetCharacter != null && enemyAI.LatchOntoAI.TargetCharacter != character;
}
return false;
}
private bool IsLatchedOnSub => LatchOntoAI != null && LatchOntoAI.IsAttachedToSub;
//goes through all the AItargets, evaluates how preferable it is to attack the target,
@@ -2376,7 +2417,8 @@ namespace Barotrauma
selectedTargetMemory = null;
targetingParams = null;
bool isAnyTargetClose = false;
bool isBeingChased = IsBeingChased;
float maxModifier = 5;
foreach (AITarget aiTarget in AITarget.List)
{
if (aiTarget.InDetectable) { continue; }
@@ -2499,11 +2541,12 @@ namespace Barotrauma
// Ignore inner walls when outside (walltargets still work)
continue;
}
valueModifier = 1;
if (!Character.AnimController.CanEnterSubmarine && IsWallDisabled(s))
{
continue;
}
// Prefer weaker walls (200 is the default for normal hull walls)
valueModifier = 200f / s.MaxHealth;
for (int i = 0; i < s.Sections.Length; i++)
{
var section = s.Sections[i];
@@ -2515,12 +2558,12 @@ namespace Barotrauma
{
if (CanPassThroughHole(s, i))
{
valueModifier *= leadsInside ? (IsAggressiveBoarder ? 3 : 1) : 0;
valueModifier *= leadsInside ? (IsAggressiveBoarder ? maxModifier : 1) : 0;
}
else if (IsAggressiveBoarder && leadsInside && canAttackWalls && AIParams.TargetOuterWalls)
else if (IsAggressiveBoarder && leadsInside && canAttackWalls)
{
// Up to 25% priority increase for every gap in the wall when an aggressive boarder is outside
valueModifier *= 1 + section.gap.Open * 0.25f;
// Up to 100% priority increase for every gap in the wall when an aggressive boarder is outside
valueModifier *= 1 + section.gap.Open;
}
}
else
@@ -2558,6 +2601,7 @@ namespace Barotrauma
// We are actually interested in breaking things -> reduce the priority when the wall is already broken
// (Terminalcells)
valueModifier *= 1 - section.gap.Open * 0.25f;
valueModifier = Math.Max(valueModifier, 0.1f);
}
}
}
@@ -2577,6 +2621,7 @@ namespace Barotrauma
valueModifier *= 1 + section.gap.Open;
}
}
valueModifier = Math.Clamp(valueModifier, 0, maxModifier);
}
}
if (door != null)
@@ -2588,7 +2633,7 @@ namespace Barotrauma
bool isOpen = door.CanBeTraversed;
if (!isOpen)
{
if (!canAttackDoors || isOutdoor && !AIParams.TargetOuterWalls) { continue; }
if (!canAttackDoors) { continue; }
}
else if (!Character.AnimController.CanEnterSubmarine)
{
@@ -2602,11 +2647,11 @@ namespace Barotrauma
// Increase the priority if the character is outside and the door is from outside to inside
if (door.CanBeTraversed)
{
valueModifier = 3;
valueModifier = maxModifier;
}
else if (door.LinkedGap != null)
{
valueModifier = 1 + door.LinkedGap.Open;
valueModifier = 1 + door.LinkedGap.Open * (maxModifier - 1);
}
}
else
@@ -2658,6 +2703,10 @@ namespace Barotrauma
}
}
}
if (targetParams.State == AIState.Eat && Character.Params.Health.HealthRegenerationWhenEating > 0)
{
valueModifier *= MathHelper.Lerp(1f, 0.1f, Character.HealthPercentage / 100f);
}
valueModifier *= targetParams.Priority;
if (valueModifier == 0.0f) { continue; }
if (targetingTag != "decoy")
@@ -2701,12 +2750,55 @@ namespace Barotrauma
if (SelectedAiTarget == aiTarget)
{
if (Character.Submarine == null && aiTarget.Entity is ISpatialEntity spatialEntity && spatialEntity.Submarine != null)
{
if (targetingTag == "door" || targetingTag == "wall")
{
Vector2 rayStart = Character.SimPosition;
Vector2 rayEnd = aiTarget.SimPosition + spatialEntity.Submarine.SimPosition;
Body closestBody = Submarine.PickBody(rayStart, rayEnd, collisionCategory: Physics.CollisionWall | Physics.CollisionLevel, allowInsideFixture: true);
if (closestBody != null && closestBody.UserData is ISpatialEntity hit)
{
Vector2 hitPos = hit.SimPosition;
if (closestBody.UserData is Submarine)
{
hitPos = Submarine.LastPickedPosition;
}
else if (hit.Submarine != null)
{
hitPos += hit.Submarine.SimPosition;
}
float subHalfWidth = spatialEntity.Submarine.Borders.Width / 2;
float subHalfHeight = spatialEntity.Submarine.Borders.Height / 2;
Vector2 diff = ConvertUnits.ToDisplayUnits(rayEnd - hitPos);
bool isOtherSideOfTheSub = Math.Abs(diff.X) > subHalfWidth || Math.Abs(diff.Y) > subHalfHeight;
if (isOtherSideOfTheSub)
{
IgnoreTarget(aiTarget);
ResetAITarget();
continue;
}
}
}
}
// Stick to the current target
valueModifier *= 1.1f;
}
if (!isBeingChased)
{
if (targetParams.State == AIState.Avoid || targetParams.State == AIState.PassiveAggressive || targetParams.State == AIState.Aggressive)
{
float reactDistance = targetParams.ReactDistance;
if (reactDistance > 0 && reactDistance < dist)
{
// The target is too far and should be ignored.
continue;
}
}
}
//if the target is very close, the distance doesn't make much difference
// -> just ignore the distance and attack whatever has the highest priority
// -> just ignore the distance and target whatever has the highest priority
dist = Math.Max(dist, 100.0f);
AITargetMemory targetMemory = GetTargetMemory(aiTarget, addIfNotFound: true);
if (Character.Submarine != null && !Character.Submarine.Info.IsRuin && Character.CurrentHull != null)
@@ -2719,19 +2811,22 @@ namespace Barotrauma
}
}
if (targetParams.AttackPattern == AttackPattern.Circle)
if (Character.Submarine == null && aiTarget.Entity?.Submarine != null && targetCharacter == null)
{
if (Character.Submarine == null && aiTarget.Entity?.Submarine != null && !isAnyTargetClose)
if (targetParams.AttackPattern == AttackPattern.Circle || targetParams.AttackPattern == AttackPattern.Sweep)
{
if (Submarine.MainSubs.Contains(aiTarget.Entity.Submarine))
if (!isAnyTargetClose)
{
// Prioritize targets that are near the horizontal center of the sub, but only when none of the targets is reachable.
float horizontalDistanceToSubCenter = Math.Abs(aiTarget.WorldPosition.X - aiTarget.Entity.Submarine.WorldPosition.X);
dist *= MathHelper.Lerp(1f, 5f, MathUtils.InverseLerp(0, 10000, horizontalDistanceToSubCenter));
}
else
{
dist *= 5;
if (Submarine.MainSubs.Contains(aiTarget.Entity.Submarine))
{
// Prioritize targets that are near the horizontal center of the sub, but only when none of the targets is reachable.
float horizontalDistanceToSubCenter = Math.Abs(aiTarget.WorldPosition.X - aiTarget.Entity.Submarine.WorldPosition.X);
dist *= MathHelper.Lerp(1f, 5f, MathUtils.InverseLerp(0, 10000, horizontalDistanceToSubCenter));
}
else if (targetParams.AttackPattern == AttackPattern.Circle)
{
dist *= 5;
}
}
}
}
@@ -2785,9 +2880,9 @@ namespace Barotrauma
{
if (Character.CurrentHull != null && targetCharacter.CurrentHull != Character.CurrentHull)
{
if (targetParams.State == AIState.Follow || targetParams.State == AIState.Protect || targetParams.State == AIState.Observe)
if (targetParams.State == AIState.Follow || targetParams.State == AIState.Protect || targetParams.State == AIState.Observe || targetParams.State == AIState.Eat)
{
// Ignore targets that cannot see
// Ignore targets that cannot be seen
if (!VisibleHulls.Contains(targetCharacter.CurrentHull))
{
continue;
@@ -2898,9 +2993,9 @@ namespace Barotrauma
if (HasValidPath(requireNonDirty: true)) { return; }
wallHits.Clear();
Structure wall = null;
Vector2 rayStart = AttackingLimb != null ? AttackingLimb.SimPosition : SimPosition;
if (AIParams.WallTargetingMethod.HasFlag(WallTargetingMethod.Target))
{
Vector2 rayStart = SimPosition;
Vector2 rayEnd = SelectedAiTarget.SimPosition;
if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null)
{
@@ -2914,7 +3009,6 @@ namespace Barotrauma
}
if (AIParams.WallTargetingMethod.HasFlag(WallTargetingMethod.Heading))
{
Vector2 rayStart = SimPosition;
Vector2 rayEnd = rayStart + VectorExtensions.Forward(Character.AnimController.Collider.Rotation + MathHelper.PiOver2, avoidLookAheadDistance * 5);
if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null)
{
@@ -2930,7 +3024,6 @@ namespace Barotrauma
}
if (AIParams.WallTargetingMethod.HasFlag(WallTargetingMethod.Steering))
{
Vector2 rayStart = SimPosition;
Vector2 rayEnd = rayStart + Steering * 5;
if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null)
{
@@ -2983,6 +3076,7 @@ namespace Barotrauma
// Blocked by a wall that shouldn't be targeted. The main intention here is to prevent monsters from entering the the tail and the nose pieces.
if (!isTargetingDoor)
{
IgnoreTarget(SelectedAiTarget);
ResetAITarget();
}
}
@@ -2994,6 +3088,7 @@ namespace Barotrauma
else
{
// Blocked by a disabled wall.
IgnoreTarget(SelectedAiTarget);
ResetAITarget();
}
}
@@ -3044,8 +3139,17 @@ namespace Barotrauma
if (!(hit.UserData is Structure w)) { return false; }
if (w.Submarine == null) { return false; }
if (w.Submarine != SelectedAiTarget.Entity.Submarine) { return false; }
if (Character.Submarine == null && w.prefab.Tags.Contains("inner")) { return false; }
if (!AIParams.TargetOuterWalls && !w.prefab.Tags.Contains("inner")) { return false; }
if (Character.Submarine == null)
{
if (w.prefab.Tags.Contains("inner"))
{
if (!Character.AnimController.CanEnterSubmarine) { return false; }
}
else if (!AIParams.TargetOuterWalls)
{
return false;
}
}
wall = w;
return true;
}
@@ -3082,7 +3186,8 @@ namespace Barotrauma
{
if (door.LinkedGap.Size > ConvertUnits.ToDisplayUnits(colliderWidth))
{
return SteerThroughGap(door.LinkedGap, door.LinkedGap.FlowTargetHull.WorldPosition, deltaTime, maxDistance: 100);
float maxDistance = Math.Max(ConvertUnits.ToDisplayUnits(colliderLength), 100);
return SteerThroughGap(door.LinkedGap, door.LinkedGap.FlowTargetHull.WorldPosition, deltaTime, maxDistance: maxDistance);
}
}
}
@@ -3279,7 +3384,7 @@ namespace Barotrauma
{
if (priority.HasValue)
{
targetParams.Priority = priority.Value;
targetParams.Priority = Math.Max(targetParams.Priority, priority.Value);
}
targetParams.State = state;
if (!modifiedParams.ContainsKey(tag))
@@ -3298,6 +3403,7 @@ namespace Barotrauma
/// <summary>
/// Temporarily changes the predefined state for a target. Eg. Idle -> Attack.
/// Note: does not change the current AIState!
/// </summary>
private void ChangeTargetState(Character target, AIState state, float? priority = null)
{
@@ -3319,14 +3425,14 @@ namespace Barotrauma
// --> Target the submarine too.
if (target.Submarine != null && Character.Submarine == null && (canAttackDoors || canAttackWalls))
{
ChangeParams("room", state, priority * 0.1f);
ChangeParams("room", state, priority / 2);
if (canAttackWalls)
{
ChangeParams("wall", state, priority * 0.1f);
ChangeParams("wall", state, priority / 2);
}
if (canAttackDoors)
{
ChangeParams("door", state, priority * 0.1f);
ChangeParams("door", state, priority / 2);
}
}
ChangeParams("provocative", state, priority, onlyExisting: true);
@@ -3378,9 +3484,15 @@ namespace Barotrauma
private bool CanPerceive(AITarget target, float dist = -1, float distSquared = -1, bool checkVisibility = false)
{
if (target?.Entity == null) { return false; }
bool insideSightRange;
bool insideSoundRange;
checkVisibility = checkVisibility && Character.Submarine != null && target.Entity.Submarine == Character.Submarine;
if (checkVisibility)
{
// We only want to check the visibility when the target is in ruins/wreck/similiar place where sneaking should be possible.
// When the monsters attack the player sub, they wall hack so that they can be more aggressive.
checkVisibility = target.Entity.Submarine != null && target.Entity.Submarine == Character.Submarine && target.Entity.Submarine.TeamID == CharacterTeamType.None;
}
if (dist > 0)
{
insideSightRange = IsInRange(dist, target.SightRange, Sight);
@@ -3539,12 +3651,12 @@ namespace Barotrauma
public override bool SteerThroughGap(Gap gap, Vector2 targetWorldPos, float deltaTime, float maxDistance = -1)
{
wallTarget = null;
LatchOntoAI?.DeattachFromBody(reset: true, cooldown: 2);
Character.AnimController.ReleaseStuckLimbs();
bool success = base.SteerThroughGap(gap, targetWorldPos, deltaTime, maxDistance);
if (success)
{
wallTarget = null;
LatchOntoAI?.DeattachFromBody(reset: true, cooldown: 2);
Character.AnimController.ReleaseStuckLimbs();
SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 1);
}
IsSteeringThroughGap = success;
@@ -20,7 +20,6 @@ namespace Barotrauma
private float reactTimer;
private float unreachableClearTimer;
private bool shouldCrouch;
public bool IsInsideCave { get; private set; }
/// <summary>
/// Resets each frame
/// </summary>
@@ -58,14 +57,14 @@ namespace Barotrauma
private float obstacleRaycastTimer;
private readonly float enemyCheckInterval = 0.2f;
private readonly float enemySpotDistanceOutside = 1500;
private readonly float enemySpotDistanceOutside = 800;
private readonly float enemySpotDistanceInside = 1000;
private float enemycheckTimer;
/// <summary>
/// How far other characters can hear reports done by this character (e.g. reports for fires, intruders). Defaults to infinity.
/// How far other characters can hear reports done by this character (e.g. reports for fires, intruders).
/// </summary>
public float ReportRange { get; set; } = float.PositiveInfinity;
public float ReportRange { get; set; }
private float _aimSpeed = 1;
public float AimSpeed
@@ -167,6 +166,7 @@ namespace Barotrauma
objectiveManager = new AIObjectiveManager(c);
reactTimer = GetReactionTime();
SortTimer = Rand.Range(0f, sortObjectiveInterval);
ReportRange = Character.IsOnPlayerTeam ? float.PositiveInfinity : 1000;
}
public override void Update(float deltaTime)
@@ -306,7 +306,7 @@ namespace Barotrauma
UseIndoorSteeringOutside = false;
}
if (Character.Submarine == null || !IsOnFriendlyTeam(Character.TeamID, Character.Submarine.TeamID) && !Character.IsEscorted)
if (Character.Submarine == null || Character.IsOnPlayerTeam && !Character.IsEscorted && !IsOnFriendlyTeam(Character.TeamID, Character.Submarine.TeamID))
{
// Spot enemies while staying outside or inside an enemy ship.
// does not apply for escorted characters, such as prisoners or terrorists who have their own behavior
@@ -327,9 +327,13 @@ namespace Barotrauma
float dist = toTarget.LengthSquared();
float maxDistance = Character.Submarine == null ? enemySpotDistanceOutside : enemySpotDistanceInside;
if (dist > maxDistance * maxDistance) { continue; }
Vector2 forward = VectorExtensions.Forward(Character.AnimController.Collider.Rotation);
forward.X *= Character.AnimController.Dir;
if (Vector2.Dot(toTarget, forward) < 0.2f) { 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.CanSeeCharacter(c)) { continue; }
if (dist < closestDistance || closestEnemy == null)
{
@@ -344,8 +348,6 @@ namespace Barotrauma
}
}
}
IsInsideCave = Character.CurrentHull == null && Level.Loaded?.Caves.FirstOrDefault(c => c.Area.Contains(Character.WorldPosition)) is Level.Cave;
if (UseIndoorSteeringOutside || Character.CurrentHull?.Submarine != null || hasValidPath || IsCloseEnoughToTarget(steeringBuffer))
{
@@ -565,7 +567,7 @@ namespace Barotrauma
Character.AnimController.HeadInWater ||
Character.Submarine == null ||
(Character.Submarine.TeamID != Character.TeamID && !Character.IsEscorted) ||
ObjectiveManager.CurrentOrders.Any(o => o.Objective.KeepDivingGearOn) ||
ObjectiveManager.CurrentOrders.Any(o => o.Objective.KeepDivingGearOnAlsoWhenInactive) ||
ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn) ||
Character.CurrentHull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 10;
bool IsOrderedToWait() => Character.IsOnPlayerTeam && ObjectiveManager.CurrentOrder is AIObjectiveGoTo goTo && goTo.Target == Character;
@@ -630,6 +632,7 @@ namespace Barotrauma
{
divingSuit.Drop(Character);
HandleRelocation(divingSuit);
ReequipUnequipped();
}
else if (findItemState == FindItemState.None || findItemState == FindItemState.DivingSuit)
{
@@ -657,6 +660,7 @@ namespace Barotrauma
{
divingSuit.Drop(Character);
HandleRelocation(divingSuit);
ReequipUnequipped();
}
}
}
@@ -675,6 +679,7 @@ namespace Barotrauma
{
mask.Drop(Character);
HandleRelocation(mask);
ReequipUnequipped();
}
else if (findItemState == FindItemState.None || findItemState == FindItemState.DivingMask)
{
@@ -699,6 +704,7 @@ namespace Barotrauma
{
mask.Drop(Character);
HandleRelocation(mask);
ReequipUnequipped();
}
}
}
@@ -832,6 +838,8 @@ namespace Barotrauma
if (container == null) { return 0; }
if (!container.HasAccess(character)) { return 0; }
if (!container.Inventory.CanBePut(containableItem)) { return 0; }
var rootContainer = container.Item.GetRootContainer();
if (rootContainer?.GetComponent<Fabricator>() != null || rootContainer?.GetComponent<Fabricator>() != null) { return 0; }
if (container.ShouldBeContained(containableItem, out bool isRestrictionsDefined))
{
if (isRestrictionsDefined)
@@ -876,7 +884,7 @@ namespace Barotrauma
foreach (Character target in Character.CharacterList)
{
if (target.CurrentHull != hull || !target.Enabled) { continue; }
if (AIObjectiveFightIntruders.IsValidTarget(target, Character))
if (AIObjectiveFightIntruders.IsValidTarget(target, Character, false))
{
if (!target.IsArrested && AddTargets<AIObjectiveFightIntruders, Character>(Character, target) && newOrder == null)
{
@@ -1096,7 +1104,7 @@ namespace Barotrauma
// excluding poisons etc
float realDamage = attackResult.Damage - healAmount;
// including poisons etc
float totalDamage = realDamage - healAmount;
float totalDamage = realDamage;
if (attackResult.Afflictions != null)
{
foreach (Affliction affliction in attackResult.Afflictions)
@@ -1135,7 +1143,15 @@ namespace Barotrauma
// Don't react to attackers that are outside of the sub (e.g. AoE attacks)
return;
}
bool isAttackerInfected = false;
bool isAttackerFightingEnemy = false;
float minorDamageThreshold = 1;
float majorDamageThreshold = 20;
if (attacker.TeamID == Character.TeamID)
{
minorDamageThreshold = 10;
majorDamageThreshold = 40;
}
if (IsFriendly(attacker))
{
if (attacker.AnimController.Anim == Barotrauma.AnimController.Animation.CPR && attacker.SelectedCharacter == Character)
@@ -1144,63 +1160,58 @@ namespace Barotrauma
// Should not cancel any existing ai objectives (so that if the character attacked you and then helped, we still would want to retaliate).
return;
}
float cumulativeDamage = Character.GetDamageDoneByAttacker(attacker);
float cumulativeDamage = realDamage + Character.GetDamageDoneByAttacker(attacker);
bool isAccidental = attacker.IsBot && !IsMentallyUnstable && !attacker.AIController.IsMentallyUnstable && Character.CombatAction == null;
if (isAccidental)
{
if (!Character.IsSecurity && cumulativeDamage > 1)
if (!Character.IsSecurity && cumulativeDamage > minorDamageThreshold)
{
AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker);
}
}
else
{
isAttackerInfected = attacker.CharacterHealth.GetAfflictionStrength("alieninfection") > 0;
// Inform other NPCs
if (cumulativeDamage > 1 || totalDamage >= 10)
if (isAttackerInfected || cumulativeDamage > minorDamageThreshold || totalDamage > minorDamageThreshold)
{
InformOtherNPCs(cumulativeDamage);
if (GameMain.IsMultiplayer || !attacker.IsPlayer || Character.TeamID != attacker.TeamID)
{
InformOtherNPCs(cumulativeDamage);
}
}
if (Character.IsBot)
{
if (ObjectiveManager.CurrentObjective is AIObjectiveFightIntruders) { return; }
if (attacker.IsPlayer)
var combatMode = DetermineCombatMode(Character, cumulativeDamage);
if (attacker.IsPlayer && !Character.IsInstigator && !ObjectiveManager.IsCurrentObjective<AIObjectiveCombat>())
{
if (Character.IsSecurity)
switch (combatMode)
{
if (attacker.TeamID != Character.TeamID && cumulativeDamage > 1 || cumulativeDamage > 10)
{
Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityarrest"), null, 0.50f, "attackedbyfriendlysecurityarrest", minDurationBetweenSimilar: 30.0f);
}
else
{
Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityresponse"), null, 0.50f, "attackedbyfriendlysecurityresponse", minDurationBetweenSimilar: 30.0f);
}
}
else if (!Character.IsInstigator && cumulativeDamage > 1)
{
Character.Speak(TextManager.Get("DialogAttackedByFriendly"), null, 0.50f, "attackedbyfriendly", minDurationBetweenSimilar: 30.0f);
}
}
if (cumulativeDamage > 1 && attacker.TeamID != Character.TeamID)
{
// If the attacker is using a low damage and high frequency weapon like a repair tool, we shouldn't use any delay.
AddCombatObjective(DetermineCombatMode(Character, cumulativeDamage), attacker, delay: realDamage > 1 ? GetReactionTime() : 0);
}
else
{
// Don't react to minor (accidental) dmg done by characters that are in the same team
if (cumulativeDamage < 10)
{
if (!Character.IsSecurity && cumulativeDamage > 1)
{
AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker);
}
}
else
{
AddCombatObjective(DetermineCombatMode(Character, cumulativeDamage, dmgThreshold: 50), attacker, GetReactionTime() * 2);
case AIObjectiveCombat.CombatMode.Defensive:
case AIObjectiveCombat.CombatMode.Retreat:
if (Character.IsSecurity)
{
Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityresponse"), null, 0.5f, "attackedbyfriendlysecurityresponse", minDurationBetweenSimilar: 10.0f);
}
else
{
Character.Speak(TextManager.Get("DialogAttackedByFriendly"), null, 0.5f, "attackedbyfriendly", minDurationBetweenSimilar: 10.0f);
}
break;
case AIObjectiveCombat.CombatMode.Offensive:
case AIObjectiveCombat.CombatMode.Arrest:
Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityarrest"), null, 0.5f, "attackedbyfriendlysecurityarrest", minDurationBetweenSimilar: 10.0f);
break;
case AIObjectiveCombat.CombatMode.None:
if (Character.IsSecurity && realDamage > 1)
{
Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityresponse"), null, 0.5f, "attackedbyfriendlysecurityresponse", minDurationBetweenSimilar: 10.0f);
}
break;
}
}
// If the attacker is using a low damage and high frequency weapon like a repair tool, we shouldn't use any delay.
AddCombatObjective(combatMode, attacker, delay: realDamage > 1 ? GetReactionTime() : 0);
}
if (!isAttackerFightingEnemy)
{
@@ -1213,15 +1224,15 @@ namespace Barotrauma
if (Character.Submarine != null && Character.Submarine.GetConnectedSubs().Contains(attacker.Submarine))
{
// Non-friendly
InformOtherNPCs(Character.GetDamageDoneByAttacker(attacker));
InformOtherNPCs();
}
if (Character.IsBot)
{
AddCombatObjective(DetermineCombatMode(Character, cumulativeDamage: realDamage), attacker);
AddCombatObjective(DetermineCombatMode(Character), attacker);
}
}
void InformOtherNPCs(float cumulativeDamage)
void InformOtherNPCs(float cumulativeDamage = 0)
{
foreach (Character otherCharacter in Character.CharacterList)
{
@@ -1237,25 +1248,25 @@ namespace Barotrauma
{
//if the other character did not witness the attack, and the character is not within report range (or capable of reporting)
//don't react to the attack
if (Character.IsDead || Character.IsUnconscious || !CheckReportRange(Character, otherCharacter, ReportRange))
if (Character.IsDead || Character.IsUnconscious || otherCharacter.TeamID != Character.TeamID || !CheckReportRange(Character, otherCharacter, ReportRange))
{
continue;
}
}
var combatMode = DetermineCombatMode(otherCharacter, cumulativeDamage, isWitnessing, dmgThreshold: attacker.TeamID == Character.TeamID ? 50 : 10);
var combatMode = DetermineCombatMode(otherCharacter, cumulativeDamage, isWitnessing);
float delay = isWitnessing ? GetReactionTime() : Rand.Range(2.0f, 5.0f, Rand.RandSync.Unsynced);
otherHumanAI.AddCombatObjective(combatMode, attacker, delay);
}
}
AIObjectiveCombat.CombatMode DetermineCombatMode(Character c, float cumulativeDamage, bool isWitnessing = false, float dmgThreshold = 10, bool allowOffensive = true)
AIObjectiveCombat.CombatMode DetermineCombatMode(Character c, float cumulativeDamage = 0, bool isWitnessing = false)
{
if (!IsFriendly(attacker))
{
if (Character.Submarine == null)
{
// Outside -> don't react.
return AIObjectiveCombat.CombatMode.None;
// Outside
return attacker.Submarine == null ? AIObjectiveCombat.CombatMode.Defensive : AIObjectiveCombat.CombatMode.Retreat;
}
if (!Character.Submarine.GetConnectedSubs().Contains(attacker.Submarine))
{
@@ -1268,6 +1279,15 @@ namespace Barotrauma
}
else
{
if (isAttackerInfected)
{
cumulativeDamage = 100;
}
if (GameMain.IsSingleplayer && attacker.IsPlayer && Character.TeamID == attacker.TeamID)
{
// Bots in the player team never act aggressively in single player when attacked by the player
return cumulativeDamage > minorDamageThreshold ? AIObjectiveCombat.CombatMode.Retreat : AIObjectiveCombat.CombatMode.None;
}
if (Character.Submarine == null || !Character.Submarine.GetConnectedSubs().Contains(attacker.Submarine))
{
// Outside or attacked from an unconnected submarine -> don't react.
@@ -1279,17 +1299,17 @@ namespace Barotrauma
isAttackerFightingEnemy = true;
return AIObjectiveCombat.CombatMode.None;
}
else if (isWitnessing && Character.CombatAction != null && !c.IsSecurity)
if (isWitnessing && Character.CombatAction != null && !c.IsSecurity)
{
return Character.CombatAction.WitnessReaction;
}
else if (attacker.IsPlayer && FindInstigator() is Character instigator)
if (attacker.IsPlayer && FindInstigator() is Character instigator)
{
// The guards don't react when the player there's an instigator around
// 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);
}
else if (attacker.TeamID == CharacterTeamType.FriendlyNPC && attacker.AIController != null && !(attacker.AIController.IsMentallyUnstable || attacker.AIController.IsMentallyUnstable))
if (attacker.TeamID == CharacterTeamType.FriendlyNPC && !(attacker.AIController.IsMentallyUnstable || attacker.AIController.IsMentallyUnstable))
{
if (c.IsSecurity)
{
@@ -1307,25 +1327,25 @@ namespace Barotrauma
// Already targeting the attacker -> treat as a more serious threat.
cumulativeDamage *= 2;
}
if (attackResult.Afflictions != null && attackResult.Afflictions.Any(a => a is AfflictionHusk))
{
cumulativeDamage = 100;
}
if (cumulativeDamage > dmgThreshold)
if (cumulativeDamage > majorDamageThreshold)
{
if (c.IsSecurity)
{
return c.IsSecurity && allowOffensive ? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Arrest;
return AIObjectiveCombat.CombatMode.Offensive;
}
else
{
return c == Character ? AIObjectiveCombat.CombatMode.Defensive : AIObjectiveCombat.CombatMode.Retreat;
}
}
else
else if (cumulativeDamage > minorDamageThreshold)
{
return c.IsSecurity ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Retreat;
}
else
{
return AIObjectiveCombat.CombatMode.None;
}
}
Character FindInstigator()
@@ -1758,7 +1778,7 @@ namespace Barotrauma
foreach (var enemy in Character.CharacterList)
{
if (enemy.CurrentHull != hull) { continue; }
if (AIObjectiveFightIntruders.IsValidTarget(enemy, character))
if (AIObjectiveFightIntruders.IsValidTarget(enemy, character, false))
{
AddTargets<AIObjectiveFightIntruders, Character>(character, enemy);
}
@@ -1838,7 +1858,7 @@ namespace Barotrauma
bool ignoreFire = objectiveManager.CurrentOrder is AIObjectiveExtinguishFires extinguishOrder && extinguishOrder.Priority > 0 || objectiveManager.HasActiveObjective<AIObjectiveExtinguishFire>();
bool ignoreWater = HasDivingSuit(character);
bool ignoreOxygen = ignoreWater || HasDivingMask(character);
bool ignoreEnemies = ObjectiveManager.IsCurrentOrder<AIObjectiveFightIntruders>() || ObjectiveManager.Objectives.Any(o => o is AIObjectiveFightIntruders);
bool ignoreEnemies = ObjectiveManager.IsCurrentOrder<AIObjectiveFightIntruders>() || ObjectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>();
float safety = CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies);
if (isCurrentHull)
{
@@ -2059,10 +2079,12 @@ namespace Barotrauma
public static bool IsItemTargetedBySomeone(ItemComponent target, CharacterTeamType team, out Character operatingCharacter)
{
operatingCharacter = null;
if (target?.Item == null) { return false; }
float highestPriority = -1.0f;
float highestPriorityModifier = -1.0f;
foreach (Character c in Character.CharacterList)
{
if (c == null) { continue; }
if (c.Removed) { continue; }
if (c.TeamID != team) { continue; }
if (c.IsIncapacitated) { continue; }
@@ -2071,12 +2093,12 @@ namespace Barotrauma
operatingCharacter = c;
return true;
}
if (c.AIController is HumanAIController humanAI)
if (c.AIController is HumanAIController humanAI && humanAI.ObjectiveManager is AIObjectiveManager objectiveManager)
{
foreach (var objective in humanAI.ObjectiveManager.Objectives)
foreach (var objective in objectiveManager.Objectives)
{
if (!(objective is AIObjectiveOperateItem operateObjective)) { continue; }
if (operateObjective.Component.Item != target.Item) { continue; }
if (operateObjective.Component?.Item != target.Item) { continue; }
if (operateObjective.Priority < highestPriority) { continue; }
if (operateObjective.PriorityModifier < highestPriorityModifier) { continue; }
operatingCharacter = c;
@@ -9,10 +9,10 @@ namespace Barotrauma
{
class IndoorsSteeringManager : SteeringManager
{
private PathFinder pathFinder;
private readonly PathFinder pathFinder;
private SteeringPath currentPath;
private bool canOpenDoors;
private readonly bool canOpenDoors;
public bool CanBreakDoors { get; set; }
private bool ShouldBreakDoor(Door door) =>
@@ -20,7 +20,7 @@ namespace Barotrauma
!door.Item.Indestructible && !door.Item.InvulnerableToDamage &&
(door.Item.Submarine == null || door.Item.Submarine.TeamID != character.TeamID);
private Character character;
private readonly Character character;
private Vector2 currentTarget;
@@ -51,19 +51,13 @@ namespace Barotrauma
private set;
}
/// <summary>
/// Returns true if the current or the next node is in ladders.
/// </summary>
public bool InLadders =>
currentPath != null && currentPath.CurrentNode != null &&
(currentPath.CurrentNode.Ladders != null && currentPath.CurrentNode.Ladders.Item.IsInteractable(character) ||
(currentPath.NextNode != null && currentPath.NextNode.Ladders != null && currentPath.NextNode.Ladders.Item.IsInteractable(character)));
/// <summary>
/// Returns true if any node in the path is in stairs
/// </summary>
public bool InStairs => currentPath != null && currentPath.Nodes.Any(n => n.Stairs != null);
public bool IsCurrentNodeLadder => currentPath?.CurrentNode?.Ladders != null && currentPath.CurrentNode.Ladders.Item.IsInteractable(character);
public bool IsNextNodeLadder => GetNextLadder() != null;
public bool IsNextLadderSameAsCurrent
@@ -83,8 +77,10 @@ namespace Barotrauma
public IndoorsSteeringManager(ISteerable host, bool canOpenDoors, bool canBreakDoors) : base(host)
{
pathFinder = new PathFinder(WayPoint.WayPointList.FindAll(wp => wp.SpawnType == SpawnType.Path), true);
pathFinder.GetNodePenalty = GetNodePenalty;
pathFinder = new PathFinder(WayPoint.WayPointList.FindAll(wp => wp.SpawnType == SpawnType.Path), true)
{
GetNodePenalty = GetNodePenalty
};
this.canOpenDoors = canOpenDoors;
this.CanBreakDoors = canBreakDoors;
@@ -99,14 +95,24 @@ namespace Barotrauma
base.Update(speed);
float step = 1.0f / 60.0f;
checkDoorsTimer -= step;
buttonPressTimer -= step;
if (lastDoor.door == null || !lastDoor.shouldBeOpen || lastDoor.door.IsOpen)
{
buttonPressTimer = 0;
}
else
{
buttonPressTimer -= step;
}
findPathTimer -= step;
}
public void SetPath(SteeringPath path)
{
currentPath = path;
if (path.Nodes.Any()) currentTarget = path.Nodes[path.Nodes.Count - 1].SimPosition;
if (path.Nodes.Any())
{
currentTarget = path.Nodes[path.Nodes.Count - 1].SimPosition;
}
findPathTimer = Math.Min(findPathTimer, 1.0f);
IsPathDirty = false;
}
@@ -124,15 +130,9 @@ namespace Barotrauma
public void SteeringSeek(Vector2 target, float weight, float minGapWidth = 0, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null, bool checkVisiblity = true)
{
if (buttonPressTimer > 0 && lastDoor.door != null && lastDoor.state && !lastDoor.door.IsOpen)
{
// We have pressed the button and are waiting for the door to open -> Hold still until we can press the button again.
Reset();
}
else
{
steering += CalculateSteeringSeek(target, weight, minGapWidth, startNodeFilter, endNodeFilter, nodeFilter, checkVisiblity);
}
// Have to use a variable here or resetting doesn't work.
Vector2 addition = CalculateSteeringSeek(target, weight, minGapWidth, startNodeFilter, endNodeFilter, nodeFilter, checkVisiblity);
steering += addition;
}
/// <summary>
@@ -328,7 +328,13 @@ namespace Barotrauma
{
CheckDoorsInPath();
doorsChecked = true;
}
}
if (buttonPressTimer > 0 && lastDoor.door != null && lastDoor.shouldBeOpen && !lastDoor.door.IsOpen)
{
// We have pressed the button and are waiting for the door to open -> Hold still until we can press the button again.
Reset();
return Vector2.Zero;
}
Vector2 pos = host.WorldPosition;
bool isDiving = character.AnimController.InWater && character.AnimController.HeadInWater;
// Only humanoids can climb ladders
@@ -378,7 +384,7 @@ namespace Barotrauma
//at the same height as the waypoint
if (Math.Abs(collider.SimPosition.Y - currentPath.CurrentNode.SimPosition.Y) < (collider.height / 2 + collider.radius) * 1.25f)
{
float heightFromFloor = character.AnimController.GetColliderBottom().Y - character.AnimController.FloorY;
float heightFromFloor = character.AnimController.GetHeightFromFloor();
if (heightFromFloor <= 0.0f)
{
diff.Y = Math.Max(diff.Y, 100);
@@ -451,12 +457,20 @@ namespace Barotrauma
// Cannot use the head position, because not all characters have head or it can be below the total height of the character
float characterHeight = Math.Max(colliderSize.Y + character.AnimController.ColliderHeightFromFloor, minHeight);
float horizontalDistance = Math.Abs(collider.SimPosition.X - currentPath.CurrentNode.SimPosition.X);
bool isAboveFeet = currentPath.CurrentNode.SimPosition.Y > colliderBottom.Y;
bool isNotTooHigh = currentPath.CurrentNode.SimPosition.Y < colliderBottom.Y + characterHeight;
bool isTargetTooHigh = currentPath.CurrentNode.SimPosition.Y > colliderBottom.Y + characterHeight;
bool isTargetTooLow = currentPath.CurrentNode.SimPosition.Y < colliderBottom.Y;
var door = currentPath.CurrentNode.ConnectedDoor;
float margin = MathHelper.Lerp(1, 10, MathHelper.Clamp(Math.Abs(velocity.X) / 5, 0, 1));
if (currentPath.CurrentNode.Stairs != null && currentPath.NextNode?.Stairs == null)
{
margin = 1;
if (currentPath.CurrentNode.SimPosition.Y < colliderBottom.Y + character.AnimController.ColliderHeightFromFloor * 0.25f)
{
isTargetTooLow = true;
}
}
float targetDistance = Math.Max(colliderSize.X / 2 * margin, minWidth / 2);
if (horizontalDistance < targetDistance && isAboveFeet && isNotTooHigh && (door == null || door.CanBeTraversed))
if (horizontalDistance < targetDistance && !isTargetTooHigh && !isTargetTooLow && (door == null || door.CanBeTraversed))
{
NextNode(!doorsChecked);
}
@@ -504,6 +518,16 @@ namespace Barotrauma
canAccessButtons = true;
}
}
foreach (var linked in door.Item.linkedTo)
{
if (!(linked is Item linkedItem)) { continue; }
var button = linkedItem.GetComponent<Controller>();
if (button == null) { continue; }
if (button.HasAccess(character) && (buttonFilter == null || buttonFilter(button)))
{
canAccessButtons = true;
}
}
return canAccessButtons || door.IsOpen || ShouldBreakDoor(door);
}
}
@@ -516,7 +540,7 @@ namespace Barotrauma
return ConvertUnits.ToDisplayUnits(Math.Max(colliderSize.X, colliderSize.Y));
}
private (Door door, bool state) lastDoor;
private (Door door, bool shouldBeOpen) lastDoor;
private float GetDoorCheckTime()
{
if (steering.LengthSquared() > 0)
@@ -539,7 +563,6 @@ namespace Barotrauma
WayPoint nextWaypoint = null;
Door door = null;
bool shouldBeOpen = false;
if (currentPath.Nodes.Count == 1)
{
door = currentPath.Nodes.First().ConnectedDoor;
@@ -645,7 +668,7 @@ namespace Barotrauma
});
if (canAccess)
{
bool pressButton = buttonPressTimer <= 0 || lastDoor.door != door || lastDoor.state != shouldBeOpen;
bool pressButton = buttonPressTimer <= 0 || lastDoor.door != door || lastDoor.shouldBeOpen != shouldBeOpen;
if (door.HasIntegratedButtons)
{
if (pressButton && character.CanSeeTarget(door.Item))
@@ -653,7 +676,7 @@ namespace Barotrauma
if (door.Item.TryInteract(character, forceSelectKey: true))
{
lastDoor = (door, shouldBeOpen);
buttonPressTimer = buttonPressCooldown;
buttonPressTimer = shouldBeOpen ? buttonPressCooldown : 0;
}
else
{
@@ -671,7 +694,7 @@ namespace Barotrauma
if (closestButton.Item.TryInteract(character, forceSelectKey: true))
{
lastDoor = (door, shouldBeOpen);
buttonPressTimer = buttonPressCooldown;
buttonPressTimer = shouldBeOpen ? buttonPressCooldown : 0;
}
else
{
@@ -697,7 +720,6 @@ namespace Barotrauma
// The button is on the wrong side of the door or a wall
currentPath.Unreachable = true;
}
lastDoor = (null, false);
return;
}
}
@@ -16,7 +16,6 @@ namespace Barotrauma
public virtual bool IgnoreUnsafeHulls => false;
public virtual bool AbandonWhenCannotCompleteSubjectives => true;
public virtual bool AllowSubObjectiveSorting => false;
public virtual bool ForceOrderPriority => true;
public virtual bool PrioritizeIfSubObjectivesActive => false;
/// <summary>
@@ -31,6 +30,8 @@ namespace Barotrauma
public virtual bool ConcurrentObjectives => false;
public virtual bool KeepDivingGearOn => false;
public virtual bool KeepDivingGearOnAlsoWhenInactive => false;
/// <summary>
/// There's a separate property for diving suit and mask: KeepDivingGearOn.
/// </summary>
@@ -85,11 +85,12 @@ namespace Barotrauma
bool equip = item.GetComponent<Holdable>() != null ||
item.AllowedSlots.Any(s => s != InvSlotType.Any) &&
item.AllowedSlots.None(s =>
s == InvSlotType.Card ||
s == InvSlotType.Head ||
s == InvSlotType.Headset ||
s == InvSlotType.InnerClothes ||
s == InvSlotType.OuterClothes);
s == InvSlotType.Card ||
s == InvSlotType.Head ||
s == InvSlotType.Headset ||
s == InvSlotType.InnerClothes ||
s == InvSlotType.OuterClothes ||
s == InvSlotType.HealthInterface);
TryAddSubObjective(ref decontainObjective, () => new AIObjectiveDecontainItem(character, item, objectiveManager, targetContainer: suitableContainer.GetComponent<ItemContainer>())
{
@@ -11,7 +11,7 @@ namespace Barotrauma
public override string Identifier { get; set; } = "cleanup items";
public override bool KeepDivingGearOn => true;
public override bool AllowAutomaticItemUnequipping => false;
public override bool ForceOrderPriority => false;
protected override bool ForceOrderPriority => false;
public readonly List<Item> prioritizedItems = new List<Item>();
@@ -52,7 +52,7 @@ namespace Barotrauma
// The validity changes when a character picks the item up.
if (!IsValidTarget(target, character, checkInventory: true)) { return Objectives.ContainsKey(target) && IsItemInsideValidSubmarine(target, character); }
if (target.CurrentHull.FireSources.Count > 0) { return false; }
// Don't repair items in rooms that have enemies inside.
// Don't clean up items in rooms that have enemies inside.
if (Character.CharacterList.Any(c => c.CurrentHull == target.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; }
return true;
}
@@ -30,7 +30,7 @@ namespace Barotrauma
private float holdFireTimer;
private bool hasAimed;
private bool isLethalWeapon;
private bool AllowCoolDown => !IsOffensiveOrArrest || Mode != initialMode;
private bool AllowCoolDown => !IsOffensiveOrArrest || Mode != initialMode || character.TeamID == Enemy.TeamID;
public Character Enemy { get; private set; }
public bool HoldPosition { get; set; }
@@ -117,7 +117,10 @@ namespace Barotrauma
private float AimSpeed => HumanAIController.AimSpeed;
private float AimAccuracy => HumanAIController.AimAccuracy;
private bool EnemyIsClose() => Enemy != null && character.CurrentHull != null && character.CurrentHull == Enemy.CurrentHull || Vector2.DistanceSquared(character.Position, Enemy.Position) < 500;
private bool IsEnemyCloserThan(float margin) =>
Enemy != null && Enemy.CurrentHull != null &&
character.InWater && Vector2.DistanceSquared(character.WorldPosition, Enemy.WorldPosition) < margin * margin ||
HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull) && Math.Abs(character.WorldPosition.X - Enemy.WorldPosition.X) < margin;
public AIObjectiveCombat(Character character, Character enemy, CombatMode mode, AIObjectiveManager objectiveManager, float priorityModifier = 1, float coolDown = 10.0f)
: base(character, objectiveManager, priorityModifier)
@@ -143,13 +146,20 @@ namespace Barotrauma
{
Mode = CombatMode.Retreat;
}
spreadTimer = Rand.Range(-10, 10);
spreadTimer = Rand.Range(-10f, 10f);
SetAimTimer(Rand.Range(1f, 1.5f) / AimSpeed);
HumanAIController.SortTimer = 0;
}
protected override float GetPriority()
{
if (character.TeamID == CharacterTeamType.FriendlyNPC && Enemy != null)
if (Enemy == null)
{
Priority = 0;
Abandon = true;
return Priority;
}
if (character.TeamID == CharacterTeamType.FriendlyNPC)
{
if (Enemy.Submarine == null || (Enemy.Submarine.TeamID != character.TeamID && Enemy.Submarine != character.Submarine))
{
@@ -160,6 +170,13 @@ namespace Barotrauma
}
float damageFactor = MathUtils.InverseLerp(0.0f, 5.0f, character.GetDamageDoneByAttacker(Enemy) / 100.0f);
Priority = TargetEliminated ? 0 : Math.Min((95 + damageFactor) * PriorityModifier, 100);
if (Priority > 0)
{
if (EnemyAIController.IsLatchedToSomeoneElse(Enemy, character))
{
Priority = 0;
}
}
return Priority;
}
@@ -366,7 +383,7 @@ namespace Barotrauma
}
}
}
bool isAllowedToSeekWeapons = !EnemyIsClose() && character.TeamID != CharacterTeamType.FriendlyNPC && IsOffensiveOrArrest;
bool isAllowedToSeekWeapons = character.CurrentHull != null && !IsEnemyCloserThan(300) && character.IsOnPlayerTeam && IsOffensiveOrArrest;
if (!isAllowedToSeekWeapons)
{
if (WeaponComponent == null)
@@ -418,9 +435,16 @@ namespace Barotrauma
onCompleted: () => RemoveSubObjective(ref seekWeaponObjective),
onAbandon: () =>
{
SpeakNoWeapons();
RemoveSubObjective(ref seekWeaponObjective);
Mode = CombatMode.Retreat;
if (Weapon == null)
{
SpeakNoWeapons();
Mode = CombatMode.Retreat;
}
else
{
Mode = CombatMode.Defensive;
}
});
}
}
@@ -478,13 +502,25 @@ namespace Barotrauma
weaponComponent = null;
float bestPriority = 0;
float lethalDmg = -1;
bool enemyIsClose = EnemyIsClose();
bool isAllowedToSeekWeapons = !IsEnemyCloserThan(300);
bool prioritizeMelee = IsEnemyCloserThan(50) || EnemyAIController.IsLatchedTo(Enemy, character);
foreach (var weapon in weaponList)
{
float priority = weapon.CombatPriority;
if (prioritizeMelee)
{
if (weapon is MeleeWeapon)
{
priority *= 5;
}
else
{
priority /= 2;
}
}
if (!weapon.IsLoaded(character))
{
if (weapon is RangedWeapon && enemyIsClose)
if (weapon is RangedWeapon && !isAllowedToSeekWeapons)
{
// Close to the enemy. Ignore weapons that don't have any ammunition (-> Don't seek ammo).
continue;
@@ -693,7 +729,7 @@ namespace Barotrauma
var slots = Weapon.AllowedSlots.Where(s => IsHandSlotType(s));
if (character.Inventory.TryPutItem(Weapon, character, slots))
{
aimTimer = Rand.Range(0.2f, 0.4f) / AimSpeed;
SetAimTimer(Rand.Range(0.2f, 0.4f) / AimSpeed);
}
else
{
@@ -1014,7 +1050,7 @@ namespace Barotrauma
}
if (!canSeeTarget)
{
aimTimer = Rand.Range(0.2f, 0.4f) / AimSpeed;
SetAimTimer(Rand.Range(0.2f, 0.4f) / AimSpeed);
return;
}
if (Weapon.RequireAimToUse)
@@ -1074,7 +1110,7 @@ namespace Barotrauma
else if (!character.IsFacing(Enemy.WorldPosition))
{
// Don't do the facing check if we are close to the target, because it easily causes the character to get stuck here when it flips around.
aimTimer = Rand.Range(1f, 1.5f) / AimSpeed;
SetAimTimer(Rand.Range(1f, 1.5f) / AimSpeed);
}
}
else
@@ -1177,7 +1213,7 @@ namespace Barotrauma
}
private void SpeakNoWeapons() => Speak("dialogcombatnoweapons", delay: 0, minDuration: 30);
private void AskHelp() => Speak("dialogcombatretreating", delay: Rand.Range(0, 1), minDuration: 20);
private void AskHelp() => Speak("dialogcombatretreating", delay: Rand.Range(0f, 1f), minDuration: 20);
private void Speak(string textIdentifier, float delay, float minDuration)
{
@@ -1191,18 +1227,6 @@ namespace Barotrauma
}
}
//private float CalculateEnemyStrength()
//{
// float enemyStrength = 0;
// AttackContext currentContext = character.GetAttackContext();
// foreach (Limb limb in Enemy.AnimController.Limbs)
// {
// if (limb.attack == null) continue;
// if (!limb.attack.IsValidContext(currentContext)) { continue; }
// if (!limb.attack.IsValidTarget(AttackTarget.Character)) { continue; }
// enemyStrength += limb.attack.GetTotalDamage(false);
// }
// return enemyStrength;
//}
private void SetAimTimer(float newTimer) => aimTimer = Math.Max(aimTimer, newTimer);
}
}
@@ -120,7 +120,7 @@ namespace Barotrauma
}
if (character.CanInteractWith(container.Item, checkLinked: false))
{
if (RemoveExisting || (RemoveExistingWhenNecessary && !container.Inventory.CanBePut(item)))
if (RemoveExisting || (RemoveExistingWhenNecessary && !container.Inventory.CanBePut(ItemToContain)))
{
HumanAIController.UnequipContainedItems(container.Item, predicate: RemoveExistingPredicate, unequipMax: RemoveMax);
}
@@ -159,7 +159,8 @@ namespace Barotrauma
{
TargetName = container.Item.Name,
AbortCondition = obj =>
container?.Item == null || container.Item.Removed || container.Item.IsThisOrAnyContainerIgnoredByAI(character) ||
container?.Item == null || container.Item.Removed || container.Item.IsThisOrAnyContainerIgnoredByAI(character) ||
(container.Item.GetRootContainer()?.OwnInventory?.Locked ?? false) ||
ItemToContain == null || ItemToContain.Removed ||
!ItemToContain.IsOwnedBy(character) || container.Item.GetRootInventoryOwner() is Character c && c != character,
SpeakIfFails = !objectiveManager.IsCurrentOrder<AIObjectiveCleanupItems>()
@@ -40,6 +40,7 @@ namespace Barotrauma
public Func<Item, bool> RemoveExistingPredicate { get; set; }
public int? RemoveExistingMax { get; set; }
public string AbandonGetItemDialogueIdentifier { get; set; }
public Func<bool> AbandonGetItemDialogueCondition { get; set; }
public AIObjectiveDecontainItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, ItemContainer sourceContainer = null, ItemContainer targetContainer = null, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
@@ -106,6 +107,7 @@ namespace Barotrauma
TryAddSubObjective(ref getItemObjective,
constructor: () => new AIObjectiveGetItem(character, targetItem, objectiveManager, Equip)
{
CannotFindDialogueCondition = AbandonGetItemDialogueCondition,
CannotFindDialogueIdentifierOverride = AbandonGetItemDialogueIdentifier,
SpeakIfFails = AbandonGetItemDialogueIdentifier != null,
TakeWholeStack = this.TakeWholeStack
@@ -12,10 +12,12 @@ namespace Barotrauma
protected override float TargetUpdateTimeMultiplier => 0.2f;
public bool TargetCharactersInOtherSubs { get; set; }
public AIObjectiveFightIntruders(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier) { }
protected override bool Filter(Character target) => IsValidTarget(target, character);
protected override bool Filter(Character target) => IsValidTarget(target, character, TargetCharactersInOtherSubs);
protected override IEnumerable<Character> GetList() => Character.CharacterList;
@@ -26,6 +28,7 @@ namespace Barotrauma
if (totalEnemies == 0) { return 0; }
if (character.IsSecurity) { return 100; }
if (objectiveManager.IsOrder(this)) { return 100; }
// If there's any security officers onboard, leave fighting for them.
return HumanAIController.IsTrueForAnyCrewMember(c => c.Character.IsSecurity && !c.Character.IsIncapacitated && c.Character.Submarine == character.Submarine) ? 0 : 100;
}
@@ -53,7 +56,7 @@ namespace Barotrauma
protected override void OnObjectiveCompleted(AIObjective objective, Character target)
=> HumanAIController.RemoveTargets<AIObjectiveFightIntruders, Character>(character, target);
public static bool IsValidTarget(Character target, Character character)
public static bool IsValidTarget(Character target, Character character, bool targetCharactersInOtherSubs)
{
if (target == null || target.Removed) { return false; }
if (target.IsDead) { return false; }
@@ -64,8 +67,10 @@ namespace Barotrauma
if (target.CurrentHull == null) { return false; }
if (HumanAIController.IsFriendly(character, target)) { return false; }
if (!character.Submarine.IsConnectedTo(target.Submarine)) { return false; }
if (!targetCharactersInOtherSubs && character.Submarine.TeamID != target.Submarine.TeamID) { return false; }
if (target.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI)) { return false; }
if (target.IsArrested) { return false; }
if (EnemyAIController.IsLatchedToSomeoneElse(target, character)) { return false; }
return true;
}
@@ -108,7 +108,7 @@ namespace Barotrauma
AllowToFindDivingGear = false,
AllowDangerousPressure = true,
ConditionLevel = MIN_OXYGEN,
RemoveExisting = true
RemoveExistingWhenNecessary = true
};
},
onAbandon: () =>
@@ -76,6 +76,10 @@ namespace Barotrauma
// -> ignore find safety unless we need to find a diving gear
Priority = 0;
}
else if (objectiveManager.Objectives.Any(o => o is AIObjectiveCombat && o.Priority > 0))
{
Priority = 0;
}
Priority = MathHelper.Clamp(Priority, 0, 100);
if (divingGearObjective != null && !divingGearObjective.IsCompleted && divingGearObjective.CanBeCompleted)
{
@@ -162,7 +162,10 @@ namespace Barotrauma
CloseEnough = reach,
DialogueIdentifier = Leak.FlowTargetHull != null ? "dialogcannotreachleak" : null,
TargetName = Leak.FlowTargetHull?.DisplayName,
CheckVisibility = false
CheckVisibility = false,
requiredCondition = () => Leak.Submarine == character.Submarine,
// The Go To objective can be abandoned if the leak is fixed (in which case we don't want to use the dialogue)
SpeakCannotReachCondition = () => !CheckObjectiveSpecific()
},
onAbandon: () =>
{
@@ -59,6 +59,7 @@ namespace Barotrauma
public bool CheckPathForEachItem { get; set; }
public bool SpeakIfFails { get; set; }
public string CannotFindDialogueIdentifierOverride { get; set; }
public Func<bool> CannotFindDialogueCondition { get; set; }
private int _itemCount = 1;
public int ItemCount
@@ -400,6 +401,11 @@ namespace Barotrauma
{
if (!ownerItem.IsInteractable(character)) { continue; }
if (!(ownerItem.GetComponent<ItemContainer>()?.HasRequiredItems(character, addMessage: false) ?? true)) { continue; }
//the item is inside an item inside an item (e.g. fuel tank in a welding tool in a cabinet -> reduce priority to prefer items that aren't inside a tool)
if (ownerItem != item.Container)
{
itemPriority *= 0.1f;
}
}
Vector2 itemPos = (rootInventoryOwner ?? item).WorldPosition;
float yDist = Math.Abs(character.WorldPosition.Y - itemPos.Y);
@@ -560,22 +566,18 @@ namespace Barotrauma
DebugConsole.NewMessage($"{character.Name}: Get item failed to reach {moveToTarget}", Color.Yellow);
#endif
}
if (SpeakIfFails)
{
SpeakCannotFind();
}
SpeakCannotFind();
}
private void SpeakCannotFind()
{
if (character.IsOnPlayerTeam && objectiveManager.CurrentOrder == objectiveManager.CurrentObjective)
{
string msg = TextManager.Get(CannotFindDialogueIdentifierOverride, returnNull: true) ?? TextManager.Get("dialogcannotfinditem", returnNull: true);
if (msg != null)
{
character.Speak(msg, identifier: "dialogcannotfinditem", minDurationBetweenSimilar: 20.0f);
}
}
if (!SpeakIfFails) { return; }
if (!character.IsOnPlayerTeam) { return; }
if (objectiveManager.CurrentOrder != objectiveManager.CurrentObjective) { return; }
if (CannotFindDialogueCondition != null && !CannotFindDialogueCondition()) { return; }
string msg = TextManager.Get(CannotFindDialogueIdentifierOverride, returnNull: true) ?? TextManager.Get("dialogcannotfinditem", returnNull: true);
if (msg == null) { return; }
character.Speak(msg, identifier: "dialogcannotfinditem", minDurationBetweenSimilar: 20.0f);
}
}
}
@@ -43,7 +43,6 @@ namespace Barotrauma
private readonly float minDistance = 50;
private readonly float seekGapsInterval = 1;
private float seekGapsTimer;
private bool cannotFollow;
/// <summary>
/// Display units
@@ -52,6 +51,11 @@ namespace Barotrauma
{
get
{
if (IsFollowOrderObjective && Target is Character targetCharacter && (targetCharacter.CurrentHull == null) != (character.CurrentHull == null))
{
// Keep close when the target is going inside/outside
return minDistance;
}
float dist = _closeEnough * CloseEnoughMultiplier;
float extraMultiplier = Math.Clamp(CloseEnoughMultiplier * 0.6f, 1, 3);
if (character.AnimController.InWater)
@@ -73,6 +77,9 @@ namespace Barotrauma
// TODO: Currently we never check the visibility (to the end node), which is actually unintentional.
// I don't think it has caused any issues so far, so let's keep defaulting to false for now, because the less we do raycasts the better.
// However, if there are cases where the bots attempt to go through walls (select the end node that is behind an obstacle), we should set this true.
// NOTE: This seemes to have caused an issue now Regalis11/Barotrauma#8067: namely, the bot was trying to use a waypoint that was obstructed by a shuttle
// because obstruction was only checked when checking visibility in PathFinder. Changed that so that obstructed nodes are no longer used.
public bool CheckVisibility { get; set; }
public bool IgnoreIfTargetDead { get; set; }
public bool AllowGoingOutside { get; set; }
@@ -96,6 +103,8 @@ namespace Barotrauma
public float? OverridePriority = null;
public Func<bool> SpeakCannotReachCondition { get; set; }
protected override float GetPriority()
{
bool isOrder = objectiveManager.IsOrder(this);
@@ -166,14 +175,14 @@ namespace Barotrauma
DebugConsole.NewMessage($"{character.Name}: Cannot reach the target: {Target}", Color.Yellow);
}
#endif
if (character.IsOnPlayerTeam && objectiveManager.CurrentOrder == objectiveManager.CurrentObjective && DialogueIdentifier != null && SpeakIfFails)
{
string msg = TargetName == null ? TextManager.Get(DialogueIdentifier, true) : TextManager.GetWithVariable(DialogueIdentifier, "[name]", TargetName, formatCapitals: !(Target is Character));
if (msg != null)
{
character.Speak(msg, identifier: DialogueIdentifier, minDurationBetweenSimilar: 20.0f);
}
}
if (!character.IsOnPlayerTeam) { return; }
if (objectiveManager.CurrentOrder != objectiveManager.CurrentObjective) { return; }
if (DialogueIdentifier == null) { return; }
if (!SpeakIfFails) { return; }
if (SpeakCannotReachCondition != null && !SpeakCannotReachCondition()) { return; }
string msg = TargetName == null ? TextManager.Get(DialogueIdentifier, true) : TextManager.GetWithVariable(DialogueIdentifier, "[name]", TargetName, formatCapitals: !(Target is Character));
if (msg == null) { return; }
character.Speak(msg, identifier: DialogueIdentifier, minDurationBetweenSimilar: 20.0f);
}
public void ForceAct(float deltaTime) => Act(deltaTime);
@@ -286,28 +295,16 @@ namespace Barotrauma
{
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager),
onAbandon: () => Abandon = true,
onCompleted: () =>
{
cannotFollow = false;
RemoveSubObjective(ref findDivingGear);
});
onCompleted: () => RemoveSubObjective(ref findDivingGear));
}
else
{
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager),
onAbandon: () => Abandon = true,
onCompleted: () =>
{
cannotFollow = false;
RemoveSubObjective(ref findDivingGear);
});
onCompleted: () => RemoveSubObjective(ref findDivingGear));
}
return;
}
else
{
cannotFollow = false;
}
}
if (repeat)
{
@@ -635,21 +632,29 @@ namespace Barotrauma
{
get
{
if (SteeringManager == PathSteering && PathSteering.CurrentPath?.CurrentNode?.Ladders != null)
if (character.IsClimbing)
{
//don't consider the character to be close enough to the target while climbing ladders,
//UNLESS the last node in the path has been reached
//otherwise characters can let go of the ladders too soon once they're close enough to the target
if (PathSteering.CurrentPath.NextNode != null) { return false; }
if (SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.CurrentPath.Finished && PathSteering.IsCurrentNodeLadder)
{
if (Target.WorldPosition.Y > character.WorldPosition.Y)
{
// The target is still above us
return false;
}
if (!character.AnimController.IsAboveFloor)
{
// Going through a hatch
return false;
}
}
}
if (!AlwaysUseEuclideanDistance && !character.AnimController.InWater)
{
float yDiff = Math.Abs(Target.WorldPosition.Y - character.WorldPosition.Y);
if (yDiff > CloseEnough) { return false; }
float xDiff = Math.Abs(Target.WorldPosition.X - character.WorldPosition.X);
return xDiff <= CloseEnough;
float yDist = Math.Abs(Target.WorldPosition.Y - character.WorldPosition.Y);
if (yDist > CloseEnough) { return false; }
float xDist = Math.Abs(Target.WorldPosition.X - character.WorldPosition.X);
return xDist <= CloseEnough;
}
Vector2 sourcePos = UseDistanceRelativeToAimSourcePos ? character.AnimController.AimSourceWorldPos : character.WorldPosition;
return Vector2.DistanceSquared(Target.WorldPosition, sourcePos) < CloseEnough * CloseEnough;
}
@@ -727,7 +732,6 @@ namespace Barotrauma
findDivingGear = null;
seekGapsTimer = 0;
TargetGap = null;
cannotFollow = false;
}
}
}
@@ -4,6 +4,7 @@ using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
namespace Barotrauma
{
@@ -20,6 +21,8 @@ namespace Barotrauma
private Item Container { get; }
private ItemContainer ItemContainer { get; }
private ImmutableArray<string> TargetContainerTags { get; }
private ImmutableHashSet<string> ValidContainableItemIdentifiers { get; }
private static Dictionary<ItemPrefab, ImmutableHashSet<string>> AllValidContainableItemIdentifiers { get; } = new Dictionary<ItemPrefab, ImmutableHashSet<string>>();
private int itemIndex = 0;
private AIObjectiveDecontainItem decontainObjective;
@@ -47,6 +50,111 @@ namespace Barotrauma
abandonGetItemDialogueIdentifier = optionSpecificDialogueIdentifier;
}
}
ValidContainableItemIdentifiers = GetValidContainableItemIdentifiers();
if (ValidContainableItemIdentifiers.None())
{
#if DEBUG
DebugConsole.ShowError($"No valid containable item identifiers found for the Load Item objective targeting {Container}");
#endif
Abandon = true;
return;
}
}
private enum CheckStatus { Unfinished, Finished }
private ImmutableHashSet<string> GetValidContainableItemIdentifiers()
{
if (AllValidContainableItemIdentifiers.TryGetValue(Container.Prefab, out var existingIdentifiers))
{
return existingIdentifiers;
}
// Status effects are often used to alter item condition so using the Containable Item Identifiers directly can lead to unwanted results
// For example, placing welding fuel tanks inside oxygen tank shelves
bool useDefaultContainableItemIdentifiers = true;
var potentialContainablePrefabs = MapEntityPrefab.List
.Where(mep => mep is ItemPrefab ip && ItemContainer.ContainableItemIdentifiers.Any(i => i == ip.Identifier || ip.Tags.Contains(i)))
.Cast<ItemPrefab>();
var validContainableItemIdentifiers = new HashSet<string>();
foreach (var component in Container.Components)
{
if (CheckComponent() == CheckStatus.Finished)
{
break;
}
CheckStatus CheckComponent()
{
if (component.statusEffectLists != null)
{
foreach (var (_, statusEffects) in component.statusEffectLists)
{
if (CheckStatusEffects(statusEffects) == CheckStatus.Finished)
{
return CheckStatus.Finished;
}
}
}
if (component is ItemContainer itemContainer && itemContainer.ContainableItems != null)
{
foreach (var item in itemContainer.ContainableItems)
{
if (CheckStatusEffects(item.statusEffects) == CheckStatus.Finished)
{
return CheckStatus.Finished;
}
}
}
return CheckStatus.Unfinished;
CheckStatus CheckStatusEffects(IEnumerable<StatusEffect> statusEffects)
{
if (statusEffects == null) { return CheckStatus.Unfinished; }
foreach (var statusEffect in statusEffects)
{
if ((statusEffect.TargetIdentifiers == null || statusEffect.TargetIdentifiers.None()) && !statusEffect.HasConditions) { continue; }
switch (TargetItemCondition)
{
case AIObjectiveLoadItems.ItemCondition.Empty:
if (!statusEffect.ReducesItemCondition()) { continue; }
break;
case AIObjectiveLoadItems.ItemCondition.Full:
if (!statusEffect.IncreasesItemCondition()) { continue; }
break;
default:
continue;
}
useDefaultContainableItemIdentifiers = false;
if (statusEffect.TargetIdentifiers != null)
{
foreach (string target in statusEffect.TargetIdentifiers)
{
foreach (var prefab in potentialContainablePrefabs)
{
if (CheckPrefab(prefab, () => prefab.Tags.Contains(target)) == CheckStatus.Finished) { return CheckStatus.Finished; }
}
}
}
foreach (var prefab in potentialContainablePrefabs)
{
if (CheckPrefab(prefab, () => statusEffect.MatchesTagConditionals(prefab)) == CheckStatus.Finished) { return CheckStatus.Finished; }
}
CheckStatus CheckPrefab(ItemPrefab prefab, Func<bool> isValid)
{
if (validContainableItemIdentifiers.Contains(prefab.Identifier)) { return CheckStatus.Unfinished; }
if (!isValid()) { return CheckStatus.Unfinished; }
validContainableItemIdentifiers.Add(prefab.Identifier);
if (potentialContainablePrefabs.Any(p => !validContainableItemIdentifiers.Contains(p.Identifier))) { return CheckStatus.Unfinished; }
return CheckStatus.Finished;
}
}
return CheckStatus.Unfinished;
}
}
}
var identifiers = useDefaultContainableItemIdentifiers ?
potentialContainablePrefabs.Select(p => p.Identifier).ToImmutableHashSet() :
validContainableItemIdentifiers.ToImmutableHashSet();
AllValidContainableItemIdentifiers.Add(Container.Prefab, identifiers);
return identifiers;
}
protected override float GetPriority()
@@ -116,7 +224,7 @@ namespace Barotrauma
base.Update(deltaTime);
if (targetItem == null)
{
if (character.FindItem(ref itemIndex, out Item item, identifiers: ItemContainer.ContainableItemIdentifiers, ignoreBroken: false, customPredicate: IsValidContainable, customPriorityFunction: GetConditionBasedPriority))
if (character.FindItem(ref itemIndex, out Item item, identifiers: ValidContainableItemIdentifiers, ignoreBroken: false, customPredicate: IsValidContainable, customPriorityFunction: GetPriority))
{
if (item == null)
{
@@ -125,17 +233,19 @@ namespace Barotrauma
}
targetItem = item;
}
// Prefer items closer to full condition when target condition is Empty, and vice versa
float GetConditionBasedPriority(Item item)
float GetPriority(Item item)
{
try
{
return TargetItemCondition switch
// Prefer items closer to full condition when target condition is Empty, and vice versa
float conditionBasedPriority = TargetItemCondition switch
{
AIObjectiveLoadItems.ItemCondition.Full => MathUtils.InverseLerp(100.0f, 0.0f, item.ConditionPercentage),
AIObjectiveLoadItems.ItemCondition.Empty => MathUtils.InverseLerp(0.0f, 100.0f, item.ConditionPercentage),
_ => throw new NotImplementedException()
};
// Prefer items that have the same identifier as one of the already contained items
return ItemContainer.ContainsItemsWithSameIdentifier(item) ? conditionBasedPriority : conditionBasedPriority / 2;
}
catch (NotImplementedException)
{
@@ -161,10 +271,11 @@ namespace Barotrauma
TryAddSubObjective(ref decontainObjective,
constructor: () => new AIObjectiveDecontainItem(character, targetItem, objectiveManager, targetContainer: ItemContainer, priorityModifier: PriorityModifier)
{
AbandonGetItemDialogueCondition = () => IsValidContainable(targetItem),
AbandonGetItemDialogueIdentifier = abandonGetItemDialogueIdentifier,
Equip = true,
RemoveExistingWhenNecessary = true,
RemoveExistingPredicate = (i) => AIObjectiveLoadItems.ItemMatchesTargetCondition(i, TargetItemCondition),
RemoveExistingPredicate = (i) => !ValidContainableItemIdentifiers.Contains(i.Prefab.Identifier) || AIObjectiveLoadItems.ItemMatchesTargetCondition(i, TargetItemCondition),
RemoveExistingMax = 1
},
onCompleted: () =>
@@ -189,6 +300,7 @@ namespace Barotrauma
{
if (item == null) { return false; }
if (item.Removed) { return false; }
if (!ValidContainableItemIdentifiers.Contains(item.Prefab.Identifier)) { return false; }
if (ignoredItems.Contains(item)) { return false; }
if ((item.SpawnedInCurrentOutpost && !item.AllowStealing) == character.IsOnPlayerTeam) { return false; }
var rootInventoryOwner = item.GetRootInventoryOwner();
@@ -46,6 +46,7 @@ namespace Barotrauma
public override bool AllowSubObjectiveSorting => true;
public virtual bool InverseTargetEvaluation => false;
protected virtual bool ResetWhenClearingIgnoreList => true;
protected virtual bool ForceOrderPriority => true;
public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace.CleanupStackTrace()); }
@@ -417,7 +417,7 @@ namespace Barotrauma
if (orderGiver == null) { return null; }
newObjective = new AIObjectiveGoTo(orderGiver, character, this, repeat: true, priorityModifier: priorityModifier)
{
CloseEnough = Rand.Range(80, 100),
CloseEnough = Rand.Range(80f, 100f),
CloseEnoughMultiplier = Math.Min(1 + HumanAIController.CountCrew(c => c.ObjectiveManager.HasOrder<AIObjectiveGoTo>(o => o.Target == orderGiver), onlyBots: true) * Rand.Range(0.8f, 1f), 4),
ExtraDistanceOutsideSub = 100,
ExtraDistanceWhileSwimming = 100,
@@ -431,7 +431,7 @@ namespace Barotrauma
case "wait":
newObjective = new AIObjectiveGoTo(order.TargetSpatialEntity ?? character, character, this, repeat: true, priorityModifier: priorityModifier)
{
AllowGoingOutside = character.Submarine == null || (order.TargetSpatialEntity != null && character.Submarine != order.TargetSpatialEntity.Submarine)
AllowGoingOutside = true
};
break;
case "return":
@@ -477,6 +477,12 @@ namespace Barotrauma
case "fightintruders":
newObjective = new AIObjectiveFightIntruders(character, this, priorityModifier);
break;
case "assaultenemy":
newObjective = new AIObjectiveFightIntruders(character, this, priorityModifier)
{
TargetCharactersInOtherSubs = true
};
break;
case "steer":
var steering = (order?.TargetEntity as Item)?.GetComponent<Steering>();
if (steering != null) { steering.PosToMaintain = steering.Item.Submarine?.WorldPosition; }
@@ -652,7 +658,12 @@ namespace Barotrauma
public bool IsOrder(AIObjective objective)
{
return objective == ForcedOrder || CurrentOrders.Any(o => o.Objective == objective);
if (objective == ForcedOrder) { return true; }
foreach (var order in CurrentOrders)
{
if (order.Objective == objective) { return true; }
}
return false;
}
public bool HasOrders()
@@ -11,6 +11,7 @@ namespace Barotrauma
public override string Identifier { get; set; } = "prepare";
public override string DebugTag => $"{Identifier}";
public override bool KeepDivingGearOn => true;
public override bool KeepDivingGearOnAlsoWhenInactive => true;
public override bool PrioritizeIfSubObjectivesActive => true;
private AIObjectiveGetItem getSingleItemObjective;
@@ -33,6 +33,7 @@ namespace Barotrauma
if (pump.Item.Submarine == null) { return false; }
if (pump.Item.CurrentHull == null) { return false; }
if (pump.Item.Submarine.TeamID != character.TeamID) { return false; }
if (pump.IsAutoControlled) { return false; }
if (pump.Item.ConditionPercentage <= 0) { return false; }
if (pump.Item.CurrentHull.FireSources.Count > 0) { return false; }
if (character.Submarine != null)
@@ -489,7 +489,7 @@ namespace Barotrauma
return Priority;
}
public static IEnumerable<Affliction> GetSortedAfflictions(Character character) => CharacterHealth.SortAfflictionsBySeverity(character.CharacterHealth.GetAllAfflictions());
public static IEnumerable<Affliction> GetSortedAfflictions(Character character, bool excludeBuffs = true) => CharacterHealth.SortAfflictionsBySeverity(character.CharacterHealth.GetAllAfflictions(), excludeBuffs);
public static IEnumerable<Affliction> GetTreatableAfflictions(Character character)
{
@@ -7,11 +7,11 @@ namespace Barotrauma
class AIObjectiveReturn : AIObjective
{
public override string Identifier { get; set; } = "return";
private AIObjectiveGoTo moveInsideObjective, moveInCaveObjective, moveOutsideObjective;
private bool usingEscapeBehavior;
private bool isSteeringThroughGap;
public Submarine ReturnTarget { get; }
private AIObjectiveGoTo moveInsideObjective, moveOutsideObjective;
private bool usingEscapeBehavior, isSteeringThroughGap;
public AIObjectiveReturn(Character character, Character orderGiver, AIObjectiveManager objectiveManager, float priorityModifier = 1.0f) : base(character, objectiveManager, priorityModifier)
{
ReturnTarget = GetReturnTarget(Submarine.MainSubs) ?? GetReturnTarget(Submarine.Loaded);
@@ -112,7 +112,6 @@ namespace Barotrauma
}
if (targetHull != null)
{
RemoveSubObjective(ref moveInCaveObjective);
RemoveSubObjective(ref moveOutsideObjective);
TryAddSubObjective(ref moveInsideObjective,
constructor: () => new AIObjectiveGoTo(targetHull, character, objectiveManager)
@@ -137,91 +136,41 @@ namespace Barotrauma
IsCompleted = true;
}
}
else if (!isSteeringThroughGap && moveInCaveObjective == null && moveOutsideObjective == null)
else if (!isSteeringThroughGap && moveOutsideObjective == null)
{
if (HumanAIController.IsInsideCave)
Hull targetHull = null;
float targetDistanceSquared = float.MaxValue;
bool targetIsAirlock = false;
foreach (var hull in ReturnTarget.GetHulls(false))
{
WayPoint closestOutsideWaypoint = null;
float closestDistance = float.MaxValue;
foreach (var w in WayPoint.WayPointList)
bool hullIsAirlock = hull.IsTaggedAirlock();
if(hullIsAirlock || (!targetIsAirlock && hull.LeadsOutside(character)))
{
if (w.Tunnel != null && w.Tunnel.Type == Level.TunnelType.Cave) { continue; }
if (w.linkedTo.None(l => l is WayPoint linkedWaypoint && linkedWaypoint.Tunnel?.Type == Level.TunnelType.Cave)) { continue; }
float distance = Vector2.DistanceSquared(character.WorldPosition, w.WorldPosition);
if (closestOutsideWaypoint == null || distance < closestDistance)
float distanceSquared = Vector2.DistanceSquared(character.WorldPosition, hull.WorldPosition);
if (targetHull == null || distanceSquared < targetDistanceSquared)
{
closestOutsideWaypoint = w;
closestDistance = distance;
targetHull = hull;
targetDistanceSquared = distanceSquared;
targetIsAirlock = hullIsAirlock;
}
}
if (closestOutsideWaypoint != null)
{
RemoveSubObjective(ref moveInsideObjective);
RemoveSubObjective(ref moveOutsideObjective);
TryAddSubObjective(ref moveInCaveObjective,
constructor: () => new AIObjectiveGoTo(closestOutsideWaypoint, character, objectiveManager)
{
endNodeFilter = n => n.Waypoint == closestOutsideWaypoint,
AllowGoingOutside = true
},
onCompleted: () => RemoveSubObjective(ref moveInCaveObjective),
onAbandon: () => Abandon = true);
}
else
{
#if DEBUG
DebugConsole.ThrowError("Error with a Return objective: no suitable main or side path node target found for 'moveOutsideObjective'");
#endif
}
}
if (targetHull != null)
{
RemoveSubObjective(ref moveInsideObjective);
TryAddSubObjective(ref moveOutsideObjective,
constructor: () => new AIObjectiveGoTo(targetHull, character, objectiveManager)
{
AllowGoingOutside = true
},
onCompleted: () => RemoveSubObjective(ref moveOutsideObjective),
onAbandon: () => Abandon = true);
}
else
{
Hull targetHull = null;
float targetDistanceSquared = float.MaxValue;
bool targetIsAirlock = false;
foreach (var hull in ReturnTarget.GetHulls(false))
{
bool hullIsAirlock = hull.IsTaggedAirlock();
if(hullIsAirlock || (!targetIsAirlock && hull.LeadsOutside(character)))
{
float distanceSquared = Vector2.DistanceSquared(character.WorldPosition, hull.WorldPosition);
if (targetHull == null || distanceSquared < targetDistanceSquared)
{
targetHull = hull;
targetDistanceSquared = distanceSquared;
targetIsAirlock = hullIsAirlock;
}
}
}
if (targetHull != null)
{
RemoveSubObjective(ref moveInsideObjective);
RemoveSubObjective(ref moveInCaveObjective);
TryAddSubObjective(ref moveOutsideObjective,
constructor: () => new AIObjectiveGoTo(targetHull, character, objectiveManager)
{
AllowGoingOutside = true
},
onCompleted: () => RemoveSubObjective(ref moveOutsideObjective),
onAbandon: () => Abandon = true);
}
else
{
#if DEBUG
DebugConsole.ThrowError("Error with a Return objective: no suitable target for 'moveOutsideObjective'");
DebugConsole.ThrowError("Error with a Return objective: no suitable target for 'moveOutsideObjective'");
#endif
}
}
}
else
{
if (HumanAIController.IsInsideCave)
{
RemoveSubObjective(ref moveOutsideObjective);
}
else
{
RemoveSubObjective(ref moveInCaveObjective);
}
}
usingEscapeBehavior = shouldUseEscapeBehavior;
@@ -249,7 +198,6 @@ namespace Barotrauma
{
base.Reset();
moveInsideObjective = null;
moveInCaveObjective = null;
moveOutsideObjective = null;
usingEscapeBehavior = false;
isSteeringThroughGap = false;
@@ -155,6 +155,9 @@ namespace Barotrauma
public OrderCategory? Category { get; private set; }
//legacy support
/// <summary>
/// If defined, the order can only be quick-assigned to characters with these jobs. Or if it's a report, the icon will only be displayed to characters with these jobs.
/// </summary>
public readonly string[] AppropriateJobs;
public readonly string[] Options;
public readonly string[] HiddenOptions;
@@ -177,6 +180,10 @@ namespace Barotrauma
public bool IsPrefab { get; private set; }
public readonly bool MustManuallyAssign;
public readonly bool AutoDismiss;
/// <summary>
/// If defined, the order will be quick-assigned to characters with these jobs before characters with other jobs.
/// </summary>
public string[] PreferredJobs { get; }
public readonly OrderTarget TargetPosition;
@@ -221,6 +228,9 @@ namespace Barotrauma
/// </summary>
public int AssignmentPriority { get; }
public bool ColoredWhenControllingGiver { get; }
public bool DisplayGiverInTooltip { get; }
public static void Init()
{
Prefabs = new Dictionary<string, Order>();
@@ -324,6 +334,7 @@ namespace Barotrauma
ControllerTags = orderElement.GetAttributeStringArray("controllertags", new string[0]);
TargetAllCharacters = orderElement.GetAttributeBool("targetallcharacters", false);
AppropriateJobs = orderElement.GetAttributeStringArray("appropriatejobs", new string[0]);
PreferredJobs = orderElement.GetAttributeStringArray("preferredjobs", new string[0]);
Options = orderElement.GetAttributeStringArray("options", new string[0]);
HiddenOptions = orderElement.GetAttributeStringArray("hiddenoptions", new string[0]);
AllOptions = Options.Concat(HiddenOptions).ToArray();
@@ -374,7 +385,7 @@ namespace Barotrauma
}
if (OptionNames.Count != Options.Length)
{
DebugConsole.ThrowError("Error in Order " + Name + " - the number of option names doesn't match the number of options.");
DebugConsole.AddWarning("Error in Order " + Name + " - the number of option names doesn't match the number of options.");
OptionNames.Clear();
Options.ForEach(o => OptionNames.Add(o, o));
}
@@ -404,8 +415,10 @@ namespace Barotrauma
MustManuallyAssign = orderElement.GetAttributeBool("mustmanuallyassign", false);
IsIgnoreOrder = Identifier == "ignorethis" || Identifier == "unignorethis";
DrawIconWhenContained = orderElement.GetAttributeBool("displayiconwhencontained", false);
AutoDismiss = orderElement.GetAttributeBool("autodismiss", Category == OrderCategory.Movement);
AutoDismiss = orderElement.GetAttributeBool("autodismiss", Category == OrderCategory.Operate || Category == OrderCategory.Movement);
AssignmentPriority = Math.Clamp(orderElement.GetAttributeInt("assignmentpriority", 100), 0, 100);
ColoredWhenControllingGiver = orderElement.GetAttributeBool("coloredwhencontrollinggiver", false);
DisplayGiverInTooltip = orderElement.GetAttributeBool("displaygiverintooltip", false);
}
/// <summary>
@@ -430,6 +443,7 @@ namespace Barotrauma
ControllerTags = prefab.ControllerTags;
TargetAllCharacters = prefab.TargetAllCharacters;
AppropriateJobs = prefab.AppropriateJobs;
PreferredJobs = prefab.PreferredJobs;
FadeOutTime = prefab.FadeOutTime;
MustSetTarget = prefab.MustSetTarget;
CanBeGeneralized = prefab.CanBeGeneralized;
@@ -441,6 +455,9 @@ namespace Barotrauma
Hidden = prefab.Hidden;
IgnoreAtOutpost = prefab.IgnoreAtOutpost;
AssignmentPriority = prefab.AssignmentPriority;
AutoDismiss = prefab.AutoDismiss;
DisplayGiverInTooltip = prefab.DisplayGiverInTooltip;
ColoredWhenControllingGiver = prefab.ColoredWhenControllingGiver;
OrderGiver = orderGiver;
TargetEntity = targetEntity;
@@ -481,34 +498,39 @@ namespace Barotrauma
WallSectionIndex = sectionIndex;
TargetType = OrderTargetType.WallSection;
}
public bool HasAppropriateJob(Character character)
{
if (character.Info == null || character.Info.Job == null) { return false; }
if (character.Info.Job.Prefab.AppropriateOrders.Any(appropriateOrderId => Identifier == appropriateOrderId)) { return true; }
if (!JobPrefab.Prefabs.Any(jp => jp.AppropriateOrders.Contains(Identifier)) &&
(AppropriateJobs == null || AppropriateJobs.Length == 0))
private bool HasSpecifiedJob(Character character, string[] jobs)
{
if (jobs == null || jobs.Length == 0) { return false; }
string jobIdentifier = character?.Info?.Job?.Prefab?.Identifier;
if (string.IsNullOrEmpty(jobIdentifier)) { return false; }
for (int i = 0; i < jobs.Length; i++)
{
return true;
}
for (int i = 0; i < AppropriateJobs.Length; i++)
{
if (character.Info.Job.Prefab.Identifier.Equals(AppropriateJobs[i], StringComparison.OrdinalIgnoreCase)) { return true; }
if (jobIdentifier.Equals(jobs[i], StringComparison.OrdinalIgnoreCase)) { return true; }
}
return false;
}
public string GetChatMessage(string targetCharacterName, string targetRoomName, bool givingOrderToSelf, string orderOption = "", int? priority = null)
public bool HasAppropriateJob(Character character) => HasSpecifiedJob(character, AppropriateJobs);
public bool HasPreferredJob(Character character) => HasSpecifiedJob(character, PreferredJobs);
public string GetChatMessage(string targetCharacterName, string targetRoomName, bool givingOrderToSelf, string orderOption = "", bool isNewOrder = true)
{
priority ??= CharacterInfo.HighestManualOrderPriority;
// If the order has a lesser priority, it means we are rearranging character orders
if (!TargetAllCharacters && priority != CharacterInfo.HighestManualOrderPriority && Identifier != "dismissed")
if (!TargetAllCharacters && !isNewOrder && Identifier != "dismissed")
{
return TextManager.GetWithVariable("rearrangedorders", "[name]", targetCharacterName ?? string.Empty, returnNull: true) ?? string.Empty;
// Use special dialogue when we're rearranging character orders
if (!givingOrderToSelf)
{
return TextManager.GetWithVariable("rearrangedorders", "[name]", targetCharacterName ?? string.Empty, returnNull: true) ?? string.Empty;
}
else
{
// Say nothing when rearranging the orders of the character you're controlling
return string.Empty;
}
}
string messageTag = $"{(givingOrderToSelf && !TargetAllCharacters ? "OrderDialogSelf" : "OrderDialog")}";
messageTag += $".{Identifier}";
string messageTag = $"{(givingOrderToSelf && !TargetAllCharacters ? "OrderDialogSelf" : "OrderDialog")}.{Identifier}";
if (!string.IsNullOrEmpty(orderOption))
{
if (Identifier != "dismissed")
@@ -333,7 +333,6 @@ namespace Barotrauma
//if searching for a path inside the sub, make sure the waypoint is visible
if (checkVisibility && isCharacter)
{
if (node.Waypoint.isObstructed) { return false; }
var body = Submarine.PickBody(rayStart, node.TempPosition,
collisionCategory: Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionStairs);
if (body != null)
@@ -350,6 +349,7 @@ namespace Barotrauma
{
if (nodeFilter != null && !nodeFilter(node)) { return false; }
if (startNodeFilter != null && !startNodeFilter(node)) { return false; }
if (node.Waypoint.isObstructed) { return false; }
// Always check the visibility for the start node
if (!IsWaypointVisible(node, start)) { return false; }
if (node.IsBlocked()) { return false; }
@@ -364,6 +364,7 @@ namespace Barotrauma
{
if (nodeFilter != null && !nodeFilter(node)) { return false; }
if (endNodeFilter != null && !endNodeFilter(node)) { return false; }
if (node.Waypoint.isObstructed) { return false; }
// Only check the visibility for the end node when allowed (fix leaks)
if (!IsWaypointVisible(node, end, checkVisibility: checkVisibility)) { return false; }
if (node.IsBlocked()) { return false; }
@@ -134,6 +134,7 @@ namespace Barotrauma
aggregate += Items[i].Commonness;
if (aggregate >= r && Items[i].Prefab != null)
{
GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "null") + ":PetProducedItem:" + pet.AiController.Character.SpeciesName + ":" + Items[i].Prefab.Identifier);
Entity.Spawner.AddToSpawnQueue(Items[i].Prefab, pet.AiController.Character.WorldPosition);
break;
}
@@ -200,6 +201,8 @@ namespace Barotrauma
break;
}
}
GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "null") + ":PetSpawned:" + aiController.Character.SpeciesName);
}
public StatusIndicatorType GetCurrentStatusIndicatorType()
@@ -210,23 +213,44 @@ namespace Barotrauma
return StatusIndicatorType.None;
}
public bool OnEat(IEnumerable<string> tags, float amount)
public bool OnEat(Item item)
{
bool success = OnEat(item.GetTags());
if (success)
{
GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "null") + ":PetEat:" + AiController.Character.SpeciesName + ":" + item.prefab.Identifier);
}
return success;
}
public bool OnEat(Character character)
{
if (character == null || !character.IsDead) { return false; }
bool success = OnEat("dead");
if (success)
{
GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "null") + ":PetEat:" + AiController.Character.SpeciesName + ":" + character.SpeciesName);
}
return success;
}
private bool OnEat(IEnumerable<string> tags)
{
foreach (string tag in tags)
{
if (OnEat(tag, amount)) { return true; }
if (OnEat(tag)) { return true; }
}
return false;
}
public bool OnEat(string tag, float amount)
private bool OnEat(string tag)
{
for (int i = 0; i < foods.Count; i++)
{
if (tag.Equals(foods[i].Tag, System.StringComparison.OrdinalIgnoreCase))
{
Hunger += foods[i].Hunger * amount;
Happiness += foods[i].Happiness * amount;
Hunger += foods[i].Hunger;
Happiness += foods[i].Happiness;
#if CLIENT
AiController.Character.PlaySound(CharacterSound.SoundType.Happy, 0.5f);
#endif
@@ -55,7 +55,7 @@ namespace Barotrauma
CommandingCharacter.Speak(SuggestedOrderPrefab.GetChatMessage(OrderedCharacter.Name, "", false), minDurationBetweenSimilar: 5);
}
CurrentOrder = new Order(SuggestedOrderPrefab, TargetItem, TargetItemComponent, CommandingCharacter);
OrderedCharacter.SetOrder(CurrentOrder, Option, priority: 3, CommandingCharacter, CommandingCharacter != OrderedCharacter);
OrderedCharacter.SetOrder(CurrentOrder, Option, priority: CharacterInfo.HighestManualOrderPriority, CommandingCharacter, CommandingCharacter != OrderedCharacter);
OrderedCharacter.Speak(TextManager.Get("DialogAffirmative"), delay: 1.0f, minDurationBetweenSimilar: 5);
}
TimeSinceLastAttempt = 0f;
@@ -22,7 +22,13 @@ namespace Barotrauma
public override void CalculateImportanceSpecific()
{
if (TargetItemComponent is Turret turret && !turret.HasPowerToShoot()) { return; }
if (TargetItemComponent is Turret turret && !turret.HasPowerToShoot())
{
//operate (= recharge the turrets) with low priority if they're out of power
//if something else (issues with reactor or the electrical grid) is preventing them from being charged, fixing those issues should take priority
Importance = ShipCommandManager.MinimumIssueThreshold * 1.05f;
return;
}
targetingImportances.Clear();
foreach (Character character in shipCommandManager.EnemyCharacters)
@@ -51,7 +51,7 @@ namespace Barotrauma
private const float RamTimerMax = 17.5f;
public readonly List<ShipIssueWorker> ShipIssueWorkers = new List<ShipIssueWorker>();
private const float MinimumIssueThreshold = 10f;
public const float MinimumIssueThreshold = 10f;
private const float IssueDevotionBuffer = 5f;
private float decisionTimer = 6f;
@@ -75,7 +75,7 @@ namespace Barotrauma
public void Update(float deltaTime)
{
if (!Active) { return; }
if (!Active || character.IsArrested) { return; }
decisionTimer -= deltaTime;
if (decisionTimer <= 0.0f)
{
@@ -344,7 +344,6 @@ namespace Barotrauma
ShipIssueWorkers.Clear();
// could have support for multiple reactors, todo m61
if (CommandedSubmarine.GetItems(false).Find(i => i.HasTag("reactor") && !i.NonInteractable)?.GetComponent<Reactor>() is Reactor reactor)
{
ShipIssueWorkers.Add(new ShipIssueWorkerPowerUpReactor(this, Order.GetPrefab("operatereactor"), reactor.Item, reactor, "powerup"));
@@ -254,7 +254,7 @@ namespace Barotrauma
private void SpawnInitialCells()
{
int brainRoomCells = Rand.Range(MinCellsPerBrainRoom, MaxCellsPerRoom);
int brainRoomCells = Rand.Range(MinCellsPerBrainRoom, MaxCellsPerRoom + 1);
if (brain.CurrentHull?.WaterPercentage >= MinWaterLevel)
{
for (int i = 0; i < brainRoomCells; i++)
@@ -262,12 +262,12 @@ namespace Barotrauma
if (!TrySpawnCell(out _, brain.CurrentHull)) { break; }
}
}
int cellsInside = Rand.Range(MinCellsInside, MaxCellsInside);
int cellsInside = Rand.Range(MinCellsInside, MaxCellsInside + 1);
for (int i = 0; i < cellsInside; i++)
{
if (!TrySpawnCell(out _)) { break; }
}
int cellsOutside = Rand.Range(MinCellsOutside, MaxCellsOutside);
int cellsOutside = Rand.Range(MinCellsOutside, MaxCellsOutside + 1);
// If we failed to spawn some of the cells in the brainroom/inside, spawn some extra cells outside.
cellsOutside = Math.Clamp(cellsOutside + brainRoomCells + cellsInside - protectiveCells.Count, cellsOutside, MaxCellsOutside);
for (int i = 0; i < cellsOutside; i++)