Build 1.1.4.0

This commit is contained in:
Markus Isberg
2023-03-31 18:40:44 +03:00
parent efba17e0ff
commit 9470edead3
483 changed files with 17487 additions and 8548 deletions
@@ -168,6 +168,7 @@ namespace Barotrauma
public void FaceTarget(ISpatialEntity target) => Character.AnimController.TargetDir = target.WorldPosition.X > Character.WorldPosition.X ? Direction.Right : Direction.Left;
public bool IsSteeringThroughGap { get; protected set; }
public bool IsTryingToSteerThroughGap { get; protected set; }
public virtual bool SteerThroughGap(Structure wall, WallSection section, Vector2 targetWorldPos, float deltaTime)
{
@@ -444,7 +445,7 @@ namespace Barotrauma
if (EscapeTarget != null)
{
var door = EscapeTarget.ConnectedDoor;
bool isClosedDoor = door != null && !door.IsOpen;
bool isClosedDoor = door != null && door.IsClosed;
Vector2 diff = EscapeTarget.WorldPosition - Character.WorldPosition;
float sqrDist = diff.LengthSquared();
bool isClose = sqrDist < MathUtils.Pow2(100);
@@ -245,11 +245,6 @@ namespace Barotrauma
{
throw new Exception($"Tried to create an enemy ai controller for human!");
}
if (Character.Params.Group == "human")
{
// Pet
Character.TeamID = CharacterTeamType.FriendlyNPC;
}
var mainElement = c.Params.OriginalElement.IsOverride() ? c.Params.OriginalElement.FirstElement() : c.Params.OriginalElement;
targetMemories = new Dictionary<AITarget, AITargetMemory>();
steeringManager = outsideSteering;
@@ -309,17 +304,20 @@ namespace Barotrauma
break;
}
}
//pets are friendly!
if (PetBehavior != null || Character.Group == "human")
{
Character.TeamID = CharacterTeamType.FriendlyNPC;
}
ReevaluateAttacks();
outsideSteering = new SteeringManager(this);
insideSteering = new IndoorsSteeringManager(this, Character.Params.AI.CanOpenDoors, canAttackDoors);
steeringManager = outsideSteering;
State = AIState.Idle;
requiredHoleCount = (int)Math.Ceiling(ConvertUnits.ToDisplayUnits(colliderWidth) / Structure.WallSectionSize);
myBodies = Character.AnimController.Limbs.Select(l => l.body.FarseerBody).ToList();
myBodies.Add(Character.AnimController.Collider.FarseerBody);
CreatureMetrics.UnlockInEditor(Character.SpeciesName);
}
private CharacterParams.AIParams _aiParams;
@@ -452,6 +450,7 @@ namespace Barotrauma
base.Update(deltaTime);
UpdateTriggers(deltaTime);
Character.ClearInputs();
IsTryingToSteerThroughGap = false;
Reverse = false;
bool ignorePlatforms = Character.AnimController.TargetMovement.Y < -0.5f && (-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X));
@@ -558,8 +557,9 @@ namespace Barotrauma
}
}
if (AIParams.CanOpenDoors)
if (Character.Params.UsePathFinding && Character.Params.AI.UsePathFindingToGetInside && AIParams.CanOpenDoors)
{
// Meant for monsters outside the player sub that target something inside the sub and can use the doors to access the sub (Husk).
bool IsCloseEnoughToTargetSub(float threshold) => SelectedAiTarget?.Entity?.Submarine is Submarine sub && sub != null && Vector2.DistanceSquared(Character.WorldPosition, sub.WorldPosition) < MathUtils.Pow(Math.Max(sub.Borders.Size.X, sub.Borders.Size.Y) / 2 + threshold, 2);
if (Character.Submarine != null || HasValidPath() && IsCloseEnoughToTargetSub(maxSteeringBuffer) || IsCloseEnoughToTargetSub(steeringBuffer))
@@ -584,6 +584,7 @@ namespace Barotrauma
}
else
{
// Normally the monsters only use pathing inside submarines, not outside.
if (Character.Submarine != null && Character.Params.UsePathFinding)
{
if (steeringManager != insideSteering)
@@ -848,7 +849,7 @@ namespace Barotrauma
IsSteeringThroughGap = false;
if (SwarmBehavior != null)
{
SwarmBehavior.IsActive = State == AIState.Idle && Character.CurrentHull == null;
SwarmBehavior.IsActive = SwarmBehavior.ForceActive || State == AIState.Idle && Character.CurrentHull == null;
SwarmBehavior.Refresh();
SwarmBehavior.UpdateSteering(deltaTime);
}
@@ -876,7 +877,7 @@ namespace Barotrauma
var pathSteering = SteeringManager as IndoorsSteeringManager;
if (pathSteering == null)
{
if (SimPosition.Y < ConvertUnits.ToSimUnits(Character.CharacterHealth.CrushDepth * 0.75f))
if (Level.Loaded != null && Level.Loaded.GetRealWorldDepth(WorldPosition.Y) > Character.CharacterHealth.CrushDepth * 0.75f)
{
// Steer straight up if very deep
SteeringManager.SteeringManual(deltaTime, Vector2.UnitY);
@@ -1144,7 +1145,6 @@ namespace Barotrauma
return;
}
}
attackLimbSelectionTimer -= deltaTime;
if (AttackLimb == null || attackLimbSelectionTimer <= 0)
{
@@ -1154,7 +1154,8 @@ namespace Barotrauma
AttackLimb = GetAttackLimb(attackWorldPos);
}
}
Character targetCharacter = SelectedAiTarget.Entity as Character;
IDamageable damageTarget = wallTarget != null ? wallTarget.Structure : SelectedAiTarget.Entity as IDamageable;
bool canAttack = true;
bool pursue = false;
if (IsCoolDownRunning && (_previousAttackLimb == null || AttackLimb == null || AttackLimb.attack.CoolDownTimer > 0))
@@ -1379,7 +1380,6 @@ namespace Barotrauma
float distance = 0;
Limb attackTargetLimb = null;
Character targetCharacter = SelectedAiTarget.Entity as Character;
if (canAttack)
{
if (!Character.AnimController.SimplePhysicsEnabled)
@@ -1400,29 +1400,29 @@ namespace Barotrauma
attackSimPos = Character.GetRelativeSimPosition(attackTargetLimb);
}
}
Vector2 attackLimbPos = Character.AnimController.SimplePhysicsEnabled ? Character.WorldPosition : AttackLimb.WorldPosition;
Vector2 toTarget = attackWorldPos - attackLimbPos;
Vector2 toTargetOffset = toTarget;
// Add a margin when the target is moving away, because otherwise it might be difficult to reach it if the attack takes some time to execute
if (wallTarget != null && Character.Submarine == null)
{
if (wallTarget.Structure.Submarine != null)
{
Vector2 margin = CalculateMargin(wallTarget.Structure.Submarine.Velocity);
toTarget += margin;
toTargetOffset += margin;
}
}
else if (targetCharacter != null)
{
Vector2 margin = CalculateMargin(targetCharacter.AnimController.Collider.LinearVelocity);
toTarget += margin;
toTargetOffset += margin;
}
else if (SelectedAiTarget.Entity is MapEntity e)
{
if (e.Submarine != null)
{
Vector2 margin = CalculateMargin(e.Submarine.Velocity);
toTarget += margin;
toTargetOffset += margin;
}
}
@@ -1430,7 +1430,7 @@ namespace Barotrauma
{
if (targetVelocity == Vector2.Zero) { return Vector2.Zero; }
float diff = AttackLimb.attack.Range - AttackLimb.attack.DamageRange;
if (diff <= 0 || toTarget.LengthSquared() <= MathUtils.Pow2(AttackLimb.attack.DamageRange)) { return Vector2.Zero; }
if (diff <= 0 || toTargetOffset.LengthSquared() <= MathUtils.Pow2(AttackLimb.attack.DamageRange)) { return Vector2.Zero; }
float dot = Vector2.Dot(Vector2.Normalize(targetVelocity), Vector2.Normalize(Character.AnimController.Collider.LinearVelocity));
if (dot <= 0 || !MathUtils.IsValid(dot)) { return Vector2.Zero; }
float distanceOffset = diff * AttackLimb.attack.Duration;
@@ -1439,7 +1439,7 @@ namespace Barotrauma
}
// Check that we can reach the target
distance = toTarget.Length();
distance = toTargetOffset.Length();
canAttack = distance < AttackLimb.attack.Range;
if (canAttack)
{
@@ -1523,20 +1523,18 @@ namespace Barotrauma
}
}
Limb steeringLimb = canAttack && !AttackLimb.attack.Ranged ? AttackLimb : null;
bool updateSteering = true;
if (steeringLimb == null)
{
// If the attacking limb is a hand or claw, for example, using it as the steering limb can end in the result where the character circles around the target.
steeringLimb = Character.AnimController.GetLimb(LimbType.Head) ?? Character.AnimController.GetLimb(LimbType.Torso);
}
if (steeringLimb == null)
{
State = AIState.Idle;
return;
}
var pathSteering = SteeringManager as IndoorsSteeringManager;
if (AttackLimb != null && AttackLimb.attack.Retreat)
{
UpdateFallBack(attackWorldPos, deltaTime, followThrough: false);
@@ -1603,7 +1601,7 @@ namespace Barotrauma
}
}
}
else
else if (!IsTryingToSteerThroughGap)
{
if (AttackLimb.attack.Ranged)
{
@@ -1624,6 +1622,10 @@ namespace Barotrauma
SteeringManager.Reset();
}
}
else
{
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(SelectedAiTarget.Entity.WorldPosition - Character.WorldPosition));
}
}
else
{
@@ -1662,40 +1664,60 @@ namespace Barotrauma
if (IsAttackRunning && CirclePhase != CirclePhase.Strike) { break; }
if (selectedTargetingParams == null) { break; }
var targetSub = SelectedAiTarget.Entity?.Submarine;
if (targetSub == null) { break; }
float subSize = Math.Max(targetSub.Borders.Width, targetSub.Borders.Height) / 2;
float sqrDistToSub = Vector2.DistanceSquared(WorldPosition, targetSub.WorldPosition);
ISpatialEntity spatialTarget = targetSub ?? SelectedAiTarget.Entity;
float targetSize = 0;
if (!selectedTargetingParams.IgnoreTargetSize)
{
targetSize =
targetSub != null ? Math.Max(targetSub.Borders.Width, targetSub.Borders.Height) / 2 :
targetCharacter != null ? ConvertUnits.ToDisplayUnits(targetCharacter.AnimController.Collider.GetSize().X) : 100;
}
float sqrDistToTarget = Vector2.DistanceSquared(WorldPosition, spatialTarget.WorldPosition);
bool isProgressive = AIParams.MaxAggression - AIParams.StartAggression > 0;
switch (CirclePhase)
{
case CirclePhase.Start:
currentAttackIntensity = MathUtils.InverseLerp(AIParams.StartAggression, AIParams.MaxAggression, aggressionIntensity * Rand.Range(0.9f, 1.1f));
currentAttackIntensity = MathUtils.InverseLerp(AIParams.StartAggression, AIParams.MaxAggression, ClampIntensity(aggressionIntensity));
inverseDir = false;
circleDir = GetDirFromHeadingInRadius();
circleRotation = 0;
strikeTimer = 0;
blockCheckTimer = 0;
breakCircling = false;
float minRotationSpeed = 0.01f * selectedTargetingParams.CircleRotationSpeed;
float maxRotationSpeed = 0.5f * selectedTargetingParams.CircleRotationSpeed;
float minFallBackDistance = selectedTargetingParams.CircleStartDistance * 0.5f;
float maxFallBackDistance = selectedTargetingParams.CircleStartDistance;
float maxRandomOffset = selectedTargetingParams.CircleMaxRandomOffset;
// The lower the rotation speed, the slower the progression. Also the distance to the target stays longer.
// So basically if the value is higher, the creature will strike the sub more quickly and with more precision.
circleRotationSpeed = MathHelper.Lerp(minRotationSpeed, maxRotationSpeed, currentAttackIntensity * Rand.Range(0.9f, 1.1f));
circleFallbackDistance = MathHelper.Lerp(maxFallBackDistance, minFallBackDistance, currentAttackIntensity * Rand.Range(0.9f, 1.1f));
circleOffset = Rand.Vector(MathHelper.Lerp(selectedTargetingParams.CircleMaxRandomOffset, 0, currentAttackIntensity * Rand.Range(0.9f, 1.1f)));
canAttack = false;
float ClampIntensity(float intensity) => MathHelper.Clamp(intensity * Rand.Range(0.9f, 1.1f), AIParams.StartAggression, AIParams.MaxAggression);
if (isProgressive)
{
float intensity = ClampIntensity(currentAttackIntensity);
float minRotationSpeed = 0.01f * selectedTargetingParams.CircleRotationSpeed;
float maxRotationSpeed = 0.5f * selectedTargetingParams.CircleRotationSpeed;
circleRotationSpeed = MathHelper.Lerp(minRotationSpeed, maxRotationSpeed, intensity);
circleFallbackDistance = MathHelper.Lerp(maxFallBackDistance, minFallBackDistance, intensity);
circleOffset = Rand.Vector(MathHelper.Lerp(maxRandomOffset, 0, intensity));
}
else
{
circleRotationSpeed = selectedTargetingParams.CircleRotationSpeed;
circleFallbackDistance = maxFallBackDistance;
circleOffset = Rand.Vector(maxRandomOffset);
}
circleRotationSpeed *= Rand.Range(1 - selectedTargetingParams.CircleRandomRotationFactor, 1 + selectedTargetingParams.CircleRandomRotationFactor);
aggressionIntensity = Math.Clamp(aggressionIntensity, AIParams.StartAggression, AIParams.MaxAggression);
if (targetSub.Borders.Width < 1000)
DisableAttacksIfLimbNotRanged();
if (targetSub != null && targetSub.Borders.Width < 1000 && AttackLimb?.attack is { Ranged: false })
{
breakCircling = true;
CirclePhase = CirclePhase.CloseIn;
}
else if (sqrDistToSub > MathUtils.Pow2(subSize + selectedTargetingParams.CircleStartDistance))
else if (sqrDistToTarget > MathUtils.Pow2(targetSize + selectedTargetingParams.CircleStartDistance))
{
CirclePhase = CirclePhase.CloseIn;
}
else if (sqrDistToSub < MathUtils.Pow2(subSize + circleFallbackDistance))
else if (sqrDistToTarget < MathUtils.Pow2(targetSize + circleFallbackDistance))
{
CirclePhase = CirclePhase.FallBack;
}
@@ -1705,52 +1727,76 @@ namespace Barotrauma
}
break;
case CirclePhase.CloseIn:
if (AttackLimb != null && distance > 0 && distance < AttackLimb.attack.Range * GetStrikeDistanceMultiplier(targetSub.Velocity))
Vector2 targetVelocity = GetTargetVelocity();
float targetDistance = selectedTargetingParams.IgnoreTargetSize ? selectedTargetingParams.CircleStartDistance * 0.9f:
targetSize + selectedTargetingParams.CircleStartDistance / 2;
if (AttackLimb != null && distance > 0 && distance < AttackLimb.attack.Range * GetStrikeDistanceMultiplier(targetVelocity))
{
strikeTimer = AttackLimb.attack.CoolDown;
CirclePhase = CirclePhase.Strike;
}
else if (!breakCircling && sqrDistToSub <= MathUtils.Pow2(subSize + selectedTargetingParams.CircleStartDistance / 2) && targetSub.Velocity.LengthSquared() <= MathUtils.Pow2(GetTargetMaxSpeed()))
else if (!breakCircling && sqrDistToTarget <= MathUtils.Pow2(targetDistance) && targetVelocity.LengthSquared() <= MathUtils.Pow2(GetTargetMaxSpeed()))
{
CirclePhase = CirclePhase.Advance;
}
canAttack = false;
DisableAttacksIfLimbNotRanged();
break;
case CirclePhase.FallBack:
updateSteering = false;
bool isBlocked = !UpdateFallBack(attackWorldPos, deltaTime, followThrough: false, checkBlocking: true);
if (isBlocked || sqrDistToSub > MathUtils.Pow2(subSize + circleFallbackDistance))
if (isBlocked || sqrDistToTarget > MathUtils.Pow2(targetSize + circleFallbackDistance))
{
CirclePhase = CirclePhase.Advance;
break;
}
return;
DisableAttacksIfLimbNotRanged();
break;
case CirclePhase.Advance:
Vector2 subSpeed = targetSub.Velocity;
float requiredDistMultiplier = 1;
// If the target sub is moving fast, just steer towards the target until close enough to strike
if (breakCircling || subSpeed.LengthSquared() > MathUtils.Pow2(GetTargetMaxSpeed()) || sqrDistToSub > MathUtils.Pow2(subSize + selectedTargetingParams.CircleStartDistance * 1.2f))
Vector2 targetVel = GetTargetVelocity();
// If the target is moving fast, just steer towards the target
if (breakCircling || targetVel.LengthSquared() > MathUtils.Pow2(GetTargetMaxSpeed()))
{
CirclePhase = CirclePhase.CloseIn;
}
else if (sqrDistToTarget > MathUtils.Pow2(targetSize + selectedTargetingParams.CircleStartDistance * 1.2f))
{
if (selectedTargetingParams.DynamicCircleRotationSpeed && circleRotationSpeed < 100)
{
circleRotationSpeed *= 1 + deltaTime;
}
else
{
CirclePhase = CirclePhase.CloseIn;
}
}
else
{
circleRotation += deltaTime * circleRotationSpeed * circleDir;
if (circleRotation < -360)
float rotationStep = circleRotationSpeed * deltaTime * circleDir;
if (isProgressive)
{
circleRotation += 360;
circleRotation += rotationStep;
}
else if (circleRotation > 360)
else
{
circleRotation -= 360;
circleRotation = rotationStep;
}
Vector2 targetPos = attackSimPos + circleOffset;
if (Vector2.DistanceSquared(SimPosition, targetPos) < 100)
float targetDist = targetSize;
if (targetDist <= 0)
{
targetDist = circleFallbackDistance;
}
if (targetSub != null && AttackLimb?.attack is { Ranged: true })
{
targetDist += circleFallbackDistance / 2;
}
if (Vector2.DistanceSquared(SimPosition, targetPos) < ConvertUnits.ToSimUnits(targetDist))
{
// Too close to the target point
// When the offset position is outside of the sub it happens that the creature sometimes reaches the target point,
// which makes it continue circling around the point (as supposed)
// But when there is some offset and the offset is too near, this is not what we want.
if (AttackLimb != null && sqrDistToSub < MathUtils.Pow2(subSize + circleFallbackDistance))
if (canAttack && AttackLimb?.attack is { Ranged: false } && sqrDistToTarget < MathUtils.Pow2(targetSize + circleFallbackDistance))
{
CirclePhase = CirclePhase.Strike;
strikeTimer = AttackLimb.attack.CoolDown;
@@ -1762,7 +1808,6 @@ namespace Barotrauma
break;
}
steerPos = MathUtils.RotatePointAroundTarget(SimPosition, targetPos, circleRotation);
requiredDistMultiplier = GetStrikeDistanceMultiplier(subSpeed);
if (IsBlocked(deltaTime, steerPos))
{
if (!inverseDir)
@@ -1774,7 +1819,7 @@ namespace Barotrauma
else if (circleRotationSpeed < 1)
{
// Then try increasing the rotation speed to change the movement curve
circleRotationSpeed *= 1.1f;
circleRotationSpeed *= 1 + deltaTime;
}
else if (circleOffset.LengthSquared() > 0.1f)
{
@@ -1784,16 +1829,24 @@ namespace Barotrauma
else
{
// If we still fail, just steer towards the target
breakCircling = true;
breakCircling = AttackLimb?.attack is { Ranged: false };
if (!breakCircling)
{
CirclePhase = CirclePhase.FallBack;
}
}
}
}
if (AttackLimb != null && distance > 0 && distance < AttackLimb.attack.Range * requiredDistMultiplier && IsFacing(margin: MathHelper.Lerp(0.5f, 0.9f, currentAttackIntensity)))
if (AttackLimb?.attack is { Ranged: false })
{
strikeTimer = AttackLimb.attack.CoolDown;
CirclePhase = CirclePhase.Strike;
canAttack = false;
float requiredDistMultiplier = GetStrikeDistanceMultiplier(targetVel);
if (distance > 0 && distance < AttackLimb.attack.Range * requiredDistMultiplier && IsFacing(margin: MathHelper.Lerp(0.5f, 0.9f, currentAttackIntensity)))
{
strikeTimer = AttackLimb.attack.CoolDown;
CirclePhase = CirclePhase.Strike;
}
}
canAttack = false;
break;
case CirclePhase.Strike:
strikeTimer -= deltaTime;
@@ -1815,18 +1868,19 @@ namespace Barotrauma
return Vector2.Dot(Vector2.Normalize(attackWorldPos - WorldPosition), forward) > margin;
}
float GetStrikeDistanceMultiplier(Vector2 subSpeed)
float GetStrikeDistanceMultiplier(Vector2 targetVelocity)
{
if (selectedTargetingParams.CircleStrikeDistanceMultiplier < 1) { return 0; }
float requiredDistMultiplier = 2;
bool isHeading = Steering != null && Vector2.Dot(Vector2.Normalize(attackWorldPos - WorldPosition), Vector2.Normalize(Steering)) > 0.9f;
bool isHeading = Vector2.Dot(Vector2.Normalize(attackWorldPos - WorldPosition), Vector2.Normalize(Steering)) > 0.9f;
if (isHeading)
{
requiredDistMultiplier = selectedTargetingParams.CircleStrikeDistanceMultiplier;
float subSpeedHorizontal = Math.Abs(subSpeed.X);
if (subSpeedHorizontal > 1)
float targetVelocityHorizontal = Math.Abs(targetVelocity.X);
if (targetVelocityHorizontal > 1)
{
// Reduce the required distance if the target is moving.
requiredDistMultiplier -= MathHelper.Lerp(0, Math.Max(selectedTargetingParams.CircleStrikeDistanceMultiplier - 1, 1), Math.Clamp(subSpeedHorizontal / 10, 0, 1));
requiredDistMultiplier -= MathHelper.Lerp(0, Math.Max(selectedTargetingParams.CircleStrikeDistanceMultiplier - 1, 1), Math.Clamp(targetVelocityHorizontal / 10, 0, 1));
if (requiredDistMultiplier < 2)
{
requiredDistMultiplier = 2;
@@ -1843,19 +1897,59 @@ namespace Barotrauma
return angle > MathHelper.Pi || angle < -MathHelper.Pi ? -1 : 1;
}
float GetTargetMaxSpeed() => Character.ApplyTemporarySpeedLimits(Character.AnimController.CurrentSwimParams.MovementSpeed * 0.3f);
Vector2 GetTargetVelocity()
{
if (targetSub != null)
{
return targetSub.Velocity;
}
else if (targetCharacter != null)
{
return targetCharacter.AnimController.Collider.LinearVelocity;
}
return Vector2.Zero;
}
float GetTargetMaxSpeed() => Character.ApplyTemporarySpeedLimits(Character.AnimController.SwimFastParams.MovementSpeed * (targetSub != null ? 0.3f : 0.5f));
}
}
if (selectedTargetingParams.AttackPattern == AttackPattern.Straight && AttackLimb is Limb attackLimb && attackLimb.attack.Ranged)
if (updateSteering)
{
bool advance = !canAttack && Character.CurrentHull == null || distance > attackLimb.attack.Range * 0.9f;
bool fallBack = canAttack && distance < Math.Min(250, attackLimb.attack.Range * 0.25f);
if (fallBack)
if (selectedTargetingParams.AttackPattern == AttackPattern.Straight && AttackLimb is Limb attackLimb && attackLimb.attack.Ranged)
{
Reverse = true;
UpdateFallBack(attackWorldPos, deltaTime, followThrough: false);
bool advance = !canAttack && Character.CurrentHull == null || distance > attackLimb.attack.Range * 0.9f;
bool fallBack = canAttack && distance < Math.Min(250, attackLimb.attack.Range * 0.25f);
if (fallBack)
{
Reverse = true;
UpdateFallBack(attackWorldPos, deltaTime, followThrough: false);
}
else if (advance)
{
if (pathSteering != null)
{
pathSteering.SteeringSeek(steerPos, weight: 10, minGapWidth: minGapSize);
}
else
{
SteeringManager.SteeringSeek(steerPos, 10);
}
}
else
{
if (Character.CurrentHull == null && !canAttack)
{
SteeringManager.SteeringWander(avoidWanderingOutsideLevel: true);
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 5);
}
else
{
SteeringManager.Reset();
FaceTarget(SelectedAiTarget.Entity);
}
}
}
else if (advance)
else if (!canAttack || distance > Math.Min(AttackLimb.attack.Range * 0.9f, 100))
{
if (pathSteering != null)
{
@@ -1866,41 +1960,18 @@ namespace Barotrauma
SteeringManager.SteeringSeek(steerPos, 10);
}
}
else
if (Character.CurrentHull == null && (SelectedAiTarget?.Entity is Character c && c.Submarine == null ||
distance == 0 ||
distance > ConvertUnits.ToDisplayUnits(avoidLookAheadDistance * 2) ||
AttackLimb != null && AttackLimb.attack.Ranged))
{
if (Character.CurrentHull == null && !canAttack)
{
SteeringManager.SteeringWander(avoidWanderingOutsideLevel: true);
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 5);
}
else
{
SteeringManager.Reset();
FaceTarget(SelectedAiTarget.Entity);
}
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 30);
}
}
else if (!canAttack || distance > Math.Min(AttackLimb.attack.Range * 0.9f, 100))
{
if (pathSteering != null)
{
pathSteering.SteeringSeek(steerPos, weight: 10, minGapWidth: minGapSize);
}
else
{
SteeringManager.SteeringSeek(steerPos, 10);
}
}
if (Character.CurrentHull == null && (SelectedAiTarget?.Entity is Character c && c.Submarine == null || distance == 0 || distance > ConvertUnits.ToDisplayUnits(avoidLookAheadDistance * 2)))
{
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 30);
}
}
}
Entity targetEntity = wallTarget?.Structure ?? SelectedAiTarget?.Entity;
IDamageable damageTarget = targetEntity as IDamageable;
if (AttackLimb?.attack is Attack { Ranged: true} attack)
if (AttackLimb?.attack is Attack { Ranged: true } attack)
{
AimRangedAttack(attack, targetEntity);
}
@@ -1915,12 +1986,21 @@ namespace Barotrauma
{
AttackLimb.attack.ResetAttackTimer();
}
void DisableAttacksIfLimbNotRanged()
{
if (AttackLimb?.attack is { Ranged: false })
{
canAttack = false;
}
}
}
public void AimRangedAttack(Attack attack, Entity targetEntity)
{
if (attack is not { Ranged: true } || targetEntity is not { Removed: false }) { return; }
Character.SetInput(InputType.Aim, false, true);
if (attack.AimRotationTorque <= 0) { return; }
Limb limb = GetLimbToRotate(attack);
if (limb != null)
{
@@ -2003,9 +2083,18 @@ namespace Barotrauma
float prio = 1 + limb.attack.Priority;
if (Character.AnimController.SimplePhysicsEnabled) { return prio; }
float dist = Vector2.Distance(limb.WorldPosition, attackPos);
// The limb is ignored if the target is not close. Prevents character going in reverse if very far away from it.
// We also need a max value that is more than the actual range.
float distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, limb.attack.Range * 3, dist));
float distanceFactor = 1;
if (limb.attack.Ranged)
{
float min = 100;
distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(min, Math.Max(limb.attack.Range / 2, min), dist));
}
else
{
// The limb is ignored if the target is not close. Prevents character going in reverse if very far away from it.
// We also need a max value that is more than the actual range.
distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, limb.attack.Range * 3, dist));
}
return prio * distanceFactor;
}
}
@@ -2164,7 +2253,9 @@ namespace Barotrauma
{
if (SelectedAiTarget?.Entity == null) { return false; }
if (AttackLimb?.attack == null) { return false; }
if (damageTarget == null) { return false; }
ISpatialEntity spatialTarget = wallTarget != null ? wallTarget.Structure : SelectedAiTarget.Entity as ISpatialEntity;
if (spatialTarget == null) { return false; }
ActiveAttack = AttackLimb.attack;
if (wallTarget != null)
{
// If the selected target is not the wall target, make the wall target the selected target.
@@ -2176,13 +2267,14 @@ namespace Barotrauma
return true;
}
}
if (damageTarget == null) { return false; }
ActiveAttack = AttackLimb.attack;
if (ActiveAttack.Ranged && ActiveAttack.RequiredAngleToShoot > 0)
{
Limb referenceLimb = GetLimbToRotate(ActiveAttack);
if (referenceLimb != null)
{
Vector2 toTarget = damageTarget.WorldPosition - referenceLimb.WorldPosition;
Vector2 toTarget = spatialTarget.WorldPosition - 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));
@@ -2200,16 +2292,20 @@ namespace Barotrauma
{
if (item.RequireAimToUse)
{
if (!Aim(deltaTime, damageTarget as ISpatialEntity, item))
if (!Aim(deltaTime, spatialTarget, item))
{
// Valid target, but can't shoot -> return true so that it will not be ignored.
return true;
}
}
Character.SetInput(item.IsShootable ? InputType.Shoot : InputType.Use, false, true);
item.Use(deltaTime, Character);
if (damageTarget != null)
{
Character.SetInput(item.IsShootable ? InputType.Shoot : InputType.Use, false, true);
item.Use(deltaTime, Character);
}
}
}
if (damageTarget == null) { return true; }
//simulate attack input to get the character to attack client-side
Character.SetInput(InputType.Attack, true, true);
if (!ActiveAttack.IsRunning)
@@ -2224,20 +2320,11 @@ namespace Barotrauma
Character.PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3);
#endif
}
if (AttackLimb.UpdateAttack(deltaTime, attackSimPos, damageTarget, out AttackResult attackResult, distance, targetLimb))
{
if (ActiveAttack.CoolDownTimer > 0)
{
SetAimTimer(Math.Min(ActiveAttack.CoolDown, 1.5f));
// Managed to hit a living/non-destroyed target. Increase the priority more if the target is low in health -> dies easily/soon
float greed = AIParams.AggressionGreed;
if (damageTarget is not Barotrauma.Character)
{
// Halve the greed for attacking non-characters.
greed /= 2;
}
selectedTargetMemory.Priority += GetRelativeDamage(attackResult.Damage, damageTarget.Health) * greed;
}
if (LatchOntoAI != null && SelectedAiTarget.Entity is Character targetCharacter)
{
@@ -2269,10 +2356,19 @@ namespace Barotrauma
private float aimTimer;
private float visibilityCheckTimer;
private bool canSeeTarget;
private float sinTime;
private bool Aim(float deltaTime, ISpatialEntity target, Item weapon)
{
if (target == null || weapon == null) { return false; }
if (AttackLimb == null) { return false; }
Vector2 toTarget = target.WorldPosition - weapon.WorldPosition;
float dist = toTarget.Length();
Character.CursorPosition = target.WorldPosition;
if (AttackLimb.attack.SwayAmount > 0)
{
sinTime += deltaTime * AttackLimb.attack.SwayFrequency;
Character.CursorPosition += VectorExtensions.Forward(weapon.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, dist / 2 * AttackLimb.attack.SwayAmount);
}
if (Character.Submarine != null)
{
Character.CursorPosition -= Character.Submarine.Position;
@@ -2294,11 +2390,11 @@ namespace Barotrauma
aimTimer -= deltaTime;
return false;
}
Vector2 toTarget = target.WorldPosition - weapon.WorldPosition;
float angle = VectorExtensions.Angle(VectorExtensions.Forward(weapon.body.TransformedRotation), toTarget);
float distanceFactor = MathHelper.Lerp(1.0f, 0.1f, MathUtils.InverseLerp(100, 1000, toTarget.Length()));
float minDistance = 300;
float distanceFactor = MathHelper.Lerp(1.0f, 0.1f, MathUtils.InverseLerp(minDistance, 1000, dist));
float margin = MathHelper.PiOver4 * distanceFactor;
if (angle < margin)
if (angle < margin || dist < minDistance)
{
var collisionCategories = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel;
var pickedBody = Submarine.PickBody(weapon.SimPosition, Character.GetRelativeSimPosition(target), myBodies, collisionCategories, allowInsideFixture: true);
@@ -2591,13 +2687,19 @@ namespace Barotrauma
{
// Ignore all structures, items, and hulls inside these subs.
if (aiTarget.Entity.Submarine != null)
{
if (aiTarget.Entity.Submarine.Info.IsWreck ||
aiTarget.Entity.Submarine.Info.IsBeacon ||
{
if (aiTarget.Entity.Submarine.Info.IsWreck ||
aiTarget.Entity.Submarine.Info.IsBeacon ||
UnattackableSubmarines.Contains(aiTarget.Entity.Submarine))
{
continue;
}
//ignore the megaruin in end levels
if (aiTarget.Entity.Submarine.Info.OutpostGenerationParams != null &&
aiTarget.Entity.Submarine.Info.OutpostGenerationParams.ForceToEndLocationIndex > -1)
{
continue;
}
}
if (aiTarget.Entity is Hull hull)
{
@@ -2698,7 +2800,7 @@ namespace Barotrauma
}
else if (CanPassThroughHole(s, i))
{
valueModifier *= isInnerWall ? 1 : 0;
valueModifier *= isInnerWall ? 0.5f : 0;
}
else if (!canAttackWalls)
{
@@ -2968,7 +3070,8 @@ namespace Barotrauma
// In the attack state allow going into non-allowed zone only when chasing a target.
if (State == targetParams.State && SelectedAiTarget == aiTarget) { break; }
}
if (!IsPositionInsideAllowedZone(aiTarget.WorldPosition, out _))
bool insideSameSub = aiTarget?.Entity?.Submarine != null && aiTarget.Entity.Submarine == Character.Submarine;
if (!insideSameSub && !IsPositionInsideAllowedZone(aiTarget.WorldPosition, out _))
{
// If we have recently been damaged by the target (or another player/bot in the same team) allow targeting it even when we are in the idle state.
bool isTargetInPlayerTeam = IsTargetInPlayerTeam(aiTarget);
@@ -3401,10 +3504,10 @@ namespace Barotrauma
private readonly float stateResetCooldown = 10;
private float stateResetTimer;
private bool isStateChanged;
private readonly Dictionary<AITrigger, CharacterParams.TargetParams> activeTriggers = new Dictionary<AITrigger, CharacterParams.TargetParams>();
private readonly HashSet<AITrigger> inactiveTriggers = new HashSet<AITrigger>();
private readonly Dictionary<StatusEffect.AITrigger, CharacterParams.TargetParams> activeTriggers = new Dictionary<StatusEffect.AITrigger, CharacterParams.TargetParams>();
private readonly HashSet<StatusEffect.AITrigger> inactiveTriggers = new HashSet<StatusEffect.AITrigger>();
public void LaunchTrigger(AITrigger trigger)
public void LaunchTrigger(StatusEffect.AITrigger trigger)
{
if (trigger.IsTriggered) { return; }
if (activeTriggers.ContainsKey(trigger)) { return; }
@@ -3424,7 +3527,7 @@ namespace Barotrauma
{
foreach (var triggerObject in activeTriggers)
{
AITrigger trigger = triggerObject.Key;
StatusEffect.AITrigger trigger = triggerObject.Key;
if (trigger.IsPermanent) { continue; }
trigger.UpdateTimer(deltaTime);
if (!trigger.IsActive)
@@ -3434,7 +3537,7 @@ namespace Barotrauma
inactiveTriggers.Add(trigger);
}
}
foreach (AITrigger trigger in inactiveTriggers)
foreach (StatusEffect.AITrigger trigger in inactiveTriggers)
{
activeTriggers.Remove(trigger);
}
@@ -3588,6 +3691,11 @@ namespace Barotrauma
observeTimer = targetParams.Timer * Rand.Range(0.75f, 1.25f);
}
reachTimer = 0;
sinTime = 0;
if (breakCircling && strikeTimer <= 0)
{
CirclePhase = CirclePhase.Start;
}
}
protected override void OnStateChanged(AIState from, AIState to)
@@ -3609,6 +3717,11 @@ namespace Barotrauma
}
blockCheckTimer = 0;
reachTimer = 0;
sinTime = 0;
if (breakCircling && strikeTimer <= 0)
{
CirclePhase = CirclePhase.Start;
}
}
private void SetStateResetTimer() => stateResetTimer = stateResetCooldown * Rand.Range(0.75f, 1.25f);
@@ -3712,6 +3825,7 @@ namespace Barotrauma
{
targetDir = Vector2.Zero;
if (Level.Loaded == null) { return true; }
if (Level.Loaded.LevelData.Biome.IsEndBiome) { return true; }
if (AIParams.AvoidAbyss)
{
if (pos.Y < Level.Loaded.AbyssStart)
@@ -3771,6 +3885,7 @@ namespace Barotrauma
public override bool SteerThroughGap(Structure wall, WallSection section, Vector2 targetWorldPos, float deltaTime)
{
IsTryingToSteerThroughGap = true;
wallTarget = null;
LatchOntoAI?.DeattachFromBody(reset: true, cooldown: 2);
Character.AnimController.ReleaseStuckLimbs();
@@ -1,10 +1,10 @@
using Barotrauma.Networking;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
namespace Barotrauma
{
@@ -395,6 +395,8 @@ namespace Barotrauma
}
objectiveManager.UpdateObjectives(deltaTime);
UpdateDragged(deltaTime);
if (reportProblemsTimer > 0)
{
reportProblemsTimer -= deltaTime;
@@ -430,7 +432,7 @@ namespace Barotrauma
}
if (reportProblemsTimer <= 0.0f)
{
if (Character.Submarine != null && (Character.Submarine.TeamID == Character.TeamID || Character.IsEscorted) && !Character.Submarine.Info.IsWreck)
if (Character.Submarine != null && (Character.Submarine.TeamID == Character.TeamID || Character.Submarine.TeamID == Character.OriginalTeamID || Character.IsEscorted) && !Character.Submarine.Info.IsWreck)
{
ReportProblems();
}
@@ -444,7 +446,7 @@ namespace Barotrauma
if (objectiveManager.CurrentObjective == null) { return; }
objectiveManager.DoCurrentObjective(deltaTime);
bool run = objectiveManager.CurrentObjective.ForceRun || !objectiveManager.CurrentObjective.ForceWalk && objectiveManager.GetCurrentPriority() > AIObjectiveManager.RunPriority;
bool run = (objectiveManager.CurrentObjective.ForceRun && !objectiveManager.CurrentObjective.ForceWalk) || (!objectiveManager.CurrentObjective.ForceWalk && objectiveManager.GetCurrentPriority() > AIObjectiveManager.RunPriority);
if (ObjectiveManager.CurrentObjective is AIObjectiveGoTo goTo && goTo.Target != null)
{
if (Character.CurrentHull == null)
@@ -546,12 +548,11 @@ namespace Barotrauma
bool NeedsDivingGearOnPath(AIObjectiveGoTo gotoObjective)
{
if (!Character.NeedsAir) { return false; }
bool insideSteering = SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.IsPathDirty;
Hull targetHull = gotoObjective.GetTargetHull();
return gotoObjective.Target != null && targetHull == null ||
return (gotoObjective.Target != null && targetHull == null && !Character.IsImmuneToPressure) ||
NeedsDivingGear(targetHull, out _) ||
insideSteering && (PathSteering.CurrentPath.HasOutdoorsNodes || PathSteering.CurrentPath.Nodes.Any(n => NeedsDivingGear(n.CurrentHull, out _)));
(insideSteering && ((PathSteering.CurrentPath.HasOutdoorsNodes && !Character.IsImmuneToPressure) || PathSteering.CurrentPath.Nodes.Any(n => NeedsDivingGear(n.CurrentHull, out _))));
}
if (isCarrying)
@@ -584,7 +585,7 @@ namespace Barotrauma
Character.AnimController.InWater ||
Character.AnimController.HeadInWater ||
Character.Submarine == null ||
(Character.Submarine.TeamID != Character.TeamID && !Character.IsEscorted) ||
(!Character.IsOnFriendlyTeam(Character.TeamID, Character.Submarine.TeamID) && !Character.IsEscorted) ||
ObjectiveManager.CurrentOrders.Any(o => o.Objective.KeepDivingGearOnAlsoWhenInactive) ||
ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn) ||
Character.CurrentHull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 10 ||
@@ -621,9 +622,10 @@ namespace Barotrauma
}
else if (gotoObjective.Mimic)
{
bool targetHasDivingGear = HasDivingGear(gotoObjective.Target as Character, requireOxygenTank: false);
if (!removeSuit)
{
removeDivingSuit = !HasDivingSuit(gotoObjective.Target as Character);
removeDivingSuit = !targetHasDivingGear;
if (removeDivingSuit)
{
removeSuit = true;
@@ -631,7 +633,7 @@ namespace Barotrauma
}
if (!removeMask)
{
takeMaskOff = !HasDivingMask(gotoObjective.Target as Character);
takeMaskOff = !targetHasDivingGear;
if (takeMaskOff)
{
removeMask = true;
@@ -783,20 +785,23 @@ namespace Barotrauma
private void HandleRelocation(Item item)
{
if (item.Submarine?.TeamID == CharacterTeamType.FriendlyNPC)
if (item.SpawnedInCurrentOutpost) { return; }
if (item.Submarine == null) { return; }
// Only affects bots in the player team
if (!Character.IsOnPlayerTeam) { return; }
// Don't relocate if the item is on a sub of the same team
if (item.Submarine.TeamID == Character.TeamID) { return; }
if (itemsToRelocate.Contains(item)) { return; }
itemsToRelocate.Add(item);
if (item.Submarine.ConnectedDockingPorts.TryGetValue(Submarine.MainSub, out DockingPort myPort))
{
if (itemsToRelocate.Contains(item)) { return; }
itemsToRelocate.Add(item);
if (item.Submarine.ConnectedDockingPorts.TryGetValue(Submarine.MainSub, out DockingPort myPort))
{
myPort.OnUnDocked += Relocate;
}
var campaign = GameMain.GameSession.Campaign;
if (campaign != null)
{
// In the campaign mode, undocking happens after leaving the outpost, so we can't use that.
campaign.BeforeLevelLoading += Relocate;
}
myPort.OnUnDocked += Relocate;
}
var campaign = GameMain.GameSession.Campaign;
if (campaign != null)
{
// In the campaign mode, undocking happens after leaving the outpost, so we can't use that.
campaign.BeforeLevelLoading += Relocate;
}
void Relocate()
@@ -907,6 +912,35 @@ namespace Barotrauma
return false;
}
private float draggedTimer;
private float refuseDraggingTimer;
private const float RefuseDraggingAfter = 10.0f;
private const float RefuseDraggingDuration = 30.0f;
private void UpdateDragged(float deltaTime)
{
if (Character.HumanPrefab is { AllowDraggingIndefinitely: true }) { return; }
//don't allow player characters who aren't in the same team to drag us for more than x seconds
if (Character.SelectedBy == null ||
!Character.SelectedBy.IsPlayer ||
Character.SelectedBy.TeamID == Character.TeamID)
{
refuseDraggingTimer -= deltaTime;
return;
}
draggedTimer += deltaTime;
if (draggedTimer > RefuseDraggingAfter ||
(draggedTimer > 0.5f && refuseDraggingTimer > 0.0f))
{
draggedTimer = 0.0f;
refuseDraggingTimer = RefuseDraggingDuration;
Character.SelectedBy.DeselectCharacter();
Character.Speak(TextManager.Get("dialogrefusedragging").Value, delay: 0.5f, identifier: "refusedragging".ToIdentifier(), minDurationBetweenSimilar: 5.0f);
}
}
protected void ReportProblems()
{
Order newOrder = null;
@@ -989,8 +1023,8 @@ namespace Barotrauma
targetHull = hull;
}
}
}
foreach (Item item in Item.ItemList)
}
foreach (Item item in Item.RepairableItems)
{
if (item.CurrentHull != hull) { continue; }
if (AIObjectiveRepairItems.IsValidTarget(item, Character))
@@ -1209,7 +1243,7 @@ namespace Barotrauma
}
else
{
isAttackerInfected = attacker.CharacterHealth.GetAfflictionStrength("alieninfection") > 0;
isAttackerInfected = attacker.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.AlienInfectedType) > 0;
// Inform other NPCs
if (isAttackerInfected || cumulativeDamage > minorDamageThreshold || totalDamage > minorDamageThreshold)
{
@@ -1523,7 +1557,7 @@ namespace Barotrauma
{
margin *= 2;
}
float minCeilingDist = mainCollider.height / 2 + mainCollider.radius + margin;
float minCeilingDist = mainCollider.Height / 2 + mainCollider.Radius + margin;
shouldCrouch = Submarine.PickBody(startPos, startPos + Vector2.UnitY * minCeilingDist, null, Physics.CollisionWall, customPredicate: (fixture) => { return fixture.Body.UserData is not Submarine; }) != null;
}
@@ -1546,23 +1580,19 @@ namespace Barotrauma
public bool NeedsDivingGear(Hull hull, out bool needsSuit)
{
if (!Character.NeedsAir)
{
needsSuit = false;
return false;
}
needsSuit = false;
bool needsAir = Character.NeedsAir && Character.CharacterHealth.OxygenLowResistance < 1;
if (hull == null ||
hull.WaterPercentage > 90 ||
hull.LethalPressure > 0 ||
hull.ConnectedGaps.Any(gap => !gap.IsRoomToRoom && gap.Open > 0.9f))
{
needsSuit = !Character.HasAbilityFlag(AbilityFlags.ImmuneToPressure);
return true;
needsSuit = !Character.IsProtectedFromPressure;
return needsAir || needsSuit;
}
if (hull.WaterPercentage > 60 || hull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 1)
{
return true;
return needsAir;
}
return false;
}
@@ -1641,7 +1671,7 @@ namespace Barotrauma
{
if (otherCharacter == character || otherCharacter.TeamID == character.TeamID || otherCharacter.IsDead ||
otherCharacter.Info?.Job == null ||
!(otherCharacter.AIController is HumanAIController otherHumanAI) ||
otherCharacter.AIController is not HumanAIController otherHumanAI ||
!otherHumanAI.VisibleHulls.Contains(character.CurrentHull))
{
continue;
@@ -1654,10 +1684,10 @@ namespace Barotrauma
float accumulatedDamage = Math.Max(otherHumanAI.structureDamageAccumulator[character], maxAccumulatedDamage);
maxAccumulatedDamage = Math.Max(accumulatedDamage, maxAccumulatedDamage);
if (GameMain.GameSession?.Campaign?.Map?.CurrentLocation != null)
if (GameMain.GameSession?.Campaign?.Map?.CurrentLocation?.Reputation != null && character.IsPlayer)
{
var reputationLoss = damageAmount * Reputation.ReputationLossPerWallDamage;
GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.AddReputation(-reputationLoss);
GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.AddReputation(-reputationLoss, Reputation.MaxReputationLossFromWallDamage);
}
if (accumulatedDamage <= WarningThreshold) { return; }
@@ -1745,12 +1775,14 @@ namespace Barotrauma
}
if (!someoneSpoke)
{
if (!item.StolenDuringRound && GameMain.GameSession?.Campaign?.Map?.CurrentLocation != null)
if (!item.StolenDuringRound &&
Level.Loaded?.Type == LevelData.LevelType.Outpost &&
GameMain.GameSession?.Campaign?.Map?.CurrentLocation != null)
{
var reputationLoss = MathHelper.Clamp(
(item.Prefab.GetMinPrice() ?? 0) * Reputation.ReputationLossPerStolenItemPrice,
Reputation.MinReputationLossPerStolenItem, Reputation.MaxReputationLossPerStolenItem);
GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.AddReputation(-reputationLoss);
GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation?.AddReputation(-reputationLoss);
}
item.StolenDuringRound = true;
otherCharacter.Speak(TextManager.Get("dialogstealwarning").Value, null, Rand.Range(0.5f, 1.0f), "thief".ToIdentifier(), 10.0f);
@@ -1843,7 +1875,7 @@ namespace Barotrauma
}
break;
case "reportbrokendevices":
foreach (var item in Item.ItemList)
foreach (var item in Item.RepairableItems)
{
if (item.CurrentHull != hull) { continue; }
if (AIObjectiveRepairItems.IsValidTarget(item, character))
@@ -1924,11 +1956,12 @@ namespace Barotrauma
bool isCurrentHull = character == Character && character.CurrentHull == hull;
if (hull == null)
{
float hullSafety = character.IsProtectedFromPressure ? 0 : 100;
if (isCurrentHull)
{
CurrentHullSafety = character.NeedsAir ? 0 : 100;
CurrentHullSafety = hullSafety;
}
return CurrentHullSafety;
return hullSafety;
}
if (isCurrentHull && visibleHulls == null)
{
@@ -1936,10 +1969,9 @@ namespace Barotrauma
visibleHulls = VisibleHulls;
}
bool ignoreFire = objectiveManager.CurrentOrder is AIObjectiveExtinguishFires extinguishOrder && extinguishOrder.Priority > 0 || objectiveManager.HasActiveObjective<AIObjectiveExtinguishFire>();
bool ignoreWater = character.IsProtectedFromPressure();
bool ignoreOxygen = HasDivingGear(character);
bool ignoreOxygen = HasDivingGear(character);
bool ignoreEnemies = ObjectiveManager.IsCurrentOrder<AIObjectiveFightIntruders>() || ObjectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>();
float safety = CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies);
float safety = CalculateHullSafety(hull, visibleHulls, character, ignoreWater: false, ignoreOxygen, ignoreFire, ignoreEnemies);
if (isCurrentHull)
{
CurrentHullSafety = safety;
@@ -1949,15 +1981,33 @@ namespace Barotrauma
private static float CalculateHullSafety(Hull hull, IEnumerable<Hull> visibleHulls, Character character, bool ignoreWater = false, bool ignoreOxygen = false, bool ignoreFire = false, bool ignoreEnemies = false)
{
if (hull == null) { return character.NeedsAir ? 0 : 100; }
if (hull.LethalPressure > 0 && character.PressureProtection <= 0 && !character.HasAbilityFlag(AbilityFlags.ImmuneToPressure)) { return 0; }
bool isProtectedFromPressure = character.IsProtectedFromPressure;
if (hull == null) { return isProtectedFromPressure ? 100 : 0; }
if (hull.LethalPressure > 0 && !isProtectedFromPressure) { return 0; }
// Oxygen factor should be 1 with 70% oxygen or more and 0.1 when the oxygen level is 30% or lower.
// With insufficient oxygen, the safety of the hull should be 39, all the other factors aside. So, just below the HULL_SAFETY_THRESHOLD.
float oxygenFactor = ignoreOxygen ? 1 : MathHelper.Lerp((HULL_SAFETY_THRESHOLD - 1) / 100, 1, MathUtils.InverseLerp(HULL_LOW_OXYGEN_PERCENTAGE, 100 - HULL_LOW_OXYGEN_PERCENTAGE, hull.OxygenPercentage));
float waterFactor = ignoreWater ? 1 : MathHelper.Lerp(1, HULL_SAFETY_THRESHOLD / 2 / 100, hull.WaterPercentage / 100);
if (!character.NeedsAir)
float waterFactor = 1;
if (!ignoreWater)
{
if (visibleHulls != null)
{
// Take the visible hulls into account too, because otherwise multi-hull rooms on several floors (with platforms) will yield unexpected results.
float relativeWaterVolume = visibleHulls.Sum(s => s.WaterVolume) / visibleHulls.Sum(s => s.Volume);
waterFactor = MathHelper.Lerp(1, HULL_SAFETY_THRESHOLD / 2 / 100, relativeWaterVolume);
}
else
{
float relativeWaterVolume = hull.WaterVolume / hull.Volume;
waterFactor = MathHelper.Lerp(1, HULL_SAFETY_THRESHOLD / 2 / 100, relativeWaterVolume);
}
}
if (!character.NeedsOxygen || character.CharacterHealth.OxygenLowResistance >= 1)
{
oxygenFactor = 1;
}
if (isProtectedFromPressure)
{
waterFactor = 1;
}
float fireFactor = 1;
@@ -2005,6 +2055,10 @@ namespace Barotrauma
public float GetHullSafety(Hull hull, Character character, IEnumerable<Hull> visibleHulls = null)
{
if (hull == null)
{
return CalculateHullSafety(hull, character, visibleHulls);
}
if (!knownHulls.TryGetValue(hull, out HullSafety hullSafety))
{
hullSafety = new HullSafety(CalculateHullSafety(hull, character, visibleHulls));
@@ -2019,6 +2073,10 @@ namespace Barotrauma
public static float GetHullSafety(Hull hull, IEnumerable<Hull> visibleHulls, Character character, bool ignoreWater = false, bool ignoreOxygen = false, bool ignoreFire = false, bool ignoreEnemies = false)
{
if (hull == null)
{
return CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies);
}
HullSafety hullSafety;
if (character.AIController is HumanAIController controller)
{
@@ -2047,21 +2105,53 @@ namespace Barotrauma
bool sameTeam = me.TeamID == other.TeamID;
bool teamGood = sameTeam || !onlySameTeam && me.IsOnFriendlyTeam(other);
if (!teamGood) { return false; }
if (!me.IsSameSpeciesOrGroup(other)) { return false; }
if (me.TeamID == CharacterTeamType.FriendlyNPC && other.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign)
if (other.IsPet)
{
// Hostile NPCs are hostile to all pets, unless they are in the same team.
if (!sameTeam && me.TeamID == CharacterTeamType.None) { return false; }
}
else
{
if (!me.IsSameSpeciesOrGroup(other)) { return false; }
}
if (GameMain.GameSession?.GameMode is CampaignMode)
{
if ((me.TeamID == CharacterTeamType.FriendlyNPC && other.TeamID == CharacterTeamType.Team1) ||
(me.TeamID == CharacterTeamType.Team1 && other.TeamID == CharacterTeamType.FriendlyNPC))
{
Character npc = me.TeamID == CharacterTeamType.FriendlyNPC ? me : other;
//NPCs that allow some campaign interaction are not turned hostile by low reputation
if (npc.CampaignInteractionType != CampaignMode.InteractionType.None) { return true; }
if (!npc.IsEscorted && npc.AIController is HumanAIController npcAI)
{
return !npcAI.IsInHostileFaction();
}
}
}
return true;
}
public bool IsInHostileFaction()
{
if (GameMain.GameSession?.GameMode is not CampaignMode campaign) { return false; }
Identifier npcFaction = Character.Faction;
Identifier currentLocationFaction = campaign.Map?.CurrentLocation?.Faction?.Prefab.Identifier ?? Identifier.Empty;
if (npcFaction.IsEmpty)
{
//if faction identifier is not specified, assume the NPC is a member of the faction that owns the outpost
npcFaction = currentLocationFaction;
}
if (!currentLocationFaction.IsEmpty && npcFaction == currentLocationFaction)
{
var reputation = campaign.Map?.CurrentLocation?.Reputation;
if (reputation != null && reputation.NormalizedValue < Reputation.HostileThreshold)
{
return false;
return true;
}
}
if (!sameTeam && me.TeamID == CharacterTeamType.None && other.IsPet)
{
// Hostile NPCs are hostile to all pets, unless they are in the same team.
return false;
}
return true;
return false;
}
public static bool IsActive(Character other) => other != null && !other.Removed && !other.IsDead && !other.IsUnconscious;
@@ -26,7 +26,7 @@ namespace Barotrauma
private float findPathTimer;
private const float buttonPressCooldown = 3;
private const float ButtonPressCooldown = 1;
private float checkDoorsTimer;
private float buttonPressTimer;
@@ -96,7 +96,7 @@ namespace Barotrauma
base.Update(speed);
float step = 1.0f / 60.0f;
checkDoorsTimer -= step;
if (lastDoor.door == null || !lastDoor.shouldBeOpen || lastDoor.door.IsOpen)
if (lastDoor.door == null || !lastDoor.shouldBeOpen || lastDoor.door.IsFullyOpen)
{
buttonPressTimer = 0;
}
@@ -211,7 +211,7 @@ namespace Barotrauma
currentTarget = target;
Vector2 currentPos = host.SimPosition;
pathFinder.InsideSubmarine = character.Submarine != null && !character.Submarine.Info.IsRuin;
pathFinder.ApplyPenaltyToOutsideNodes = character.Submarine != null && character.PressureProtection <= 0;
pathFinder.ApplyPenaltyToOutsideNodes = character.Submarine != null && !character.IsProtectedFromPressure;
var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", minGapSize, startNodeFilter, endNodeFilter, nodeFilter, checkVisibility: checkVisibility);
bool useNewPath = needsNewPath || currentPath == null || currentPath.CurrentNode == null || character.Submarine != null && findPathTimer < -1 && Math.Abs(character.AnimController.TargetMovement.Combine()) <= 0;
if (!useNewPath && currentPath?.CurrentNode != null && newPath.Nodes.Any() && !newPath.Unreachable)
@@ -310,7 +310,7 @@ namespace Barotrauma
// Only humanoids can climb ladders
bool canClimb = character.AnimController is HumanoidAnimController;
//if not in water and the waypoint is between the top and bottom of the collider, no need to move vertically
if (canClimb && !character.AnimController.InWater && !character.IsClimbing && diff.Y < collider.height / 2 + collider.radius)
if (canClimb && !character.AnimController.InWater && !character.IsClimbing && diff.Y < collider.Height / 2 + collider.Radius)
{
diff.Y = 0.0f;
}
@@ -342,7 +342,7 @@ namespace Barotrauma
CheckDoorsInPath();
doorsChecked = true;
}
if (buttonPressTimer > 0 && lastDoor.door != null && lastDoor.shouldBeOpen && !lastDoor.door.IsOpen)
if (buttonPressTimer > 0 && lastDoor.door != null && lastDoor.shouldBeOpen && !lastDoor.door.IsFullyOpen)
{
// We have pressed the button and are waiting for the door to open -> Hold still until we can press the button again.
Reset();
@@ -395,7 +395,7 @@ namespace Barotrauma
}
//at the same height as the waypoint
float heightDiff = Math.Abs(collider.SimPosition.Y - currentPath.CurrentNode.SimPosition.Y);
float colliderSize = (collider.height / 2 + collider.radius) * 1.25f;
float colliderSize = (collider.Height / 2 + collider.Radius) * 1.25f;
if (heightDiff < colliderSize)
{
float heightFromFloor = character.AnimController.GetHeightFromFloor();
@@ -510,7 +510,7 @@ namespace Barotrauma
private bool CanAccessDoor(Door door, Func<Controller, bool> buttonFilter = null)
{
if (door.IsBroken) { return true; }
if (!door.IsOpen)
if (door.IsClosed)
{
if (!door.Item.IsInteractable(character)) { return false; }
if (!ShouldBreakDoor(door))
@@ -536,7 +536,7 @@ namespace Barotrauma
}
foreach (var linked in door.Item.linkedTo)
{
if (!(linked is Item linkedItem)) { continue; }
if (linked is not Item linkedItem) { continue; }
var button = linkedItem.GetComponent<Controller>();
if (button == null) { continue; }
if (button.HasAccess(character) && (buttonFilter == null || buttonFilter(button)))
@@ -694,7 +694,7 @@ namespace Barotrauma
if (door.Item.TryInteract(character, forceSelectKey: true))
{
lastDoor = (door, shouldBeOpen);
buttonPressTimer = shouldBeOpen ? buttonPressCooldown : 0;
buttonPressTimer = shouldBeOpen ? ButtonPressCooldown : 0;
}
else
{
@@ -712,7 +712,7 @@ namespace Barotrauma
if (closestButton.Item.TryInteract(character, forceSelectKey: true))
{
lastDoor = (door, shouldBeOpen);
buttonPressTimer = shouldBeOpen ? buttonPressCooldown : 0;
buttonPressTimer = shouldBeOpen ? ButtonPressCooldown : 0;
}
else
{
@@ -785,7 +785,7 @@ namespace Barotrauma
{
if (hull.WaterVolume / hull.Rect.Width > 100.0f)
{
if (!HumanAIController.HasDivingSuit(character))
if (!HumanAIController.HasDivingSuit(character) && character.CharacterHealth.OxygenLowResistance < 1)
{
penalty += 500.0f;
}
@@ -808,7 +808,7 @@ namespace Barotrauma
private float? GetSingleNodePenalty(PathNode node)
{
if (node.Waypoint.isObstructed) { return null; }
if (!node.Waypoint.IsTraversable) { return null; }
if (node.IsBlocked()) { return null; }
float penalty = 0.0f;
if (node.Waypoint.ConnectedGap != null && node.Waypoint.ConnectedGap.Open < 0.9f)
@@ -244,7 +244,7 @@ namespace Barotrauma
else
{
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;
float targetDistance = Math.Max(Math.Max(character.AnimController.Collider.Radius, character.AnimController.Collider.Width), character.AnimController.Collider.Height) * 1.2f;
if (squaredDistance < targetDistance * targetDistance)
{
//close enough to a wall -> attach
@@ -107,7 +107,6 @@ namespace Barotrauma
{
return MentalType.Normal;
}
// test this later
int psychosisIndex = (int)(affliction.Strength / (affliction.Prefab.MaxStrength / MentalTypeCount) * Rand.Range(1f, 1.2f));
psychosisIndex = Math.Clamp(psychosisIndex, 0, 4);
MentalType mentalType = psychosisIndex switch
@@ -83,11 +83,16 @@ namespace Barotrauma
{
if (GameMain.GameSession.RoundDuration < 120.0f &&
speaker?.CurrentHull != null &&
GameMain.GameSession.Map?.CurrentLocation?.Reputation?.Value >= 0.0f &&
(speaker.TeamID == CharacterTeamType.FriendlyNPC || speaker.TeamID == CharacterTeamType.None) &&
Character.CharacterList.Any(c => c.TeamID != speaker.TeamID && c.CurrentHull == speaker.CurrentHull))
{
currentFlags.Add("EnterOutpost".ToIdentifier());
}
if (Level.Loaded.IsEndBiome)
{
currentFlags.Add("EndLevel".ToIdentifier());
}
}
if (GameMain.GameSession.EventManager.CurrentIntensity <= 0.2f)
{
@@ -117,7 +122,7 @@ namespace Barotrauma
foreach (Affliction affliction in afflictions)
{
var currentEffect = affliction.GetActiveEffect();
if (currentEffect != null && !string.IsNullOrEmpty(currentEffect.DialogFlag.Value) && !currentFlags.Contains(currentEffect.DialogFlag))
if (currentEffect is { DialogFlag.IsEmpty: false } && !currentFlags.Contains(currentEffect.DialogFlag))
{
currentFlags.Add(currentEffect.DialogFlag);
}
@@ -126,6 +131,10 @@ namespace Barotrauma
if (speaker.TeamID == CharacterTeamType.FriendlyNPC && speaker.Submarine != null && speaker.Submarine.Info.IsOutpost)
{
currentFlags.Add("OutpostNPC".ToIdentifier());
if (GameMain.GameSession?.Level?.StartLocation?.Faction is Faction faction)
{
currentFlags.Add($"OutpostNPC{faction.Prefab.Identifier}".ToIdentifier());
}
}
if (speaker.CampaignInteractionType != CampaignMode.InteractionType.None)
{
@@ -256,7 +256,9 @@ namespace Barotrauma
if (!AllowOutsideSubmarine && character.Submarine == null) { return false; }
if (AllowInAnySub) { return true; }
if ((AllowInFriendlySubs && character.Submarine.TeamID == CharacterTeamType.FriendlyNPC) || character.IsEscorted) { return true; }
return character.Submarine.TeamID == character.TeamID || character.Submarine.DockedTo.Any(sub => sub.TeamID == character.TeamID);
return character.Submarine.TeamID == character.TeamID ||
character.Submarine.TeamID == character.OriginalTeamID ||
character.Submarine.DockedTo.Any(sub => sub.TeamID == character.TeamID || sub.TeamID == character.OriginalTeamID);
}
}
@@ -648,11 +648,11 @@ namespace Barotrauma
{
statusEffects = statusEffects.Concat(hitEffects);
}
float afflictionsStun = attack.Afflictions.Keys.Sum(a => a.Identifier == "stun" ? a.Strength : 0);
float afflictionsStun = attack.Afflictions.Keys.Sum(a => a.Identifier == AfflictionPrefab.StunType ? a.Strength : 0);
float effectsStun = statusEffects.None() ? 0 : statusEffects.Max(se =>
{
float stunAmount = 0;
var stunAffliction = se.Afflictions.Find(a => a.Identifier == "stun");
var stunAffliction = se.Afflictions.Find(a => a.Identifier == AfflictionPrefab.StunType);
if (stunAffliction != null)
{
stunAmount = stunAffliction.Strength;
@@ -1176,30 +1176,31 @@ namespace Barotrauma
if (sqrDistance > repairTool.Range * repairTool.Range) { return; }
}
float aimFactor = MathHelper.PiOver2 * (1 - AimAccuracy);
if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.Position - Weapon.Position) < MathHelper.PiOver4 + aimFactor)
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);
}
var collisionCategories = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel;
var pickedBody = Submarine.PickBody(Weapon.SimPosition, Enemy.SimPosition, myBodies, collisionCategories, allowInsideFixture: true);
if (pickedBody != null)
// 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, Character.GetRelativeSimPosition(from: Weapon, to: Enemy), myBodies, Physics.CollisionCharacter);
foreach (var body in pickedBodies)
{
Character target = null;
if (pickedBody.UserData is Character c)
if (body.UserData is Character c)
{
target = c;
}
else if (pickedBody.UserData is Limb limb)
else if (body.UserData is Limb limb)
{
target = limb.character;
}
if (target != null && (target == Enemy || !HumanAIController.IsFriendly(target)))
if (target != null && target != Enemy && HumanAIController.IsFriendly(target))
{
UseWeapon(deltaTime);
return;
}
}
UseWeapon(deltaTime);
}
}
}
@@ -4,6 +4,7 @@ using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
@@ -200,7 +201,8 @@ namespace Barotrauma
(container.Item.GetRootContainer()?.OwnInventory?.Locked ?? false) ||
ItemToContain == null || ItemToContain.Removed ||
!ItemToContain.IsOwnedBy(character) || container.Item.GetRootInventoryOwner() is Character c && c != character,
SpeakIfFails = !objectiveManager.IsCurrentOrder<AIObjectiveCleanupItems>()
SpeakIfFails = !objectiveManager.IsCurrentOrder<AIObjectiveCleanupItems>(),
endNodeFilter = n => Vector2.DistanceSquared(n.Waypoint.WorldPosition, container.Item.WorldPosition) <= MathUtils.Pow2(AIObjectiveGetItem.DefaultReach)
},
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref goToObjective));
@@ -19,7 +19,6 @@ namespace Barotrauma
private AIObjectiveGetItem getExtinguisherObjective;
private AIObjectiveGoTo gotoObjective;
private float useExtinquisherTimer;
public AIObjectiveExtinguishFire(Character character, Hull targetHull, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
@@ -44,7 +43,8 @@ namespace Barotrauma
}
else
{
float yDist = Math.Abs(character.WorldPosition.Y - targetHull.WorldPosition.Y);
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));
@@ -119,24 +119,18 @@ namespace Barotrauma
Abandon = true;
break;
}
float xDist = Math.Abs(character.WorldPosition.X - fs.WorldPosition.X) - fs.DamageRange;
float yDist = Math.Abs(character.WorldPosition.Y - fs.WorldPosition.Y);
bool inRange = xDist + yDist < extinguisher.Range;
// Use the hull position, because the fire x pos is sometimes inside a wall -> the bot can't ever see it and continues running towards the wall.
ISpatialEntity lookTarget = character.CurrentHull == targetHull || character.CurrentHull.linkedTo.Contains(targetHull) ? targetHull : fs as ISpatialEntity;
bool move = !inRange || !character.CanSeeTarget(lookTarget);
if ((inRange && character.CanSeeTarget(lookTarget)) || useExtinquisherTimer > 0)
float xDist = Math.Abs(character.WorldPosition.X - fs.WorldPosition.X);
float yDist = Math.Abs(character.CurrentHull.WorldPosition.Y - targetHull.WorldPosition.Y);
float dist = xDist + yDist;
bool inRange = dist < extinguisher.Range;
bool isInDamageRange = fs.IsInDamageRange(character, fs.DamageRange) && character.CanSeeTarget(targetHull);
bool moveCloser = !isInDamageRange && (!inRange || !character.CanSeeTarget(targetHull));
bool operateExtinguisher = !moveCloser || (dist < extinguisher.Range * 1.2f && character.CanSeeTarget(targetHull));
if (operateExtinguisher)
{
useExtinquisherTimer += deltaTime;
if (useExtinquisherTimer > 2.0f)
{
useExtinquisherTimer = 0.0f;
}
// Aim
character.CursorPosition = fs.Position;
Vector2 fromCharacterToFireSource = fs.WorldPosition - character.WorldPosition;
float dist = fromCharacterToFireSource.Length();
character.CursorPosition += VectorExtensions.Forward(extinguisherItem.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, dist / 2);
character.CursorPosition += VectorExtensions.Forward(extinguisherItem.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, fromCharacterToFireSource.Length() / 2);
if (extinguisherItem.RequireAimToUse)
{
character.SetInput(InputType.Aim, false, true);
@@ -148,25 +142,29 @@ namespace Barotrauma
{
character.Speak(TextManager.GetWithVariable("DialogPutOutFire", "[roomname]", targetHull.DisplayName, FormatCapitals.Yes).Value, null, 0, "putoutfire".ToIdentifier(), 10.0f);
}
// Prevents running into the flames.
objectiveManager.CurrentObjective.ForceWalk = true;
}
if (move)
if (moveCloser)
{
//go to the first firesource
if (TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(fs, character, objectiveManager, closeEnough: Math.Max(fs.DamageRange, extinguisher.Range * 0.7f))
{
DialogueIdentifier = "dialogcannotreachfire".ToIdentifier(),
TargetName = fs.Hull.DisplayName
},
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref gotoObjective)))
if (TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(fs, character, objectiveManager, closeEnough: extinguisher.Range * 0.8f)
{
DialogueIdentifier = "dialogcannotreachfire".ToIdentifier(),
TargetName = fs.Hull.DisplayName,
},
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref gotoObjective)))
{
gotoObjective.requiredCondition = () => character.CanSeeTarget(targetHull);
}
}
else
else if (!operateExtinguisher || isInDamageRange)
{
character.AIController.SteeringManager.Reset();
// Don't walk into the flames.
RemoveSubObjective(ref gotoObjective);
SteeringManager.Reset();
}
// Only target one fire source at the time.
break;
}
}
@@ -177,8 +175,20 @@ namespace Barotrauma
base.Reset();
getExtinguisherObjective = null;
gotoObjective = null;
useExtinquisherTimer = 0;
sinTime = 0;
SteeringManager.Reset();
}
protected override void OnCompleted()
{
base.OnCompleted();
SteeringManager.Reset();
}
protected override void OnAbandon()
{
base.OnAbandon();
SteeringManager.Reset();
}
}
}
@@ -24,7 +24,7 @@ namespace Barotrauma
protected override float TargetEvaluation()
{
if (Targets.None()) { return 0; }
if (!character.IsOnPlayerTeam) { return 100; }
if (!character.IsOnPlayerTeam && !character.IsOriginallyOnPlayerTeam) { return 100; }
if (character.IsSecurity) { return 100; }
if (objectiveManager.IsOrder(this)) { return 100; }
// If there's any security officers onboard, leave fighting for them.
@@ -66,7 +66,13 @@ namespace Barotrauma
if (target.CurrentHull == null) { return false; }
if (HumanAIController.IsFriendly(character, target)) { return false; }
if (!character.Submarine.IsConnectedTo(target.Submarine)) { return false; }
if (!targetCharactersInOtherSubs && character.Submarine.TeamID != target.Submarine.TeamID) { return false; }
if (!targetCharactersInOtherSubs)
{
if (character.Submarine.TeamID != target.Submarine.TeamID && character.OriginalTeamID != target.Submarine.TeamID)
{
return false;
}
}
if (target.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI)) { return false; }
if (target.IsArrested) { return false; }
if (EnemyAIController.IsLatchedToSomeoneElse(target, character)) { return false; }
@@ -47,19 +47,12 @@ namespace Barotrauma
}
if (character.CurrentHull == null)
{
if (!character.NeedsAir)
{
Priority = 0;
}
else
{
Priority = (
objectiveManager.HasOrder<AIObjectiveGoTo>(o => o.Priority > 0) ||
objectiveManager.HasOrder<AIObjectiveReturn>(o => o.Priority > 0) ||
objectiveManager.HasActiveObjective<AIObjectiveRescue>() ||
objectiveManager.Objectives.Any(o => o is AIObjectiveCombat && o.Priority > 0))
&& HumanAIController.HasDivingSuit(character) ? 0 : 100;
}
Priority = (
objectiveManager.HasOrder<AIObjectiveGoTo>(o => o.Priority > 0) ||
objectiveManager.HasOrder<AIObjectiveReturn>(o => o.Priority > 0) ||
objectiveManager.HasActiveObjective<AIObjectiveRescue>() ||
objectiveManager.Objectives.Any(o => o is AIObjectiveCombat && o.Priority > 0))
&& ((character.IsImmuneToPressure && !character.IsLowInOxygen)|| HumanAIController.HasDivingSuit(character)) ? 0 : 100;
}
else
{
@@ -118,6 +111,11 @@ namespace Barotrauma
if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD)
{
Priority -= priorityDecrease * deltaTime;
if (currenthullSafety >= 100)
{
// Reduce the priority to zero so that the bot can get switch to other objectives immediately, e.g. when entering the airlock.
Priority = 0;
}
}
else
{
@@ -140,8 +138,8 @@ namespace Barotrauma
{
if (resetPriority) { return; }
var currentHull = character.CurrentHull;
bool dangerousPressure = !character.IsProtectedFromPressure && (currentHull == null || currentHull.LethalPressure > 0);
bool shouldActOnSuffocation = character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false);
bool dangerousPressure = currentHull == null || currentHull.LethalPressure > 0 && character.PressureProtection <= 0;
if (!character.LockHands && (!dangerousPressure || shouldActOnSuffocation || cannotFindSafeHull))
{
bool needsDivingGear = HumanAIController.NeedsDivingGear(currentHull, out bool needsDivingSuit);
@@ -221,7 +219,11 @@ namespace Barotrauma
TryAddSubObjective(ref goToObjective,
constructor: () => new AIObjectiveGoTo(currentSafeHull, character, objectiveManager, getDivingGearIfNeeded: true)
{
AllowGoingOutside = HumanAIController.HasDivingSuit(character, conditionPercentage: 50)
AllowGoingOutside =
character.IsProtectedFromPressure ||
character.CurrentHull == null ||
character.CurrentHull.IsTaggedAirlock() ||
character.CurrentHull.LeadsOutside(character)
},
onCompleted: () =>
{
@@ -352,8 +354,8 @@ namespace Barotrauma
//tends to make the method much faster, because we find a potential hull earlier and can discard further-away hulls more easily
//(for instance, an NPC in an outpost might otherwise go through all the hulls in the main sub first and do tons of expensive
//path calculations, only to discard all of them when going through the hulls in the outpost)
float hullSuitability = EstimateHullSuitability(character, hull);
if (!hulls.Any())
float hullSuitability = EstimateHullSuitability(character, hull);
if (hulls.None())
{
hulls.Add(hull);
}
@@ -448,9 +450,12 @@ namespace Barotrauma
{
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 distance = Vector2.DistanceSquared(character.WorldPosition, potentialHull.WorldPosition);
float distanceFactor = MathHelper.Lerp(1, 0.2f, MathUtils.InverseLerp(0, MathUtils.Pow(100000, 2), distance));
float distanceFactor = MathHelper.Lerp(1, 0.2f, MathUtils.InverseLerp(0, 10000, distance));
hullSafety *= distanceFactor;
// If the target is not inside a friendly submarine, considerably reduce the hull safety.
// Intentionally exclude wrecks from this check
@@ -155,17 +155,21 @@ namespace Barotrauma
bool canOperate = toLeak.LengthSquared() < reach * reach;
if (canOperate)
{
TryAddSubObjective(ref operateObjective, () => new AIObjectiveOperateItem(repairTool, character, objectiveManager, option: Identifier.Empty, requireEquip: true, operateTarget: Leak),
onAbandon: () => Abandon = true,
onCompleted: () =>
TryAddSubObjective(ref operateObjective, () => new AIObjectiveOperateItem(repairTool, character, objectiveManager, option: Identifier.Empty, requireEquip: true, operateTarget: Leak)
{
// Use an empty filter to override the default
EndNodeFilter = n => true
},
onAbandon: () => Abandon = true,
onCompleted: () =>
{
if (CheckObjectiveSpecific()) { IsCompleted = true; }
else
{
if (CheckObjectiveSpecific()) { IsCompleted = true; }
else
{
// Failed to operate. Probably too far.
Abandon = true;
}
});
// Failed to operate. Probably too far.
Abandon = true;
}
});
}
else
{
@@ -123,6 +123,11 @@ namespace Barotrauma
return ignoredTags;
}
public static Func<PathNode, bool> CreateEndNodeFilter(ISpatialEntity targetEntity)
{
return n => (n.Waypoint.Ladders == null || n.Waypoint.IsInWater) && Vector2.DistanceSquared(n.Waypoint.WorldPosition, targetEntity.WorldPosition) <= MathUtils.Pow2(DefaultReach);
}
private bool CheckInventory()
{
if (IdentifiersOrTags == null) { return false; }
@@ -155,11 +160,6 @@ namespace Barotrauma
Abandon = true;
return;
}
if (character.Submarine == null)
{
Abandon = true;
return;
}
if (IdentifiersOrTags != null && !isDoneSeeking)
{
if (checkInventory)
@@ -171,9 +171,14 @@ namespace Barotrauma
}
if (!isDoneSeeking)
{
if (character.Submarine == null)
{
Abandon = true;
return;
}
if (!AllowDangerousPressure)
{
bool dangerousPressure = character.CurrentHull == null || character.CurrentHull.LethalPressure > 0 && character.PressureProtection <= 0;
bool dangerousPressure = !character.IsProtectedFromPressure && (character.CurrentHull == null || character.CurrentHull.LethalPressure > 0);
if (dangerousPressure)
{
#if DEBUG
@@ -192,6 +197,11 @@ namespace Barotrauma
return;
}
}
else if (character.Submarine == null)
{
Abandon = true;
return;
}
if (targetItem == null || targetItem.Removed)
{
#if DEBUG
@@ -307,7 +317,8 @@ namespace Barotrauma
{
// If the root container changes, the item is no longer where it was (taken by someone -> need to find another item)
AbortCondition = obj => targetItem == null || targetItem.GetRootInventoryOwner() != moveToTarget,
SpeakIfFails = false
SpeakIfFails = false,
endNodeFilter = CreateEndNodeFilter(moveToTarget)
};
},
onAbandon: () =>
@@ -33,6 +33,11 @@ namespace Barotrauma
public bool DebugLogWhenFails { get; set; } = true;
public bool UsePathingOutside { get; set; } = true;
/// <summary>
/// Which event action created this objective (if any)
/// </summary>
public EventAction SourceEventAction;
public float ExtraDistanceWhileSwimming;
public float ExtraDistanceOutsideSub;
private float _closeEnoughMultiplier = 1;
@@ -45,6 +50,7 @@ namespace Barotrauma
private readonly float minDistance = 50;
private readonly float seekGapsInterval = 1;
private float seekGapsTimer;
private bool cantFindDivingGear;
/// <summary>
/// Display units
@@ -85,7 +91,7 @@ namespace Barotrauma
/// </summary>
public bool UseDistanceRelativeToAimSourcePos { get; set; } = false;
public override bool AbandonWhenCannotCompleteSubjectives => !repeat;
public override bool AbandonWhenCannotCompleteSubjectives => false;
public override bool AllowOutsideSubmarine => AllowGoingOutside;
public override bool AllowInAnySub => true;
@@ -258,48 +264,73 @@ namespace Barotrauma
}
if (!Abandon)
{
if (getDivingGearIfNeeded && !character.LockHands)
if (getDivingGearIfNeeded)
{
Character followTarget = Target as Character;
bool needsDivingSuit = (!isInside || hasOutdoorNodes) && character.NeedsAir && !character.HasAbilityFlag(AbilityFlags.ImmuneToPressure);
bool needsDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit);
if (Mimic)
bool needsDivingSuit = (!isInside || hasOutdoorNodes) && !character.IsImmuneToPressure;
bool tryToGetDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit);
bool tryToGetDivingSuit = needsDivingSuit;
if (Mimic && !character.IsImmuneToPressure)
{
if (HumanAIController.HasDivingSuit(followTarget))
{
needsDivingGear = true;
needsDivingSuit = true;
tryToGetDivingGear = true;
tryToGetDivingSuit = true;
}
else if (HumanAIController.HasDivingMask(followTarget))
else if (HumanAIController.HasDivingMask(followTarget) && character.CharacterHealth.OxygenLowResistance < 1)
{
needsDivingGear = true;
tryToGetDivingGear = true;
}
}
bool needsEquipment = false;
float minOxygen = AIObjectiveFindDivingGear.GetMinOxygen(character);
if (needsDivingSuit)
if (tryToGetDivingSuit)
{
needsEquipment = !HumanAIController.HasDivingSuit(character, minOxygen);
}
else if (needsDivingGear)
else if (tryToGetDivingGear)
{
needsEquipment = !HumanAIController.HasDivingGear(character, minOxygen);
}
if (needsEquipment)
if (character.LockHands)
{
cantFindDivingGear = true;
}
if (cantFindDivingGear && needsDivingSuit)
{
// Don't try to reach the target without a suit because it's lethal.
Abandon = true;
return;
}
if (needsEquipment && !cantFindDivingGear)
{
SteeringManager.Reset();
if (findDivingGear != null && !findDivingGear.CanBeCompleted)
{
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager),
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref findDivingGear));
}
else
{
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager),
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref findDivingGear));
}
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: tryToGetDivingSuit, objectiveManager),
onAbandon: () =>
{
cantFindDivingGear = true;
if (needsDivingSuit)
{
// Shouldn't try to reach the target without a suit, because it's lethal.
Abandon = true;
}
else
{
// Try again without requiring the diving suit
RemoveSubObjective(ref findDivingGear);
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager),
onAbandon: () =>
{
Abandon = character.CurrentHull != null && (objectiveManager.CurrentOrder != this || Target.Submarine == null);
RemoveSubObjective(ref findDivingGear);
},
onCompleted: () =>
{
RemoveSubObjective(ref findDivingGear);
});
}
},
onCompleted: () => RemoveSubObjective(ref findDivingGear));
return;
}
}
@@ -593,7 +624,7 @@ namespace Barotrauma
}
else if (target is Character c)
{
return c.CurrentHull;
return c.CurrentHull ?? c.AnimController.CurrentHull;
}
else if (target is Structure structure)
{
@@ -170,7 +170,8 @@ namespace Barotrauma
TargetHull = character.CurrentHull;
}
if (behavior == BehaviorType.StayInHull)
bool currentTargetIsInvalid = currentTarget == null || IsForbidden(currentTarget) || (PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull)));
if (behavior == BehaviorType.StayInHull && !currentTargetIsInvalid)
{
currentTarget = TargetHull;
bool stayInHull = character.CurrentHull == currentTarget && IsSteeringFinished() && !character.IsClimbing;
@@ -190,9 +191,6 @@ namespace Barotrauma
}
else
{
bool currentTargetIsInvalid = currentTarget == null || IsForbidden(currentTarget) ||
(PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull)));
if (currentTarget != null && !currentTargetIsInvalid)
{
if (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted)
@@ -98,7 +98,7 @@ namespace Barotrauma
{
foreach (var item in itemContainer.ContainableItems)
{
if (CheckStatusEffects(item.statusEffects) == CheckStatus.Finished)
if (CheckStatusEffects(item.StatusEffects) == CheckStatus.Finished)
{
return CheckStatus.Finished;
}
@@ -23,6 +23,11 @@ namespace Barotrauma
private AIObjectiveGoTo goToObjective;
private AIObjectiveGetItem getItemObjective;
/// <summary>
/// If undefined, a default filter will be used.
/// </summary>
public Func<PathNode, bool> EndNodeFilter;
public bool Override { get; set; } = true;
public override bool CanBeCompleted => base.CanBeCompleted && (!useController || controller != null);
@@ -222,7 +227,7 @@ namespace Barotrauma
{
target.Item.TryInteract(character, forceSelectKey: true);
}
if (component.AIOperate(deltaTime, character, this))
if (component.CrewAIOperate(deltaTime, character, this))
{
isDoneOperating = completionCondition == null || completionCondition();
}
@@ -232,7 +237,7 @@ namespace Barotrauma
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(target.Item, character, objectiveManager, closeEnough: 50)
{
TargetName = target.Item.Name,
endNodeFilter = node => node.Waypoint.Ladders == null
endNodeFilter = EndNodeFilter ?? AIObjectiveGetItem.CreateEndNodeFilter(target.Item)
},
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref goToObjective));
@@ -290,7 +295,7 @@ namespace Barotrauma
}
return;
}
if (component.AIOperate(deltaTime, character, this))
if (component.CrewAIOperate(deltaTime, character, this))
{
isDoneOperating = completionCondition == null || completionCondition();
}
@@ -17,7 +17,6 @@ namespace Barotrauma
private AIObjectiveGoTo goToObjective;
private AIObjectiveContainItem refuelObjective;
private float previousCondition = -1;
private RepairTool repairTool;
private const float WaitTimeBeforeRepair = 0.5f;
@@ -196,15 +195,7 @@ namespace Barotrauma
Abandon = true;
}
}
if (previousCondition == -1)
{
previousCondition = Item.Condition;
}
else if (Item.Condition < previousCondition)
{
// If the current condition is less than the previous condition, we can't complete the task, so let's abandon it. The item is probably deteriorating at a greater speed than we can repair it.
Abandon = true;
}
CheckPreviousCondition(deltaTime);
}
if (Abandon)
{
@@ -229,7 +220,6 @@ namespace Barotrauma
TryAddSubObjective(ref goToObjective,
constructor: () =>
{
previousCondition = -1;
var objective = new AIObjectiveGoTo(Item, character, objectiveManager)
{
TargetName = Item.Name
@@ -251,6 +241,27 @@ namespace Barotrauma
}
}
private const float conditionCheckDelay = 1;
private float conditionCheckTimer;
private float previousCondition;
private void CheckPreviousCondition(float deltaTime)
{
if (Item == null || Item.Removed) { return; }
conditionCheckTimer -= deltaTime;
if (conditionCheckTimer > 0) { return; }
conditionCheckTimer = conditionCheckDelay;
if (previousCondition > -1 && Item.Condition < previousCondition)
{
// If the current condition is less than the previous condition, we can't complete the task, so let's abandon it. The item is probably deteriorating at a greater speed than we can repair it.
Abandon = true;
}
else
{
// If the previous condition is not yet stored or if it's valid (greater or equal to current condition), save the condition for the next check here.
previousCondition = Item.Condition;
}
}
private void FindRepairTool()
{
foreach (Repairable repairable in Item.Repairables)
@@ -303,7 +314,6 @@ namespace Barotrauma
base.Reset();
goToObjective = null;
refuelObjective = null;
previousCondition = -1;
repairTool = null;
}
}
@@ -139,14 +139,14 @@ namespace Barotrauma
recursive: true);
}
}
if (character.Submarine != null)
if (character.Submarine != null && targetCharacter.CurrentHull != null)
{
if (HumanAIController.GetHullSafety(targetCharacter.CurrentHull, targetCharacter) < HumanAIController.HULL_SAFETY_THRESHOLD)
{
// Incapacitated target is not in a safe place -> Move to a safe place first
if (character.SelectedCharacter != targetCharacter)
{
if (targetCharacter.CurrentHull != null && HumanAIController.VisibleHulls.Contains(targetCharacter.CurrentHull) && targetCharacter.CurrentHull.DisplayName != null)
if (HumanAIController.VisibleHulls.Contains(targetCharacter.CurrentHull) && targetCharacter.CurrentHull.DisplayName != null)
{
character.Speak(TextManager.GetWithVariables("DialogFoundUnconsciousTarget",
("[targetname]", targetCharacter.Name, FormatCapitals.No),
@@ -71,11 +71,11 @@ namespace Barotrauma
{
float strength = character.CharacterHealth.GetPredictedStrength(affliction, predictFutureDuration: 10.0f);
vitality -= affliction.GetVitalityDecrease(character.CharacterHealth, strength) / character.MaxVitality * 100;
if (affliction.Prefab.AfflictionType == "paralysis")
if (affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType)
{
vitality -= affliction.Strength;
}
else if (affliction.Prefab.AfflictionType == "poison")
else if (affliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType)
{
vitality -= affliction.Strength;
}
@@ -12,6 +12,9 @@ namespace Barotrauma
private AIObjectiveGoTo moveInsideObjective, moveOutsideObjective;
private bool usingEscapeBehavior, isSteeringThroughGap;
public override bool AllowOutsideSubmarine => true;
public override bool AllowInAnySub => true;
public AIObjectiveReturn(Character character, Character orderGiver, AIObjectiveManager objectiveManager, float priorityModifier = 1.0f) : base(character, objectiveManager, priorityModifier)
{
ReturnTarget = GetReturnTarget(Submarine.MainSubs) ?? GetReturnTarget(Submarine.Loaded);
@@ -465,6 +465,10 @@ namespace Barotrauma
public readonly OrderTarget TargetPosition;
private ISpatialEntity targetSpatialEntity;
/// <summary>
/// Note this property doesn't return the follow target of the Follow objective, as expected!
/// </summary>
public ISpatialEntity TargetSpatialEntity
{
get
@@ -348,6 +348,7 @@ namespace Barotrauma
{
if (body.UserData is Submarine) { return false; }
if (body.UserData is Structure s && !s.IsPlatform) { return false; }
if (body.UserData is Voronoi2.VoronoiCell) { return false; }
if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { return false; }
}
}
@@ -47,7 +47,8 @@ namespace Barotrauma
public void SetOrder(Character orderedCharacter)
{
OrderedCharacter = orderedCharacter;
if (OrderedCharacter.AIController is HumanAIController humanAI && humanAI.ObjectiveManager.CurrentOrders.None(o => o.MatchesOrder(SuggestedOrder.Identifier, Option)))
if (OrderedCharacter.AIController is HumanAIController humanAI &&
humanAI.ObjectiveManager.CurrentOrders.None(o => o.MatchesOrder(SuggestedOrder.Identifier, Option) && o.TargetEntity == TargetItem))
{
if (orderedCharacter != CommandingCharacter)
{
@@ -17,7 +17,20 @@ namespace Barotrauma
float GetTargetingImportance(Entity entity)
{
float currentDistanceToEnemy = Vector2.Distance(entity.WorldPosition, TargetItem.WorldPosition);
return MathHelper.Clamp(100 - (currentDistanceToEnemy / 100f), MinImportance, MaxImportance);
float importance = MathHelper.Clamp(100 - (currentDistanceToEnemy / 100f), MinImportance, MaxImportance * 0.5f);
if (TargetItem.Submarine != null)
{
Vector2 dir = entity.WorldPosition - TargetItem.WorldPosition;
Vector2 submarineDir = TargetItem.WorldPosition - TargetItem.Submarine.WorldPosition;
if (Vector2.Dot(dir, submarineDir) < 0)
{
//direction from the weapon to the target is opposite to the direction from the sub to the weapon
// = the turret is most likely on the wrong side of the sub, reduce importance
importance *= 0.1f;
}
}
return importance;
}
public override void CalculateImportanceSpecific()
@@ -229,7 +229,7 @@ namespace Barotrauma
#if DEBUG
ShipCommandLog("Current importance for " + shipIssueWorker + " was " + importance + " and it was already being attended by " + shipIssueWorker.OrderedCharacter);
#endif
attendedIssues.Add(shipIssueWorker);
InsertIssue(shipIssueWorker, attendedIssues);
}
else
{
@@ -237,12 +237,19 @@ namespace Barotrauma
ShipCommandLog("Current importance for " + shipIssueWorker + " was " + importance + " and it is not attended to");
#endif
shipIssueWorker.RemoveOrder();
availableIssues.Add(shipIssueWorker);
InsertIssue(shipIssueWorker, availableIssues);
}
}
availableIssues.Sort((x, y) => y.Importance.CompareTo(x.Importance));
attendedIssues.Sort((x, y) => x.Importance.CompareTo(y.Importance));
static void InsertIssue(ShipIssueWorker issue, List<ShipIssueWorker> list)
{
int index = 0;
while (index < list.Count && list[index].Importance > issue.Importance)
{
index++;
}
list.Insert(index, issue);
}
ShipIssueWorker mostImportantIssue = availableIssues.FirstOrDefault();
@@ -13,6 +13,7 @@ namespace Barotrauma
private readonly float minDistFromClosest;
private readonly float maxDistFromCenter;
private readonly float cohesion;
public bool ForceActive { get; private set; }
public List<AICharacter> Members { get; private set; } = new List<AICharacter>();
public HashSet<AICharacter> ActiveMembers { get; private set; } = new HashSet<AICharacter>();
@@ -26,9 +27,10 @@ namespace Barotrauma
public SwarmBehavior(XElement element, EnemyAIController ai)
{
this.ai = ai;
minDistFromClosest = ConvertUnits.ToSimUnits(element.GetAttributeFloat("mindistfromclosest", 10.0f));
maxDistFromCenter = ConvertUnits.ToSimUnits(element.GetAttributeFloat("maxdistfromcenter", 1000.0f));
cohesion = element.GetAttributeFloat("cohesion", 1) / 10;
minDistFromClosest = ConvertUnits.ToSimUnits(element.GetAttributeFloat(nameof(minDistFromClosest), 10.0f));
maxDistFromCenter = ConvertUnits.ToSimUnits(element.GetAttributeFloat(nameof(maxDistFromCenter), 1000.0f));
cohesion = element.GetAttributeFloat(nameof(cohesion), 1) / 10;
ForceActive = element.GetAttributeBool(nameof(ForceActive), false);
}
public static void CreateSwarm(IEnumerable<AICharacter> swarm)
@@ -8,16 +8,90 @@ using System;
namespace Barotrauma
{
partial class WreckAI : IServerSerializable
internal class SubmarineTurretAI
{
public Submarine Wreck { get; private set; }
public Submarine Submarine { get; protected set; }
protected readonly List<Turret> turrets = new List<Turret>();
public Identifier FriendlyTag;
public SubmarineTurretAI(Submarine submarine, Identifier friendlyTag = default)
{
FriendlyTag = friendlyTag;
Submarine = submarine;
foreach (Item item in Item.ItemList)
{
if (item.Submarine != Submarine) { continue; }
var turret = item.GetComponent<Turret>();
if (turret != null)
{
turrets.Add(turret);
// Set false, because we manage the turrets in the Update method.
turret.AutoOperate = false;
// Set to full condition, because items don't work when they are broken.
turret.Item.Condition = turret.Item.MaxCondition;
foreach (MapEntity linkedEntity in turret.Item.linkedTo)
{
if (linkedEntity is Item linkedItem)
{
linkedItem.Condition = linkedItem.MaxCondition;
}
}
}
}
LoadAllTurrets();
}
public virtual void Update(float deltaTime)
{
if (Submarine == null || Submarine.Removed) { return; }
OperateTurrets(deltaTime, FriendlyTag);
}
protected virtual void LoadAllTurrets()
{
foreach (var turret in turrets)
{
LoadTurret(turret);
}
}
protected void LoadTurret(Turret turret, Func<ItemPrefab, bool> ammoFilter = null)
{
foreach (var linkedItem in turret.Item.GetLinkedEntities<Item>())
{
var container = linkedItem.GetComponent<ItemContainer>();
if (container == null) { continue; }
for (int i = 0; i < container.Inventory.Capacity; i++)
{
if (container.Inventory.GetItemAt(i) != null) { continue; }
if (MapEntityPrefab.List.GetRandom(e => e is ItemPrefab ip && container.CanBeContained(ip, i) && (ammoFilter == null || ammoFilter(ip)), Rand.RandSync.ServerAndClient) is ItemPrefab ammoPrefab)
{
Item ammo = new Item(ammoPrefab, container.Item.WorldPosition, Submarine);
if (!container.Inventory.TryPutItem(ammo, i, allowSwapping: false, allowCombine: false, user: null, createNetworkEvent: false))
{
turret.Item.Remove();
}
}
}
}
}
protected void OperateTurrets(float deltaTime, Identifier friendlyTag)
{
foreach (var turret in turrets)
{
turret.UpdateAutoOperate(deltaTime, friendlyTag);
}
}
}
partial class WreckAI : SubmarineTurretAI, IServerSerializable
{
public bool IsAlive { get; private set; }
private readonly List<Item> allItems;
private readonly List<Item> thalamusItems;
private readonly List<Structure> thalamusStructures;
private readonly List<Turret> turrets = new List<Turret>();
private readonly List<WayPoint> wayPoints = new List<WayPoint>();
private readonly List<Hull> hulls = new List<Hull>();
private readonly List<Item> spawnOrgans = new List<Item>();
@@ -25,7 +99,7 @@ namespace Barotrauma
private bool initialCellsSpawned;
public readonly WreckAIConfig Config;
public WreckAIConfig Config { get; private set; }
private bool IsClient => GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient;
@@ -44,15 +118,10 @@ namespace Barotrauma
return wreckAI;
}
private WreckAI(Submarine wreck)
private WreckAI(Submarine wreck) : base(wreck)
{
Wreck = wreck;
Config = WreckAIConfig.GetRandom();
if (Config == null)
{
DebugConsole.ThrowError("WreckAI: No wreck AI config found!");
return;
}
GetConfig();
if (Config == null) { return; }
var thalamusPrefabs = ItemPrefab.Prefabs.Where(p => IsThalamus(p));
var brainPrefab = thalamusPrefabs.GetRandom(i => i.Tags.Contains(Config.Brain), Rand.RandSync.ServerAndClient);
if (brainPrefab == null)
@@ -60,20 +129,20 @@ namespace Barotrauma
DebugConsole.ThrowError($"WreckAI: Could not find any brain prefab with the tag {Config.Brain}! Cannot continue. Failed to create wreck AI.");
return;
}
allItems = Wreck.GetItems(false);
allItems = wreck.GetItems(false);
thalamusItems = allItems.FindAll(i => IsThalamus(((MapEntity)i).Prefab));
hulls.AddRange(Wreck.GetHulls(false));
hulls.AddRange(wreck.GetHulls(false));
var potentialBrainHulls = new List<(Hull hull, float weight)>();
brain = new Item(brainPrefab, Vector2.Zero, Wreck);
brain = new Item(brainPrefab, Vector2.Zero, wreck);
thalamusItems.Add(brain);
Point minSize = brain.Rect.Size.Multiply(brain.Scale);
// Bigger hulls are allowed, but not preferred more than what's sufficent.
Vector2 sufficentSize = new Vector2(minSize.X * 2, minSize.Y * 1.1f);
// Shrink the horizontal axis so that the brain is not placed in the left or right side, where we often have curved walls.
Rectangle shrinkedBounds = ToolBox.GetWorldBounds(Wreck.WorldPosition.ToPoint(), new Point(Wreck.Borders.Width - 500, Wreck.Borders.Height));
Rectangle shrinkedBounds = ToolBox.GetWorldBounds(wreck.WorldPosition.ToPoint(), new Point(wreck.Borders.Width - 500, wreck.Borders.Height));
foreach (Hull hull in hulls)
{
float distanceFromCenter = Vector2.Distance(Wreck.WorldPosition, hull.WorldPosition);
float distanceFromCenter = Vector2.Distance(wreck.WorldPosition, hull.WorldPosition);
float distanceFactor = MathHelper.Lerp(1.0f, 0.5f, MathUtils.InverseLerp(0, Math.Max(shrinkedBounds.Width, shrinkedBounds.Height) / 2, distanceFromCenter));
float horizontalSizeFactor = MathHelper.Lerp(0.5f, 1.0f, MathUtils.InverseLerp(minSize.X, sufficentSize.X, hull.Rect.Width));
float verticalSizeFactor = MathHelper.Lerp(0.5f, 1.0f, MathUtils.InverseLerp(minSize.Y, sufficentSize.Y, hull.Rect.Height));
@@ -121,7 +190,7 @@ namespace Barotrauma
var backgroundPrefab = thalamusStructurePrefabs.GetRandom(i => i.Tags.Contains(Config.BrainRoomBackground), Rand.RandSync.ServerAndClient);
if (backgroundPrefab != null)
{
new Structure(brainHull.Rect, backgroundPrefab, Wreck);
new Structure(brainHull.Rect, backgroundPrefab, wreck);
}
var horizontalWallPrefab = thalamusStructurePrefabs.GetRandom(p => p.Tags.Contains(Config.BrainRoomHorizontalWall), Rand.RandSync.ServerAndClient);
if (horizontalWallPrefab != null)
@@ -129,8 +198,8 @@ namespace Barotrauma
int height = (int)horizontalWallPrefab.Size.Y;
int halfHeight = height / 2;
int quarterHeight = halfHeight / 2;
new Structure(new Rectangle(brainHull.Rect.Left, brainHull.Rect.Top + quarterHeight, brainHull.Rect.Width, height), horizontalWallPrefab, Wreck);
new Structure(new Rectangle(brainHull.Rect.Left, brainHull.Rect.Top - brainHull.Rect.Height + halfHeight + quarterHeight, brainHull.Rect.Width, height), horizontalWallPrefab, Wreck);
new Structure(new Rectangle(brainHull.Rect.Left, brainHull.Rect.Top + quarterHeight, brainHull.Rect.Width, height), horizontalWallPrefab, wreck);
new Structure(new Rectangle(brainHull.Rect.Left, brainHull.Rect.Top - brainHull.Rect.Height + halfHeight + quarterHeight, brainHull.Rect.Width, height), horizontalWallPrefab, wreck);
}
var verticalWallPrefab = thalamusStructurePrefabs.GetRandom(p => p.Tags.Contains(Config.BrainRoomVerticalWall), Rand.RandSync.ServerAndClient);
if (verticalWallPrefab != null)
@@ -138,50 +207,13 @@ namespace Barotrauma
int width = (int)verticalWallPrefab.Size.X;
int halfWidth = width / 2;
int quarterWidth = halfWidth / 2;
new Structure(new Rectangle(brainHull.Rect.Left - quarterWidth, brainHull.Rect.Top, width, brainHull.Rect.Height), verticalWallPrefab, Wreck);
new Structure(new Rectangle(brainHull.Rect.Right - halfWidth - quarterWidth, brainHull.Rect.Top, width, brainHull.Rect.Height), verticalWallPrefab, Wreck);
new Structure(new Rectangle(brainHull.Rect.Left - quarterWidth, brainHull.Rect.Top, width, brainHull.Rect.Height), verticalWallPrefab, wreck);
new Structure(new Rectangle(brainHull.Rect.Right - halfWidth - quarterWidth, brainHull.Rect.Top, width, brainHull.Rect.Height), verticalWallPrefab, wreck);
}
foreach (Item item in allItems)
foreach (Item item in thalamusItems)
{
if (thalamusItems.Contains(item))
{
// Ensure that thalamus items are visible
item.HiddenInGame = false;
}
else
{
// Load regular turrets
var turret = item.GetComponent<Turret>();
if (turret != null)
{
foreach (var linkedItem in item.GetLinkedEntities<Item>())
{
var container = linkedItem.GetComponent<ItemContainer>();
if (container == null) { continue; }
for (int i = 0; i < container.Inventory.Capacity; i++)
{
if (container.Inventory.GetItemAt(i) != null) { continue; }
if (MapEntityPrefab.List.GetRandom(e => e is ItemPrefab ip && container.CanBeContained(ip, i) &&
Config.ForbiddenAmmunition.None(id => id == ip.Identifier), Rand.RandSync.ServerAndClient) is ItemPrefab ammoPrefab)
{
Item ammo = new Item(ammoPrefab, container.Item.WorldPosition, Wreck);
if (!container.Inventory.TryPutItem(ammo, i, allowSwapping: false, allowCombine: false, user: null, createNetworkEvent: false))
{
item.Remove();
}
}
}
}
}
}
}
foreach (var item in allItems)
{
var turret = item.GetComponent<Turret>();
if (turret != null)
{
turrets.Add(turret);
}
// Ensure that thalamus items are visible
item.HiddenInGame = false;
if (item.HasTag(Config.Spawner))
{
if (!spawnOrgans.Contains(item))
@@ -195,16 +227,34 @@ namespace Barotrauma
}
}
}
wayPoints.AddRange(Wreck.GetWaypoints(false));
wayPoints.AddRange(wreck.GetWaypoints(false));
IsAlive = true;
thalamusStructures = GetThalamusEntities<Structure>(Wreck, Config.Entity).ToList();
thalamusStructures = GetThalamusEntities<Structure>(wreck, Config.Entity).ToList();
}
private void GetConfig()
{
Config ??= WreckAIConfig.GetRandom();
if (Config == null)
{
DebugConsole.ThrowError("WreckAI: No wreck AI config found!");
}
}
protected override void LoadAllTurrets()
{
GetConfig();
foreach (var turret in turrets)
{
LoadTurret(turret, ip => Config.ForbiddenAmmunition.None(id => id == ip.Identifier));
}
}
private readonly List<Item> destroyedOrgans = new List<Item>();
public void Update(float deltaTime)
public override void Update(float deltaTime)
{
if (!IsAlive) { return; }
if (Wreck == null || Wreck.Removed)
if (Submarine == null || Submarine.Removed)
{
Remove();
return;
@@ -223,34 +273,60 @@ namespace Barotrauma
}
}
destroyedOrgans.ForEach(o => spawnOrgans.Remove(o));
bool someoneNearby = false;
bool isSomeoneNearby = false;
float minDist = Sonar.DefaultSonarRange * 2.0f;
foreach (Submarine submarine in Submarine.Loaded)
#if SERVER
foreach (var client in GameMain.Server.ConnectedClients)
{
if (submarine.Info.Type != SubmarineType.Player) { continue; }
if (Vector2.DistanceSquared(submarine.WorldPosition, Wreck.WorldPosition) < minDist * minDist)
var spectatePos = client.SpectatePos;
if (spectatePos.HasValue)
{
someoneNearby = true;
break;
if (IsCloseEnough(spectatePos.Value, minDist))
{
isSomeoneNearby = true;
break;
}
}
}
foreach (Character c in Character.CharacterList)
#else
if (IsCloseEnough(GameMain.GameScreen.Cam.Position, minDist))
{
if (c != Character.Controlled && !c.IsRemotePlayer) { continue; }
if (Vector2.DistanceSquared(c.WorldPosition, Wreck.WorldPosition) < minDist * minDist)
isSomeoneNearby = true;
}
#endif
if (!isSomeoneNearby)
{
foreach (Submarine submarine in Submarine.Loaded)
{
someoneNearby = true;
break;
if (submarine.Info.Type != SubmarineType.Player) { continue; }
if (IsCloseEnough(submarine.WorldPosition, minDist))
{
isSomeoneNearby = true;
break;
}
}
}
if (!someoneNearby) { return; }
OperateTurrets(deltaTime);
if (!isSomeoneNearby)
{
foreach (Character c in Character.CharacterList)
{
if (!c.IsPlayer && !c.IsOnPlayerTeam) { continue; }
if (IsCloseEnough(c.WorldPosition, minDist))
{
isSomeoneNearby = true;
break;
}
}
}
if (!isSomeoneNearby) { return; }
OperateTurrets(deltaTime, Config.Entity);
if (!IsClient)
{
if (!initialCellsSpawned) { SpawnInitialCells(); }
UpdateReinforcements(deltaTime);
}
}
private bool IsCloseEnough(Vector2 targetPos, float minDist) => Vector2.DistanceSquared(targetPos, Submarine.WorldPosition) < minDist * minDist;
private void SpawnInitialCells()
{
@@ -287,7 +363,7 @@ namespace Barotrauma
// Snap all tendons
foreach (Item item in turret.ActiveProjectiles)
{
if (item.GetComponent<Projectile>()?.IsStuckToTarget ?? false)
if (item.GetComponent<Projectile>() is { IsStuckToTarget: true })
{
item.Condition = 0;
}
@@ -314,7 +390,7 @@ namespace Barotrauma
{
// Sonar distance is used also for wreck positioning. No wreck should be closer to each other than this.
float maxDistance = Sonar.DefaultSonarRange;
if (Vector2.DistanceSquared(character.WorldPosition, Wreck.WorldPosition) < maxDistance * maxDistance)
if (Vector2.DistanceSquared(character.WorldPosition, Submarine.WorldPosition) < maxDistance * maxDistance)
{
character.Kill(CauseOfDeathType.Unknown, null);
}
@@ -333,7 +409,7 @@ namespace Barotrauma
public void Remove()
{
Kill();
RemoveThalamusItems(Wreck);
RemoveThalamusItems(Submarine);
thalamusItems?.Clear();
thalamusStructures?.Clear();
}
@@ -387,7 +463,7 @@ namespace Barotrauma
return MathHelper.Lerp(max, min, MathUtils.InverseLerp(0, 100, t));
}
void UpdateReinforcements(float deltaTime)
private void UpdateReinforcements(float deltaTime)
{
if (spawnOrgans.Count == 0) { return; }
cellSpawnTimer -= deltaTime;
@@ -398,7 +474,7 @@ namespace Barotrauma
}
}
bool TrySpawnCell(out Character cell, ISpatialEntity targetEntity = null)
private bool TrySpawnCell(out Character cell, ISpatialEntity targetEntity = null)
{
cell = null;
if (protectiveCells.Count >= MaxCellCount) { return false; }
@@ -424,19 +500,6 @@ namespace Barotrauma
cellSpawnTimer = GetSpawnTime();
return true;
}
void OperateTurrets(float deltaTime)
{
foreach (var turret in turrets)
{
// Never target other creatures than humans with the turrets.
turret.ThalamusOperate(this, deltaTime,
!turret.Item.HasTag("ignorecharacters"),
targetOtherCreatures: false,
!turret.Item.HasTag("ignoresubmarines"),
turret.Item.HasTag("ignoreaimdelay"));
}
}
void OnCellDeath(Character character, CauseOfDeath causeOfDeath)
{