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

This commit is contained in:
EvilFactory
2024-04-24 12:20:11 -03:00
397 changed files with 15250 additions and 6473 deletions
@@ -76,9 +76,9 @@ namespace Barotrauma
get { return Character.AnimController.Collider.LinearVelocity; }
}
public virtual bool CanEnterSubmarine
public virtual CanEnterSubmarine CanEnterSubmarine
{
get { return true; }
get { return Character.AnimController.CanEnterSubmarine; }
}
public virtual bool CanFlip
@@ -327,8 +327,17 @@ namespace Barotrauma
{
if (otherItem.Prefab.Identifier == item.Prefab.Identifier || otherItem.HasIdentifierOrTags(targetTags))
{
// Shouldn't try dropping identical items, because that causes infinite looping when trying to get multiple items of the same type and if can't fit them all in the inventory.
return false;
bool switchingToBetterSuit =
targetTags != null &&
targetTags.FirstOrDefault() == Tags.HeavyDivingGear &&
AIObjectiveFindDivingGear.IsSuitablePressureProtection(item, Tags.HeavyDivingGear, Character) &&
!AIObjectiveFindDivingGear.IsSuitablePressureProtection(otherItem, Tags.HeavyDivingGear, Character);
// Shouldn't try dropping identical items, because that causes infinite looping when trying to get multiple items
// of the same type and if can't fit them all in the inventory.
if (!switchingToBetterSuit)
{
return false;
}
}
//if everything else fails, simply drop the existing item
otherItem.Drop(Character);
@@ -176,12 +176,13 @@ namespace Barotrauma
}
}
public override bool CanEnterSubmarine
public override CanEnterSubmarine CanEnterSubmarine
{
get
{
//can't enter a submarine when attached to something
return Character.AnimController.CanEnterSubmarine && (LatchOntoAI == null || !LatchOntoAI.IsAttachedToSub);
//can't enter a submarine when attached to one
if (LatchOntoAI is { IsAttachedToSub: true }) { return CanEnterSubmarine.False; }
return Character.AnimController.CanEnterSubmarine;
}
}
@@ -535,12 +536,27 @@ namespace Barotrauma
FadeMemories(updateMemoriesInverval);
updateMemoriesTimer = updateMemoriesInverval;
}
if (Math.Max(Character.HealthPercentage, 0) < FleeHealthThreshold && SelectedAiTarget != null &&
SelectedAiTarget.Entity is Character target && (target.IsHuman && CanPerceive(SelectedAiTarget) || IsBeingChasedBy(target)))
if (Math.Max(Character.HealthPercentage, 0) < FleeHealthThreshold && SelectedAiTarget != null)
{
// Keep fleeing if being chased
State = AIState.Flee;
Character target = SelectedAiTarget.Entity as Character;
if (target == null && SelectedAiTarget.Entity is Item targetItem)
{
target = GetOwner(targetItem);
}
bool shouldFlee = false;
if (target != null)
{
// Keep fleeing if being chased or if we see a human target (that don't have enemy ai).
shouldFlee = target.IsHuman && CanPerceive(SelectedAiTarget) || IsBeingChasedBy(target);
}
// If we should not flee, just idle. Don't allow any other AI state when below the health threshold.
State = shouldFlee ? AIState.Flee : AIState.Idle;
wallTarget = null;
if (State != AIState.Flee)
{
SelectedAiTarget = null;
_lastAiTarget = null;
}
}
else
{
@@ -614,7 +630,7 @@ namespace Barotrauma
steeringManager = outsideSteering;
}
}
bool useSteeringLengthAsMovementSpeed = State == AIState.Idle && Character.AnimController.InWater;
bool run = false;
switch (State)
@@ -870,11 +886,11 @@ namespace Barotrauma
}
// Ensure that the creature keeps inside the level
SteerInsideLevel(deltaTime);
float defaultSpeed = Character.AnimController.GetCurrentSpeed(run && Character.CanRun);
//calculate a normalized Steering value at this point: we multiply it with the actual, desired speed in ApplyMovementLimits
steeringManager.Update(1.0f);
float speed = useSteeringLengthAsMovementSpeed ? Steering.Length() : defaultSpeed;
Character.AnimController.TargetMovement = Character.ApplyMovementLimits(Steering, speed);
float speed = Character.AnimController.GetCurrentSpeed(run && Character.CanRun);
// Doesn't work if less than 1, when we use steering length as movement speed.
steeringManager.Update(Math.Max(speed, 1.0f));
float movementSpeed = useSteeringLengthAsMovementSpeed ? Steering.Length() : speed;
Character.AnimController.TargetMovement = Character.ApplyMovementLimits(Steering, movementSpeed);
if (Character.CurrentHull != null && Character.AnimController.InWater)
{
// Limit the swimming speed inside the sub.
@@ -1087,6 +1103,22 @@ namespace Barotrauma
// How long the monster tries to reach out for the target when it's close to it before ignoring it.
private const float reachTimeOut = 10;
private bool IsSameTarget(AITarget target, AITarget otherTarget)
{
if (target?.Entity == otherTarget?.Entity) { return true; }
if (IsItemInCharacterInventory(target, otherTarget) || IsItemInCharacterInventory(otherTarget, target)) { return true; }
return false;
bool IsItemInCharacterInventory(AITarget potentialItem, AITarget potentialCharacter)
{
if (potentialItem?.Entity is Item item && potentialCharacter?.Entity is Character character)
{
return item.ParentInventory?.Owner == character;
}
return false;
}
}
private void UpdateAttack(float deltaTime)
{
if (SelectedAiTarget == null || SelectedAiTarget.Entity == null || SelectedAiTarget.Entity.Removed)
@@ -1132,7 +1164,7 @@ namespace Barotrauma
attackSimPos = Character.GetRelativeSimPosition(SelectedAiTarget.Entity);
}
if (Character.AnimController.CanEnterSubmarine)
if (Character.AnimController.CanEnterSubmarine == CanEnterSubmarine.True)
{
if (TrySteerThroughGaps(deltaTime))
{
@@ -1177,13 +1209,21 @@ namespace Barotrauma
if (IsCoolDownRunning && (_previousAttackLimb == null || AttackLimb == null || AttackLimb.attack.CoolDownTimer > 0))
{
var currentAttackLimb = AttackLimb ?? _previousAttackLimb;
if (currentAttackLimb.attack.CoolDownTimer >= currentAttackLimb.attack.CoolDown + currentAttackLimb.attack.CurrentRandomCoolDown - currentAttackLimb.attack.AfterAttackDelay)
if (currentAttackLimb.attack.CoolDownTimer >=
currentAttackLimb.attack.CoolDown + currentAttackLimb.attack.CurrentRandomCoolDown - currentAttackLimb.attack.AfterAttackDelay)
{
return;
}
AIBehaviorAfterAttack activeBehavior = currentAttackLimb.attack.AfterAttack;
currentAttackLimb.attack.AfterAttackTimer += deltaTime;
AIBehaviorAfterAttack activeBehavior =
currentAttackLimb.attack.AfterAttackSecondaryDelay > 0 && currentAttackLimb.attack.AfterAttackTimer > currentAttackLimb.attack.AfterAttackSecondaryDelay ?
currentAttackLimb.attack.AfterAttackSecondary :
currentAttackLimb.attack.AfterAttack;
switch (activeBehavior)
{
case AIBehaviorAfterAttack.Eat:
UpdateEating(deltaTime);
return;
case AIBehaviorAfterAttack.Pursue:
case AIBehaviorAfterAttack.PursueIfCanAttack:
if (currentAttackLimb.attack.SecondaryCoolDown <= 0)
@@ -1205,7 +1245,7 @@ namespace Barotrauma
if (currentAttackLimb.attack.SecondaryCoolDownTimer <= 0)
{
// Don't allow attacking when the attack target has just changed.
if (_previousAiTarget != null && SelectedAiTarget != _previousAiTarget)
if (_previousAiTarget != null && !IsSameTarget(SelectedAiTarget, _previousAiTarget))
{
canAttack = false;
if (activeBehavior == AIBehaviorAfterAttack.PursueIfCanAttack)
@@ -1266,27 +1306,24 @@ namespace Barotrauma
if (currentAttackLimb.attack.SecondaryCoolDownTimer <= 0)
{
// Don't allow attacking when the attack target has just changed.
if (_previousAiTarget != null && SelectedAiTarget != _previousAiTarget)
if (_previousAiTarget != null && !IsSameTarget(SelectedAiTarget, _previousAiTarget))
{
UpdateFallBack(attackWorldPos, deltaTime, activeBehavior == AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
return;
}
// If the secondary cooldown is defined and expired, check if we can switch the attack
var newLimb = GetAttackLimb(attackWorldPos, currentAttackLimb);
if (newLimb != null)
{
// Attack with the new limb
AttackLimb = newLimb;
}
else
{
// If the secondary cooldown is defined and expired, check if we can switch the attack
var newLimb = GetAttackLimb(attackWorldPos, currentAttackLimb);
if (newLimb != null)
{
// Attack with the new limb
AttackLimb = newLimb;
}
else
{
// No new limb was found.
UpdateFallBack(attackWorldPos, deltaTime, activeBehavior == AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
return;
}
}
// No new limb was found.
UpdateFallBack(attackWorldPos, deltaTime, activeBehavior == AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
return;
}
}
else
{
@@ -1308,27 +1345,24 @@ namespace Barotrauma
if (currentAttackLimb.attack.SecondaryCoolDownTimer <= 0)
{
// Don't allow attacking when the attack target has just changed.
if (_previousAiTarget != null && SelectedAiTarget != _previousAiTarget)
if (_previousAiTarget != null && !IsSameTarget(SelectedAiTarget, _previousAiTarget))
{
UpdateIdle(deltaTime, followLastTarget: false);
return;
}
// If the secondary cooldown is defined and expired, check if we can switch the attack
var newLimb = GetAttackLimb(attackWorldPos, currentAttackLimb);
if (newLimb != null)
{
// Attack with the new limb
AttackLimb = newLimb;
}
else
{
// If the secondary cooldown is defined and expired, check if we can switch the attack
var newLimb = GetAttackLimb(attackWorldPos, currentAttackLimb);
if (newLimb != null)
{
// Attack with the new limb
AttackLimb = newLimb;
}
else
{
// No new limb was found.
UpdateIdle(deltaTime, followLastTarget: false);
return;
}
}
// No new limb was found.
UpdateIdle(deltaTime, followLastTarget: false);
return;
}
}
else
{
@@ -1341,6 +1375,9 @@ namespace Barotrauma
case AIBehaviorAfterAttack.FollowThrough:
UpdateFallBack(attackWorldPos, deltaTime, followThrough: true);
return;
case AIBehaviorAfterAttack.FollowThroughWithoutObstacleAvoidance:
UpdateFallBack(attackWorldPos, deltaTime, followThrough: true, avoidObstacles: false);
return;
case AIBehaviorAfterAttack.FallBack:
case AIBehaviorAfterAttack.Reverse:
default:
@@ -1724,7 +1761,7 @@ namespace Barotrauma
circleRotationSpeed *= Rand.Range(1 - selectedTargetingParams.CircleRandomRotationFactor, 1 + selectedTargetingParams.CircleRandomRotationFactor);
aggressionIntensity = Math.Clamp(aggressionIntensity, AIParams.StartAggression, AIParams.MaxAggression);
DisableAttacksIfLimbNotRanged();
if (targetSub != null && targetSub.Borders.Width < 1000 && AttackLimb?.attack is { Ranged: false })
if (targetSub is { Borders.Width: < 1000 } && AttackLimb?.attack is { Ranged: false })
{
breakCircling = true;
CirclePhase = CirclePhase.CloseIn;
@@ -1982,7 +2019,7 @@ namespace Barotrauma
Entity targetEntity = wallTarget?.Structure ?? SelectedAiTarget?.Entity;
if (AttackLimb?.attack is Attack { Ranged: true } attack)
{
AimRangedAttack(attack, targetEntity);
AimRangedAttack(attack, attackTargetLimb as ISpatialEntity ?? targetEntity);
}
if (canAttack)
{
@@ -2005,9 +2042,10 @@ namespace Barotrauma
}
}
public void AimRangedAttack(Attack attack, Entity targetEntity)
public void AimRangedAttack(Attack attack, ISpatialEntity targetEntity)
{
if (attack is not { Ranged: true } || targetEntity is not { Removed: false }) { return; }
if (attack is not { Ranged: true }) { return; }
if (targetEntity is Entity { Removed: true }) { return; }
Character.SetInput(InputType.Aim, false, true);
if (attack.AimRotationTorque <= 0) { return; }
Limb limb = GetLimbToRotate(attack);
@@ -2115,7 +2153,10 @@ namespace Barotrauma
bool wasLatched = IsLatchedOnSub;
Character.AnimController.ReleaseStuckLimbs();
LatchOntoAI?.DeattachFromBody(reset: true, cooldown: 1);
if (attackResult.Damage > 0)
{
LatchOntoAI?.DeattachFromBody(reset: true, cooldown: 1);
}
if (attacker == null || attacker.AiTarget == null || attacker.Removed || attacker.IsDead) { return; }
if (attackResult.Damage >= AIParams.DamageThreshold)
{
@@ -2283,7 +2324,7 @@ namespace Barotrauma
Limb referenceLimb = GetLimbToRotate(ActiveAttack);
if (referenceLimb != null)
{
Vector2 toTarget = spatialTarget.WorldPosition - referenceLimb.WorldPosition;
Vector2 toTarget = attackWorldPos - referenceLimb.WorldPosition;
float offset = referenceLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2;
Vector2 forward = VectorExtensions.Forward(referenceLimb.body.TransformedRotation - offset * referenceLimb.Dir);
float angle = MathHelper.ToDegrees(VectorExtensions.Angle(forward, toTarget));
@@ -2456,7 +2497,7 @@ namespace Barotrauma
}
private Vector2? attackVector = null;
private bool UpdateFallBack(Vector2 attackWorldPos, float deltaTime, bool followThrough, bool checkBlocking = false)
private bool UpdateFallBack(Vector2 attackWorldPos, float deltaTime, bool followThrough, bool checkBlocking = false, bool avoidObstacles = true)
{
if (attackVector == null)
{
@@ -2468,7 +2509,7 @@ namespace Barotrauma
dir = Vector2.UnitY;
}
steeringManager.SteeringManual(deltaTime, dir);
if (Character.AnimController.InWater && !Reverse)
if (Character.AnimController.InWater && !Reverse && avoidObstacles)
{
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15);
}
@@ -2779,10 +2820,16 @@ namespace Barotrauma
// Ignore inner walls when outside (walltargets still work)
continue;
}
if (!Character.AnimController.CanEnterSubmarine && IsWallDisabled(s))
bool attemptToGetInside =
Character.AnimController.CanEnterSubmarine == CanEnterSubmarine.True ||
//characters that are aggressive boarders can partially enter the sub can attempt to push through holes
(Character.AnimController.CanEnterSubmarine == CanEnterSubmarine.Partial && IsAggressiveBoarder);
if (!attemptToGetInside && 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++)
@@ -2790,7 +2837,7 @@ namespace Barotrauma
var section = s.Sections[i];
if (section.gap == null) { continue; }
bool leadsInside = !section.gap.IsRoomToRoom && section.gap.FlowTargetHull != null;
if (Character.AnimController.CanEnterSubmarine)
if (attemptToGetInside)
{
if (!isCharacterInside)
{
@@ -2873,9 +2920,11 @@ namespace Barotrauma
{
if (!canAttackDoors) { continue; }
}
else if (!Character.AnimController.CanEnterSubmarine)
else if (Character.AnimController.CanEnterSubmarine != CanEnterSubmarine.True)
{
// Ignore broken and open doors, if cannot enter submarine
// Also ignore them if the monster can only partially enter the sub:
// these monsters tend to be too large to get through doors anyway.
continue;
}
if (IsAggressiveBoarder)
@@ -2911,6 +2960,13 @@ namespace Barotrauma
if (targetParams.IgnoreInside && Character.CurrentHull != null) { continue; }
if (targetParams.IgnoreOutside && Character.CurrentHull == null) { continue; }
if (targetParams.IgnoreIncapacitated && targetCharacter != null && targetCharacter.IsIncapacitated) { continue; }
if (targetParams.IgnoreTargetInside && aiTarget.Entity.Submarine != null) { continue; }
if (targetParams.IgnoreTargetOutside && aiTarget.Entity.Submarine == null) { continue; }
if (aiTarget.Entity is ISerializableEntity se)
{
if (targetParams.Conditionals.Any(c => !c.TargetSelf && !c.Matches(se))) { continue; }
}
if (targetParams.Conditionals.Any(c => c.TargetSelf && !c.Matches(Character))) { continue; }
if (targetParams.IgnoreIfNotInSameSub)
{
if (aiTarget.Entity.Submarine != Character.Submarine) { continue; }
@@ -2981,6 +3037,16 @@ namespace Barotrauma
{
dist *= 0.9f;
}
if (targetParams.PerceptionDistanceMultiplier > 0.0f)
{
dist /= targetParams.PerceptionDistanceMultiplier;
}
if (targetParams.MaxPerceptionDistance > 0.0f &&
dist * dist > targetParams.MaxPerceptionDistance * targetParams.MaxPerceptionDistance)
{
continue;
}
if (!CanPerceive(aiTarget, dist, checkVisibility: SelectedAiTarget != aiTarget))
{
@@ -3196,7 +3262,7 @@ namespace Barotrauma
{
if ((SelectedAiTarget != null || wallTarget != null) && IsLatchedOnSub)
{
if (!(SelectedAiTarget?.Entity is Structure wall))
if (SelectedAiTarget?.Entity is not Structure wall)
{
wall = wallTarget?.Structure;
}
@@ -3251,9 +3317,10 @@ namespace Barotrauma
if (HasValidPath(requireNonDirty: true)) { return; }
wallHits.Clear();
Structure wall = null;
Vector2 rayStart = AttackLimb != null ? AttackLimb.SimPosition : SimPosition;
Vector2 refPos = AttackLimb != null ? AttackLimb.SimPosition : SimPosition;
if (AIParams.WallTargetingMethod.HasFlag(WallTargetingMethod.Target))
{
Vector2 rayStart = refPos;
Vector2 rayEnd = SelectedAiTarget.SimPosition;
if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null)
{
@@ -3267,6 +3334,7 @@ namespace Barotrauma
}
if (AIParams.WallTargetingMethod.HasFlag(WallTargetingMethod.Heading))
{
Vector2 rayStart = refPos;
Vector2 rayEnd = rayStart + VectorExtensions.Forward(Character.AnimController.Collider.Rotation + MathHelper.PiOver2, avoidLookAheadDistance * 5);
if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null)
{
@@ -3282,6 +3350,7 @@ namespace Barotrauma
}
if (AIParams.WallTargetingMethod.HasFlag(WallTargetingMethod.Steering))
{
Vector2 rayStart = refPos;
Vector2 rayEnd = rayStart + Steering * 5;
if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null)
{
@@ -3297,18 +3366,25 @@ namespace Barotrauma
}
if (wallHits.Any())
{
Vector2 targetdiff = ConvertUnits.ToSimUnits(SelectedAiTarget.WorldPosition - (AttackLimb != null ? AttackLimb.WorldPosition : WorldPosition));
float targetDistance = targetdiff.LengthSquared();
Body closestBody = null;
float closestDistance = 0;
int sectionIndex = -1;
Vector2 sectionPos = Vector2.Zero;
foreach ((Body body, int index, Vector2 sectionPosition) in wallHits)
{
float distance = Vector2.DistanceSquared(SimPosition, sectionPosition);
Structure structure = body.UserData as Structure;
float distance = Vector2.DistanceSquared(
refPos,
Submarine.GetRelativeSimPosition(ConvertUnits.ToSimUnits(sectionPosition), Character.Submarine, structure.Submarine));
//if the wall is further than the target (e.g. at the other side of the sub?), we shouldn't be targeting it
if (distance > targetDistance) { continue; }
if (closestBody == null || closestDistance == 0 || distance < closestDistance)
{
closestBody = body;
closestDistance = distance;
wall = closestBody.UserData as Structure;
wall = structure;
sectionPos = sectionPosition;
sectionIndex = index;
}
@@ -3326,14 +3402,18 @@ namespace Barotrauma
sectionPos.X += (wall.BodyWidth <= 0.0f ? wall.Rect.Width : wall.BodyWidth) / 2 * attachTargetNormal.X;
}
LatchOntoAI?.SetAttachTarget(wall, ConvertUnits.ToSimUnits(sectionPos), attachTargetNormal);
if (Character.AnimController.CanEnterSubmarine || !wall.SectionBodyDisabled(sectionIndex) && !IsWallDisabled(wall))
if (Character.AnimController.CanEnterSubmarine == CanEnterSubmarine.True ||
!wall.SectionBodyDisabled(sectionIndex) && !IsWallDisabled(wall))
{
if (wall.NoAITarget && Character.AnimController.CanEnterSubmarine)
if (wall.NoAITarget && Character.AnimController.CanEnterSubmarine == CanEnterSubmarine.True)
{
bool isTargetingDoor = SelectedAiTarget.Entity is Item i && i.GetComponent<Door>() != null;
// 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)
{
//TODO: this might cause problems: many wall pieces (like smaller shuttle pieces
//and small decorative wall structures are currently marked as having no AI target,
//which can mean a monster very frequently ignores targets inside because they're blocked by those structures
IgnoreTarget(SelectedAiTarget);
ResetAITarget();
}
@@ -3353,7 +3433,9 @@ namespace Barotrauma
void DoRayCast(Vector2 rayStart, Vector2 rayEnd)
{
Body hitTarget = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true, ignoreSensors: CanEnterSubmarine, ignoreDisabledWalls: CanEnterSubmarine);
Body hitTarget = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true,
ignoreSensors: CanEnterSubmarine != CanEnterSubmarine.False,
ignoreDisabledWalls: CanEnterSubmarine != CanEnterSubmarine.False);
if (hitTarget != null && IsValid(hitTarget, out wall))
{
int sectionIndex = wall.FindSectionIndex(ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition));
@@ -3371,7 +3453,8 @@ namespace Barotrauma
{
if (wall.SectionBodyDisabled(i))
{
if (Character.AnimController.CanEnterSubmarine && CanPassThroughHole(wall, i, requiredHoleCount))
if (Character.AnimController.CanEnterSubmarine != CanEnterSubmarine.False &&
CanPassThroughHole(wall, i, requiredHoleCount))
{
sectionIndex = i;
break;
@@ -3394,14 +3477,14 @@ namespace Barotrauma
{
wall = null;
if (Submarine.LastPickedFraction == 1.0f) { return false; }
if (!(hit.UserData is Structure w)) { return false; }
if (hit.UserData is not Structure w) { return false; }
if (w.Submarine == null) { return false; }
if (w.Submarine != SelectedAiTarget.Entity.Submarine) { return false; }
if (Character.Submarine == null)
{
if (w.Prefab.Tags.Contains("inner"))
{
if (!Character.AnimController.CanEnterSubmarine) { return false; }
if (Character.AnimController.CanEnterSubmarine == CanEnterSubmarine.False) { return false; }
}
else if (!AIParams.TargetOuterWalls)
{
@@ -3731,7 +3814,7 @@ namespace Barotrauma
}
reachTimer = 0;
sinTime = 0;
if (breakCircling && strikeTimer <= 0)
if (breakCircling && strikeTimer <= 0 && CirclePhase != CirclePhase.CloseIn)
{
CirclePhase = CirclePhase.Start;
}
@@ -3757,7 +3840,7 @@ namespace Barotrauma
blockCheckTimer = 0;
reachTimer = 0;
sinTime = 0;
if (breakCircling && strikeTimer <= 0)
if (breakCircling && strikeTimer <= 0 && CirclePhase != CirclePhase.CloseIn)
{
CirclePhase = CirclePhase.Start;
}
@@ -3765,7 +3848,12 @@ namespace Barotrauma
private void SetStateResetTimer() => stateResetTimer = stateResetCooldown * Rand.Range(0.75f, 1.25f);
private float GetPerceivingRange(AITarget target) => Math.Max(target.SightRange * Sight, target.SoundRange * Hearing);
private float GetPerceivingRange(AITarget target)
{
float maxSightOrSoundRange = Math.Max(target.SightRange * Sight, target.SoundRange * Hearing);
if (AIParams.MaxPerceptionDistance >= 0 && maxSightOrSoundRange > AIParams.MaxPerceptionDistance) { return AIParams.MaxPerceptionDistance; }
return maxSightOrSoundRange;
}
private bool CanPerceive(AITarget target, float dist = -1, float distSquared = -1, bool checkVisibility = false)
{
@@ -3783,6 +3871,7 @@ namespace Barotrauma
}
if (dist > 0)
{
if (AIParams.MaxPerceptionDistance >= 0 && dist > AIParams.MaxPerceptionDistance) { return false; }
insideSightRange = IsInRange(dist, target.SightRange, Sight);
if (!checkVisibility && insideSightRange) { return true; }
insideSoundRange = IsInRange(dist, target.SoundRange, Hearing);
@@ -3793,6 +3882,7 @@ namespace Barotrauma
{
distSquared = Vector2.DistanceSquared(Character.WorldPosition, target.WorldPosition);
}
if (AIParams.MaxPerceptionDistance >= 0 && distSquared > AIParams.MaxPerceptionDistance * AIParams.MaxPerceptionDistance) { return false; }
insideSightRange = IsInRangeSqr(distSquared, target.SightRange, Sight);
if (!checkVisibility && insideSightRange) { return true; }
insideSoundRange = IsInRangeSqr(distSquared, target.SoundRange, Hearing);
@@ -72,6 +72,11 @@ namespace Barotrauma
/// How far other characters can hear reports done by this character (e.g. reports for fires, intruders). Defaults to infinity.
/// </summary>
public float ReportRange { get; set; } = float.PositiveInfinity;
/// <summary>
/// How far the character can seek new weapons from.
/// </summary>
public float FindWeaponsRange { get; set; } = float.PositiveInfinity;
private float _aimSpeed = 1;
public float AimSpeed
@@ -150,9 +155,13 @@ namespace Barotrauma
}
public override bool IsMentallyUnstable =>
MentalStateManager == null ? false :
MentalStateManager.CurrentMentalType != MentalStateManager.MentalType.Normal &&
MentalStateManager.CurrentMentalType != MentalStateManager.MentalType.Confused;
MentalStateManager is
{
CurrentMentalType:
MentalStateManager.MentalType.Afraid or
MentalStateManager.MentalType.Desperate or
MentalStateManager.MentalType.Berserk
};
public ShipCommandManager ShipCommandManager { get; private set; }
@@ -817,7 +826,7 @@ namespace Barotrauma
private readonly HashSet<Item> itemsToRelocate = new HashSet<Item>();
private void HandleRelocation(Item item)
public void HandleRelocation(Item item)
{
if (item.SpawnedInCurrentOutpost) { return; }
if (item.Submarine == null) { return; }
@@ -837,7 +846,10 @@ namespace Barotrauma
// In the campaign mode, undocking happens after leaving the outpost, so we can't use that.
campaign.BeforeLevelLoading += Relocate;
}
campaign.ItemsRelocatedToMainSub = true;
#if CLIENT
HintManager.OnItemMarkedForRelocation();
#endif
void Relocate()
{
if (item == null || item.Removed) { return; }
@@ -1566,7 +1578,7 @@ namespace Barotrauma
{
HoldPosition = Character.Info?.Job?.Prefab.Identifier == "watchman",
AbortCondition = abortCondition,
allowHoldFire = allowHoldFire,
AllowHoldFire = allowHoldFire,
};
if (onAbort != null)
{
@@ -1583,12 +1595,16 @@ namespace Barotrauma
public void SetOrder(Order order, bool speak = true)
{
objectiveManager.SetOrder(order, speak);
#if CLIENT
HintManager.OnSetOrder(Character, order);
#endif
}
public void SetForcedOrder(Order order)
public AIObjective SetForcedOrder(Order order)
{
var objective = ObjectiveManager.CreateObjective(order);
ObjectiveManager.SetForcedOrder(objective);
return objective;
}
public void ClearForcedOrder()
@@ -1677,14 +1693,17 @@ namespace Barotrauma
return false;
}
public static bool HasDivingGear(Character character, float conditionPercentage = 0, bool requireOxygenTank = true) => HasDivingSuit(character, conditionPercentage, requireOxygenTank) || HasDivingMask(character, conditionPercentage, requireOxygenTank);
public static bool HasDivingGear(Character character, float conditionPercentage = 0, bool requireOxygenTank = true) =>
HasDivingSuit(character, conditionPercentage, requireOxygenTank) || HasDivingMask(character, conditionPercentage, requireOxygenTank);
/// <summary>
/// Check whether the character has a diving suit in usable condition plus some oxygen.
/// Check whether the character has a diving suit in usable condition, suitable pressure protection for the depth, plus some oxygen.
/// </summary>
public static bool HasDivingSuit(Character character, float conditionPercentage = 0, bool requireOxygenTank = true)
public static bool HasDivingSuit(Character character, float conditionPercentage = 0, bool requireOxygenTank = true, bool requireSuitablePressureProtection = true)
=> HasItem(character, Tags.HeavyDivingGear, out _, requireOxygenTank ? Tags.OxygenSource : Identifier.Empty, conditionPercentage, requireEquipped: true,
predicate: (Item item) => character.HasEquippedItem(item, InvSlotType.OuterClothes | InvSlotType.InnerClothes));
predicate: (Item item) =>
character.HasEquippedItem(item, InvSlotType.OuterClothes | InvSlotType.InnerClothes) &&
(!requireSuitablePressureProtection || AIObjectiveFindDivingGear.IsSuitablePressureProtection(item, Tags.HeavyDivingGear, character)));
/// <summary>
/// Check whether the character has a diving mask in usable condition plus some oxygen.
@@ -1838,7 +1857,7 @@ namespace Barotrauma
foreach (Character otherCharacter in Character.CharacterList)
{
if (otherCharacter == thief || otherCharacter.TeamID == thief.TeamID || otherCharacter.IsIncapacitated || otherCharacter.Stun > 0.0f ||
otherCharacter.Info?.Job == null || otherCharacter.AIController is not HumanAIController otherHumanAI ||
otherCharacter.Info?.Job == null || otherCharacter.AIController is not HumanAIController otherHumanAI || otherCharacter.IsEscorted ||
Vector2.DistanceSquared(otherCharacter.WorldPosition, thief.WorldPosition) > 1000.0f * 1000.0f)
{
continue;
@@ -2064,7 +2083,7 @@ namespace Barotrauma
visibleHulls = VisibleHulls;
}
bool ignoreFire = objectiveManager.CurrentOrder is AIObjectiveExtinguishFires extinguishOrder && extinguishOrder.Priority > 0 || objectiveManager.HasActiveObjective<AIObjectiveExtinguishFire>();
bool ignoreOxygen = HasDivingGear(character);
bool ignoreOxygen = HasDivingGear(character);
bool ignoreEnemies = ObjectiveManager.IsCurrentOrder<AIObjectiveFightIntruders>() || ObjectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>();
float safety = CalculateHullSafety(hull, visibleHulls, character, ignoreWater: false, ignoreOxygen, ignoreFire, ignoreEnemies);
if (isCurrentHull)
@@ -2197,11 +2216,29 @@ namespace Barotrauma
public static bool IsFriendly(Character me, Character other, bool onlySameTeam = false)
{
if (other.IsHusk)
{
// Disguised as husk
return me.IsDisguisedAsHusk;
}
else
{
if (other.IsPrisoner && me.IsPrisoner)
{
// Both prisoners
return true;
}
if (other.IsHostileEscortee && me.IsHostileEscortee)
{
// Both hostile escortees
return true;
}
}
bool sameTeam = me.TeamID == other.TeamID;
bool teamGood = sameTeam || !onlySameTeam && me.IsOnFriendlyTeam(other);
if (!teamGood)
{
return other.IsHusk && me.IsDisguisedAsHusk;
return false;
}
if (other.IsPet)
{
@@ -6,6 +6,7 @@ using System;
using System.Collections.Generic;
using System.Xml.Linq;
using System.Linq;
using Voronoi2;
namespace Barotrauma
{
@@ -32,14 +33,21 @@ namespace Barotrauma
private Vector2 _attachPos;
/// <summary>
/// The character won't latch onto anything when the cooldown is active (activates after the character deattaches for whatever reason).
/// </summary>
private float attachCooldown;
private Limb attachLimb;
private readonly Limb attachLimb;
private Vector2 localAttachPos;
private float attachLimbRotation;
private readonly float attachLimbRotation;
private float jointDir;
private float latchedDuration;
private readonly bool freezeWhenLatched;
public List<Joint> AttachJoints { get; } = new List<Joint>();
public Vector2? AttachPos
@@ -54,18 +62,19 @@ namespace Barotrauma
public LatchOntoAI(XElement element, EnemyAIController enemyAI)
{
AttachToWalls = element.GetAttributeBool("attachtowalls", false);
AttachToSub = element.GetAttributeBool("attachtosub", false);
AttachToCharacters = element.GetAttributeBool("attachtocharacters", false);
minDeattachSpeed = element.GetAttributeFloat("mindeattachspeed", 5.0f);
maxDeattachSpeed = Math.Max(minDeattachSpeed, element.GetAttributeFloat("maxdeattachspeed", 8.0f));
maxAttachDuration = element.GetAttributeFloat("maxattachduration", -1.0f);
coolDown = element.GetAttributeFloat("cooldown", 2f);
damageOnDetach = element.GetAttributeFloat("damageondetach", 0.0f);
detachStun = element.GetAttributeFloat("detachstun", 0.0f);
localAttachPos = ConvertUnits.ToSimUnits(element.GetAttributeVector2("localattachpos", Vector2.Zero));
attachLimbRotation = MathHelper.ToRadians(element.GetAttributeFloat("attachlimbrotation", 0.0f));
weld = element.GetAttributeBool("weld", true);
AttachToWalls = element.GetAttributeBool(nameof(AttachToWalls), false);
AttachToSub = element.GetAttributeBool(nameof(AttachToSub), false);
AttachToCharacters = element.GetAttributeBool(nameof(AttachToCharacters), false);
minDeattachSpeed = element.GetAttributeFloat(nameof(minDeattachSpeed), 5.0f);
maxDeattachSpeed = Math.Max(minDeattachSpeed, element.GetAttributeFloat(nameof(maxDeattachSpeed), 8.0f));
maxAttachDuration = element.GetAttributeFloat(nameof(maxAttachDuration), -1.0f);
coolDown = element.GetAttributeFloat(nameof(coolDown), 2f);
damageOnDetach = element.GetAttributeFloat(nameof(damageOnDetach), 0.0f);
detachStun = element.GetAttributeFloat(nameof(detachStun), 0.0f);
localAttachPos = ConvertUnits.ToSimUnits(element.GetAttributeVector2(nameof(localAttachPos), Vector2.Zero));
attachLimbRotation = MathHelper.ToRadians(element.GetAttributeFloat(nameof(attachLimbRotation), 0.0f));
weld = element.GetAttributeBool(nameof(weld), true);
freezeWhenLatched = element.GetAttributeBool(nameof(freezeWhenLatched), false);
string limbString = element.GetAttributeString("attachlimb", null);
attachLimb = enemyAI.Character.AnimController.Limbs.FirstOrDefault(l => string.Equals(l.Name, limbString, StringComparison.OrdinalIgnoreCase));
@@ -108,7 +117,23 @@ namespace Barotrauma
targetBody = target.AnimController.Collider.FarseerBody;
attachSurfaceNormal = Vector2.Normalize(character.WorldPosition - target.WorldPosition);
}
public void SetAttachTarget(VoronoiCell levelWall)
{
if (!AttachToWalls) { return; }
Reset();
foreach (Voronoi2.GraphEdge edge in levelWall.Edges)
{
if (MathUtils.GetLineSegmentIntersection(edge.Point1, edge.Point2, character.WorldPosition, levelWall.Center, out Vector2 intersection))
{
attachSurfaceNormal = edge.GetNormal(levelWall);
targetBody = levelWall.Body;
_attachPos = ConvertUnits.ToSimUnits(intersection);
return;
}
}
}
public void Update(EnemyAIController enemyAI, float deltaTime)
{
if (TargetCharacter != null && character.Submarine != TargetCharacter.Submarine ||
@@ -119,6 +144,17 @@ namespace Barotrauma
}
if (IsAttached)
{
latchedDuration += deltaTime;
if (freezeWhenLatched && targetBody is { BodyType: BodyType.Static } &&
/*brief delay to let the ragdoll "settle"*/
latchedDuration > 5.0f)
{
foreach (var limb in character.AnimController.Limbs)
{
limb.body.LinearVelocity = Vector2.Zero;
limb.body.AngularVelocity = 0.0f;
}
}
if (Math.Sign(attachLimb.Dir) != Math.Sign(jointDir))
{
var attachJoint = AttachJoints[0];
@@ -241,7 +277,7 @@ namespace Barotrauma
{
DeattachFromBody(reset: false);
}
else
else if (attachCooldown <= 0.0f)
{
float squaredDistance = Vector2.DistanceSquared(character.SimPosition, _attachPos);
float targetDistance = Math.Max(Math.Max(character.AnimController.Collider.Radius, character.AnimController.Collider.Width), character.AnimController.Collider.Height) * 1.2f;
@@ -259,6 +295,10 @@ namespace Barotrauma
enemyAI.SteeringManager.SteeringSeek(_attachPos);
}
}
else if (IsAttached)
{
enemyAI.SteeringManager.Reset();
}
break;
case AIState.Attack:
case AIState.Aggressive:
@@ -281,11 +321,11 @@ namespace Barotrauma
if (IsAttached && targetBody != null && deattachCheckTimer <= 0.0f)
{
attachCooldown = coolDown;
bool deattach = false;
if (maxAttachDuration > 0)
{
deattach = true;
attachCooldown = coolDown;
}
if (!deattach && TargetWall != null && TargetSubmarine != null)
{
@@ -294,7 +334,6 @@ namespace Barotrauma
if (enemyAI.CanPassThroughHole(TargetWall, targetSection))
{
deattach = true;
attachCooldown = coolDown;
}
if (!deattach)
{
@@ -327,7 +366,7 @@ namespace Barotrauma
}
}
private void AttachToBody(Vector2 attachPos)
public void AttachToBody(Vector2 attachPos, Vector2? forceAttachSurfaceNormal = null, Vector2? forceColliderSimPosition = null)
{
if (attachLimb == null) { return; }
if (targetBody == null) { return; }
@@ -343,6 +382,12 @@ namespace Barotrauma
jointDir = attachLimb.Dir;
if (forceAttachSurfaceNormal.HasValue) { attachSurfaceNormal = forceAttachSurfaceNormal.Value; }
if (forceColliderSimPosition.HasValue)
{
character.TeleportTo(ConvertUnits.ToDisplayUnits(forceColliderSimPosition.Value));
}
Vector2 transformedLocalAttachPos = localAttachPos * attachLimb.Scale * attachLimb.Params.Ragdoll.LimbScale;
if (jointDir < 0.0f)
{
@@ -350,6 +395,9 @@ namespace Barotrauma
}
float angle = MathUtils.VectorToAngle(-attachSurfaceNormal) - MathHelper.PiOver2 + attachLimbRotation * attachLimb.Dir;
//make sure the angle "has the same number of revolutions" as the reference limb
//(e.g. we don't want to rotate the legs to 0 if the torso is at 360, because that'd blow up the hip joints)
angle = attachLimb.body.WrapAngleToSameNumberOfRevolutions(angle);
attachLimb.body.SetTransform(attachPos + attachSurfaceNormal * transformedLocalAttachPos.Length(), angle);
var limbJoint = new WeldJoint(attachLimb.body.FarseerBody, targetBody,
@@ -392,10 +440,26 @@ namespace Barotrauma
{
deattachCheckTimer = maxAttachDuration;
}
#if SERVER
if (TargetCharacter != null)
{
GameMain.Server.CreateEntityEvent(character, new Character.LatchedOntoTargetEventData(character, TargetCharacter, attachSurfaceNormal, attachPos));
}
else if (TargetWall != null)
{
GameMain.Server.CreateEntityEvent(character, new Character.LatchedOntoTargetEventData(character, TargetWall, attachSurfaceNormal, attachPos));
}
else if (targetBody.UserData is Voronoi2.VoronoiCell cell)
{
GameMain.Server.CreateEntityEvent(character, new Character.LatchedOntoTargetEventData(character, cell, attachSurfaceNormal, attachPos));
}
#endif
}
public void DeattachFromBody(bool reset, float cooldown = 0)
{
bool wasAttached = IsAttached;
foreach (Joint joint in AttachJoints)
{
GameMain.World.Remove(joint);
@@ -410,6 +474,12 @@ namespace Barotrauma
{
Reset();
}
#if SERVER
if (wasAttached)
{
GameMain.Server.CreateEntityEvent(character, new Character.LatchedOntoTargetEventData());
}
#endif
}
private void Reset()
@@ -294,6 +294,41 @@ namespace Barotrauma
return Priority;
}
/// <summary>
/// Get a normalized value representing how close the target position is.
/// The value is a rough estimation, where vertical movement is assumed to be more costly than horizontal.
/// </summary>
/// <param name="targetWorldPos">Position of the target</param>
/// <param name="verticalDistanceMultiplier">How much more costly vertical movement is than horizontal</param>
/// <param name="maxDistance">Maximum distance, after which the factor will reach it's minimum value (= anything beyond this point is "as far as it can be").</param>
/// <param name="factorAtMaxDistance">The factor at the maximum distance and beyond (= how "viable" very far-away targets should be considered).</param>
/// <param name="factorAtMinDistance">The factor at the minimum distance (= how viable a target that's 0 units a way is considered).</param>
public static float GetDistanceFactor(Vector2 selfPos, Vector2 targetWorldPos, float factorAtMaxDistance, float verticalDistanceMultiplier = 3, float maxDistance = 10000.0f, float factorAtMinDistance = 1.0f)
{
float yDist = Math.Abs(selfPos.Y - targetWorldPos.Y);
yDist = yDist > 100 ? yDist * verticalDistanceMultiplier : 0;
float distance = Math.Abs(selfPos.X - targetWorldPos.X) + yDist;
float distanceFactor = MathHelper.Lerp(factorAtMinDistance, factorAtMaxDistance, MathUtils.InverseLerp(0, maxDistance, distance));
return
factorAtMinDistance > factorAtMaxDistance ?
MathHelper.Clamp(distanceFactor, factorAtMaxDistance, factorAtMinDistance) :
MathHelper.Clamp(distanceFactor, factorAtMinDistance, factorAtMaxDistance);
}
/// <summary>
/// Get a normalized value representing how close the target position is.
/// The value is a rough estimation, where vertical movement is assumed to be more costly than horizontal.
/// </summary>
/// <param name="targetWorldPos">Position of the target</param>
/// <param name="verticalDistanceMultiplier">How much more costly vertical movement is than horizontal</param>
/// <param name="maxDistance">Maximum distance, after which the factor will reach it's minimum value (= anything beyond this point is "as far as it can be").</param>
/// <param name="factorAtMaxDistance">The factor at the maximum distance and beyond (= how "viable" very far-away targets should be considered).</param>
/// <param name="factorAtMinDistance">The factor at the minimum distance (= how viable a target that's 0 units a way is considered).</param>
protected float GetDistanceFactor(Vector2 targetWorldPos, float factorAtMaxDistance, float verticalDistanceMultiplier = 3, float maxDistance = 10000.0f, float factorAtMinDistance = 1.0f)
{
return GetDistanceFactor(character.WorldPosition, targetWorldPos, factorAtMaxDistance, verticalDistanceMultiplier, maxDistance, factorAtMinDistance);
}
private void UpdateDevotion(float deltaTime)
{
var currentObjective = objectiveManager.CurrentObjective;
@@ -463,7 +498,7 @@ namespace Barotrauma
{
hasBeenChecked = true;
CheckSubObjectives();
if (subObjectives.None() || ConcurrentObjectives && subObjectives.All(so => so is AIObjectiveGoTo))
if (subObjectives.None() || ConcurrentObjectives)
{
if (Check())
{
@@ -509,7 +544,7 @@ namespace Barotrauma
public virtual void SpeakAfterOrderReceived() { }
protected static bool CanEquip(Character character, Item item, bool allowWearing)
protected static bool CanPutInInventory(Character character, Item item, bool allowWearing)
{
if (item == null) { return false; }
bool canEquip = false;
@@ -550,6 +585,6 @@ namespace Barotrauma
return canEquip && character.Inventory.CanBePut(item);
}
protected bool CanEquip(Item item, bool allowWearing) => CanEquip(character, item, allowWearing);
protected bool CanEquip(Item item, bool allowWearing) => CanPutInInventory(character, item, allowWearing);
}
}
@@ -21,6 +21,11 @@ namespace Barotrauma
private AIObjectiveDecontainItem decontainObjective;
private int itemIndex = 0;
/// <summary>
/// Allows decontainObjective to be interrupted if this objective gets abandoned (e.g. due to the item no longer being eligible for cleanup)
/// </summary>
public override bool ConcurrentObjectives => true;
public AIObjectiveCleanupItem(Item item, Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
@@ -39,10 +44,8 @@ namespace Barotrauma
float distanceFactor = 0.9f;
if (!IsPriority && item.CurrentHull != character.CurrentHull)
{
float yDist = Math.Abs(character.WorldPosition.Y - item.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 5 : 0;
float dist = Math.Abs(character.WorldPosition.X - item.WorldPosition.X) + yDist;
distanceFactor = MathHelper.Lerp(0.9f, 0, MathUtils.InverseLerp(0, 5000, dist));
distanceFactor = GetDistanceFactor(item.WorldPosition, verticalDistanceMultiplier: 5, maxDistance: 5000,
factorAtMinDistance: 0.9f, factorAtMaxDistance: 0);
}
bool isSelected = character.HasItem(item);
float selectedBonus = isSelected ? 100 - MaxDevotion : 0;
@@ -116,7 +119,7 @@ namespace Barotrauma
protected override bool CheckObjectiveSpecific()
{
if (item.IgnoreByAI(character))
if (item.IgnoreByAI(character) || Item.DeconstructItems.Contains(item))
{
Abandon = true;
}
@@ -56,8 +56,15 @@ 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 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; }
foreach (Character c in Character.CharacterList)
{
if (c == character || !HumanAIController.IsActive(c)) { continue; }
if (c.CurrentHull == target.CurrentHull && !HumanAIController.IsFriendly(c))
{
// Don't clean up items in rooms that have enemies inside.
return false;
}
}
return true;
}
@@ -89,9 +96,10 @@ namespace Barotrauma
IsItemInsideValidSubmarine(container, character) &&
!container.IsClaimedByBallastFlora;
public static bool IsValidTarget(Item item, Character character, bool checkInventory, bool allowUnloading = true)
public static bool IsValidTarget(Item item, Character character, bool checkInventory, bool allowUnloading = true, bool requireValidContainer = true, bool ignoreItemsMarkedForDeconstruction = true)
{
if (item == null) { return false; }
if (item.DontCleanUp) { return false; }
if ((item.SpawnedInCurrentOutpost && !item.AllowStealing) == character.IsOnPlayerTeam) { return false; }
if (item.ParentInventory != null)
{
@@ -101,8 +109,9 @@ namespace Barotrauma
return false;
}
if (!allowUnloading) { return false; }
if (!IsValidContainer(item.Container, character)) { return false; }
if (requireValidContainer && !IsValidContainer(item.Container, character)) { return false; }
}
if (ignoreItemsMarkedForDeconstruction && Item.DeconstructItems.Contains(item)) { return false; }
if (!item.HasAccess(character)) { return false; }
if (character != null && !IsItemInsideValidSubmarine(item, character)) { return false; }
if (item.HasBallastFloraInHull) { return false; }
@@ -121,11 +130,16 @@ namespace Barotrauma
return false;
}
}
if (item.GetComponent<Rope>() is { IsActive: true, Snapped: false })
{
// Don't clean up spears with an active rope component.
return false;
}
if (!checkInventory)
{
return true;
}
return CanEquip(character, item, allowWearing: false);
return CanPutInInventory(character, item, allowWearing: false);
}
public override void OnDeselected()
@@ -22,13 +22,13 @@ namespace Barotrauma
private readonly CombatMode initialMode;
private float checkWeaponsTimer;
private readonly float checkWeaponsInterval = 1;
private const float checkWeaponsInterval = 1;
private float ignoreWeaponTimer;
private readonly float ignoredWeaponsClearTime = 10;
private const float ignoredWeaponsClearTime = 10;
private readonly float goodWeaponPriority = 30;
private const float goodWeaponPriority = 30;
private readonly float arrestHoldFireTime = 8;
private const float arrestHoldFireTime = 8;
private float holdFireTimer;
private bool hasAimed;
private bool isLethalWeapon;
@@ -79,14 +79,17 @@ namespace Barotrauma
private bool canSeeTarget;
private float visibilityCheckTimer;
private readonly float visibilityCheckInterval = 0.2f;
private const float visibilityCheckInterval = 0.2f;
private float sqrDistance;
private readonly float maxDistance = 2000;
private readonly float distanceCheckInterval = 0.2f;
private const float maxDistance = 2000;
private const float distanceCheckInterval = 0.2f;
private float distanceTimer;
private const float closeDistanceThreshold = 300;
private const float floorHeightApproximate = 100;
public bool allowHoldFire;
public bool AllowHoldFire;
/// <summary>
/// Don't start using a weapon if this condition is true
@@ -95,26 +98,63 @@ namespace Barotrauma
public enum CombatMode
{
Defensive, // Use weapons against the enemy, but try to retreat to a safe place
Offensive, // Engage the enemy and keep attacking it
Arrest, // Try to arrest the enemy without using lethal weapons (stunning + handcuffs)
Retreat, // Run to a safe place without attacking the target
None // Don't use
/// <summary>
/// Use weapons against the enemy, but try to retreat to a safe place.
/// </summary>
Defensive,
/// <summary>
/// Engage the enemy and keep attacking it.
/// </summary>
Offensive,
/// <summary>
/// Try to arrest the enemy without using lethal weapons (stunning + handcuffs).
/// </summary>
Arrest,
/// <summary>
/// Attempt to retreat to a safe place. Unlike in the Defensive mode, the character won't try to attack the enemy.
/// </summary>
Retreat,
/// <summary>
/// Does nothing.
/// </summary>
None
}
public CombatMode Mode { get; private set; }
private bool IsOffensiveOrArrest => initialMode == CombatMode.Offensive || initialMode == CombatMode.Arrest;
private bool IsOffensiveOrArrest => initialMode is CombatMode.Offensive or CombatMode.Arrest;
private bool TargetEliminated => IsEnemyDisabled || Enemy.IsUnconscious && Enemy.Params.Health.ConstantHealthRegeneration <= 0.0f || Enemy.IsArrested && !character.IsInstigator;
private bool IsEnemyDisabled => Enemy == null || Enemy.Removed || Enemy.IsDead;
private float AimSpeed => HumanAIController.AimSpeed;
private float AimAccuracy => HumanAIController.AimAccuracy;
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;
/// <summary>
/// This is just an approximation that attempts to take different rooms and floors into account.
/// It can be equal to a simple distance check, but when the target is nearby, we only use the horizontal axis.
/// It's used for checking whether the enemy is close in certain situations, not for checking the distance to the enemy in general.
/// </summary>
private bool IsEnemyClose(float margin)
{
if (Enemy == null) { return false; }
Vector2 toEnemy = Enemy.WorldPosition - character.WorldPosition;
if (character.CurrentHull != null && Enemy.CurrentHull != null && character.CurrentHull != Enemy.CurrentHull)
{
// Inside, not in the same hull with the enemy
if (Math.Abs(toEnemy.Y) > floorHeightApproximate)
{
// Different floor
return false;
}
if (HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull))
{
// Potentially visible and on the same floor -> use only the horizontal distance.
return Math.Abs(toEnemy.X) < margin;
}
}
// Outside or inside in the same hull -> use the normal distance check.
return Vector2.DistanceSquared(character.WorldPosition, Enemy.WorldPosition) < margin * margin;
}
public AIObjectiveCombat(Character character, Character enemy, CombatMode mode, AIObjectiveManager objectiveManager, float priorityModifier = 1, float coolDown = 10.0f)
: base(character, objectiveManager, priorityModifier)
@@ -147,7 +187,7 @@ namespace Barotrauma
protected override float GetPriority()
{
if (Enemy == null)
if (Enemy == null || Enemy.Removed)
{
Priority = 0;
Abandon = true;
@@ -169,9 +209,9 @@ namespace Barotrauma
else
{
// 91-100
float minPriority = AIObjectiveManager.EmergencyObjectivePriority + 1;
float maxPriority = AIObjectiveManager.MaxObjectivePriority;
float priorityScale = maxPriority - minPriority;
const float minPriority = AIObjectiveManager.EmergencyObjectivePriority + 1;
const float maxPriority = AIObjectiveManager.MaxObjectivePriority;
const float priorityScale = maxPriority - minPriority;
float xDist = Math.Abs(character.WorldPosition.X - Enemy.WorldPosition.X);
float yDist = Math.Abs(character.WorldPosition.Y - Enemy.WorldPosition.Y);
if (HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull))
@@ -208,12 +248,12 @@ namespace Barotrauma
ignoredWeapons.Clear();
ignoreWeaponTimer = ignoredWeaponsClearTime;
}
bool isCurrentObjective = objectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>();
if (findSafety != null && isCurrentObjective)
bool isFightingIntruders = objectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>();
if (findSafety != null && isFightingIntruders)
{
findSafety.Priority = 0;
}
if (!AllowCoolDown && !character.IsOnPlayerTeam && !isCurrentObjective)
if (!AllowCoolDown && !character.IsOnPlayerTeam && !isFightingIntruders)
{
distanceTimer -= deltaTime;
if (distanceTimer < 0)
@@ -226,7 +266,7 @@ namespace Barotrauma
protected override bool CheckObjectiveSpecific()
{
if (character.Submarine == null || character.Submarine.TeamID != CharacterTeamType.FriendlyNPC)
if (character.Submarine is not { TeamID: CharacterTeamType.FriendlyNPC })
{
// Can't lose the target in friendly outposts.
if (sqrDistance > maxDistance * maxDistance)
@@ -343,12 +383,15 @@ namespace Barotrauma
RemoveSubObjective(ref seekAmmunitionObjective);
return false;
}
bool isAllowedToSeekWeapons = character.CurrentHull != null && !IsEnemyCloserThan(300) && character.IsOnPlayerTeam && IsOffensiveOrArrest;
bool isAllowedToSeekWeapons = character.IsHostileEscortee || character.IsPrisoner || // Prisoners and terrorists etc are always allowed to seek new weapons.
(character.IsInFriendlySub // Other characters need to be on a friendly sub in order to "know" where the weapons are. This also prevents NPCs "stealing" player items.
&& IsOffensiveOrArrest // = Defensive or retreating AI shouldn't seek new weapons.
&& !character.IsInstigator); // Instigators (= aggressive NPCs spawned with events) shouldn't seek new weapons, because we don't want them to grab e.g. an smg, if they spawn with a wrench or something.
if (checkWeaponsTimer < 0)
{
checkWeaponsTimer = checkWeaponsInterval;
// First go through all weapons and try to reload without seeking ammunition
var allWeapons = FindWeaponsFromInventory();
HashSet<ItemComponent> allWeapons = FindWeaponsFromInventory();
while (allWeapons.Any())
{
Weapon = GetWeapon(allWeapons, out _weaponComponent);
@@ -369,14 +412,20 @@ namespace Barotrauma
// All good, the weapon is loaded
break;
}
if (Reload(seekAmmo: isAllowedToSeekWeapons))
bool seekAmmo = isAllowedToSeekWeapons && seekAmmunitionObjective == null && !IsEnemyClose(closeDistanceThreshold);
if (Reload(seekAmmo: seekAmmo))
{
// All good, we can use the weapon.
break;
}
else if (seekAmmunitionObjective != null)
{
// Seeking ammo.
break;
}
else
{
// No ammo.
// No ammo and should not try to seek ammo.
allWeapons.Remove(WeaponComponent);
Weapon = null;
}
@@ -409,16 +458,16 @@ namespace Barotrauma
Mode = CombatMode.Retreat;
}
}
else if (seekAmmunitionObjective == null && (WeaponComponent == null || (WeaponComponent.CombatPriority < goodWeaponPriority)))
else if (seekAmmunitionObjective == null && (WeaponComponent == null || (WeaponComponent.CombatPriority < goodWeaponPriority && !IsEnemyClose(closeDistanceThreshold))))
{
// Poor weapon equipped -> try to find better.
RemoveSubObjective(ref seekAmmunitionObjective);
// No weapon or only a poor weapon equipped -> try to find better.
RemoveSubObjective(ref retreatObjective);
RemoveSubObjective(ref followTargetObjective);
TryAddSubObjective(ref seekWeaponObjective,
constructor: () => new AIObjectiveGetItem(character, "weapon".ToIdentifier(), objectiveManager, equip: true, checkInventory: false)
{
AllowStealing = HumanAIController.IsMentallyUnstable,
AbortCondition = obj => IsEnemyClose(200),
EvaluateCombatPriority = false, // Use a custom formula instead
GetItemPriority = i =>
{
@@ -427,7 +476,39 @@ namespace Barotrauma
float priority = 0;
if (GetWeaponComponent(i) is ItemComponent ic)
{
priority = GetWeaponPriority(ic, prioritizeMelee: false, isCloseToEnemy: false, out _) / 100;
priority = GetWeaponPriority(ic, prioritizeMelee: false, canSeekAmmo: true, out _) / 100;
}
if (priority <= 0) { return 0; }
// Check that we are not running directly towards the enemy.
Vector2 toItem = i.WorldPosition - character.WorldPosition;
float range = HumanAIController.FindWeaponsRange;
if (range is > 0 and < float.PositiveInfinity)
{
// Y distance is irrelevant when we are on the same floor. If we are on a different floor, let's double it.
float yDiff = Math.Abs(toItem.Y) > floorHeightApproximate ? toItem.Y * 2 : 0;
Vector2 adjustedDiff = new Vector2(toItem.X, yDiff);
if (adjustedDiff.LengthSquared() > MathUtils.Pow2(range))
{
// Too far -> not allowed to seek.
return 0;
}
}
Vector2 toEnemy = Enemy.WorldPosition - character.WorldPosition;
if (Math.Sign(toItem.X) == Math.Sign(toEnemy.X))
{
// Going towards the enemy -> reduce the priority.
priority *= 0.5f;
}
if (i.CurrentHull != null && !HumanAIController.VisibleHulls.Contains(i.CurrentHull))
{
if (Math.Abs(toItem.Y) > floorHeightApproximate && Math.Abs(toEnemy.Y) > floorHeightApproximate)
{
if (Math.Sign(toItem.Y) == Math.Sign(toEnemy.Y))
{
// Different floor, at the direction of the enemy -> reduce the priority.
priority *= 0.75f;
}
}
}
return priority;
}
@@ -441,19 +522,19 @@ namespace Barotrauma
SpeakNoWeapons();
Mode = CombatMode.Retreat;
}
else
else if (!objectiveManager.HasActiveObjective<AIObjectiveFightIntruders>())
{
// Poor weapon equipped
Mode = CombatMode.Defensive;
}
});
}
}
else
else if (seekAmmunitionObjective == null && seekWeaponObjective == null)
{
if (!CheckWeapon(seekAmmo: false))
{
Weapon = null;
RemoveSubObjective(ref seekAmmunitionObjective);
}
}
return Weapon != null;
@@ -504,10 +585,14 @@ namespace Barotrauma
item.GetComponent<RepairTool>() ??
item.GetComponent<Holdable>() as ItemComponent;
private float GetWeaponPriority(ItemComponent weapon, bool prioritizeMelee, bool isCloseToEnemy, out float lethalDmg)
/// <summary>
/// Normal range of combat priority is 0-100, but the value is not clamped.
/// </summary>
private float GetWeaponPriority(ItemComponent weapon, bool prioritizeMelee, bool canSeekAmmo, out float lethalDmg)
{
lethalDmg = -1;
float priority = weapon.CombatPriority;
if (priority <= 0) { return 0; }
if (weapon is RepairTool repairTool)
{
switch (repairTool.UsableIn)
@@ -531,9 +616,9 @@ namespace Barotrauma
}
if (weapon.IsEmpty(character))
{
if (weapon is RangedWeapon && isCloseToEnemy)
if (weapon is RangedWeapon && !canSeekAmmo)
{
// Ignore weapons that don't have any ammunition (-> Don't seek ammo).
// Ignore weapons that don't have any ammunition, when we are not allowed to seek more ammo.
return 0;
}
else
@@ -605,7 +690,45 @@ namespace Barotrauma
Attack attack = GetAttackDefinition(weapon);
priority = attack?.GetTotalDamage() ?? priority / 2;
}
// Reduce the priority of the weapon, if we don't have requires skills to use it.
float startPriority = priority;
var skillRequirementHints = weapon.Item.Prefab.SkillRequirementHints;
if (skillRequirementHints != null)
{
// If there are any skill requirement hints defined, let's use them.
// This should be the most accurate (manually defined) representation of the requirements (taking into account property conditionals etc).
foreach (SkillRequirementHint hint in skillRequirementHints)
{
float skillLevel = character.GetSkillLevel(hint.Skill);
float targetLevel = hint.Level;
priority = ReducePriority(priority, skillLevel, targetLevel);
}
}
else
{
// If no skill requirement hints are defined, let's rely on the required skill definition.
// This can be inaccurate in some cases (hmg, rifle), but in those cases there should be a skill requirement hint defined for the weapon.
foreach (Skill skill in weapon.RequiredSkills)
{
float skillLevel = character.GetSkillLevel(skill.Identifier);
// Skill multiplier is currently always 1, so it's not really needed, but that could change(?)
float targetLevel = skill.Level * weapon.GetSkillMultiplier();
priority = ReducePriority(priority, skillLevel, targetLevel);
}
}
// Don't allow to reduce more than half, because an assault rifle is still an assault rifle, even in untrained hands.
priority = Math.Max(priority, startPriority / 2);
return priority;
float ReducePriority(float prio, float skillLevel, float targetLevel)
{
float diff = targetLevel - skillLevel;
if (diff > 0)
{
prio -= diff;
}
return prio;
}
}
private float ApproximateStunDamage(ItemComponent weapon, Attack attack)
@@ -632,12 +755,12 @@ namespace Barotrauma
return attack.Stun + afflictionsStun + effectsStun;
}
private bool CanMeleeStunnerStun(ItemComponent weapon)
private static bool CanMeleeStunnerStun(ItemComponent weapon)
{
// If there's an item container that takes a battery,
// assume that it's required for the stun effect
// as we can't check the status effect conditions here.
var mobileBatteryTag = Tags.MobileBattery;
Identifier mobileBatteryTag = Tags.MobileBattery;
var containers = weapon.Item.Components.Where(ic =>
ic is ItemContainer container &&
container.ContainableItemIdentifiers.Contains(mobileBatteryTag));
@@ -651,11 +774,11 @@ namespace Barotrauma
weaponComponent = null;
float bestPriority = 0;
float lethalDmg = -1;
bool isCloseToEnemy = IsEnemyCloserThan(300);
bool prioritizeMelee = IsEnemyCloserThan(50) || EnemyAIController.IsLatchedTo(Enemy, character);
bool prioritizeMelee = IsEnemyClose(50) || EnemyAIController.IsLatchedTo(Enemy, character);
bool isCloseToEnemy = prioritizeMelee || IsEnemyClose(closeDistanceThreshold);
foreach (var weapon in weaponList)
{
float priority = GetWeaponPriority(weapon, prioritizeMelee, isCloseToEnemy, out lethalDmg);
float priority = GetWeaponPriority(weapon, prioritizeMelee, canSeekAmmo: !isCloseToEnemy, out lethalDmg);
if (priority > bestPriority)
{
weaponComponent = weapon;
@@ -678,7 +801,7 @@ namespace Barotrauma
}
isLethalWeapon = lethalDmg > 1;
}
if (allowHoldFire && !hasAimed && holdFireTimer <= 0)
if (AllowHoldFire && !hasAimed && holdFireTimer <= 0)
{
holdFireTimer = arrestHoldFireTime * Rand.Range(0.75f, 1.25f);
}
@@ -699,15 +822,12 @@ namespace Barotrauma
private static Attack GetAttackDefinition(ItemComponent weapon)
{
Attack attack = null;
if (weapon is MeleeWeapon meleeWeapon)
Attack attack = weapon switch
{
attack = meleeWeapon.Attack;
}
else if (weapon is RangedWeapon rangedWeapon)
{
attack = rangedWeapon.FindProjectile(triggerOnUseOnContainers: false)?.Attack;
}
MeleeWeapon meleeWeapon => meleeWeapon.Attack,
RangedWeapon rangedWeapon => rangedWeapon.FindProjectile(triggerOnUseOnContainers: false)?.Attack,
_ => null
};
return attack;
}
@@ -726,7 +846,7 @@ namespace Barotrauma
return weapons;
}
private void GetWeapons(Item item, ICollection<ItemComponent> weaponList)
private static void GetWeapons(Item item, ICollection<ItemComponent> weaponList)
{
if (item == null) { return; }
foreach (var component in item.Components)
@@ -765,14 +885,13 @@ namespace Barotrauma
}
if (!character.HasEquippedItem(Weapon, predicate: CharacterInventory.IsHandSlotType))
{
//clear aim and shoot inputs so the bot doesn't immediately fire the weapon if it was previously e.g. using a scooter
character.ClearInput(InputType.Aim);
character.ClearInput(InputType.Shoot);
ClearInputs();
Weapon.TryInteract(character, forceSelectKey: true);
var slots = Weapon.AllowedSlots.Where(s => CharacterInventory.IsHandSlotType(s));
var slots = Weapon.AllowedSlots.Where(CharacterInventory.IsHandSlotType);
if (character.Inventory.TryPutItem(Weapon, character, slots))
{
SetAimTimer(Rand.Range(0.2f, 0.4f) / AimSpeed);
SetReloadTime(WeaponComponent);
}
else
{
@@ -786,7 +905,7 @@ namespace Barotrauma
}
private float findHullTimer;
private readonly float findHullInterval = 1.0f;
private const float findHullInterval = 1.0f;
private void Retreat(float deltaTime)
{
@@ -796,6 +915,18 @@ namespace Barotrauma
}
RemoveFollowTarget();
RemoveSubObjective(ref seekAmmunitionObjective);
if (retreatTarget != null)
{
if (HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull))
{
// In the same hull with the enemy
if (retreatTarget == character.CurrentHull)
{
// Go elsewhere
retreatTarget = null;
}
}
}
if (retreatObjective != null && retreatObjective.Target != retreatTarget)
{
RemoveSubObjective(ref retreatObjective);
@@ -809,7 +940,7 @@ namespace Barotrauma
SteeringManager.SteeringAvoid(deltaTime, 5, weight: 2);
return;
}
if (retreatTarget == null || (retreatObjective != null && !retreatObjective.CanBeCompleted))
if (retreatTarget == null || retreatObjective is { CanBeCompleted: false })
{
if (findHullTimer > 0)
{
@@ -942,9 +1073,13 @@ namespace Barotrauma
if (!arrestingRegistered && followTargetObjective != null)
{
followTargetObjective.CloseEnough =
WeaponComponent is RangedWeapon ? 1000 :
WeaponComponent is MeleeWeapon mw ? mw.Range :
WeaponComponent is RepairTool rt ? rt.Range : 50;
WeaponComponent switch
{
RangedWeapon => 1000,
MeleeWeapon mw => mw.Range,
RepairTool rt => rt.Range,
_ => 50
};
}
}
@@ -976,9 +1111,8 @@ namespace Barotrauma
foreach (var item in Enemy.Inventory.AllItemsMod)
{
if (character.TeamID == CharacterTeamType.FriendlyNPC && item.StolenDuringRound ||
item.HasTag(Tags.Weapon) ||
item.GetComponent<MeleeWeapon>() != null ||
item.GetComponent<RangedWeapon>() != null)
item.HasTag(Tags.Weapon) || item.HasTag(Tags.Poison) ||
GetWeaponComponent(item) is { CombatPriority: > 0 })
{
item.Drop(character);
character.Inventory.TryPutItem(item, character, CharacterInventory.AnySlot);
@@ -1024,10 +1158,11 @@ namespace Barotrauma
RemoveSubObjective(ref retreatObjective);
RemoveSubObjective(ref seekWeaponObjective);
RemoveFollowTarget();
var itemContainer = Weapon.GetComponent<ItemContainer>();
TryAddSubObjective(ref seekAmmunitionObjective,
constructor: () => new AIObjectiveContainItem(character, ammunitionIdentifiers, Weapon.GetComponent<ItemContainer>(), objectiveManager)
constructor: () => new AIObjectiveContainItem(character, ammunitionIdentifiers, itemContainer, objectiveManager)
{
ItemCount = Weapon.GetComponent<ItemContainer>().Capacity * Weapon.GetComponent<ItemContainer>().MaxStackSize,
ItemCount = itemContainer.MainContainerCapacity * itemContainer.MaxStackSize,
checkInventory = false,
MoveWholeStack = true
},
@@ -1052,9 +1187,9 @@ namespace Barotrauma
// Eject empty ammo
HumanAIController.UnequipEmptyItems(Weapon);
ImmutableHashSet<Identifier> ammunitionIdentifiers = null;
if (WeaponComponent.requiredItems.ContainsKey(RelatedItem.RelationType.Contained))
if (WeaponComponent.RequiredItems.ContainsKey(RelatedItem.RelationType.Contained))
{
foreach (RelatedItem requiredItem in WeaponComponent.requiredItems[RelatedItem.RelationType.Contained])
foreach (RelatedItem requiredItem in WeaponComponent.RequiredItems[RelatedItem.RelationType.Contained])
{
if (Weapon.OwnInventory.AllItems.Any(it => it.Condition > 0 && requiredItem.MatchesItem(it))) { continue; }
ammunitionIdentifiers = requiredItem.Identifiers;
@@ -1075,12 +1210,14 @@ namespace Barotrauma
if (ammunition != null)
{
var container = Weapon.GetComponent<ItemContainer>();
if (!container.Inventory.TryPutItem(ammunition, user: character))
if (container.Inventory.TryPutItem(ammunition, user: character))
{
if (ammunition.ParentInventory == character.Inventory)
{
ammunition.Drop(character);
}
ClearInputs();
SetReloadTime(WeaponComponent);
}
else if (ammunition.ParentInventory == character.Inventory)
{
ammunition.Drop(character);
}
}
}
@@ -1127,7 +1264,7 @@ namespace Barotrauma
}
if (Weapon.RequireAimToUse)
{
character.SetInput(InputType.Aim, false, true);
character.SetInput(InputType.Aim, hit: false, held: true);
}
hasAimed = true;
if (holdFireTimer > 0)
@@ -1194,23 +1331,17 @@ namespace Barotrauma
float aimFactor = MathHelper.PiOver2 * (1 - AimAccuracy);
if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.WorldPosition - Weapon.WorldPosition) < MathHelper.PiOver4 + aimFactor)
{
if (myBodies == null)
{
myBodies = character.AnimController.Limbs.Select(l => l.body.FarseerBody);
}
myBodies ??= character.AnimController.Limbs.Select(l => l.body.FarseerBody);
// Check that we don't hit friendlies. No need to check the walls, because there's a separate check for that at 1096 (which intentionally has a small delay)
var pickedBodies = Submarine.PickBodies(Weapon.SimPosition, Submarine.GetRelativeSimPosition(from: Weapon, to: Enemy), myBodies, Physics.CollisionCharacter);
foreach (var body in pickedBodies)
{
Character target = null;
if (body.UserData is Character c)
Character target = body.UserData switch
{
target = c;
}
else if (body.UserData is Limb limb)
{
target = limb.character;
}
Character c => c,
Limb limb => limb.character,
_ => null
};
if (target != null && target != Enemy && HumanAIController.IsFriendly(target))
{
return;
@@ -1225,26 +1356,48 @@ namespace Barotrauma
{
// Never allow to attack characters with deadly weapons while trying to arrest.
if (Mode == CombatMode.Arrest && isLethalWeapon) { return; }
float reloadTime = 0;
if (WeaponComponent is RangedWeapon rangedWeapon)
{
// If the weapon is just equipped, we can't shoot just yet.
if (rangedWeapon.ReloadTimer <= 0 && !rangedWeapon.HoldTrigger)
{
reloadTime = rangedWeapon.Reload;
}
}
if (WeaponComponent is MeleeWeapon mw)
{
if (!((HumanoidAnimController)character.AnimController).Crouching)
{
reloadTime = mw.Reload;
}
}
character.SetInput(InputType.Shoot, false, true);
character.SetInput(InputType.Shoot, hit: false, held: true);
Weapon.Use(deltaTime, user: character);
SetReloadTime(WeaponComponent);
}
private float GetReloadTime(ItemComponent weaponComponent)
{
float reloadTime = 0;
switch (weaponComponent)
{
case RangedWeapon rangedWeapon:
{
if (rangedWeapon.ReloadTimer <= 0 && !rangedWeapon.HoldTrigger)
{
reloadTime = rangedWeapon.Reload;
}
break;
}
case MeleeWeapon mw:
{
if (character.AnimController is HumanoidAnimController { Crouching: false })
{
reloadTime = mw.Reload;
}
break;
}
}
return reloadTime;
}
private void SetReloadTime(ItemComponent weaponComponent)
{
float reloadTime = GetReloadTime(weaponComponent);
reloadTimer = Math.Max(reloadTime, reloadTime * Rand.Range(1f, 1.25f) / AimSpeed);
}
private void ClearInputs()
{
//clear aim and shoot inputs so the bot doesn't immediately fire the weapon if it was previously e.g. using a scooter
character.ClearInput(InputType.Aim);
character.ClearInput(InputType.Shoot);
}
private bool ShouldUnequipWeapon =>
Weapon != null &&
@@ -0,0 +1,116 @@
using Barotrauma.Items.Components;
using System.Linq;
namespace Barotrauma
{
class AIObjectiveDeconstructItem : AIObjective
{
public override Identifier Identifier { get; set; } = "deconstruct item".ToIdentifier();
public override bool AllowWhileHandcuffed => false;
public override bool AllowInFriendlySubs => true;
public readonly Item Item;
private Deconstructor deconstructor;
private AIObjectiveDecontainItem decontainObjective;
public AIObjectiveDeconstructItem(Item item, Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
Item = item;
}
protected override void Act(float deltaTime)
{
if (subObjectives.Any()) { return; }
if (deconstructor == null)
{
deconstructor = FindDeconstructor();
if (deconstructor == null)
{
Abandon = true;
return;
}
}
TryAddSubObjective(ref decontainObjective,
constructor: () => new AIObjectiveDecontainItem(character, Item, objectiveManager,
sourceContainer: Item.Container?.GetComponent<ItemContainer>(), targetContainer: deconstructor.InputContainer, priorityModifier: PriorityModifier)
{
Equip = true,
RemoveExistingWhenNecessary = true
},
onCompleted: () =>
{
StartDeconstructor();
//make sure the item gets moved to the main sub if the crew leaves while a bot is deconstructing something in the outpost
if (deconstructor.Item.Submarine is { Info.IsOutpost: true })
{
HumanAIController.HandleRelocation(Item);
deconstructor.RelocateOutputToMainSub = true;
}
IsCompleted = true;
RemoveSubObjective(ref decontainObjective);
},
onAbandon: () =>
{
Abandon = true;
});
}
private Deconstructor FindDeconstructor()
{
Deconstructor closestDeconstructor = null;
float bestDistFactor = 0;
foreach (var otherItem in Item.ItemList)
{
var potentialDeconstructor = otherItem.GetComponent<Deconstructor>();
if (potentialDeconstructor?.InputContainer == null) { continue; }
if (!potentialDeconstructor.InputContainer.Inventory.CanBePut(Item)) { continue; }
if (!potentialDeconstructor.Item.HasAccess(character)) { continue; }
float distFactor = GetDistanceFactor(Item.WorldPosition, potentialDeconstructor.Item.WorldPosition, factorAtMaxDistance: 0.2f);
if (distFactor > bestDistFactor)
{
closestDeconstructor = potentialDeconstructor;
bestDistFactor = distFactor;
}
}
return closestDeconstructor;
}
private void StartDeconstructor()
{
deconstructor.SetActive(active: true, user: character, createNetworkEvent: true);
}
protected override bool CheckObjectiveSpecific()
{
if (Item.IgnoreByAI(character))
{
Abandon = true;
}
else if (deconstructor != null && deconstructor.Item.IgnoreByAI(character))
{
Abandon = true;
}
return !Abandon && IsCompleted;
}
public override void Reset()
{
base.Reset();
decontainObjective = null;
}
public void DropTarget()
{
if (Item != null && character.HasItem(Item))
{
Item.Drop(character);
}
}
}
}
@@ -0,0 +1,122 @@
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using System.Collections.Generic;
namespace Barotrauma
{
class AIObjectiveDeconstructItems : AIObjectiveLoop<Item>
{
public override Identifier Identifier { get; set; } = "deconstruct items".ToIdentifier();
//Clear periodically, because we may ending up ignoring items when all deconstructors are full
protected override float IgnoreListClearInterval => 30;
public override bool AllowInFriendlySubs => true;
protected override int MaxTargets => 10;
private bool checkedDeconstructorExists;
public AIObjectiveDeconstructItems(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
}
public override void OnSelected()
{
base.OnSelected();
if (!checkedDeconstructorExists)
{
if (character.Submarine == null ||
Item.ItemList.None(it =>
it.GetComponent<Deconstructor>() != null &&
it.IsInteractable(character) &&
character.Submarine.IsEntityFoundOnThisSub(it, includingConnectedSubs: true, allowDifferentTeam: true, allowDifferentType: true)))
{
character.Speak(TextManager.Get("orderdialogself.deconstructitem.nodeconstructor").Value, delay: 5.0f,
identifier: "nodeconstructor".ToIdentifier(), minDurationBetweenSimilar: 30.0f);
Abandon = true;
}
checkedDeconstructorExists = true;
}
}
public override void Reset()
{
base.Reset();
checkedDeconstructorExists = false;
}
protected override float TargetEvaluation()
{
if (Targets.None()) { return 0; }
if (objectiveManager.IsOrder(this))
{
return objectiveManager.GetOrderPriority(this);
}
return AIObjectiveManager.RunPriority - 0.5f;
}
protected override bool Filter(Item target)
{
// If the target was selected as a valid target, we'll have to accept it so that the objective can be completed.
// The validity changes when a character picks the item up.
if (!IsValidTarget(target, character, checkInventory: true))
{
return Objectives.ContainsKey(target) && AIObjectiveCleanupItems.IsItemInsideValidSubmarine(target, character);
}
if (target.CurrentHull.FireSources.Count > 0) { return false; }
foreach (Character c in Character.CharacterList)
{
if (c == character || !HumanAIController.IsActive(c)) { continue; }
if (c.CurrentHull == target.CurrentHull && !HumanAIController.IsFriendly(c))
{
// Don't deconstruct items in rooms that have enemies inside.
return false;
}
else if (c.TeamID == character.TeamID && c.AIController is HumanAIController humanAi)
{
if (humanAi.ObjectiveManager.CurrentObjective is AIObjectiveDeconstructItem deconstruct && deconstruct.Item == target)
{
return false;
}
}
}
return true;
}
protected override IEnumerable<Item> GetList() => Item.DeconstructItems;
protected override AIObjective ObjectiveConstructor(Item item)
=> new AIObjectiveDeconstructItem(item, character, objectiveManager, priorityModifier: PriorityModifier);
protected override void OnObjectiveCompleted(AIObjective objective, Item target)
=> HumanAIController.RemoveTargets<AIObjectiveDeconstructItems, Item>(character, target);
private static bool IsValidTarget(Item item, Character character, bool checkInventory)
{
if (item == null) { return false; }
if (item.GetRootInventoryOwner() == character) { return true; }
return AIObjectiveCleanupItems.IsValidTarget(
item,
character,
checkInventory,
allowUnloading: true,
requireValidContainer: false,
ignoreItemsMarkedForDeconstruction: false);
}
public override void OnDeselected()
{
base.OnDeselected();
foreach (var subObjective in SubObjectives)
{
if (subObjective is AIObjectiveDeconstructItem deconstructObjective)
{
deconstructObjective.DropTarget();
}
}
}
}
}
@@ -39,6 +39,9 @@ namespace Barotrauma
/// </summary>
public bool DropIfFails { get; set; } = true;
/// <summary>
/// Should existing item(s) be removed from the targetContainer if the targetItem won't fit otherwise?
/// </summary>
public bool RemoveExistingWhenNecessary { get; set; }
public Func<Item, bool> RemoveExistingPredicate { get; set; }
public int? RemoveExistingMax { get; set; }
@@ -45,13 +45,18 @@ namespace Barotrauma
else
{
float characterY = character.CurrentHull?.WorldPosition.Y ?? character.WorldPosition.Y;
float yDist = Math.Abs(characterY - targetHull.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 3 : 0;
float dist = Math.Abs(character.WorldPosition.X - targetHull.WorldPosition.X) + yDist;
float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, dist));
if (targetHull == character.CurrentHull || HumanAIController.VisibleHulls.Contains(targetHull))
float distanceFactor = 1.0f;
if (targetHull != character.CurrentHull &&
!HumanAIController.VisibleHulls.Contains(targetHull))
{
distanceFactor = 1;
distanceFactor =
GetDistanceFactor(
new Vector2(character.WorldPosition.Y, characterY),
targetHull.WorldPosition,
verticalDistanceMultiplier: 3,
maxDistance: 5000,
factorAtMaxDistance: 0.1f);
}
float severity = AIObjectiveExtinguishFires.GetFireSeverity(targetHull);
if (severity > 0.75f && !isOrder &&
@@ -12,7 +12,7 @@ namespace Barotrauma
protected override float TargetUpdateTimeMultiplier => 0.2f;
public bool TargetCharactersInOtherSubs { get; set; }
public bool TargetCharactersInOtherSubs { get; init; }
public AIObjectiveFightIntruders(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier) { }
@@ -33,20 +33,22 @@ namespace Barotrauma
protected override AIObjective ObjectiveConstructor(Character target)
{
AIObjectiveCombat.CombatMode combatMode = ShouldArrest(target, character) ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Offensive;
var combatObjective = new AIObjectiveCombat(character, target, combatMode, objectiveManager, PriorityModifier);
if (character.TeamID == CharacterTeamType.FriendlyNPC && target.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign)
AIObjectiveCombat.CombatMode combatMode = AIObjectiveCombat.CombatMode.Offensive;
if (character.IsOnPlayerTeam && target is { IsEscorted: true })
{
if (campaign.CurrentLocation is { IsFactionHostile: true })
// Try to arrest escorted characters, instead of killing them.
combatMode = AIObjectiveCombat.CombatMode.Arrest;
}
var combatObjective = new AIObjectiveCombat(character, target, combatMode, objectiveManager, PriorityModifier);
if (character.TeamID == CharacterTeamType.FriendlyNPC && target.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode { CurrentLocation.IsFactionHostile: true })
{
combatObjective.holdFireCondition = () =>
{
combatObjective.holdFireCondition = () =>
{
//hold fire while the enemy is in the airlock (except if they've attacked us)
if (character.GetDamageDoneByAttacker(target) > 0.0f) { return false; }
return target.CurrentHull == null || target.CurrentHull.OutpostModuleTags.Any(t => t == "airlock");
};
character.Speak(TextManager.Get("dialogenteroutpostwarning").Value, null, Rand.Range(0.5f, 1.0f), "leaveoutpostwarning".ToIdentifier(), 30.0f);
}
//hold fire while the enemy is in the airlock (except if they've attacked us)
if (character.GetDamageDoneByAttacker(target) > 0.0f) { return false; }
return target.CurrentHull == null || target.CurrentHull.OutpostModuleTags.Any(t => t == "airlock");
};
character.Speak(TextManager.Get("dialogenteroutpostwarning").Value, null, Rand.Range(0.5f, 1.0f), "leaveoutpostwarning".ToIdentifier(), 30.0f);
}
return combatObjective;
}
@@ -77,10 +79,5 @@ namespace Barotrauma
if (EnemyAIController.IsLatchedToSomeoneElse(target, character)) { return false; }
return true;
}
public static bool ShouldArrest(Character target, Character character)
{
return target != null && target.IsEscorted && character.TeamID == CharacterTeamType.Team1;
}
}
}
@@ -33,22 +33,27 @@ namespace Barotrauma
protected override void Act(float deltaTime)
{
TrySetTargetItem(character.Inventory.FindItemByTag(gearTag, true));
TrySetTargetItem(character.Inventory.FindItem(it => it.HasTag(gearTag) && IsSuitablePressureProtection(it, gearTag, character), true));
if (targetItem == null && gearTag == Tags.LightDivingGear)
{
TrySetTargetItem(character.Inventory.FindItemByTag(Tags.HeavyDivingGear, true));
TrySetTargetItem(character.Inventory.FindItem(
it => it.HasTag(Tags.HeavyDivingGear) && IsSuitablePressureProtection(it, Tags.HeavyDivingGear, character), recursive: true));
}
if (targetItem == null ||
!character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head) &&
targetItem.ContainedItems.Any(it => IsSuitableContainedOxygenSource(it)))
{
bool mustFindMorePressureProtection =
!objectiveManager.FailedToFindDivingGearForDepth &&
character.Inventory.FindItem(
it => it.HasTag(Tags.HeavyDivingGear) && !IsSuitablePressureProtection(it, Tags.HeavyDivingGear, character), recursive: true) != null;
TryAddSubObjective(ref getDivingGear, () =>
{
if (targetItem == null && character.IsOnPlayerTeam)
{
character.Speak(TextManager.Get("DialogGetDivingGear").Value, null, 0.0f, "getdivinggear".ToIdentifier(), 30.0f);
}
return new AIObjectiveGetItem(character, gearTag, objectiveManager, equip: true)
var getItemObjective = new AIObjectiveGetItem(character, gearTag, objectiveManager, equip: true)
{
AllowStealing = HumanAIController.NeedsDivingGear(character.CurrentHull, out _),
AllowToFindDivingGear = false,
@@ -56,8 +61,42 @@ namespace Barotrauma
EquipSlotType = InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head,
Wear = true
};
if (gearTag == Tags.HeavyDivingGear)
{
if (mustFindMorePressureProtection)
{
//if we're looking for a suit specifically because the current suit isn't enough,
//let's ignore unsuitable suits altogether...
getItemObjective.ItemFilter = it => IsSuitablePressureProtection(it, gearTag, character);
}
else
{
//...Otherwise it's fine to give a very small priority
//to inadequate suits (a suit not adequate for the depth is better than no suit)
getItemObjective.GetItemPriority = it => IsSuitablePressureProtection(it, gearTag, character) ? 1000.0f : 1.0f;
}
getItemObjective.GetItemPriority = it =>
{
if (IsSuitablePressureProtection(it, gearTag, character))
{
return 1000.0f;
}
else
{
//if we're looking for a suit specifically because the current suit isn't enough,
//let's ignore unsuitable suits altogether. Otherwise it's fine to give a very small priority
//to inadequate suits (a suit not adequate for the depth is better than no suit)
return mustFindMorePressureProtection ? 0.0f : 1.0f;
}
};
}
return getItemObjective;
},
onAbandon: () => Abandon = true,
onAbandon: () =>
{
if (mustFindMorePressureProtection) { objectiveManager.FailedToFindDivingGearForDepth = true; }
Abandon = true;
},
onCompleted: () =>
{
RemoveSubObjective(ref getDivingGear);
@@ -160,6 +199,20 @@ namespace Barotrauma
}
}
public static bool IsSuitablePressureProtection(Item item, Identifier tag, Character character)
{
if (tag == Tags.HeavyDivingGear)
{
float realWorldDepth = Level.Loaded?.GetRealWorldDepth(character.WorldPosition.Y) ?? 0.0f;
if (item.GetComponent<Wearable>() is not { } wearable || wearable.PressureProtection < realWorldDepth + Steering.PressureWarningThreshold)
{
return false;
}
}
return true;
}
private bool IsSuitableContainedOxygenSource(Item item)
{
return
@@ -52,12 +52,26 @@ namespace Barotrauma
{
bool isSuffocatingInDivingSuit = character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false);
static bool IsSuffocatingWithoutDivingGear(Character c) => c.IsLowInOxygen && c.AnimController.HeadInWater && !HumanAIController.HasDivingGear(c, requireOxygenTank: true);
if (isSuffocatingInDivingSuit ||
NeedMoreDivingGear(character.CurrentHull, AIObjectiveFindDivingGear.GetMinOxygen(character)) ||
(!objectiveManager.HasActiveObjective<AIObjectiveFindDivingGear>() && IsSuffocatingWithoutDivingGear(character)))
if (isSuffocatingInDivingSuit || (!objectiveManager.HasActiveObjective<AIObjectiveFindDivingGear>() && IsSuffocatingWithoutDivingGear(character)))
{
Priority = AIObjectiveManager.MaxObjectivePriority;
}
else if (NeedMoreDivingGear(character.CurrentHull, AIObjectiveFindDivingGear.GetMinOxygen(character)))
{
if (objectiveManager.FailedToFindDivingGearForDepth &&
HumanAIController.HasDivingSuit(character, requireSuitablePressureProtection: false))
{
//we have a suit that's not suitable for the pressure,
//but we've failed to find a better one
// shit, not much we can do here, let's just allow the bot to get on with their current objective
Priority = 0;
}
else
{
Priority = AIObjectiveManager.MaxObjectivePriority;
}
}
else if ((objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() || objectiveManager.IsCurrentOrder<AIObjectiveReturn>()) &&
character.Submarine != null && !character.IsOnFriendlyTeam(character.Submarine.TeamID))
{
@@ -259,7 +273,7 @@ namespace Barotrauma
bool inFriendlySub =
character.IsInFriendlySub ||
(character.IsEscorted && character.IsInPlayerSub);
if (cannotFindSafeHull && !inFriendlySub && objectiveManager.Objectives.None(o => o is AIObjectiveReturn))
if (cannotFindSafeHull && !inFriendlySub && character.IsOnPlayerTeam && objectiveManager.Objectives.None(o => o is AIObjectiveReturn))
{
if (OrderPrefab.Prefabs.TryGet("return".ToIdentifier(), out OrderPrefab orderPrefab))
{
@@ -401,10 +415,7 @@ namespace Barotrauma
if (isCharacterInside)
{
hullSafety = HumanAIController.GetHullSafety(potentialHull, potentialHull.GetConnectedHulls(true, 1), character);
float yDist = Math.Abs(character.WorldPosition.Y - potentialHull.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 3 : 0;
float dist = Math.Abs(character.WorldPosition.X - potentialHull.WorldPosition.X) + yDist;
float distanceFactor = MathHelper.Lerp(1, 0.9f, MathUtils.InverseLerp(0, 10000, dist));
float distanceFactor = GetDistanceFactor(potentialHull.WorldPosition, factorAtMaxDistance: 0.9f);
hullSafety *= distanceFactor;
//skip the hull if the safety is already less than the best hull
//(no need to do the expensive pathfinding if we already know we're not going to choose this hull)
@@ -446,16 +457,13 @@ namespace Barotrauma
hullSafety = 100;
hullIsAirlock = true;
}
else if(!bestHullIsAirlock && potentialHull.LeadsOutside(character))
else if (!bestHullIsAirlock && potentialHull.LeadsOutside(character))
{
hullSafety = 100;
}
float characterY = character.CurrentHull?.WorldPosition.Y ?? character.WorldPosition.Y;
float yDist = Math.Abs(characterY - potentialHull.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 3 : 0;
float distance = Math.Abs(character.WorldPosition.X - potentialHull.WorldPosition.X) + yDist;
// Huge preference for closer targets
float distanceFactor = MathHelper.Lerp(1, 0.2f, MathUtils.InverseLerp(0, 10000, distance));
float distanceFactor = GetDistanceFactor(new Vector2(character.WorldPosition.X, characterY), potentialHull.WorldPosition, factorAtMaxDistance: 0.2f);
hullSafety *= distanceFactor;
// If the target is not inside a friendly submarine, considerably reduce the hull safety.
// Intentionally exclude wrecks from this check
@@ -39,8 +39,8 @@ namespace Barotrauma
if (campaign.Map?.CurrentLocation?.Reputation is { } reputation)
{
return MathHelper.Lerp(
campaign.Settings.MaxStolenItemInspectionProbability,
campaign.Settings.MinStolenItemInspectionProbability,
campaign.Settings.PatdownProbabilityMax,
campaign.Settings.PatdownProbabilityMin,
reputation.NormalizedValue);
}
}
@@ -120,7 +120,7 @@ namespace Barotrauma
Abandon = true;
return;
}
if (weldingTool.OwnInventory == null && repairTool.requiredItems.Any(r => r.Key == RelatedItem.RelationType.Contained))
if (weldingTool.OwnInventory == null && repairTool.RequiredItems.Any(r => r.Key == RelatedItem.RelationType.Contained))
{
#if DEBUG
DebugConsole.ThrowError($"{character.Name}: AIObjectiveFixLeak failed - the item \"{weldingTool}\" has no proper inventory");
@@ -159,13 +159,14 @@ namespace Barotrauma
protected override void Act(float deltaTime)
{
if (IdentifiersOrTags != null && !isDoneSeeking)
if (IdentifiersOrTags != null)
{
if (checkInventory)
{
if (CheckInventory())
{
isDoneSeeking = true;
itemCandidates.Clear();
}
}
if (!isDoneSeeking)
@@ -189,7 +190,14 @@ namespace Barotrauma
}
}
FindTargetItem();
if (!objectiveManager.IsCurrentOrder<AIObjectiveGoTo>())
}
if (targetItem == null)
{
if (isDoneSeeking)
{
HandlePotentialItems();
}
if (objectiveManager.CurrentOrder is not AIObjectiveGoTo)
{
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
}
@@ -201,20 +209,28 @@ namespace Barotrauma
Abandon = true;
return;
}
if (targetItem == null || targetItem.Removed)
bool ShouldAbort() => IdentifiersOrTags is null || isDoneSeeking && itemCandidates.None();
if (targetItem is null or { Removed: true })
{
if (ShouldAbort())
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Target null or removed. Aborting.", Color.Red);
DebugConsole.NewMessage($"{character.Name}: Target null or removed. Aborting.", Color.Red);
#endif
Abandon = true;
Abandon = true;
}
return;
}
else if (isDoneSeeking && moveToTarget == null)
if (moveToTarget is null)
{
if (ShouldAbort())
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Move target null. Aborting.", Color.Red);
DebugConsole.NewMessage($"{character.Name}: Move target null. Aborting.", Color.Red);
#endif
Abandon = true;
Abandon = true;
return;
}
return;
}
if (character.IsItemTakenBySomeoneElse(targetItem))
@@ -399,16 +415,8 @@ namespace Barotrauma
{
StopWatch.Restart();
}
float priority = Math.Clamp(objectiveManager.GetCurrentPriority(), 10, 100);
if (!CheckPathForEachItem)
{
// While following the player, let's ensure that there's a valid path to the target before accepting it.
// Otherwise it will take some time for us to find a valid item when there are multiple items that we can't reach and some that we can.
// This is relatively expensive, so let's do this only when it significantly improves the behavior.
// Only allow one path find call per frame.
CheckPathForEachItem = priority >= AIObjectiveManager.LowestOrderPriority && (objectiveManager.IsCurrentOrder<AIObjectiveFixLeaks>() || objectiveManager.CurrentOrder is AIObjectiveGoTo gotoOrder && gotoOrder.IsFollowOrder);
}
bool checkPath = CheckPathForEachItem;
float priority = objectiveManager.GetCurrentPriority();
bool checkPath = CheckPathForEachItem || priority >= AIObjectiveManager.RunPriority || ItemCount > 1;
// Reset if the character has switched subs.
if (itemList != null && !character.Submarine.IsEntityFoundOnThisSub(itemList.FirstOrDefault(), includingConnectedSubs: true))
{
@@ -434,9 +442,9 @@ namespace Barotrauma
// Ignore items in the inventory when defined not to check it.
if (item.IsOwnedBy(character)) { continue; }
}
if (!AllowStealing)
if (!AllowStealing && character.IsOnPlayerTeam)
{
if (character.TeamID == CharacterTeamType.FriendlyNPC != item.SpawnedInCurrentOutpost) { continue; }
if (item.SpawnedInCurrentOutpost && !item.AllowStealing) { continue; }
}
if (!CheckItem(item)) { continue; }
if (item.Container != null)
@@ -454,11 +462,11 @@ namespace Barotrauma
if (!itemInventory.Container.HasRequiredItems(character, addMessage: false)) { continue; }
}
float itemPriority = item.Prefab.BotPriority;
if (itemPriority <= 0) { continue; }
if (GetItemPriority != null)
{
itemPriority *= GetItemPriority(item);
}
if (itemPriority <= 0) { continue; }
Entity rootInventoryOwner = item.GetRootInventoryOwner();
if (rootInventoryOwner is Item ownerItem)
{
@@ -474,11 +482,13 @@ namespace Barotrauma
}
}
Vector2 itemPos = (rootInventoryOwner ?? item).WorldPosition;
float yDist = Math.Abs(character.WorldPosition.Y - itemPos.Y);
yDist = yDist > 100 ? yDist * 5 : 0;
float dist = Math.Abs(character.WorldPosition.X - itemPos.X) + yDist;
float minDistFactor = EvaluateCombatPriority ? 0.1f : 0;
float distanceFactor = MathHelper.Lerp(1, minDistFactor, MathUtils.InverseLerp(100, 10000, dist));
float distanceFactor =
GetDistanceFactor(
itemPos,
verticalDistanceMultiplier: 5,
maxDistance: 10000,
factorAtMinDistance: 1.0f,
factorAtMaxDistance: EvaluateCombatPriority ? 0.1f : 0);
itemPriority *= distanceFactor;
if (EvaluateCombatPriority)
{
@@ -510,7 +520,7 @@ namespace Barotrauma
}
else
{
combatFactor = Math.Min(item.Components.Sum(ic => AIObjectiveCombat.GetLethalDamage(ic)) / 1000, 0.1f);
combatFactor = Math.Min(item.Components.Sum(AIObjectiveCombat.GetLethalDamage) / 1000, 0.1f);
}
itemPriority *= combatFactor;
}
@@ -518,10 +528,6 @@ namespace Barotrauma
{
itemPriority *= item.Condition / item.MaxCondition;
}
if (checkPath)
{
itemCandidates.Add((item, itemPriority));
}
// Ignore if the item has a lower priority than the currently selected one
if (itemPriority < currItemPriority) { continue; }
if (EvaluateCombatPriority && itemPriority <= 0)
@@ -529,23 +535,27 @@ namespace Barotrauma
// Not good enough
continue;
}
currItemPriority = itemPriority;
targetItem = item;
moveToTarget = rootInventoryOwner ?? item;
if (checkPath)
{
itemCandidates.Add((item, itemPriority));
}
else
{
currItemPriority = itemPriority;
targetItem = item;
moveToTarget = rootInventoryOwner ?? item;
}
}
if (currentSearchIndex >= itemList.Count - 1)
{
isDoneSeeking = true;
}
if (checkedItems > 0)
{
if (isDoneSeeking && itemCandidates.Any())
if (itemCandidates.Any())
{
itemCandidates.Sort((x, y) => y.priority.CompareTo(x.priority));
}
if (HumanAIController.DebugAI && targetItem != null && StopWatch.ElapsedMilliseconds > 2)
{
var msg = $"Went through {checkedItems} of total {itemList.Count} items. Found item {targetItem.Name} in {StopWatch.ElapsedMilliseconds} ms. Completed: {isDoneSeeking}";
if (HumanAIController.DebugAI && StopWatch.ElapsedMilliseconds > 2)
{
string msg = $"Went through {checkedItems} of total {itemList.Count} items. Found item {targetItem?.Name ?? "NULL"} in {StopWatch.ElapsedMilliseconds} ms. Completed: {isDoneSeeking}";
if (StopWatch.ElapsedMilliseconds > 5)
{
DebugConsole.ThrowError(msg);
@@ -557,60 +567,66 @@ namespace Barotrauma
}
}
}
if (isDoneSeeking)
}
private void HandlePotentialItems()
{
Debug.Assert(isDoneSeeking);
if (itemCandidates.Any())
{
if (PathSteering == null)
{
itemCandidates.Clear();
Abandon = true;
return;
}
if (itemCandidates.Any())
if (itemCandidates.FirstOrDefault() is var itemCandidate)
{
if (itemCandidates.FirstOrDefault() is { } itemCandidate)
var path = PathSteering.PathFinder.FindPath(character.SimPosition, character.GetRelativeSimPosition(itemCandidate.item), character.Submarine, errorMsgStr: $"AIObjectiveGetItem {character.DisplayName}", nodeFilter: node => node.Waypoint.CurrentHull != null);
if (path.Unreachable)
{
var path = PathSteering.PathFinder.FindPath(character.SimPosition, character.GetRelativeSimPosition(itemCandidate.item), character.Submarine, errorMsgStr: $"AIObjectiveGetItem {character.DisplayName}", nodeFilter: node => node.Waypoint.CurrentHull != null);
if (path.Unreachable)
{
// Remove the invalid candidates and continue on the next frame.
itemCandidates.Remove(itemCandidate);
}
else
{
// The path was valid -> we are done.
itemCandidates.Clear();
}
}
}
if (targetItem == null && itemCandidates.None())
{
if (spawnItemIfNotFound)
{
ItemPrefab prefab = FindItemToSpawn();
if (prefab == null)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot find an item with the following identifier(s) or tag(s): {string.Join(", ", IdentifiersOrTags)}, tried to spawn the item but no matching item prefabs were found.", Color.Yellow);
#endif
Abandon = true;
}
else
{
Entity.Spawner.AddItemToSpawnQueue(prefab, character.Inventory, onSpawned: (Item spawnedItem) =>
{
targetItem = spawnedItem;
if (character.TeamID == CharacterTeamType.FriendlyNPC && (character.Submarine?.Info.IsOutpost ?? false))
{
spawnedItem.SpawnedInCurrentOutpost = true;
}
});
}
// Remove the invalid candidates and continue on the next frame.
itemCandidates.Remove(itemCandidate);
}
else
{
// The path was valid -> we are done.
itemCandidates.Clear();
targetItem = itemCandidate.item;
moveToTarget = targetItem.GetRootInventoryOwner() ?? targetItem;
}
}
}
if (targetItem == null)
{
if (spawnItemIfNotFound)
{
ItemPrefab prefab = FindItemToSpawn();
if (prefab == null)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot find an item with the following identifier(s) or tag(s): {string.Join(", ", IdentifiersOrTags)}", Color.Yellow);
DebugConsole.NewMessage($"{character.Name}: Cannot find an item with the following identifier(s) or tag(s): {string.Join(", ", IdentifiersOrTags)}, tried to spawn the item but no matching item prefabs were found.", Color.Yellow);
#endif
Abandon = true;
}
else
{
Entity.Spawner.AddItemToSpawnQueue(prefab, character.Inventory, onSpawned: (Item spawnedItem) =>
{
targetItem = spawnedItem;
if (character.TeamID == CharacterTeamType.FriendlyNPC && (character.Submarine?.Info.IsOutpost ?? false))
{
spawnedItem.SpawnedInCurrentOutpost = true;
}
});
}
}
else
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot find an item with the following identifier(s) or tag(s): {string.Join(", ", IdentifiersOrTags)}", Color.Yellow);
#endif
Abandon = true;
}
}
}
@@ -3,6 +3,7 @@ using Barotrauma.Extensions;
using System.Linq;
using System.Collections.Generic;
using System.Collections.Immutable;
using System;
namespace Barotrauma
{
@@ -24,6 +25,12 @@ namespace Barotrauma
public bool CheckPathForEachItem { get; set; }
public bool RequireNonEmpty { get; set; }
public bool RequireAllItems { get; set; }
public bool RequireDivingSuitAdequate { get; set; }
/// <summary>
/// T1 = item to check, T2 = tag we're trying to find a suitable item for
/// </summary>
public Func<Item, Identifier, bool>? ItemFilter;
private readonly ImmutableArray<Identifier> gearTags;
private readonly ImmutableHashSet<Identifier> ignoredTags;
@@ -48,7 +55,8 @@ namespace Barotrauma
int count = gearTags.Count(t => t == tag);
AIObjectiveGetItem? getItem = null;
TryAddSubObjective(ref getItem, () =>
new AIObjectiveGetItem(character, tag, objectiveManager, Equip, CheckInventory && count <= 1)
{
var getItem = new AIObjectiveGetItem(character, tag, objectiveManager, Equip, CheckInventory && count <= 1)
{
AllowVariants = AllowVariants,
Wear = Wear,
@@ -58,29 +66,36 @@ namespace Barotrauma
CheckPathForEachItem = CheckPathForEachItem,
RequireNonEmpty = RequireNonEmpty,
ItemCount = count,
SpeakIfFails = RequireAllItems
},
onCompleted: () =>
SpeakIfFails = RequireAllItems,
};
if (ItemFilter != null)
{
var item = getItem?.TargetItem;
if (item?.IsOwnedBy(character) != null)
{
achievedItems.Add(item);
}
},
onAbandon: () =>
getItem.ItemFilter = (Item it) => ItemFilter(it, tag);
}
return getItem;
},
onCompleted: () =>
{
var item = getItem?.TargetItem;
if (item?.IsOwnedBy(character) != null)
{
var item = getItem?.TargetItem;
if (item != null)
{
achievedItems.Remove(item);
}
RemoveSubObjective(ref getItem);
if (RequireAllItems)
{
Abandon = true;
}
});
achievedItems.Add(item);
}
},
onAbandon: () =>
{
var item = getItem?.TargetItem;
if (item != null)
{
achievedItems.Remove(item);
}
RemoveSubObjective(ref getItem);
if (RequireAllItems)
{
Abandon = true;
}
});
}
subObjectivesCreated = true;
}
@@ -85,6 +85,8 @@ namespace Barotrauma
public bool IgnoreIfTargetDead { get; set; }
public bool AllowGoingOutside { get; set; }
public bool FaceTargetOnCompleted { get; set; } = true;
public bool AlwaysUseEuclideanDistance { get; set; } = true;
/// <summary>
@@ -324,7 +326,7 @@ namespace Barotrauma
float minOxygen = AIObjectiveFindDivingGear.GetMinOxygen(character);
if (tryToGetDivingSuit)
{
needsEquipment = !HumanAIController.HasDivingSuit(character, minOxygen);
needsEquipment = !HumanAIController.HasDivingSuit(character, minOxygen, requireSuitablePressureProtection: !objectiveManager.FailedToFindDivingGearForDepth);
}
else if (tryToGetDivingGear)
{
@@ -346,26 +348,26 @@ namespace Barotrauma
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: tryToGetDivingSuit, objectiveManager),
onAbandon: () =>
{
cantFindDivingGear = true;
if (needsDivingSuit)
{
// Shouldn't try to reach the target without a suit, because it's lethal.
Abandon = true;
}
else
{
// Try again without requiring the diving suit
RemoveSubObjective(ref findDivingGear);
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager),
onAbandon: () =>
{
Abandon = character.CurrentHull != null && (objectiveManager.CurrentOrder != this || Target.Submarine == null);
RemoveSubObjective(ref findDivingGear);
},
onCompleted: () =>
{
RemoveSubObjective(ref findDivingGear);
});
cantFindDivingGear = true;
if (needsDivingSuit)
{
// Shouldn't try to reach the target without a suit, because it's lethal.
Abandon = true;
}
else
{
// Try again without requiring the diving suit
RemoveSubObjective(ref findDivingGear);
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager),
onAbandon: () =>
{
Abandon = character.CurrentHull != null && (objectiveManager.CurrentOrder != this || Target.Submarine == null);
RemoveSubObjective(ref findDivingGear);
},
onCompleted: () =>
{
RemoveSubObjective(ref findDivingGear);
});
}
},
onCompleted: () => RemoveSubObjective(ref findDivingGear));
@@ -450,10 +452,8 @@ namespace Barotrauma
{
useScooter = false;
checkScooterTimer = checkScooterTime * Rand.Range(0.75f, 1.25f);
Identifier scooterTag = "scooter".ToIdentifier();
Identifier batteryTag = "mobilebattery".ToIdentifier();
Item scooter = null;
bool shouldUseScooter = Mimic && targetCharacter != null && targetCharacter.HasEquippedItem(scooterTag, allowBroken: false);
bool shouldUseScooter = Mimic && targetCharacter != null && targetCharacter.HasEquippedItem(Tags.Scooter, allowBroken: false);
if (!shouldUseScooter)
{
float threshold = 500;
@@ -467,7 +467,7 @@ namespace Barotrauma
shouldUseScooter = Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition) > threshold * threshold;
}
}
if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> equippedScooters, recursive: false, requireEquipped: true))
if (HumanAIController.HasItem(character, Tags.Scooter, out IEnumerable<Item> equippedScooters, recursive: false, requireEquipped: true))
{
// Currently equipped scooter
scooter = equippedScooters.FirstOrDefault();
@@ -477,23 +477,23 @@ namespace Barotrauma
var leftHandItem = character.GetEquippedItem(slotType: InvSlotType.LeftHand);
var rightHandItem = character.GetEquippedItem(slotType: InvSlotType.RightHand);
bool handsFull =
(leftHandItem != null && !character.Inventory.IsAnySlotAvailable(leftHandItem)) ||
(rightHandItem != null && !character.Inventory.IsAnySlotAvailable(rightHandItem));
(leftHandItem != null && !character.Inventory.IsAnySlotAvailable(leftHandItem) && !character.Inventory.TryPutItem(leftHandItem, character, InvSlotType.Bag.ToEnumerable())) ||
(rightHandItem != null && !character.Inventory.IsAnySlotAvailable(rightHandItem) && !character.Inventory.TryPutItem(rightHandItem, character, InvSlotType.Bag.ToEnumerable()));
if (!handsFull)
{
bool hasBattery = false;
if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> nonEquippedScooters, containedTag: batteryTag, conditionPercentage: 1, requireEquipped: false))
if (HumanAIController.HasItem(character, Tags.Scooter, out IEnumerable<Item> nonEquippedScooters, containedTag: Tags.MobileBattery, conditionPercentage: 1, requireEquipped: false))
{
// Non-equipped scooter with a battery
scooter = nonEquippedScooters.FirstOrDefault();
hasBattery = true;
}
else if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> _nonEquippedScooters, requireEquipped: false))
else if (HumanAIController.HasItem(character, Tags.Scooter, out IEnumerable<Item> _nonEquippedScooters, requireEquipped: false))
{
// Non-equipped scooter without a battery
scooter = _nonEquippedScooters.FirstOrDefault();
// Non-recursive so that the bots won't take batteries from other items. Also means that they can't find batteries inside containers. Not sure how to solve this.
hasBattery = HumanAIController.HasItem(character, batteryTag, out _, requireEquipped: false, conditionPercentage: 1, recursive: false);
hasBattery = HumanAIController.HasItem(character, Tags.MobileBattery, out _, requireEquipped: false, conditionPercentage: 1, recursive: false);
}
if (scooter != null && hasBattery)
{
@@ -511,7 +511,7 @@ namespace Barotrauma
if (scooter.ContainedItems.None(i => i.Condition > 0))
{
// Try to switch batteries
if (HumanAIController.HasItem(character, batteryTag, out IEnumerable<Item> batteries, conditionPercentage: 1, recursive: false))
if (HumanAIController.HasItem(character, Tags.MobileBattery, out IEnumerable<Item> batteries, conditionPercentage: 1, recursive: false))
{
scooter.ContainedItems.ForEachMod(emptyBattery => character.Inventory.TryPutItem(emptyBattery, character, CharacterInventory.AnySlot));
if (!scooter.Combine(batteries.OrderByDescending(b => b.Condition).First(), character))
@@ -811,16 +811,15 @@ namespace Barotrauma
private void StopMovement()
{
SteeringManager?.Reset();
if (Target != null)
if (FaceTargetOnCompleted && Target is Entity { Removed: false })
{
character.AnimController.TargetDir = Target.WorldPosition.X > character.WorldPosition.X ? Direction.Right : Direction.Left;
HumanAIController.FaceTarget(Target);
}
}
protected override void OnCompleted()
{
StopMovement();
HumanAIController.FaceTarget(Target);
if (Target is WayPoint { Ladders: null })
{
// Release ladders when ordered to wait at a spawnpoint.
@@ -454,14 +454,16 @@ namespace Barotrauma
{
targetHulls.Add(hull);
float weight = hull.RectWidth;
// Prefer rooms that are closer. Avoid rooms that are not in the same level.
// If the behavior is active, prefer rooms that are not close.
float yDist = Math.Abs(character.WorldPosition.Y - hull.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 5 : 0;
float dist = Math.Abs(character.WorldPosition.X - hull.WorldPosition.X) + yDist;
float distanceFactor = behavior == BehaviorType.Patrol ? MathHelper.Lerp(1, 0, MathUtils.InverseLerp(2500, 0, dist)) : MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, 2500, dist));
float distanceFactor = GetDistanceFactor(hull.WorldPosition, verticalDistanceMultiplier: 5, maxDistance: 2500,
factorAtMinDistance: 1, factorAtMaxDistance: 0);
if (behavior == BehaviorType.Patrol)
{
//invert when patrolling (= prefer travelling to far-away hulls)
distanceFactor = 1.0f - distanceFactor;
}
float waterFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, 100, hull.WaterPercentage * 2));
weight *= distanceFactor * waterFactor;
System.Diagnostics.Debug.Assert(weight >= 0);
hullWeights.Add(weight);
}
}
@@ -193,7 +193,10 @@ namespace Barotrauma
if (yDist > 100) { dist += yDist * 5; }
dist += Math.Abs(character.WorldPosition.X - targetPos.X);
}
float distanceFactor = dist > 0.0f ? MathHelper.Lerp(0.9f, 0, MathUtils.InverseLerp(0, 5000, dist)) : 0.9f;
float distanceFactor =
GetDistanceFactor(targetItem.WorldPosition, verticalDistanceMultiplier: 5, maxDistance: 5000, factorAtMinDistance: 0.9f, factorAtMaxDistance: 0);
bool hasContainable = character.HasItem(targetItem);
float devotion = (CumulatedDevotion + (hasContainable ? 100 - MaxDevotion : 0)) / 100;
float max = AIObjectiveManager.LowestOrderPriority - (hasContainable ? 1 : 2);
@@ -67,6 +67,10 @@ namespace Barotrauma
}
private AIObjective currentOrder;
public AIObjective ForcedOrder { get; private set; }
/// <summary>
/// Includes orders.
/// </summary>
public AIObjective CurrentObjective { get; private set; }
public AIObjectiveManager(Character character)
@@ -113,6 +117,8 @@ namespace Barotrauma
public Dictionary<AIObjective, CoroutineHandle> DelayedObjectives { get; private set; } = new Dictionary<AIObjective, CoroutineHandle>();
public bool FailedAutonomousObjectives { get; private set; }
public bool FailedToFindDivingGearForDepth;
private void ClearIgnored()
{
if (character.AIController is HumanAIController humanAi)
@@ -229,8 +235,11 @@ namespace Barotrauma
if (previousObjective == CurrentObjective) { return CurrentObjective; }
previousObjective?.OnDeselected();
CurrentObjective?.OnSelected();
GetObjective<AIObjectiveIdle>().CalculatePriority(Math.Max(CurrentObjective.Priority - 10, 0));
if (CurrentObjective != null)
{
CurrentObjective.OnSelected();
GetObjective<AIObjectiveIdle>().CalculatePriority(Math.Max(CurrentObjective.Priority - 10, 0));
}
if (GameMain.NetworkMember is { IsServer: true })
{
GameMain.NetworkMember.CreateEntityEvent(character,
@@ -239,9 +248,14 @@ namespace Barotrauma
return CurrentObjective;
}
/// <summary>
/// Returns the highest priority of the current objective and its subobjectives.
/// </summary>
public float GetCurrentPriority()
{
return CurrentObjective == null ? 0.0f : CurrentObjective.Priority;
if (CurrentObjective == null) { return 0; }
float subObjectivePriority = CurrentObjective.SubObjectives.Any() ? CurrentObjective.SubObjectives.Max(so => so.Priority) : 0;
return Math.Max(CurrentObjective.Priority, subObjectivePriority);
}
public void UpdateObjectives(float deltaTime)
@@ -250,7 +264,7 @@ namespace Barotrauma
if (CurrentOrders.Any())
{
foreach(var order in CurrentOrders)
foreach (var order in CurrentOrders)
{
var orderObjective = order.Objective;
UpdateOrderObjective(orderObjective);
@@ -405,6 +419,9 @@ namespace Barotrauma
}
}
//reset this here so the bots can retry finding a better suit if it's needed for the new order
FailedToFindDivingGearForDepth = false;
var newCurrentObjective = CreateObjective(order);
if (newCurrentObjective != null)
{
@@ -601,6 +618,9 @@ namespace Barotrauma
case "loaditems":
newObjective = new AIObjectiveLoadItems(character, this, order.Option, order.GetTargetItems(order.Option), order.TargetEntity as Item, priorityModifier);
break;
case "deconstructitems":
newObjective = new AIObjectiveDeconstructItems(character, this, priorityModifier);
break;
default:
if (order.TargetItemComponent == null) { return null; }
if (!order.TargetItemComponent.Item.IsInteractable(character)) { return null; }
@@ -622,6 +642,11 @@ namespace Barotrauma
return newObjective;
}
/// <summary>
/// Sets the order as dismissed, and enables the option to reissue the order on the crew list.
/// Note that this is not the same thing as just removing the order entirely!
/// </summary>
/// <param name="order"></param>
private void DismissSelf(Order order)
{
var currentOrder = CurrentOrders.FirstOrDefault(oi => oi.MatchesOrder(order.Identifier, order.Option));
@@ -660,13 +685,27 @@ namespace Barotrauma
return true;
}
/// <summary>
/// Only checks the current order. Deprecated, use pattern matching instead.
/// </summary>
public bool IsCurrentOrder<T>() where T : AIObjective => CurrentOrder is T;
/// <summary>
/// Checks the current objective (which can be an order too). Deprecated, use pattern matching instead.
/// </summary>
public bool IsCurrentObjective<T>() where T : AIObjective => CurrentObjective is T;
public bool IsActiveObjective<T>() where T : AIObjective => GetActiveObjective() is T;
public AIObjective GetActiveObjective() => CurrentObjective?.GetActiveObjective();
/// <summary>
/// Return the first order whose objective is of the given type. Can return null.
/// </summary>
public T GetOrder<T>() where T : AIObjective => CurrentOrders.FirstOrDefault(o => o.Objective is T)?.Objective as T;
/// <summary>
/// Return the first order with the specified objective. Can return null.
/// </summary>
public Order GetOrder(AIObjective objective) => CurrentOrders.FirstOrDefault(o => o.Objective == objective);
public T GetLastActiveObjective<T>() where T : AIObjective
=> CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).LastOrDefault(so => so is T) as T;
@@ -674,12 +713,12 @@ namespace Barotrauma
=> CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).FirstOrDefault(so => so is T) as T;
/// <summary>
/// Returns all active objectives of the specific type. Creates a new collection -> don't use too frequently.
/// Returns all active objectives of the specific type.
/// </summary>
public IEnumerable<T> GetActiveObjectives<T>() where T : AIObjective
{
if (CurrentObjective == null) { return Enumerable.Empty<T>(); }
return CurrentObjective.GetSubObjectivesRecursive(includingSelf: true).Where(so => so is T).Select(so => so as T);
return CurrentObjective.GetSubObjectivesRecursive(includingSelf: true).OfType<T>();
}
public bool HasActiveObjective<T>() where T : AIObjective => CurrentObjective is T || CurrentObjective != null && CurrentObjective.GetSubObjectivesRecursive().Any(so => so is T);
@@ -211,6 +211,10 @@ namespace Barotrauma
return;
}
}
//the character shouldn't be grabbing anyone if it's trying to operate an item
character.SelectedCharacter = null;
if (target.CanBeSelected)
{
if (!character.IsClimbing && character.CanInteractWith(target.Item, out _, checkLinked: false))
@@ -87,13 +87,29 @@ namespace Barotrauma
AIObjectiveGetItems CreateObjectives(IEnumerable<Identifier> itemTags, bool requireAll)
{
AIObjectiveGetItems objectiveReference = null;
if (!TryAddSubObjective(ref objectiveReference, () => new AIObjectiveGetItems(character, objectiveManager, itemTags)
if (!TryAddSubObjective(ref objectiveReference, () =>
{
CheckInventory = CheckInventory,
Equip = Equip,
EvaluateCombatPriority = EvaluateCombatPriority,
RequireNonEmpty = RequireNonEmpty,
RequireAllItems = requireAll
var getItems = new AIObjectiveGetItems(character, objectiveManager, itemTags)
{
CheckInventory = CheckInventory,
Equip = Equip,
EvaluateCombatPriority = EvaluateCombatPriority,
RequireNonEmpty = RequireNonEmpty,
RequireAllItems = requireAll
};
if (itemTags.Contains(Tags.HeavyDivingGear))
{
getItems.ItemFilter = (Item it, Identifier tag) =>
{
if (tag == Tags.HeavyDivingGear)
{
return AIObjectiveFindDivingGear.IsSuitablePressureProtection(it, tag, character);
}
return true;
};
}
return getItems;
},
onCompleted: () =>
{
@@ -64,10 +64,7 @@ namespace Barotrauma
float distanceFactor = 1;
if (!isPriority && Item.CurrentHull != character.CurrentHull)
{
float yDist = Math.Abs(character.WorldPosition.Y - Item.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 5 : 0;
float dist = Math.Abs(character.WorldPosition.X - Item.WorldPosition.X) + yDist;
distanceFactor = MathHelper.Lerp(1, 0.25f, MathUtils.InverseLerp(0, 4000, dist));
distanceFactor = GetDistanceFactor(Item.WorldPosition, factorAtMaxDistance: 0.25f, verticalDistanceMultiplier: 5, maxDistance: 4000);
}
float requiredSuccessFactor = objectiveManager.HasOrder<AIObjectiveRepairItems>() ? 0 : AIObjectiveRepairItems.RequiredSuccessFactor;
float severity = isPriority ? 1 : AIObjectiveRepairItems.GetTargetPriority(Item, character, requiredSuccessFactor) / 100;
@@ -113,7 +110,7 @@ namespace Barotrauma
if (!repairable.HasRequiredItems(character, false))
{
//make sure we have all the items required to fix the target item
foreach (var kvp in repairable.requiredItems)
foreach (var kvp in repairable.RequiredItems)
{
foreach (RelatedItem requiredItem in kvp.Value)
{
@@ -140,7 +137,7 @@ namespace Barotrauma
}
if (repairTool != null)
{
if (repairTool.requiredItems.TryGetValue(RelatedItem.RelationType.Contained, out var requiredItems))
if (repairTool.RequiredItems.TryGetValue(RelatedItem.RelationType.Contained, out var requiredItems))
{
if (repairTool.Item.OwnInventory == null)
{
@@ -282,7 +279,7 @@ namespace Barotrauma
{
foreach (Repairable repairable in Item.Repairables)
{
foreach (var kvp in repairable.requiredItems)
foreach (var kvp in repairable.RequiredItems)
{
foreach (RelatedItem requiredItem in kvp.Value)
{
@@ -74,7 +74,7 @@ namespace Barotrauma
}
if (!RelevantSkill.IsEmpty)
{
if (item.Repairables.None(r => r.requiredSkills.Any(s => s.Identifier == RelevantSkill))) { return false; }
if (item.Repairables.None(r => r.RequiredSkills.Any(s => s.Identifier == RelevantSkill))) { return false; }
}
return !HumanAIController.IsItemRepairedByAnother(item, out _);
}
@@ -278,52 +278,66 @@ namespace Barotrauma
float cprSuitability = Target.Oxygen < 0.0f ? -Target.Oxygen * 100.0f : 0.0f;
//find which treatments are the most suitable to treat the character's current condition
Target.CharacterHealth.GetSuitableTreatments(currentTreatmentSuitabilities, user: character, normalize: false, predictFutureDuration: 10.0f);
//check if we already have a suitable treatment for any of the afflictions
float bestSuitability = 0.0f;
Item bestItem = null;
Affliction afflictionToTreat = null;
foreach (Affliction affliction in GetSortedAfflictions(Target))
{
if (affliction == null) { throw new Exception("Affliction was null"); }
if (affliction.Prefab == null) { throw new Exception("Affliction prefab was null"); }
float bestSuitability = 0.0f;
Item bestItem = null;
foreach (KeyValuePair<Identifier, float> treatmentSuitability in affliction.Prefab.TreatmentSuitabilities)
//find which treatments are the most suitable to treat the character's current condition
Target.CharacterHealth.GetSuitableTreatments(
currentTreatmentSuitabilities,
limb: Target.CharacterHealth.GetAfflictionLimb(affliction),
user: character,
predictFutureDuration: 10.0f);
foreach (KeyValuePair<Identifier, float> treatmentSuitability in currentTreatmentSuitabilities)
{
if (currentTreatmentSuitabilities.ContainsKey(treatmentSuitability.Key) &&
currentTreatmentSuitabilities[treatmentSuitability.Key] > bestSuitability)
float thisSuitability = currentTreatmentSuitabilities[treatmentSuitability.Key];
if (thisSuitability <= 0) { continue; }
Item matchingItem = FindMedicalItem(character.Inventory, treatmentSuitability.Key);
//allow taking items from the target's inventory too if the target is unconscious
if (matchingItem == null && Target.IsIncapacitated)
{
Item matchingItem = character.Inventory.FindItemByIdentifier(treatmentSuitability.Key, true);
//allow taking items from the target's inventory too if the target is unconscious
if (matchingItem == null && Target.IsIncapacitated)
{
matchingItem ??= Target.Inventory?.FindItemByIdentifier(treatmentSuitability.Key, true);
}
if (matchingItem != null)
{
bestItem = matchingItem;
bestSuitability = currentTreatmentSuitabilities[treatmentSuitability.Key];
}
matchingItem = FindMedicalItem(Target.Inventory, treatmentSuitability.Key);
}
}
if (bestItem != null)
{
if (Target != character) { character.SelectCharacter(Target); }
ApplyTreatment(affliction, bestItem);
//wait a bit longer after applying a treatment to wait for potential side-effects to manifest
treatmentTimer = TreatmentDelay * 4;
return;
if (matchingItem == null) { continue; }
//also check how suitable the treatment is for the specific affliction we're now checking
//we don't want to e.g. give fentanyl for oxygen low just because the character has burns on other limbs
//that would also be healed by it!
float suitabilityForThisAffliction = affliction.Prefab.GetTreatmentSuitability(matchingItem);
float totalSuitability = thisSuitability * suitabilityForThisAffliction;
if (matchingItem != null && totalSuitability > bestSuitability)
{
bestItem = matchingItem;
afflictionToTreat = affliction;
bestSuitability = totalSuitability;
}
}
}
if (bestItem != null && bestSuitability > cprSuitability)
{
if (Target != character) { character.SelectCharacter(Target); }
ApplyTreatment(afflictionToTreat, bestItem);
//wait a bit longer after applying a treatment to wait for potential side-effects to manifest
treatmentTimer = TreatmentDelay * 4;
return;
}
// Find treatments outside of own inventory only if inside the own sub.
if (character.Submarine != null && character.Submarine.TeamID == character.TeamID)
{
//get "overall" suitability for no specific limb at this point
Target.CharacterHealth.GetSuitableTreatments(
currentTreatmentSuitabilities, user: character, predictFutureDuration: 10.0f);
//didn't have any suitable treatments available, try to find some medical items
if (currentTreatmentSuitabilities.Any(s => s.Value > cprSuitability))
{
itemNameList.Clear();
suitableItemIdentifiers.Clear();
foreach (KeyValuePair<Identifier, float> treatmentSuitability in currentTreatmentSuitabilities)
foreach (KeyValuePair<Identifier, float> treatmentSuitability in currentTreatmentSuitabilities.OrderByDescending(s => s.Value))
{
if (treatmentSuitability.Value <= cprSuitability) { continue; }
if (ItemPrefab.Prefabs.TryGet(treatmentSuitability.Key, out ItemPrefab itemPrefab))
@@ -420,6 +434,28 @@ namespace Barotrauma
}
}
public static Item FindMedicalItem(Inventory inventory, Identifier itemIdentifier)
{
return FindMedicalItem(inventory, it => it.Prefab.Identifier == itemIdentifier);
}
public static Item FindMedicalItem(Inventory inventory, Func<Item, bool> predicate)
{
if (inventory == null) { return null; }
//prefer items not in a container
Item match = inventory.FindItem(predicate, recursive: false);
if (match != null) { return match; }
//start from the inventories with most slots
//= prefer taking items from things like toolbelts or doctor's uniforms, as opposed to e.g. autoinjectors which tend to have one or two slots
foreach (var potentialContainer in inventory.AllItems.OrderByDescending(it => it.OwnInventory?.Capacity ?? -1))
{
match = potentialContainer.OwnInventory?.FindItem(predicate, recursive: true);
if (match != null) { return match; }
}
return null;
}
private void SpeakCannotTreat()
{
LocalizedString msg = character == Target ?
@@ -122,6 +122,11 @@ namespace Barotrauma
public bool HasOptions => Options.Length > 1;
public readonly bool MustManuallyAssign;
/// <summary>
/// If enabled and this is an Operate order, it will remove Operate orders of the same item from other characters.
/// If this is a Movement order, removes other Movement orders from the character who receives the order.
/// </summary>
public readonly bool AutoDismiss;
/// <summary>
@@ -137,7 +142,9 @@ namespace Barotrauma
}
public OrderTargetType TargetType { get; }
public int? WallSectionIndex { get; }
public bool IsIgnoreOrder => Identifier == "ignorethis" || Identifier == "unignorethis";
public bool IsIgnoreOrder => Identifier == Tags.IgnoreThis || Identifier == Tags.UnignoreThis;
public bool IsDeconstructOrder => Identifier == Tags.DeconstructThis || Identifier == Tags.DontDeconstructThis;
/// <summary>
/// Should the order icon be drawn when the order target is inside a container
@@ -273,7 +280,7 @@ namespace Barotrauma
public bool HasPreferredJob(Character character) => HasSpecifiedJob(character, PreferredJobs);
public string GetChatMessage(string targetCharacterName, string targetRoomName, bool givingOrderToSelf, Identifier orderOption = default, bool isNewOrder = true)
public string GetChatMessage(string targetCharacterName, string targetRoomName, Entity targetEntity, bool givingOrderToSelf, Identifier orderOption = default, bool isNewOrder = true)
{
if (!TargetAllCharacters && !isNewOrder && Identifier != "dismissed")
{
@@ -304,8 +311,27 @@ namespace Barotrauma
}
}
}
LocalizedString targetEntityName = string.Empty;
switch (targetEntity)
{
case Item item:
targetEntityName = item.Name;
break;
case Hull hull:
targetEntityName = hull.DisplayName;
break;
case Structure structure:
targetEntityName = structure.Name;
break;
case Character character:
targetEntityName = character.DisplayName;
break;
}
return TextManager.GetWithVariables(messageTag,
("[name]", targetCharacterName ?? string.Empty, FormatCapitals.No),
("[target]", targetEntityName, FormatCapitals.No),
("[roomname]", targetRoomName ?? string.Empty, FormatCapitals.Yes)).Fallback("").Value;
}
@@ -413,6 +439,8 @@ namespace Barotrauma
public bool TargetItemsMatchItem(Item item, Identifier option = default)
{
if (item == null) { return false; }
if (Identifier == Tags.DeconstructThis && item.AllowDeconstruct && !Item.DeconstructItems.Contains(item)) { return true; }
if (Identifier == Tags.DontDeconstructThis && Item.DeconstructItems.Contains(item)) { return true; }
ImmutableArray<Identifier> targetItems = GetTargetItems(option);
return TargetItemsMatchItem(targetItems, item);
}
@@ -528,6 +556,7 @@ namespace Barotrauma
public OrderCategory? Category => Prefab.Category;
public bool MustManuallyAssign => Prefab.MustManuallyAssign;
public bool IsIgnoreOrder => Prefab.IsIgnoreOrder;
public bool IsDeconstructOrder => Prefab.IsDeconstructOrder;
public bool DrawIconWhenContained => Prefab.DrawIconWhenContained;
public bool Hidden => Prefab.Hidden;
public bool IgnoreAtOutpost => Prefab.IgnoreAtOutpost;
@@ -538,7 +567,6 @@ namespace Barotrauma
public bool ColoredWhenControllingGiver => Prefab.ColoredWhenControllingGiver;
public bool DisplayGiverInTooltip => Prefab.DisplayGiverInTooltip;
public readonly bool UseController;
/// <summary>
@@ -762,7 +790,7 @@ namespace Barotrauma
public string GetChatMessage(
string targetCharacterName, string targetRoomName, bool givingOrderToSelf, Identifier orderOption = default, bool isNewOrder = true)
=> Prefab.GetChatMessage(targetCharacterName, targetRoomName, givingOrderToSelf, orderOption, isNewOrder);
=> Prefab.GetChatMessage(targetCharacterName, targetRoomName, TargetEntity, givingOrderToSelf, orderOption, isNewOrder);
/// <summary>
/// Get the target item component based on the target item type
@@ -52,7 +52,7 @@ namespace Barotrauma
{
if (orderedCharacter != CommandingCharacter)
{
CommandingCharacter.Speak(SuggestedOrder.GetChatMessage(OrderedCharacter.Name, "", false), minDurationBetweenSimilar: 5);
CommandingCharacter.Speak(SuggestedOrder.GetChatMessage(OrderedCharacter.Name, "", givingOrderToSelf: false), minDurationBetweenSimilar: 5);
}
CurrentOrder = SuggestedOrder
.WithOption(Option)
@@ -75,7 +75,8 @@ namespace Barotrauma
{
steering.Y = 0.0f;
}
/// <param name="speed">Update speed for the steering. Should normally match the characters current animation speed.</param>
public virtual void Update(float speed)
{
if (steering == Vector2.Zero || !MathUtils.IsValid(steering))
@@ -86,6 +87,7 @@ namespace Barotrauma
}
if (steering.LengthSquared() > speed * speed)
{
// Can't steer faster than the max speed.
steering = Vector2.Normalize(steering) * Math.Abs(speed);
}
if (host is AIController aiController && aiController?.Character.CharacterHealth.GetAfflictionOfType("invertcontrols".ToIdentifier()) != null)