Merge branch 'master' of https://github.com/Regalis11/Barotrauma into develop

This commit is contained in:
EvilFactory
2023-03-13 13:32:14 -03:00
335 changed files with 11052 additions and 5179 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);
@@ -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();
@@ -430,7 +430,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 +444,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 +546,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 +583,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 +620,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 +631,7 @@ namespace Barotrauma
}
if (!removeMask)
{
takeMaskOff = !HasDivingMask(gotoObjective.Target as Character);
takeMaskOff = !targetHasDivingGear;
if (takeMaskOff)
{
removeMask = true;
@@ -783,20 +783,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()
@@ -989,8 +992,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 +1212,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 +1526,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 +1549,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 +1640,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,7 +1653,7 @@ 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);
@@ -1745,12 +1744,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 +1844,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 +1925,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 +1938,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 +1950,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;
@@ -2047,19 +2066,37 @@ 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)
{
var reputation = campaign.Map?.CurrentLocation?.Reputation;
if (reputation != null && reputation.NormalizedValue < Reputation.HostileThreshold)
{
return false;
}
}
if (!sameTeam && me.TeamID == CharacterTeamType.None && other.IsPet)
if (other.IsPet)
{
// Hostile NPCs are hostile to all pets, unless they are in the same team.
return false;
if (!sameTeam && me.TeamID == CharacterTeamType.None) { return false; }
}
else
{
if (!me.IsSameSpeciesOrGroup(other)) { return false; }
}
if (GameMain.GameSession?.GameMode is CampaignMode campaign)
{
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;
Identifier npcFaction = npc.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;
}
@@ -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)
{
@@ -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,7 @@ 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 && character.Submarine.TeamID != target.Submarine.TeamID && character.Submarine.TeamID != character.OriginalTeamID) { 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;
}
}
@@ -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)
@@ -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;
}
}
@@ -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; }
}
}
@@ -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)
{
@@ -541,11 +541,11 @@ namespace Barotrauma
float wobbleStrength = 0.0f;
if (character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand) == heldItem)
{
wobbleStrength += Character.CharacterHealth.GetLimbDamage(rightHand, afflictionType: "damage");
wobbleStrength += Character.CharacterHealth.GetLimbDamage(rightHand, afflictionType: AfflictionPrefab.DamageType);
}
if (character.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand) == heldItem)
{
wobbleStrength += Character.CharacterHealth.GetLimbDamage(leftHand, afflictionType: "damage");
wobbleStrength += Character.CharacterHealth.GetLimbDamage(leftHand, afflictionType: AfflictionPrefab.DamageType);
}
if (wobbleStrength <= 0.1f) { return 0.0f; }
wobbleStrength = (float)Math.Min(wobbleStrength, 1.0f);
@@ -135,8 +135,14 @@ namespace Barotrauma
public override void UpdateAnim(float deltaTime)
{
if (Frozen) return;
if (MainLimb == null) { return; }
//wait a bit for the ragdoll to "settle" (for joints to force the limbs to appropriate positions) before starting to animate
if (Timing.TotalTime - character.SpawnTime < 0.1f) { return; }
if (Frozen) { return; }
if (MainLimb == null)
{
ResetState();
return;
}
var mainLimb = MainLimb;
levitatingCollider = !IsHanging;
@@ -164,6 +170,7 @@ namespace Barotrauma
//cannot walk but on dry land -> wiggle around
UpdateDying(deltaTime);
}
ResetState();
return;
}
else
@@ -176,11 +183,17 @@ namespace Barotrauma
{
var lowestLimb = FindLowestLimb();
Collider.SetTransform(new Vector2(
Collider.SimPosition.X,
Math.Max(lowestLimb.SimPosition.Y + (Collider.radius + Collider.height / 2), Collider.SimPosition.Y)),
0.0f);
if (InWater)
{
Collider.SetTransform(new Vector2(Collider.SimPosition.X, MainLimb.SimPosition.Y), 0.0f);
}
else
{
Collider.SetTransform(new Vector2(
Collider.SimPosition.X,
Math.Max(lowestLimb.SimPosition.Y + (Collider.Radius + Collider.Height / 2), Collider.SimPosition.Y)),
0.0f);
}
Collider.Enabled = true;
}
@@ -223,6 +236,7 @@ namespace Barotrauma
if (character.SelectedCharacter != null)
{
DragCharacter(character.SelectedCharacter, deltaTime);
ResetState();
return;
}
if (character.AnimController.AnimationTestPose)
@@ -230,7 +244,11 @@ namespace Barotrauma
ApplyTestPose();
}
//don't flip when simply physics is enabled
if (SimplePhysicsEnabled) { return; }
if (SimplePhysicsEnabled)
{
ResetState();
return;
}
if (!character.IsRemotelyControlled && (character.AIController == null || character.AIController.CanFlip) && !Aiming)
{
@@ -264,43 +282,47 @@ namespace Barotrauma
}
}
if (!CurrentFishAnimation.Flip) { return; }
if (IsStuck) { return; }
if (character.AIController != null && !character.AIController.CanFlip) { return; }
flipCooldown -= deltaTime;
if (TargetDir != Direction.None && TargetDir != dir)
if (!IsStuck && CurrentFishAnimation.Flip && character.AIController is not { CanFlip: false })
{
flipTimer += deltaTime;
// Speed reductions are not taken into account here. It's intentional: an ai character cannot flip if it's heavily paralyzed (for example).
float requiredSpeed = CurrentAnimationParams.MovementSpeed / 2;
if (CurrentHull != null)
flipCooldown -= deltaTime;
if (TargetDir != Direction.None && TargetDir != dir)
{
// Enemy movement speeds are halved inside submarines
requiredSpeed /= 2;
}
bool isMovingFastEnough = Math.Abs(MainLimb.LinearVelocity.X) > requiredSpeed;
bool isTryingToMoveHorizontally = Math.Abs(TargetMovement.X) > Math.Abs(TargetMovement.Y);
if ((flipTimer > CurrentFishAnimation.FlipDelay && flipCooldown <= 0.0f && ((isMovingFastEnough && isTryingToMoveHorizontally) || IsMovingBackwards))
|| character.IsRemotePlayer)
{
Flip();
if (!inWater || (CurrentSwimParams != null && CurrentSwimParams.Mirror))
flipTimer += deltaTime;
// Speed reductions are not taken into account here. It's intentional: an ai character cannot flip if it's heavily paralyzed (for example).
float requiredSpeed = CurrentAnimationParams.MovementSpeed / 2;
if (CurrentHull != null)
{
Mirror(CurrentSwimParams != null ? CurrentSwimParams.MirrorLerp : true);
// Enemy movement speeds are halved inside submarines
requiredSpeed /= 2;
}
bool isMovingFastEnough = Math.Abs(MainLimb.LinearVelocity.X) > requiredSpeed;
bool isTryingToMoveHorizontally = Math.Abs(TargetMovement.X) > Math.Abs(TargetMovement.Y);
if ((flipTimer > CurrentFishAnimation.FlipDelay && flipCooldown <= 0.0f && ((isMovingFastEnough && isTryingToMoveHorizontally) || IsMovingBackwards))
|| character.IsRemotePlayer)
{
Flip();
if (!inWater || (CurrentSwimParams != null && CurrentSwimParams.Mirror))
{
Mirror(CurrentSwimParams != null ? CurrentSwimParams.MirrorLerp : true);
}
flipTimer = 0.0f;
flipCooldown = CurrentFishAnimation.FlipCooldown;
}
}
else
{
flipTimer = 0.0f;
flipCooldown = CurrentFishAnimation.FlipCooldown;
}
}
else
ResetState();
void ResetState()
{
flipTimer = 0.0f;
wasAiming = aiming;
aiming = false;
wasAimingMelee = aimingMelee;
aimingMelee = false;
}
wasAiming = aiming;
aiming = false;
wasAimingMelee = aimingMelee;
aimingMelee = false;
}
private bool CanDrag(Character target)
@@ -463,19 +485,26 @@ namespace Barotrauma
if (SimplePhysicsEnabled) { return; }
mainLimb.PullJointEnabled = true;
if (aiming && movement.Length() <= 0.1f)
{
Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition);
Vector2 diff = (mousePos - (GetLimb(LimbType.Torso) ?? MainLimb).SimPosition) * Dir;
TargetMovement = new Vector2(0.0f, -0.1f);
float newRotation = MathUtils.VectorToAngle(diff);
Collider.SmoothRotate(newRotation, CurrentSwimParams.SteerTorque * character.SpeedMultiplier);
}
if (!isMoving)
if (!isMoving && !CurrentSwimParams.UpdateAnimationWhenNotMoving)
{
WalkPos = MathHelper.SmoothStep(WalkPos, MathHelper.PiOver2, deltaTime * 5);
mainLimb.PullJointWorldAnchorB = Collider.SimPosition;
if (aiming)
{
Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition);
Vector2 diff = (mousePos - (GetLimb(LimbType.Torso) ?? MainLimb).SimPosition) * Dir;
TargetMovement = new Vector2(0.0f, -0.1f);
float newRotation = MathHelper.WrapAngle(MathUtils.VectorToAngle(diff) - MathHelper.PiOver2 * Dir);
Collider.SmoothRotate(newRotation, CurrentSwimParams.SteerTorque * character.SpeedMultiplier * 2);
if (TorsoAngle.HasValue)
{
Limb torso = GetLimb(LimbType.Torso);
if (torso != null)
{
SmoothRotateWithoutWrapping(torso, newRotation + TorsoAngle.Value * Dir, mainLimb, TorsoTorque * 2);
}
}
}
}
else
{
@@ -170,7 +170,7 @@ namespace Barotrauma
{
get
{
float shoulderHeight = Collider.height / 2.0f;
float shoulderHeight = Collider.Height / 2.0f;
if (inWater)
{
shoulderHeight += 0.4f;
@@ -308,7 +308,7 @@ namespace Barotrauma
Collider.SetTransform(new Vector2(
Collider.SimPosition.X,
Math.Max(lowestLimb.SimPosition.Y + (Collider.radius + Collider.height / 2), Collider.SimPosition.Y)),
Math.Max(lowestLimb.SimPosition.Y + (Collider.Radius + Collider.Height / 2), Collider.SimPosition.Y)),
Collider.Rotation);
Collider.FarseerBody.ResetDynamics();
@@ -459,7 +459,8 @@ namespace Barotrauma
void UpdateStanding()
{
if (CurrentGroundedParams == null) { return; }
var currentGroundedParams = CurrentGroundedParams;
if (currentGroundedParams == null) { return; }
Vector2 handPos;
Limb leftFoot = GetLimb(LimbType.LeftFoot);
@@ -482,7 +483,7 @@ namespace Barotrauma
walkCycleMultiplier *= 1.5f;
}
float getUpForce = CurrentGroundedParams.GetUpForce / RagdollParams.JointScale;
float getUpForce = currentGroundedParams.GetUpForce / RagdollParams.JointScale;
Vector2 colliderPos = GetColliderBottom();
if (Math.Abs(TargetMovement.X) > 1.0f)
@@ -583,7 +584,7 @@ namespace Barotrauma
}
float stepLift = TargetMovement.X == 0.0f ? 0 :
(float)Math.Sin(WalkPos * CurrentGroundedParams.StepLiftFrequency + MathHelper.Pi * CurrentGroundedParams.StepLiftOffset) * (CurrentGroundedParams.StepLiftAmount / 100);
(float)Math.Sin(WalkPos * currentGroundedParams.StepLiftFrequency + MathHelper.Pi * currentGroundedParams.StepLiftOffset) * (currentGroundedParams.StepLiftAmount / 100);
float y = colliderPos.Y + stepLift;
@@ -598,7 +599,7 @@ namespace Barotrauma
if (!head.Disabled)
{
y = colliderPos.Y + stepLift * CurrentGroundedParams.StepLiftHeadMultiplier;
y = colliderPos.Y + stepLift * currentGroundedParams.StepLiftHeadMultiplier;
if (HeadPosition.HasValue) { y += HeadPosition.Value; }
if (Crouching && !movingHorizontally) { y -= HumanCrouchParams.MoveDownAmountWhenStationary; }
head.PullJointWorldAnchorB =
@@ -615,18 +616,18 @@ namespace Barotrauma
if (TorsoAngle.HasValue && !torso.Disabled)
{
float torsoAngle = TorsoAngle.Value;
float herpesStrength = character.CharacterHealth.GetAfflictionStrength("spaceherpes");
float herpesStrength = character.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.SpaceHerpesType);
if (Crouching && !movingHorizontally && !Aiming) { torsoAngle -= HumanCrouchParams.ExtraTorsoAngleWhenStationary; }
torsoAngle -= herpesStrength / 150.0f;
torso.body.SmoothRotate(torsoAngle * Dir, CurrentGroundedParams.TorsoTorque);
torso.body.SmoothRotate(torsoAngle * Dir, currentGroundedParams.TorsoTorque);
}
if (!head.Disabled)
{
if (!Aiming && CurrentGroundedParams.FixedHeadAngle && HeadAngle.HasValue)
if (!Aiming && currentGroundedParams.FixedHeadAngle && HeadAngle.HasValue)
{
float headAngle = HeadAngle.Value;
if (Crouching && !movingHorizontally) { headAngle -= HumanCrouchParams.ExtraHeadAngleWhenStationary; }
head.body.SmoothRotate(headAngle * Dir, CurrentGroundedParams.HeadTorque);
head.body.SmoothRotate(headAngle * Dir, currentGroundedParams.HeadTorque);
}
else
{
@@ -665,16 +666,16 @@ namespace Barotrauma
if (footPos.Y < 0.0f) { footPos.Y = -0.15f; }
//make the character limp if the feet are damaged
float footAfflictionStrength = character.CharacterHealth.GetAfflictionStrength("damage", foot, true);
float footAfflictionStrength = character.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.DamageType, foot, true);
footPos.X *= MathHelper.Lerp(1.0f, 0.75f, MathHelper.Clamp(footAfflictionStrength / 50.0f, 0.0f, 1.0f));
if (CurrentGroundedParams.FootLiftHorizontalFactor > 0)
if (currentGroundedParams.FootLiftHorizontalFactor > 0)
{
// Calculate the foot y dynamically based on the foot position relative to the waist,
// so that the foot aims higher when it's behind the waist and lower when it's in the front.
float xDiff = (foot.SimPosition.X - waistPos.X + FootMoveOffset.X) * Dir;
float min = MathUtils.InverseLerp(1, 0, CurrentGroundedParams.FootLiftHorizontalFactor);
float max = 1 + MathUtils.InverseLerp(0, 1, CurrentGroundedParams.FootLiftHorizontalFactor);
float min = MathUtils.InverseLerp(1, 0, currentGroundedParams.FootLiftHorizontalFactor);
float max = 1 + MathUtils.InverseLerp(0, 1, currentGroundedParams.FootLiftHorizontalFactor);
float xFactor = MathHelper.Lerp(min, max, MathUtils.InverseLerp(RagdollParams.JointScale, -RagdollParams.JointScale, xDiff));
footPos.Y *= xFactor;
}
@@ -698,19 +699,19 @@ namespace Barotrauma
{
foot.DebugRefPos = colliderPos;
foot.DebugTargetPos = colliderPos + footPos;
MoveLimb(foot, colliderPos + footPos, CurrentGroundedParams.FootMoveStrength);
MoveLimb(foot, colliderPos + footPos, currentGroundedParams.FootMoveStrength);
FootIK(foot, colliderPos + footPos,
CurrentGroundedParams.LegBendTorque, CurrentGroundedParams.FootTorque, CurrentGroundedParams.FootAngleInRadians);
currentGroundedParams.LegBendTorque, currentGroundedParams.FootTorque, currentGroundedParams.FootAngleInRadians);
}
}
//calculate the positions of hands
handPos = torso.SimPosition;
handPos.X = -walkPosX * CurrentGroundedParams.HandMoveAmount.X;
handPos.X = -walkPosX * currentGroundedParams.HandMoveAmount.X;
float lowerY = CurrentGroundedParams.HandClampY;
float lowerY = currentGroundedParams.HandClampY;
handPos.Y = lowerY + (float)(Math.Abs(Math.Sin(WalkPos - Math.PI * 1.5f) * CurrentGroundedParams.HandMoveAmount.Y));
handPos.Y = lowerY + (float)(Math.Abs(Math.Sin(WalkPos - Math.PI * 1.5f) * currentGroundedParams.HandMoveAmount.Y));
Vector2 posAddition = new Vector2(Math.Sign(movement.X) * HandMoveOffset.X, HandMoveOffset.Y);
@@ -718,13 +719,13 @@ namespace Barotrauma
{
HandIK(rightHand,
torso.SimPosition + posAddition + new Vector2(-handPos.X, (Math.Sign(walkPosX) == Math.Sign(Dir)) ? handPos.Y : lowerY),
CurrentGroundedParams.ArmMoveStrength, CurrentGroundedParams.HandMoveStrength);
currentGroundedParams.ArmMoveStrength, currentGroundedParams.HandMoveStrength);
}
if (leftHand != null && !leftHand.Disabled)
{
HandIK(leftHand,
torso.SimPosition + posAddition + new Vector2(handPos.X, (Math.Sign(walkPosX) == Math.Sign(-Dir)) ? handPos.Y : lowerY),
CurrentGroundedParams.ArmMoveStrength, CurrentGroundedParams.HandMoveStrength);
currentGroundedParams.ArmMoveStrength, currentGroundedParams.HandMoveStrength);
}
}
else
@@ -755,8 +756,8 @@ namespace Barotrauma
{
foot.DebugRefPos = colliderPos;
foot.DebugTargetPos = footPos;
float footMoveForce = CurrentGroundedParams.FootMoveStrength;
float legBendTorque = CurrentGroundedParams.LegBendTorque;
float footMoveForce = currentGroundedParams.FootMoveStrength;
float legBendTorque = currentGroundedParams.LegBendTorque;
if (Crouching)
{
// Keeps the pose
@@ -764,7 +765,7 @@ namespace Barotrauma
footMoveForce *= 2;
}
MoveLimb(foot, footPos, footMoveForce);
FootIK(foot, footPos, legBendTorque, CurrentGroundedParams.FootTorque, CurrentGroundedParams.FootAngleInRadians);
FootIK(foot, footPos, legBendTorque, currentGroundedParams.FootTorque, currentGroundedParams.FootAngleInRadians);
}
}
@@ -780,7 +781,7 @@ namespace Barotrauma
var arm = GetLimb(armType);
if (arm != null && Math.Abs(arm.body.AngularVelocity) < 10.0f)
{
arm.body.SmoothRotate(MathHelper.Clamp(-arm.body.AngularVelocity, -0.5f, 0.5f), arm.Mass * 50.0f * CurrentGroundedParams.ArmMoveStrength);
arm.body.SmoothRotate(MathHelper.Clamp(-arm.body.AngularVelocity, -0.5f, 0.5f), arm.Mass * 50.0f * currentGroundedParams.ArmMoveStrength);
}
//get the elbow to a neutral rotation
@@ -791,14 +792,14 @@ namespace Barotrauma
if (elbow != null)
{
float diff = elbow.JointAngle - (Dir > 0 ? elbow.LowerLimit : elbow.UpperLimit);
forearm.body.ApplyTorque(MathHelper.Clamp(-diff, -MathHelper.PiOver2, MathHelper.PiOver2) * forearm.Mass * 100.0f * CurrentGroundedParams.ArmMoveStrength);
forearm.body.ApplyTorque(MathHelper.Clamp(-diff, -MathHelper.PiOver2, MathHelper.PiOver2) * forearm.Mass * 100.0f * currentGroundedParams.ArmMoveStrength);
}
}
// Try to keep the wrist straight
LimbJoint wrist = GetJointBetweenLimbs(foreArmType, hand.type);
if (wrist != null)
{
hand.body.ApplyTorque(MathHelper.Clamp(-wrist.JointAngle, -MathHelper.PiOver2, MathHelper.PiOver2) * hand.Mass * 100f * CurrentGroundedParams.HandMoveStrength);
hand.body.ApplyTorque(MathHelper.Clamp(-wrist.JointAngle, -MathHelper.PiOver2, MathHelper.PiOver2) * hand.Mass * 100f * currentGroundedParams.HandMoveStrength);
}
}
}
@@ -1130,7 +1131,7 @@ namespace Barotrauma
ladderSimPos -= currentHull.Submarine.SimPosition;
}
float bottomPos = Collider.SimPosition.Y - ColliderHeightFromFloor - Collider.radius - Collider.height / 2.0f;
float bottomPos = Collider.SimPosition.Y - ColliderHeightFromFloor - Collider.Radius - Collider.Height / 2.0f;
float torsoPos = TorsoPosition ?? 0;
MoveLimb(torso, new Vector2(ladderSimPos.X - 0.35f * Dir, bottomPos + torsoPos), 10.5f);
float headPos = HeadPosition ?? 0;
@@ -1225,7 +1226,7 @@ namespace Barotrauma
if (character.SimPosition.Y > ladderSimPos.Y) { climbForce.Y = Math.Min(0.0f, climbForce.Y); }
//reached the bottom -> can't go further down
float minHeightFromFloor = ColliderHeightFromFloor / 2 + Collider.height;
float minHeightFromFloor = ColliderHeightFromFloor / 2 + Collider.Height;
if (floorFixture != null &&
!floorFixture.CollisionCategories.HasFlag(Physics.CollisionStairs) &&
!floorFixture.CollisionCategories.HasFlag(Physics.CollisionPlatform) &&
@@ -174,18 +174,18 @@ namespace Barotrauma
if (value == colliderIndex || collider == null) { return; }
if (value >= collider.Count || value < 0) { return; }
if (collider[colliderIndex].height < collider[value].height)
if (collider[colliderIndex].Height < collider[value].Height)
{
Vector2 pos1 = collider[colliderIndex].SimPosition;
pos1.Y -= collider[colliderIndex].height * ColliderHeightFromFloor;
pos1.Y -= collider[colliderIndex].Height * ColliderHeightFromFloor;
Vector2 pos2 = pos1;
pos2.Y += collider[value].height * 1.1f;
pos2.Y += collider[value].Height * 1.1f;
if (GameMain.World.RayCast(pos1, pos2).Any(f => f.CollisionCategories.HasFlag(Physics.CollisionWall) && !(f.Body.UserData is Submarine))) { return; }
}
Vector2 pos = collider[colliderIndex].SimPosition;
pos.Y -= collider[colliderIndex].height * 0.5f;
pos.Y += collider[value].height * 0.5f;
pos.Y -= collider[colliderIndex].Height * 0.5f;
pos.Y += collider[value].Height * 0.5f;
collider[value].SetTransform(pos, collider[colliderIndex].Rotation);
collider[value].LinearVelocity = collider[colliderIndex].LinearVelocity;
@@ -576,6 +576,10 @@ namespace Barotrauma
protected void AddLimb(LimbParams limbParams)
{
if (limbParams.ID < 0 || limbParams.ID > 255)
{
throw new Exception($"Invalid limb params in limb \"{limbParams.Type}\". \"{limbParams.ID}\" is not a valid limb ID.");
}
byte ID = Convert.ToByte(limbParams.ID);
Limb limb = new Limb(this, character, limbParams);
limb.body.FarseerBody.OnCollision += OnLimbCollision;
@@ -681,6 +685,10 @@ namespace Barotrauma
}
return true;
}
else if (character.Submarine != null && structure.Submarine != null && character.Submarine != structure.Submarine)
{
return false;
}
Vector2 colliderBottom = GetColliderBottom();
if (structure.IsPlatform)
@@ -1212,7 +1220,9 @@ namespace Barotrauma
RefreshFloorY(ignoreStairs: Stairs == null);
if (currentHull.WaterPercentage > 0.001f)
{
float waterSurface = ConvertUnits.ToSimUnits(GetSurfaceY());
(float waterSurfaceDisplayUnits, float ceilingDisplayUnits) = GetWaterSurfaceAndCeilingY();
float waterSurfaceY = ConvertUnits.ToSimUnits(waterSurfaceDisplayUnits);
float ceilingY = ConvertUnits.ToSimUnits(ceilingDisplayUnits);
if (targetMovement.Y < 0.0f)
{
Vector2 colliderBottom = GetColliderBottom();
@@ -1222,13 +1232,21 @@ namespace Barotrauma
{
//set floorY to the position of the floor in the hull below the character
var lowerHull = Hull.FindHull(ConvertUnits.ToDisplayUnits(colliderBottom), useWorldCoordinates: false);
if (lowerHull != null) floorY = ConvertUnits.ToSimUnits(lowerHull.Rect.Y - lowerHull.Rect.Height);
if (lowerHull != null)
{
floorY = ConvertUnits.ToSimUnits(lowerHull.Rect.Y - lowerHull.Rect.Height);
}
}
}
float standHeight = HeadPosition ?? TorsoPosition ?? Collider.GetMaxExtent() * 0.5f;
if (Collider.SimPosition.Y < waterSurface && waterSurface - floorY > standHeight * 0.8f)
if (Collider.SimPosition.Y < waterSurfaceY)
{
inWater = true;
//too deep to stand up, or not enough room to stand up
if (waterSurfaceY - floorY > standHeight * 0.8f ||
ceilingY - floorY < standHeight * 0.8f)
{
inWater = true;
}
}
}
}
@@ -1291,7 +1309,7 @@ namespace Barotrauma
if (!inWater && character.AllowInput && levitatingCollider && Collider.LinearVelocity.Y > -ImpactTolerance && onGround)
{
float targetY = standOnFloorY + ((float)Math.Abs(Math.Cos(Collider.Rotation)) * Collider.height * 0.5f) + Collider.radius + ColliderHeightFromFloor;
float targetY = standOnFloorY + ((float)Math.Abs(Math.Cos(Collider.Rotation)) * Collider.Height * 0.5f) + Collider.Radius + ColliderHeightFromFloor;
if (Math.Abs(Collider.SimPosition.Y - targetY) > 0.01f && onGround)
{
if (Stairs != null)
@@ -1624,7 +1642,7 @@ namespace Barotrauma
{
floorFixture = standOnFloorFixture;
standOnFloorY = rayStart.Y + (rayEnd.Y - rayStart.Y) * standOnFloorFraction;
if (rayStart.Y - standOnFloorY < Collider.height * 0.5f + Collider.radius + ColliderHeightFromFloor * 1.2f)
if (rayStart.Y - standOnFloorY < Collider.Height * 0.5f + Collider.Radius + ColliderHeightFromFloor * 1.2f)
{
onGround = true;
if (standOnFloorFixture.CollisionCategories == Physics.CollisionStairs)
@@ -1663,22 +1681,34 @@ namespace Barotrauma
}
}
/// <summary>
/// Get the position of the surface of water at the position of the character, in display units (taking into account connected hulls above the hull the character is in)
/// </summary>
public float GetSurfaceY()
{
return GetWaterSurfaceAndCeilingY().WaterSurfaceY;
}
/// <summary>
/// Get the position of the surface of water and the ceiling (= upper edge of the hull) at the position of the character, in display units (taking into account connected hulls above the hull the character is in).
/// </summary>
private (float WaterSurfaceY, float CeilingY) GetWaterSurfaceAndCeilingY()
{
//check both hulls: the hull whose coordinate space the ragdoll is in, and the hull whose bounds the character's origin actually is inside
if (currentHull == null || character.CurrentHull == null)
{
return float.PositiveInfinity;
return (float.PositiveInfinity, float.PositiveInfinity);
}
float surfacePos = currentHull.Surface;
float surfaceY = currentHull.Surface;
float ceilingY = currentHull.Rect.Y;
float surfaceThreshold = ConvertUnits.ToDisplayUnits(Collider.SimPosition.Y + 1.0f);
//if the hull is almost full of water, check if there's a water-filled hull above it
//and use its water surface instead of the current hull's
if (currentHull.Rect.Y - currentHull.Surface < 5.0f)
{
GetSurfacePos(currentHull, ref surfacePos);
void GetSurfacePos(Hull hull, ref float prevSurfacePos)
{
GetSurfacePos(currentHull, ref surfaceY, ref ceilingY);
void GetSurfacePos(Hull hull, ref float prevSurfacePos, ref float ceilingPos)
{
if (prevSurfacePos > surfaceThreshold) { return; }
foreach (Gap gap in hull.ConnectedGaps)
@@ -1689,6 +1719,7 @@ namespace Barotrauma
//if the gap is above us and leads outside, there's no surface to limit the movement
if (!gap.IsRoomToRoom && gap.Position.Y > hull.Position.Y)
{
ceilingPos += 100000.0f;
prevSurfacePos += 100000.0f;
return;
}
@@ -1697,15 +1728,16 @@ namespace Barotrauma
{
if (linkedTo is Hull otherHull && otherHull != hull && otherHull != currentHull)
{
prevSurfacePos = Math.Max(surfacePos, otherHull.Surface);
GetSurfacePos(otherHull, ref prevSurfacePos);
prevSurfacePos = Math.Max(surfaceY, otherHull.Surface);
ceilingPos = Math.Max(ceilingPos, otherHull.Rect.Y);
GetSurfacePos(otherHull, ref prevSurfacePos, ref ceilingPos);
break;
}
}
}
}
}
return surfacePos;
return (surfaceY, ceilingY);
}
public void SetPosition(Vector2 simPosition, bool lerp = false, bool ignorePlatforms = true, bool forceMainLimbToCollider = false, bool detachProjectiles = true)
@@ -1814,7 +1846,7 @@ namespace Barotrauma
protected void CheckDistFromCollider()
{
float allowedDist = Math.Max(Math.Max(Collider.radius, Collider.width), Collider.height) * 2.0f;
float allowedDist = Math.Max(Math.Max(Collider.Radius, Collider.Width), Collider.Height) * 2.0f;
allowedDist = Math.Max(allowedDist, 1.0f);
float resetDist = allowedDist * 5.0f;
@@ -181,13 +181,13 @@ namespace Barotrauma
[Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)]
public float LevelWallDamage { get; set; }
[Serialize(false, IsPropertySaveable.Yes)]
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool Ranged { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description:"Only affects ranged attacks.")]
[Serialize(false, IsPropertySaveable.Yes, description:"Only affects ranged attacks."), Editable]
public bool AvoidFriendlyFire { get; set; }
[Serialize(20f, IsPropertySaveable.Yes)]
[Serialize(20f, IsPropertySaveable.Yes, description: "Only affects ranged attacks."), Editable]
public float RequiredAngle { get; set; }
[Serialize(0f, IsPropertySaveable.Yes, description: "By default uses the same value as RequiredAngle. Use if you want to allow selecting the attack but not shooting until the angle is smaller. Only affects ranged attacks."), Editable]
@@ -199,6 +199,12 @@ namespace Barotrauma
[Serialize(-1, IsPropertySaveable.Yes, description: "Reference to the limb we apply the aim rotation to. By default same as the attack limb. Only affects ranged attacks."), Editable]
public int RotationLimbIndex { get; set; }
[Serialize(0f, IsPropertySaveable.Yes, description:"How much the held weapon is swayed back and forth while aiming. Only affects monsters using ranged weapons (items). Default 0 means the weapon is not swayed at all."), Editable]
public float SwayAmount { get; set; }
[Serialize(5f, IsPropertySaveable.Yes, description: "How fast the held weapon is swayed back and forth while aiming. Only affects monsters using ranged weapons (items)."), Editable]
public float SwayFrequency { get; set; }
/// <summary>
/// Legacy support. Use Afflictions.
/// </summary>
@@ -337,9 +343,10 @@ namespace Barotrauma
return (Duration == 0.0f) ? LevelWallDamage : LevelWallDamage * deltaTime;
}
public float GetItemDamage(float deltaTime)
public float GetItemDamage(float deltaTime, float multiplier = 1)
{
return (Duration == 0.0f) ? ItemDamage : ItemDamage * deltaTime;
float dmg = ItemDamage * multiplier;
return (Duration == 0.0f) ? dmg : dmg * deltaTime;
}
public float GetTotalDamage(bool includeStructureDamage = false)
@@ -139,6 +139,12 @@ namespace Barotrauma
public bool IsEscorted { get; set; }
public Identifier JobIdentifier => Info?.Job?.Prefab.Identifier ?? Identifier.Empty;
public bool DoesBleed
{
get => Params.Health.DoesBleed;
set => Params.Health.DoesBleed = value;
}
public readonly Dictionary<Identifier, SerializableProperty> Properties;
public Dictionary<Identifier, SerializableProperty> SerializableProperties
{
@@ -176,6 +182,13 @@ namespace Barotrauma
}
}
private Identifier? faction;
public Identifier Faction
{
get { return faction ?? HumanPrefab?.Faction ?? Identifier.Empty; }
set { faction = value; }
}
private CharacterTeamType teamID;
public CharacterTeamType TeamID
{
@@ -187,6 +200,13 @@ namespace Barotrauma
}
}
private CharacterTeamType? originalTeamID;
public CharacterTeamType OriginalTeamID
{
get { return originalTeamID ?? teamID; }
}
private Wallet wallet;
public Wallet Wallet
@@ -208,7 +228,7 @@ namespace Barotrauma
protected readonly Dictionary<string, ActiveTeamChange> activeTeamChanges = new Dictionary<string, ActiveTeamChange>();
protected ActiveTeamChange currentTeamChange;
const string OriginalTeamIdentifier = "original";
private const string OriginalChangeTeamIdentifier = "original";
private void ThrowIfAccessingWalletsInSingleplayer()
{
@@ -223,20 +243,16 @@ namespace Barotrauma
public void SetOriginalTeam(CharacterTeamType newTeam)
{
TryRemoveTeamChange(OriginalTeamIdentifier);
TryRemoveTeamChange(OriginalChangeTeamIdentifier);
currentTeamChange = new ActiveTeamChange(newTeam, ActiveTeamChange.TeamChangePriorities.Base);
TryAddNewTeamChange(OriginalTeamIdentifier, currentTeamChange);
TryAddNewTeamChange(OriginalChangeTeamIdentifier, currentTeamChange);
}
protected void ChangeTeam(CharacterTeamType newTeam)
private void ChangeTeam(CharacterTeamType newTeam)
{
if (newTeam == teamID)
{
return;
}
teamID = newTeam;
if (info != null) { info.TeamID = newTeam; }
if (newTeam == teamID) { return; }
if (originalTeamID == null) { originalTeamID = teamID; }
TeamID = newTeam;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
{
return;
@@ -280,7 +296,7 @@ namespace Barotrauma
{
if (currentTeamChange == removedTeamChange)
{
currentTeamChange = activeTeamChanges[OriginalTeamIdentifier];
currentTeamChange = activeTeamChanges[OriginalChangeTeamIdentifier];
}
}
return activeTeamChanges.Remove(identifier);
@@ -314,7 +330,9 @@ namespace Barotrauma
}
}
public bool IsOnPlayerTeam => TeamID == CharacterTeamType.Team1 || TeamID == CharacterTeamType.Team2;
public bool IsOnPlayerTeam => teamID == CharacterTeamType.Team1 || teamID == CharacterTeamType.Team2;
public bool IsOriginallyOnPlayerTeam => originalTeamID == CharacterTeamType.Team1 || originalTeamID == CharacterTeamType.Team2;
public bool IsInstigator => CombatAction != null && CombatAction.IsInstigator;
public CombatAction CombatAction;
@@ -363,7 +381,7 @@ namespace Barotrauma
public Identifier SpeciesName => Params?.SpeciesName ?? "null".ToIdentifier();
public Identifier Group => Params.Group;
public Identifier Group => HumanPrefab is HumanPrefab humanPrefab && !humanPrefab.Group.IsEmpty ? humanPrefab.Group : Params.Group;
public bool IsHumanoid => Params.Humanoid;
@@ -461,10 +479,15 @@ namespace Barotrauma
}
set
{
if (info != null && info != value) info.Remove();
if (info != null && info != value)
{
info.Remove();
}
info = value;
if (info != null) info.Character = this;
if (info != null)
{
info.Character = this;
}
}
}
@@ -524,8 +547,13 @@ namespace Barotrauma
}
set
{
bool wasHidden = HideFace;
hideFaceTimer = MathHelper.Clamp(hideFaceTimer + (value ? 1.0f : -0.5f), 0.0f, 10.0f);
if (info != null && info.IsDisguisedAsAnother != HideFace) info.CheckDisguiseStatus(true);
bool isHidden = HideFace;
if (isHidden != wasHidden && info != null && info.IsDisguisedAsAnother != isHidden)
{
info.CheckDisguiseStatus(true);
}
}
}
@@ -755,7 +783,7 @@ namespace Barotrauma
get
{
if (IsUnconscious) { return true; }
return CharacterHealth.GetAllAfflictions().Any(a => a.Prefab.Identifier == "paralysis" && a.Strength >= a.Prefab.MaxStrength);
return CharacterHealth.GetAllAfflictions().Any(a => a.Prefab.AfflictionType == AfflictionPrefab.ParalysisType && a.Strength >= a.Prefab.MaxStrength);
}
}
@@ -839,7 +867,7 @@ namespace Barotrauma
public float Bleeding
{
get { return CharacterHealth.GetAfflictionStrength("bleeding", true); }
get { return CharacterHealth.GetAfflictionStrength(AfflictionPrefab.BleedingType, true); }
}
private bool speechImpedimentSet;
@@ -1044,7 +1072,7 @@ namespace Barotrauma
public bool InWater => AnimController is AnimController { InWater: true };
public bool IsLowInOxygen => NeedsOxygen && OxygenAvailable < CharacterHealth.LowOxygenThreshold;
public bool IsLowInOxygen => CharacterHealth.OxygenAmount < 100;
public bool GodMode = false;
@@ -1102,6 +1130,12 @@ namespace Barotrauma
public bool IsInFriendlySub => Submarine != null && Submarine.TeamID == TeamID;
public float AITurretPriority
{
get => Params.AITurretPriority;
private set => Params.AITurretPriority = value;
}
public delegate void OnDeathHandler(Character character, CauseOfDeath causeOfDeath);
public OnDeathHandler OnDeath;
@@ -1625,7 +1659,7 @@ namespace Barotrauma
{
DebugConsole.ThrowError($"Failed to give job items for the character \"{Name}\" - could not find human prefab with the id \"{info.HumanPrefabIds.NpcIdentifier}\" from \"{info.HumanPrefabIds.NpcSetIdentifier}\".");
}
else if (humanPrefab.GiveItems(this, Submarine))
else if (humanPrefab.GiveItems(this, spawnPoint?.Submarine ?? Submarine, spawnPoint))
{
return;
}
@@ -1698,7 +1732,7 @@ namespace Barotrauma
if (wearable.SkillModifiers.TryGetValue(skillIdentifier, out float skillValue))
{
skillLevel += skillValue;
break;
break;
}
}
@@ -1707,9 +1741,7 @@ namespace Barotrauma
}
skillLevel += GetStatValue(GetSkillStatType(skillIdentifier));
return skillLevel;
return Math.Max(skillLevel, 0);
}
// TODO: reposition? there's also the overrideTargetMovement variable, but it's not in the same manner
@@ -1907,7 +1939,7 @@ namespace Barotrauma
{
if (limb != null)
{
sum += MathHelper.Lerp(0, max, CharacterHealth.GetLimbDamage(limb, afflictionType: "damage"));
sum += MathHelper.Lerp(0, max, CharacterHealth.GetLimbDamage(limb, afflictionType: AfflictionPrefab.DamageType));
}
return Math.Clamp(sum, 0, 1f);
}
@@ -2203,24 +2235,7 @@ namespace Barotrauma
if (SelectedItem != null)
{
if (IsKeyDown(InputType.Aim) || !SelectedItem.RequireAimToSecondaryUse)
{
SelectedItem.SecondaryUse(deltaTime, this);
}
if (IsKeyDown(InputType.Use) && SelectedItem != null && !SelectedItem.IsShootable)
{
if (!SelectedItem.RequireAimToUse || IsKeyDown(InputType.Aim))
{
SelectedItem.Use(deltaTime, this);
}
}
if (IsKeyDown(InputType.Shoot) && SelectedItem != null && SelectedItem.IsShootable)
{
if (!SelectedItem.RequireAimToUse || IsKeyDown(InputType.Aim))
{
SelectedItem.Use(deltaTime, this);
}
}
tryUseItem(SelectedItem, deltaTime);
}
if (SelectedCharacter != null)
@@ -3036,7 +3051,7 @@ namespace Barotrauma
foreach (Character character in GameMain.LuaCs.Game.UpdatePriorityCharacters)
{
if (character.Removed) continue;
if (character.Removed) { continue; }
character.Update(deltaTime, cam);
}
@@ -3129,8 +3144,7 @@ namespace Barotrauma
if (NeedsAir)
{
//implode if not protected from pressure, and either outside or in a high-pressure hull
if (!IsProtectedFromPressure() &&
(AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure >= 80.0f))
if (!IsProtectedFromPressure && (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure >= 80.0f))
{
if (CharacterHealth.PressureKillDelay <= 0.0f)
{
@@ -3157,15 +3171,17 @@ namespace Barotrauma
PressureTimer = 0.0f;
}
}
else if ((GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) &&
PressureProtection < (Level.Loaded?.GetRealWorldDepth(WorldPosition.Y) ?? 1.0f) &&
WorldPosition.Y < CharacterHealth.CrushDepth && !HasAbilityFlag(AbilityFlags.ImmuneToPressure))
else if ((GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) && !IsProtectedFromPressure)
{
//implode if below crush depth, and either outside or in a high-pressure hull
if (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure >= 80.0f)
float realWorldDepth = Level.Loaded?.GetRealWorldDepth(WorldPosition.Y) ?? 0.0f;
if (PressureProtection < realWorldDepth && realWorldDepth > CharacterHealth.CrushDepth)
{
Implode();
if (IsDead) { return; }
//implode if below crush depth, and either outside or in a high-pressure hull
if (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure >= 80.0f)
{
Implode();
if (IsDead) { return; }
}
}
}
@@ -4085,17 +4101,7 @@ namespace Barotrauma
CheckTalents(AbilityEffectType.OnKillCharacter, abilityCharacterKill);
if (!IsOnPlayerTeam) { return; }
if (CreatureMetrics.Instance.Killed.Contains(target.SpeciesName)) { return; }
CreatureMetrics.Instance.Killed.Add(target.SpeciesName);
AddEncounter(target);
}
public void AddEncounter(Character other)
{
if (!IsOnPlayerTeam) { return; }
if (CreatureMetrics.Instance.Encountered.Contains(other.SpeciesName)) { return; }
CreatureMetrics.Instance.Encountered.Add(other.SpeciesName);
CreatureMetrics.Instance.RecentlyEncountered.Add(other.SpeciesName);
CreatureMetrics.RecordKill(target.SpeciesName);
}
public AttackResult DamageLimb(Vector2 worldPosition, Limb hitLimb, IEnumerable<Affliction> afflictions, float stun, bool playSound, float attackImpulse, Character attacker = null, float damageMultiplier = 1, bool allowStacking = true, float penetration = 0f, bool shouldImplode = false)
@@ -4130,13 +4136,6 @@ namespace Barotrauma
}
}
#if CLIENT
if (Params.UseBossHealthBar && Controlled != null && Controlled.teamID == attacker?.teamID)
{
CharacterHUD.ShowBossHealthBar(this);
}
#endif
Vector2 dir = hitLimb.WorldPosition - worldPosition;
if (Math.Abs(attackImpulse) > 0.0f)
{
@@ -4178,13 +4177,24 @@ namespace Barotrauma
if (attacker != null && attacker != this && !attacker.Removed)
{
AddAttacker(attacker, attackResult.Damage);
AddEncounter(attacker);
attacker.AddEncounter(this);
if (IsOnPlayerTeam)
{
CreatureMetrics.AddEncounter(attacker.SpeciesName);
}
if (attacker.IsOnPlayerTeam)
{
CreatureMetrics.AddEncounter(SpeciesName);
}
}
ApplyStatusEffects(ActionType.OnDamaged, 1.0f);
hitLimb.ApplyStatusEffects(ActionType.OnDamaged, 1.0f);
}
#if CLIENT
if (Params.UseBossHealthBar && Controlled != null && Controlled.teamID == attacker?.teamID)
{
CharacterHUD.ShowBossHealthBar(this, attackResult.Damage);
}
#endif
return attackResult;
}
@@ -4202,7 +4212,8 @@ namespace Barotrauma
{
if (affliction.Prefab.IsBuff) { continue; }
if (Params.IsMachine && !affliction.Prefab.AffectMachines) { continue; }
if (affliction.Prefab.AfflictionType == "poison" || affliction.Prefab.AfflictionType == "paralysis")
if (affliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType ||
affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType)
{
if (!Params.Health.PoisonImmunity)
{
@@ -4282,7 +4293,7 @@ namespace Barotrauma
if (Screen.Selected != GameMain.GameScreen) { return; }
if (newStun > 0 && Params.Health.StunImmunity)
{
if (EmpVulnerability <= 0 || CharacterHealth.GetAfflictionStrength("emp", allowLimbAfflictions: false) <= 0)
if (EmpVulnerability <= 0 || CharacterHealth.GetAfflictionStrength(AfflictionPrefab.EMPType, allowLimbAfflictions: false) <= 0)
{
return;
}
@@ -4309,7 +4320,7 @@ namespace Barotrauma
float eatingRegen = Params.Health.HealthRegenerationWhenEating;
if (eatingRegen > 0)
{
CharacterHealth.ReduceAfflictionOnAllLimbs("damage".ToIdentifier(), eatingRegen * deltaTime);
CharacterHealth.ReduceAfflictionOnAllLimbs(AfflictionPrefab.DamageType, eatingRegen * deltaTime);
}
}
if (statusEffects.TryGetValue(actionType, out var statusEffectList))
@@ -4538,6 +4549,9 @@ namespace Barotrauma
{
foreach (Item heldItem in HeldItems.ToList())
{
//if the item is both wearable and holdable, and currently worn, don't drop the item
var wearable = heldItem.GetComponent<Wearable>();
if (wearable is { IsActive: true }) { continue; }
heldItem.Drop(this);
}
}
@@ -4678,6 +4692,7 @@ namespace Barotrauma
Submarine = null;
AnimController.SetPosition(ConvertUnits.ToSimUnits(worldPos), lerp: false);
AnimController.FindHull(worldPos, setSubmarine: true);
CurrentHull = AnimController.CurrentHull;
if (AIController is HumanAIController humanAI)
{
humanAI.PathSteering?.ResetPath();
@@ -4924,34 +4939,36 @@ namespace Barotrauma
return visibleHulls;
}
public Vector2 GetRelativeSimPosition(ISpatialEntity target, Vector2? worldPos = null)
public Vector2 GetRelativeSimPosition(ISpatialEntity target, Vector2? worldPos = null) => GetRelativeSimPosition(this, target, worldPos);
public static Vector2 GetRelativeSimPosition(ISpatialEntity from, ISpatialEntity to, Vector2? worldPos = null)
{
Vector2 targetPos = target.SimPosition;
Vector2 targetPos = to.SimPosition;
if (worldPos.HasValue)
{
Vector2 wp = worldPos.Value;
if (target.Submarine != null)
if (to.Submarine != null)
{
wp -= target.Submarine.Position;
wp -= to.Submarine.Position;
}
targetPos = ConvertUnits.ToSimUnits(wp);
}
if (Submarine == null && target.Submarine != null)
if (from.Submarine == null && to.Submarine != null)
{
// outside and targeting inside
targetPos += target.Submarine.SimPosition;
targetPos += to.Submarine.SimPosition;
}
else if (Submarine != null && target.Submarine == null)
else if (from.Submarine != null && to.Submarine == null)
{
// inside and targeting outside
targetPos -= Submarine.SimPosition;
targetPos -= from.Submarine.SimPosition;
}
else if (Submarine != target.Submarine)
else if (from.Submarine != to.Submarine)
{
if (Submarine != null && target.Submarine != null)
if (from.Submarine != null && to.Submarine != null)
{
// both inside, but in different subs
Vector2 diff = Submarine.SimPosition - target.Submarine.SimPosition;
Vector2 diff = from.Submarine.SimPosition - to.Submarine.SimPosition;
targetPos -= diff;
}
}
@@ -4973,13 +4990,14 @@ namespace Barotrauma
public bool HasJob(Identifier identifier) => Info?.Job?.Prefab.Identifier == identifier;
public bool IsProtectedFromPressure()
{
return HasAbilityFlag(AbilityFlags.ImmuneToPressure) || PressureProtection >= (Level.Loaded?.GetRealWorldDepth(WorldPosition.Y) ?? 1.0f);
}
/// <summary>
/// Is the character currently protected from the pressure by immunity/ability or a status effect (e.g. from a diving suit).
/// </summary>
public bool IsProtectedFromPressure => IsImmuneToPressure || PressureProtection >= (Level.Loaded?.GetRealWorldDepth(WorldPosition.Y) ?? 1.0f);
// Talent logic begins here. Should be encapsulated to its own controller soon
public bool IsImmuneToPressure => !NeedsAir || HasAbilityFlag(AbilityFlags.ImmuneToPressure);
#region Talents
private readonly List<CharacterTalent> characterTalents = new List<CharacterTalent>();
public void LoadTalents()
@@ -5053,6 +5071,49 @@ namespace Barotrauma
return info.UnlockedTalents.Contains(identifier);
}
public bool HasUnlockedAllTalents()
{
if (TalentTree.JobTalentTrees.TryGet(Info.Job.Prefab.Identifier, out TalentTree talentTree))
{
foreach (TalentSubTree talentSubTree in talentTree.TalentSubTrees)
{
foreach (TalentOption talentOption in talentSubTree.TalentOptionStages)
{
if (!talentOption.HasMaxTalents(info.UnlockedTalents))
{
return false;
}
}
}
}
return true;
}
public bool HasTalents()
{
return characterTalents.Any();
}
public void CheckTalents(AbilityEffectType abilityEffectType, AbilityObject abilityObject)
{
foreach (var characterTalent in characterTalents)
{
characterTalent.CheckTalent(abilityEffectType, abilityObject);
}
}
public void CheckTalents(AbilityEffectType abilityEffectType)
{
foreach (var characterTalent in characterTalents)
{
characterTalent.CheckTalent(abilityEffectType, null);
}
}
partial void OnTalentGiven(TalentPrefab talentPrefab);
#endregion
private readonly HashSet<Hull> sameRoomHulls = new();
/// <summary>
@@ -5079,24 +5140,6 @@ namespace Barotrauma
return sameRoomHulls.Contains(character.CurrentHull);
}
public bool HasUnlockedAllTalents()
{
if (TalentTree.JobTalentTrees.TryGet(Info.Job.Prefab.Identifier, out TalentTree talentTree))
{
foreach (TalentSubTree talentSubTree in talentTree.TalentSubTrees)
{
foreach (TalentOption talentOption in talentSubTree.TalentOptionStages)
{
if (!talentOption.HasMaxTalents(info.UnlockedTalents))
{
return false;
}
}
}
}
return true;
}
public static IEnumerable<Character> GetFriendlyCrew(Character character)
{
if (character is null)
@@ -5106,27 +5149,6 @@ namespace Barotrauma
return CharacterList.Where(c => HumanAIController.IsFriendly(character, c, onlySameTeam: true) && !c.IsDead);
}
public bool HasTalents()
{
return characterTalents.Any();
}
public void CheckTalents(AbilityEffectType abilityEffectType, AbilityObject abilityObject)
{
foreach (var characterTalent in characterTalents)
{
characterTalent.CheckTalent(abilityEffectType, abilityObject);
}
}
public void CheckTalents(AbilityEffectType abilityEffectType)
{
foreach (var characterTalent in characterTalents)
{
characterTalent.CheckTalent(abilityEffectType, null);
}
}
public bool HasRecipeForItem(Identifier recipeIdentifier)
{
return characterTalents.Any(t => t.UnlockedRecipes.Contains(recipeIdentifier));
@@ -5190,7 +5212,6 @@ namespace Barotrauma
#endif
partial void OnMoneyChanged(int prevAmount, int newAmount);
partial void OnTalentGiven(TalentPrefab talentPrefab);
/// <summary>
/// This dictionary is used for stats that are required very frequently. Not very performant, but easier to develop with for now.
@@ -5366,7 +5387,7 @@ namespace Barotrauma
public bool IsSameSpeciesOrGroup(Character other) => IsSameSpeciesOrGroup(this, other);
public static bool IsSameSpeciesOrGroup(Character me, Character other) => other.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.Group);
public static bool IsSameSpeciesOrGroup(Character me, Character other) => other.SpeciesName == me.SpeciesName || CharacterParams.CompareGroup(me.Group, other.Group);
public void StopClimbing()
{
@@ -315,6 +315,8 @@ namespace Barotrauma
public HashSet<Identifier> UnlockedTalents { get; private set; } = new HashSet<Identifier>();
public (Identifier factionId, float reputation) MinReputationToHire;
/// <summary>
/// Endocrine boosters can unlock talents outside the user's talent tree. This method is used to cull them from the selection
/// </summary>
@@ -508,8 +510,11 @@ namespace Barotrauma
public List<Order> CurrentOrders { get; } = new List<Order>();
//unique ID given to character infos in MP
//used by clients to identify which infos are the same to prevent duplicate characters in round summary
/// <summary>
/// Unique ID given to character infos in MP. Non-persistent.
/// Used by clients to identify which infos are the same to prevent duplicate characters in round summary.
/// </summary>
public ushort ID;
public List<Identifier> SpriteTags
@@ -667,7 +672,6 @@ namespace Barotrauma
{
Name = GetRandomName(randSync);
}
TryLoadNameAndTitle(npcIdentifier);
SetPersonalityTrait();
@@ -824,6 +828,8 @@ namespace Barotrauma
MissionsCompletedSinceDeath = infoElement.GetAttributeInt("missionscompletedsincedeath", 0);
UnlockedTalents = new HashSet<Identifier>();
MinReputationToHire = (infoElement.GetAttributeIdentifier("factionId", Identifier.Empty), infoElement.GetAttributeFloat("minreputation", 0.0f));
foreach (var subElement in infoElement.Elements())
{
bool jobCreated = false;
@@ -919,17 +925,25 @@ namespace Barotrauma
}
}
/// <summary>
/// Returns a presumably (not guaranteed) unique hash using the (current) Name, appearence, and job.
/// So unless there's another character with the exactly same name, job, and appearance, the hash should be unique.
/// </summary>
public int GetIdentifier()
{
return GetIdentifier(Name);
return GetIdentifierHash(Name);
}
/// <summary>
/// Returns a presumably (not guaranteed) unique hash using the OriginalName, appearence, and job.
/// So unless there's another character with the exactly same name, job, and appearance, the hash should be unique.
/// </summary>
public int GetIdentifierUsingOriginalName()
{
return GetIdentifier(OriginalName);
return GetIdentifierHash(OriginalName);
}
private int GetIdentifier(string name)
private int GetIdentifierHash(string name)
{
int id = ToolBox.StringToInt(name + string.Join("", Head.Preset.TagSet.OrderBy(s => s)));
id ^= Head.HairIndex << 12;
@@ -1152,7 +1166,7 @@ namespace Barotrauma
partial void LoadAttachmentSprites();
private int CalculateSalary()
public int CalculateSalary()
{
if (Name == null || Job == null) { return 0; }
@@ -1394,6 +1408,13 @@ namespace Barotrauma
charElement.Add(new XAttribute("missionscompletedsincedeath", MissionsCompletedSinceDeath));
if (MinReputationToHire.factionId != default)
{
charElement.Add(
new XAttribute("factionId", Name),
new XAttribute("minreputation", MinReputationToHire.reputation));
}
if (Character != null)
{
if (Character.AnimController.CurrentHull != null)
@@ -1502,7 +1523,7 @@ namespace Barotrauma
}
if (order.OrderGiver != null)
{
orderElement.Add(new XAttribute("ordergiverinfoid", order.OrderGiver.Info.ID));
orderElement.Add(new XAttribute("ordergiver", order.OrderGiver.Info?.GetIdentifier()));
}
if (order.TargetSpatialEntity?.Submarine is Submarine targetSub)
{
@@ -1596,8 +1617,8 @@ namespace Barotrauma
continue;
}
var targetType = (Order.OrderTargetType)orderElement.GetAttributeInt("targettype", 0);
int orderGiverInfoId = orderElement.GetAttributeInt("ordergiverinfoid", -1);
var orderGiver = orderGiverInfoId >= 0 ? Character.CharacterList.FirstOrDefault(c => c.Info?.ID == orderGiverInfoId) : null;
int orderGiverInfoId = orderElement.GetAttributeInt("ordergiver", -1);
var orderGiver = orderGiverInfoId >= 0 ? Character.CharacterList.FirstOrDefault(c => c.Info?.GetIdentifier() == orderGiverInfoId) : null;
Entity targetEntity = null;
switch (targetType)
{
@@ -1,10 +1,8 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;
using Microsoft.Xna.Framework;
using static Barotrauma.CharacterInfo;
namespace Barotrauma
{
@@ -19,6 +17,8 @@ namespace Barotrauma
public string Name => Identifier.Value;
public Identifier VariantOf { get; }
public CharacterPrefab ParentPrefab { get; set; }
public void InheritFrom(CharacterPrefab parent)
{
ConfigElement = CharacterParams.CreateVariantXml(originalElement, parent.ConfigElement).FromPackage(ConfigElement.ContentPackage);
@@ -38,7 +38,7 @@ namespace Barotrauma
}
}
private XElement originalElement;
private readonly XElement originalElement;
public ContentXElement ConfigElement { get; private set; }
public CharacterInfoPrefab CharacterInfoPrefab { get; private set; }
@@ -49,10 +49,6 @@ namespace Barotrauma
public static CharacterFile HumanConfigFile => HumanPrefab.ContentFile as CharacterFile;
public static CharacterPrefab HumanPrefab => FindBySpeciesName(HumanSpeciesName);
/// <summary>
/// Searches for a character config file from all currently selected content packages,
/// or from a specific package if the contentPackage parameter is given.
/// </summary>
public static CharacterPrefab FindBySpeciesName(Identifier speciesName)
{
if (!Prefabs.ContainsKey(speciesName)) { return null; }
@@ -19,6 +19,10 @@ namespace Barotrauma
private float fluctuationTimer;
private AfflictionPrefab.Effect activeEffect;
private float prevActiveEffectStrength;
protected bool activeEffectDirty = true;
protected float _strength;
[Serialize(0f, IsPropertySaveable.Yes), Editable]
@@ -46,6 +50,7 @@ namespace Barotrauma
Duration = Prefab.Duration;
}
_strength = newValue;
activeEffectDirty = true;
}
}
@@ -147,7 +152,16 @@ namespace Barotrauma
MathHelper.Clamp((int)Math.Floor(strength / maxStrength * strengthTexts.Length), 0, strengthTexts.Length - 1)];
}
public AfflictionPrefab.Effect GetActiveEffect() => Prefab.GetActiveEffect(Strength);
public AfflictionPrefab.Effect GetActiveEffect()
{
if (activeEffectDirty)
{
activeEffect = Prefab.GetActiveEffect(_strength);
prevActiveEffectStrength = _strength;
activeEffectDirty = false;
}
return activeEffect;
}
public float GetVitalityDecrease(CharacterHealth characterHealth)
{
@@ -158,7 +172,7 @@ namespace Barotrauma
{
if (strength < Prefab.ActivationThreshold) { return 0.0f; }
strength = MathHelper.Clamp(strength, 0.0f, Prefab.MaxStrength);
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(strength);
AfflictionPrefab.Effect currentEffect = GetActiveEffect();
if (currentEffect == null) { return 0.0f; }
if (currentEffect.MaxStrength - currentEffect.MinStrength <= 0.0f) { return 0.0f; }
@@ -349,7 +363,7 @@ namespace Barotrauma
public float GetStatValue(StatTypes statType)
{
if (!(GetViableEffect() is AfflictionPrefab.Effect currentEffect)) { return 0.0f; }
if (GetViableEffect() is not AfflictionPrefab.Effect currentEffect) { return 0.0f; }
if (currentEffect.AfflictionStatValues.TryGetValue(statType, out var value))
{
@@ -363,7 +377,7 @@ namespace Barotrauma
public bool HasFlag(AbilityFlags flagType)
{
if (!(GetViableEffect() is AfflictionPrefab.Effect currentEffect)) { return false; }
if (GetViableEffect() is not AfflictionPrefab.Effect currentEffect) { return false; }
return currentEffect.AfflictionAbilityFlags.HasFlag(flagType);
}
@@ -415,6 +429,7 @@ namespace Barotrauma
}
// Don't use the property, because it's virtual and some afflictions like husk overload it for external use.
_strength = MathHelper.Clamp(_strength, 0.0f, Prefab.MaxStrength);
activeEffectDirty |= !MathUtils.NearlyEqual(prevActiveEffectStrength, _strength);
foreach (StatusEffect statusEffect in currentEffect.StatusEffects)
{
@@ -445,7 +460,10 @@ namespace Barotrauma
var currentEffect = GetActiveEffect();
if (currentEffect != null)
{
currentEffect.StatusEffects.ForEach(se => ApplyStatusEffect(type, se, deltaTime, characterHealth, targetLimb));
foreach (var statusEffect in currentEffect.StatusEffects)
{
ApplyStatusEffect(type, statusEffect, deltaTime, characterHealth, targetLimb);
}
}
}
@@ -484,6 +502,7 @@ namespace Barotrauma
{
_nonClampedStrength = strength;
_strength = _nonClampedStrength;
activeEffectDirty |= !MathUtils.NearlyEqual(_strength, prevActiveEffectStrength);
}
public bool ShouldShowIcon(Character afflictedCharacter)
@@ -22,7 +22,7 @@ namespace Barotrauma
private Character character;
private bool stun = true;
private bool stun = false;
private readonly List<Affliction> huskInfection = new List<Affliction>();
@@ -43,6 +43,7 @@ namespace Barotrauma
DeactivateHusk();
highestStrength = 0;
}
activeEffectDirty = true;
}
}
private float highestStrength;
@@ -216,7 +217,8 @@ namespace Barotrauma
private void DeactivateHusk()
{
if (character?.AnimController == null || character.Removed) { return; }
if (Prefab is AfflictionPrefabHusk { NeedsAir: false })
if (Prefab is AfflictionPrefabHusk { NeedsAir: false } &&
!character.CharacterHealth.GetAllAfflictions().Any(a => a != this && a.Prefab is AfflictionPrefabHusk { NeedsAir: false }))
{
character.NeedsAir = character.Params.MainElement.GetAttributeBool("needsair", false);
}
@@ -68,7 +68,6 @@ namespace Barotrauma
}
// Remove "[speciesname]" for backward support (we don't use it anymore)
HuskedSpeciesName = HuskedSpeciesName.Remove("[speciesname]").ToIdentifier();
TargetSpecies = element.GetAttributeIdentifierArray("targets", Array.Empty<Identifier>(), trim: true);
if (TargetSpecies.Length == 0)
{
DebugConsole.NewMessage($"No 'targets' defined for the husk affliction ({Identifier}) in {element}", Color.Orange);
@@ -110,7 +109,6 @@ namespace Barotrauma
public float TransformThresholdOnDeath;
public readonly Identifier HuskedSpeciesName;
public readonly Identifier[] TargetSpecies;
public readonly bool TransferBuffs;
public readonly bool SendMessages;
@@ -344,17 +342,30 @@ namespace Barotrauma
}
}
public static readonly Identifier DamageType = "damage".ToIdentifier();
public static readonly Identifier BurnType = "burn".ToIdentifier();
public static readonly Identifier BleedingType = "bleeding".ToIdentifier();
public static readonly Identifier ParalysisType = "paralysis".ToIdentifier();
public static readonly Identifier PoisonType = "poison".ToIdentifier();
public static readonly Identifier StunType = "stun".ToIdentifier();
public static readonly Identifier EMPType = "emp".ToIdentifier();
public static readonly Identifier SpaceHerpesType = "spaceherpes".ToIdentifier();
public static readonly Identifier AlienInfectedType = "alieninfected".ToIdentifier();
public static readonly Identifier InvertControlsType = "invertcontrols".ToIdentifier();
public static readonly Identifier HuskInfectionType = "huskinfection".ToIdentifier();
public static AfflictionPrefab InternalDamage => Prefabs["internaldamage"];
public static AfflictionPrefab BiteWounds => Prefabs["bitewounds"];
public static AfflictionPrefab ImpactDamage => Prefabs["blunttrauma"];
public static AfflictionPrefab Bleeding => Prefabs["bleeding"];
public static AfflictionPrefab Burn => Prefabs["burn"];
public static AfflictionPrefab Bleeding => Prefabs[BleedingType];
public static AfflictionPrefab Burn => Prefabs[BurnType];
public static AfflictionPrefab OxygenLow => Prefabs["oxygenlow"];
public static AfflictionPrefab Bloodloss => Prefabs["bloodloss"];
public static AfflictionPrefab Pressure => Prefabs["pressure"];
public static AfflictionPrefab Stun => Prefabs["stun"];
public static AfflictionPrefab Stun => Prefabs[StunType];
public static AfflictionPrefab RadiationSickness => Prefabs["radiationsickness"];
public static readonly PrefabCollection<AfflictionPrefab> Prefabs = new PrefabCollection<AfflictionPrefab>();
public override void Dispose() { }
@@ -421,6 +432,9 @@ namespace Barotrauma
public readonly float BurnOverlayAlpha;
public readonly float DamageOverlayAlpha;
//steam achievement given when the controlled character receives the affliction
public readonly Identifier AchievementOnReceived;
//steam achievement given when the affliction is removed from the controlled character
public readonly Identifier AchievementOnRemoved;
@@ -453,6 +467,8 @@ namespace Barotrauma
private readonly ConstructorInfo constructor;
public Identifier[] TargetSpecies { get; protected set; }
public readonly bool ResetBetweenRounds;
public IEnumerable<KeyValuePair<Identifier, float>> TreatmentSuitability
@@ -536,13 +552,22 @@ namespace Barotrauma
KarmaChangeOnApplied = element.GetAttributeFloat(nameof(KarmaChangeOnApplied), 0.0f);
CauseOfDeathDescription = TextManager.Get($"AfflictionCauseOfDeath.{TranslationIdentifier}").Fallback(element.GetAttributeString("causeofdeathdescription", ""));
SelfCauseOfDeathDescription = TextManager.Get($"AfflictionCauseOfDeathSelf.{TranslationIdentifier}").Fallback(element.GetAttributeString("selfcauseofdeathdescription", ""));
CauseOfDeathDescription =
TextManager.Get($"AfflictionCauseOfDeath.{TranslationIdentifier}")
.Fallback(TextManager.Get(element.GetAttributeString("causeofdeathdescription", "")))
.Fallback(element.GetAttributeString("causeofdeathdescription", ""));
SelfCauseOfDeathDescription =
TextManager.Get($"AfflictionCauseOfDeathSelf.{TranslationIdentifier}")
.Fallback(TextManager.Get(element.GetAttributeString("selfcauseofdeathdescription", "")))
.Fallback(element.GetAttributeString("selfcauseofdeathdescription", ""));
IconColors = element.GetAttributeColorArray(nameof(IconColors), null);
AfflictionOverlayAlphaIsLinear = element.GetAttributeBool(nameof(AfflictionOverlayAlphaIsLinear), false);
AchievementOnReceived = element.GetAttributeIdentifier(nameof(AchievementOnReceived), "");
AchievementOnRemoved = element.GetAttributeIdentifier(nameof(AchievementOnRemoved), "");
TargetSpecies = element.GetAttributeIdentifierArray("targets", Array.Empty<Identifier>(), trim: true);
ResetBetweenRounds = element.GetAttributeBool("resetbetweenrounds", false);
DamageParticles = element.GetAttributeBool(nameof(DamageParticles), true);
@@ -344,12 +344,12 @@ namespace Barotrauma
return null;
}
public T GetAffliction<T>(string identifier, bool allowLimbAfflictions = true) where T : Affliction
public T GetAffliction<T>(Identifier identifier, bool allowLimbAfflictions = true) where T : Affliction
{
return GetAffliction(identifier, allowLimbAfflictions) as T;
}
public Affliction GetAffliction(string identifier, Limb limb)
public Affliction GetAffliction(Identifier identifier, Limb limb)
{
if (limb.HealthIndex < 0 || limb.HealthIndex >= limbHealths.Count)
{
@@ -385,7 +385,7 @@ namespace Barotrauma
/// <param name="limb">The limb the affliction is attached to</param>
/// <param name="requireLimbSpecific">Does the affliction have to be attached to only the specific limb.
/// Most monsters for example don't have separate healths for different limbs, essentially meaning that every affliction is applied to every limb.</param>
public float GetAfflictionStrength(string afflictionType, Limb limb, bool requireLimbSpecific)
public float GetAfflictionStrength(Identifier afflictionType, Limb limb, bool requireLimbSpecific)
{
if (requireLimbSpecific && limbHealths.Count == 1) { return 0.0f; }
@@ -406,7 +406,7 @@ namespace Barotrauma
return strength;
}
public float GetAfflictionStrength(string afflictionType, bool allowLimbAfflictions = true)
public float GetAfflictionStrength(Identifier afflictionType, bool allowLimbAfflictions = true)
{
float strength = 0.0f;
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
@@ -489,16 +489,19 @@ namespace Barotrauma
ReduceMatchingAfflictions(amount, treatmentAction);
}
public void ReduceAfflictionOnAllLimbs(Identifier affliction, float amount, ActionType? treatmentAction = null)
public void ReduceAfflictionOnAllLimbs(Identifier afflictionIdOrType, float amount, ActionType? treatmentAction = null)
{
if (affliction.IsEmpty) { throw new ArgumentException($"{nameof(affliction)} is empty"); }
if (afflictionIdOrType.IsEmpty) { throw new ArgumentException($"{nameof(afflictionIdOrType)} is empty"); }
matchingAfflictions.Clear();
matchingAfflictions.AddRange(afflictions.Keys);
matchingAfflictions.RemoveAll(a =>
a.Prefab.Identifier != affliction &&
a.Prefab.AfflictionType != affliction);
foreach (var affliction in afflictions)
{
if (affliction.Key.Prefab.Identifier == afflictionIdOrType || affliction.Key.Prefab.AfflictionType == afflictionIdOrType)
{
matchingAfflictions.Add(affliction.Key);
}
}
ReduceMatchingAfflictions(amount, treatmentAction);
}
@@ -515,18 +518,21 @@ namespace Barotrauma
ReduceMatchingAfflictions(amount, treatmentAction);
}
public void ReduceAfflictionOnLimb(Limb targetLimb, Identifier affliction, float amount, ActionType? treatmentAction = null)
public void ReduceAfflictionOnLimb(Limb targetLimb, Identifier afflictionIdOrType, float amount, ActionType? treatmentAction = null)
{
if (affliction.IsEmpty) { throw new ArgumentException($"{nameof(affliction)} is empty"); }
if (afflictionIdOrType.IsEmpty) { throw new ArgumentException($"{nameof(afflictionIdOrType)} is empty"); }
if (targetLimb is null) { throw new ArgumentNullException(nameof(targetLimb)); }
matchingAfflictions.Clear();
matchingAfflictions.AddRange(GetAfflictionsForLimb(targetLimb));
matchingAfflictions.RemoveAll(a =>
a.Prefab.Identifier != affliction &&
a.Prefab.AfflictionType != affliction);
var targetLimbHealth = limbHealths[targetLimb.HealthIndex];
foreach (var affliction in afflictions)
{
if ((affliction.Key.Prefab.Identifier == afflictionIdOrType || affliction.Key.Prefab.AfflictionType == afflictionIdOrType) &&
affliction.Value == targetLimbHealth)
{
matchingAfflictions.Add(affliction.Key);
}
}
ReduceMatchingAfflictions(amount, treatmentAction);
}
@@ -633,7 +639,7 @@ namespace Barotrauma
KillIfOutOfVitality();
}
public float GetLimbDamage(Limb limb, string afflictionType = null)
public float GetLimbDamage(Limb limb, Identifier afflictionType)
{
float damageStrength;
if (limb.IsSevered)
@@ -646,16 +652,16 @@ namespace Barotrauma
// Therefore with e.g. 80 health, the max damage per limb would be 40.
// Having at least 40 damage on both legs would cause maximum limping.
float max = MaxVitality / 2;
if (string.IsNullOrEmpty(afflictionType))
if (afflictionType.IsEmpty)
{
float damage = GetAfflictionStrength("damage", limb, true);
float bleeding = GetAfflictionStrength("bleeding", limb, true);
float burn = GetAfflictionStrength("burn", limb, true);
float damage = GetAfflictionStrength(AfflictionPrefab.DamageType, limb, true);
float bleeding = GetAfflictionStrength(AfflictionPrefab.BleedingType, limb, true);
float burn = GetAfflictionStrength(AfflictionPrefab.BurnType, limb, true);
damageStrength = Math.Min(damage + bleeding + burn, max);
}
else
{
damageStrength = Math.Min(GetAfflictionStrength("damage", limb, true), max);
damageStrength = Math.Min(GetAfflictionStrength(afflictionType, limb, true), max);
}
return damageStrength / max;
}
@@ -712,22 +718,17 @@ namespace Barotrauma
if (Character.Params.IsMachine && !newAffliction.Prefab.AffectMachines) { return; }
if (!DoesBleed && newAffliction is AfflictionBleeding) { return; }
if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) { return; }
if (Character.Params.Health.StunImmunity && newAffliction.Prefab.AfflictionType == "stun")
if (Character.Params.Health.StunImmunity && newAffliction.Prefab.AfflictionType == AfflictionPrefab.StunType)
{
if (Character.EmpVulnerability <= 0 || GetAfflictionStrength("emp", allowLimbAfflictions: false) <= 0)
{
return;
}
}
if (Character.Params.Health.PoisonImmunity && (newAffliction.Prefab.AfflictionType == "poison" || newAffliction.Prefab.AfflictionType == "paralysis")) { return; }
if (Character.EmpVulnerability <= 0 && newAffliction.Prefab.AfflictionType == "emp") { return; }
if (newAffliction.Prefab is AfflictionPrefabHusk huskPrefab)
{
if (huskPrefab.TargetSpecies.None(s => s == Character.SpeciesName))
if (Character.EmpVulnerability <= 0 || GetAfflictionStrength(AfflictionPrefab.EMPType, allowLimbAfflictions: false) <= 0)
{
return;
}
}
if (Character.Params.Health.PoisonImmunity &&
(newAffliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType || newAffliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType)) { return; }
if (Character.EmpVulnerability <= 0 && newAffliction.Prefab.AfflictionType == AfflictionPrefab.EMPType) { return; }
if (newAffliction.Prefab.TargetSpecies.Any() && newAffliction.Prefab.TargetSpecies.None(s => s == Character.SpeciesName)) { return; }
var should = GameMain.LuaCs.Hook.Call<bool?>("character.applyAffliction", this, limbHealth, newAffliction, allowStacking);
@@ -769,7 +770,9 @@ namespace Barotrauma
Math.Min(newAffliction.Prefab.MaxStrength, newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(newAffliction.Prefab))),
newAffliction.Source);
afflictions.Add(copyAffliction, limbHealth);
SteamAchievementManager.OnAfflictionReceived(copyAffliction, Character);
MedicalClinic.OnAfflictionCountChanged(Character);
Character.HealthUpdateInterval = 0.0f;
CalculateVitality();
@@ -842,10 +845,16 @@ namespace Barotrauma
}
Character.StackSpeedMultiplier(affliction.GetSpeedMultiplier());
}
foreach (var affliction in afflictionsToRemove)
{
afflictions.Remove(affliction);
}
}
if (afflictionsToRemove.Count is not 0)
{
MedicalClinic.OnAfflictionCountChanged(Character);
}
}
Character.StackSpeedMultiplier(1f + Character.GetStatValue(StatTypes.MovementSpeed));
@@ -899,6 +908,11 @@ namespace Barotrauma
}
}
/// <summary>
/// 0-1.
/// </summary>
public float OxygenLowResistance => !Character.NeedsOxygen ? 1 : GetResistance(oxygenLowAffliction.Prefab);
private void UpdateOxygen(float deltaTime)
{
if (!Character.NeedsOxygen)
@@ -27,6 +27,32 @@ namespace Barotrauma
[Serialize(1f, IsPropertySaveable.No)]
public float AimAccuracy { get; protected set; }
[Serialize(1f, IsPropertySaveable.No)]
public float SkillMultiplier { get; protected set; }
[Serialize(0, IsPropertySaveable.No)]
public int ExperiencePoints { get; private set; }
private readonly HashSet<Identifier> tags = new HashSet<Identifier>();
[Serialize("", IsPropertySaveable.Yes)]
public string Tags
{
get => string.Join(",", tags);
set
{
tags.Clear();
if (!string.IsNullOrWhiteSpace(value))
{
string[] splitTags = value.Split(',');
foreach (var tag in splitTags)
{
tags.Add(tag.ToIdentifier());
}
}
}
}
private readonly HashSet<Identifier> moduleFlags = new HashSet<Identifier>();
[Serialize("", IsPropertySaveable.Yes, "What outpost module tags does the NPC prefer to spawn in.")]
@@ -79,6 +105,12 @@ namespace Barotrauma
public Identifier[] PreferredOutpostModuleTypes { get; protected set; }
[Serialize("", IsPropertySaveable.No)]
public Identifier Faction { get; set; }
[Serialize("", IsPropertySaveable.No)]
public Identifier Group { get; set; }
public XElement Element { get; protected set; }
@@ -97,6 +129,11 @@ namespace Barotrauma
this.NpcSetIdentifier = npcSetIdentifier;
}
public IEnumerable<Identifier> GetTags()
{
return tags;
}
public IEnumerable<Identifier> GetModuleFlags()
{
return moduleFlags;
@@ -148,7 +185,7 @@ namespace Barotrauma
}
}
public bool GiveItems(Character character, Submarine submarine, Rand.RandSync randSync = Rand.RandSync.Unsynced, bool createNetworkEvents = true)
public bool GiveItems(Character character, Submarine submarine, WayPoint spawnPoint, Rand.RandSync randSync = Rand.RandSync.Unsynced, bool createNetworkEvents = true)
{
if (ItemSets == null || !ItemSets.Any()) { return false; }
var spawnItems = ToolBox.SelectWeightedRandom(ItemSets, it => it.commonness, randSync).element;
@@ -159,7 +196,7 @@ namespace Barotrauma
int amount = itemElement.GetAttributeInt("amount", 1);
for (int i = 0; i < amount; i++)
{
InitializeItem(character, itemElement, submarine, this, createNetworkEvents: createNetworkEvents);
InitializeItem(character, itemElement, submarine, this, spawnPoint, createNetworkEvents: createNetworkEvents);
}
}
}
@@ -177,17 +214,26 @@ namespace Barotrauma
CharacterInfo characterInfo;
if (characterElement == null)
{
characterInfo= new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: GetJobPrefab(randSync), npcIdentifier: Identifier);
}
characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: GetJobPrefab(randSync), npcIdentifier: Identifier, randSync: randSync);}
else
{
characterInfo = new CharacterInfo(characterElement, Identifier);
}
if (characterInfo.Job != null && !MathUtils.NearlyEqual(SkillMultiplier, 1.0f))
{
foreach (var skill in characterInfo.Job.GetSkills())
{
float newSkill = skill.Level * SkillMultiplier;
skill.IncreaseSkill(newSkill - skill.Level, increasePastMax: false);
}
characterInfo.Salary = characterInfo.CalculateSalary();
}
characterInfo.HumanPrefabIds = (NpcSetIdentifier, Identifier);
characterInfo.GiveExperience(ExperiencePoints);
return characterInfo;
}
public static void InitializeItem(Character character, XElement itemElement, Submarine submarine, HumanPrefab humanPrefab, Item parentItem = null, bool createNetworkEvents = true)
public static void InitializeItem(Character character, XElement itemElement, Submarine submarine, HumanPrefab humanPrefab, WayPoint spawnPoint = null, Item parentItem = null, bool createNetworkEvents = true)
{
ItemPrefab itemPrefab;
string itemIdentifier = itemElement.GetAttributeString("identifier", "");
@@ -231,7 +277,7 @@ namespace Barotrauma
IdCard idCardComponent = item.GetComponent<IdCard>();
if (idCardComponent != null)
{
idCardComponent.Initialize(null, character);
idCardComponent.Initialize(spawnPoint, character);
if (submarine != null && (submarine.Info.IsWreck || submarine.Info.IsOutpost))
{
idCardComponent.SubmarineSpecificID = submarine.SubmarineSpecificIDTag;
@@ -254,7 +300,7 @@ namespace Barotrauma
int amount = childItemElement.GetAttributeInt("amount", 1);
for (int i = 0; i < amount; i++)
{
InitializeItem(character, childItemElement, submarine, humanPrefab, item, createNetworkEvents);
InitializeItem(character, childItemElement, submarine, humanPrefab, spawnPoint, item, createNetworkEvents);
}
}
}
@@ -21,7 +21,7 @@ namespace Barotrauma
level = MathHelper.Clamp(level + value, 0.0f, increasePastMax ? SkillSettings.Current.MaximumSkillWithTalents : MaximumSkill);
}
private Identifier iconJobId;
private readonly Identifier iconJobId;
public Sprite Icon => !iconJobId.IsEmpty && JobPrefab.Prefabs.TryGet(iconJobId, out var jobPrefab)
? jobPrefab.Icon
@@ -666,13 +666,13 @@ namespace Barotrauma
switch (body.BodyShape)
{
case PhysicsBody.Shape.Circle:
attack.DamageRange = body.radius;
attack.DamageRange = body.Radius;
break;
case PhysicsBody.Shape.Capsule:
attack.DamageRange = body.height / 2 + body.radius;
attack.DamageRange = body.Height / 2 + body.Radius;
break;
case PhysicsBody.Shape.Rectangle:
attack.DamageRange = new Vector2(body.width / 2.0f, body.height / 2.0f).Length();
attack.DamageRange = new Vector2(body.Width / 2.0f, body.Height / 2.0f).Length();
break;
}
attack.DamageRange = ConvertUnits.ToDisplayUnits(attack.DamageRange);
@@ -786,11 +786,12 @@ namespace Barotrauma
}
if (!foundMatchingModifier && random > affliction.Probability) { continue; }
float finalDamageModifier = damageMultiplier;
if (character.EmpVulnerability > 0 && affliction.Prefab.AfflictionType == "emp")
if (character.EmpVulnerability > 0 && affliction.Prefab.AfflictionType == AfflictionPrefab.EMPType)
{
finalDamageModifier *= character.EmpVulnerability;
}
if (!character.Params.Health.PoisonImmunity && (affliction.Prefab.AfflictionType == "poison" || affliction.Prefab.AfflictionType == "paralysis"))
if (!character.Params.Health.PoisonImmunity &&
(affliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType || affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType))
{
finalDamageModifier *= character.PoisonVulnerability;
}
@@ -1108,7 +1109,7 @@ namespace Barotrauma
Vector2 forceWorld = attack.CalculateAttackPhase(attack.RootTransitionEasing);
forceWorld.X *= character.AnimController.Dir;
character.AnimController.MainLimb.body.ApplyLinearImpulse(character.Mass * forceWorld, character.SimPosition, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
if (!attack.IsRunning)
if (!attack.IsRunning && !attack.Ranged)
{
// Set the main collider where the body lands after the attack
if (Vector2.DistanceSquared(character.AnimController.Collider.SimPosition, character.AnimController.MainLimb.body.SimPosition) > 0.1f * 0.1f)
@@ -1303,7 +1304,8 @@ namespace Barotrauma
}
private float blinkTimer;
private float blinkPhase;
public float BlinkPhase;
public bool FreezeBlinkState;
private float TotalBlinkDurationOut => Params.BlinkDurationOut + Params.BlinkHoldTime;
@@ -1316,16 +1318,25 @@ namespace Barotrauma
{
if (blinkTimer > -TotalBlinkDurationOut)
{
blinkPhase -= deltaTime;
if (blinkPhase > 0)
if (!FreezeBlinkState)
{
BlinkPhase -= deltaTime;
}
if (BlinkPhase > 0)
{
// in
float t = ToolBox.GetEasing(Params.BlinkTransitionIn, MathUtils.InverseLerp(1, 0, blinkPhase / Params.BlinkDurationIn));
float t = ToolBox.GetEasing(Params.BlinkTransitionIn, MathUtils.InverseLerp(1, 0, BlinkPhase / Params.BlinkDurationIn));
body.SmoothRotate(referenceRotation + MathHelper.ToRadians(Params.BlinkRotationIn) * Dir, Mass * Params.BlinkForce * t, wrapAngle: true);
if (Params.UseTextureOffsetForBlinking)
{
#if CLIENT
ActiveSprite.RelativeOrigin = Vector2.Lerp(Params.BlinkTextureOffsetOut, Params.BlinkTextureOffsetIn, t);
#endif
}
}
else
{
if (Math.Abs(blinkPhase) < Params.BlinkHoldTime)
if (Math.Abs(BlinkPhase) < Params.BlinkHoldTime)
{
// hold
body.SmoothRotate(referenceRotation + MathHelper.ToRadians(Params.BlinkRotationIn) * Dir, Mass * Params.BlinkForce, wrapAngle: true);
@@ -1333,15 +1344,25 @@ namespace Barotrauma
else
{
// out
float t = ToolBox.GetEasing(Params.BlinkTransitionOut, MathUtils.InverseLerp(0, 1, -blinkPhase / TotalBlinkDurationOut));
//float t = ToolBox.GetEasing(Params.BlinkTransitionOut, MathUtils.InverseLerp(0, 1, -blinkPhase / TotalBlinkDurationOut));
float t = ToolBox.GetEasing(Params.BlinkTransitionOut, MathUtils.InverseLerp(0, 1, (-BlinkPhase - Params.BlinkHoldTime) / Params.BlinkDurationOut));
body.SmoothRotate(referenceRotation + MathHelper.ToRadians(Params.BlinkRotationOut) * Dir, Mass * Params.BlinkForce * t, wrapAngle: true);
if (Params.UseTextureOffsetForBlinking)
{
#if CLIENT
ActiveSprite.RelativeOrigin = Vector2.Lerp(Params.BlinkTextureOffsetIn, Params.BlinkTextureOffsetOut, t);
#endif
}
}
}
}
else
{
// out
blinkPhase = Params.BlinkDurationIn;
if (!FreezeBlinkState)
{
BlinkPhase = Params.BlinkDurationIn;
}
body.SmoothRotate(referenceRotation + MathHelper.ToRadians(Params.BlinkRotationOut) * Dir, Mass * Params.BlinkForce, wrapAngle: true);
}
}
@@ -177,6 +177,9 @@ namespace Barotrauma
set => SetFootAngles(FootAnglesInRadians, value);
}
[Serialize(false, IsPropertySaveable.Yes, description: "Should the animation be updated even if the character is not moving?"), Editable]
public bool UpdateAnimationWhenNotMoving { get; set; }
/// <summary>
/// Key = limb id, value = angle in radians
/// </summary>
@@ -56,6 +56,9 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.No), Editable]
public bool CanSpeak { get; set; }
[Serialize(true, IsPropertySaveable.Yes), Editable]
public bool ShowHealthBar { get; private set; }
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool UseBossHealthBar { get; private set; }
@@ -110,6 +113,12 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool DrawLast { get; set; }
[Serialize(1.0f, IsPropertySaveable.Yes, "Tells the bots how much they should prefer targeting this character with submarine weapons. Defaults to 1. Set 0 to tell the bots not to target this character at all. Distance to the target affects the decision making."), Editable]
public float AITurretPriority { get; set; }
[Serialize(1.0f, IsPropertySaveable.Yes, "Tells the bots how much they should prefer targeting this character with submarine weapons tagged as \"slowturret\", like railguns. The tag is arbitrary and can be added to any turrets, just like the priority. Defaults to 1. Not used if AITurretPriority is 0. Distance to the target affects the decision making."), Editable]
public float AISlowTurretPriority { get; set; }
public readonly CharacterFile File;
public XDocument VariantFile { get; private set; }
@@ -214,7 +223,7 @@ namespace Barotrauma
return true;
}
public bool CompareGroup(Identifier group) => group != Identifier.Empty && Group != Identifier.Empty && group == Group;
public static bool CompareGroup(Identifier group1, Identifier group2) => group1 != Identifier.Empty && group2 != Identifier.Empty && group1 == group2;
protected void CreateSubParams()
{
@@ -476,7 +485,7 @@ namespace Barotrauma
[Serialize(true, IsPropertySaveable.Yes), Editable]
public bool DoesBleed { get; set; }
[Serialize(float.NegativeInfinity, IsPropertySaveable.Yes), Editable(minValue: float.NegativeInfinity, maxValue: 0)]
[Serialize(float.PositiveInfinity, IsPropertySaveable.Yes), Editable(minValue: 0, maxValue: float.PositiveInfinity)]
public float CrushDepth { get; set; }
// Make editable?
@@ -512,7 +521,20 @@ namespace Barotrauma
// TODO: limbhealths, sprite?
public HealthParams(ContentXElement element, CharacterParams character) : base(element, character) { }
public HealthParams(ContentXElement element, CharacterParams character) : base(element, character)
{
//backwards compatibility
if (CrushDepth < 0)
{
//invert y, convert to meters, and add 1000 to be on the safe side (previously the value would be from the bottom of the level)
float newCrushDepth = -CrushDepth * Physics.DisplayToRealWorldRatio + 1000;
DebugConsole.AddWarning($"Character \"{character.SpeciesName}\" has a negative crush depth. "+
"Previously the crush depths were defined as display units (e.g. -30000 would correspond to 300 meters below the level), "+
"but now they're in meters (e.g. 3000 would correspond to a depth of 3000 meters displayed on the nav terminal). "+
$"Changing the crush depth from {CrushDepth} to {newCrushDepth}.");
CrushDepth = newCrushDepth;
}
}
}
public class InventoryParams : SubParam
@@ -615,6 +637,9 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.Yes, description:"Does the creature know how to open doors (still requires a proper ID card). Humans can always open doors (They don't use this AI definition)."), Editable]
public bool CanOpenDoors { get; private set; }
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool UsePathFindingToGetInside { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Does the creature close the doors behind it. Humans don't use this AI definition."), Editable]
public bool KeepDoorsClosed { get; private set; }
@@ -823,10 +848,19 @@ namespace Barotrauma
[Serialize(5000f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 20000f)]
public float CircleStartDistance { get; private set; }
[Serialize(1f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.5f, MaxValueFloat = 2f)]
[Serialize(false, IsPropertySaveable.Yes, description:"Normally the target size is taken into account when calculating the distance to the target. Set this true to skip that.")]
public bool IgnoreTargetSize { get; private set; }
[Serialize(1f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 100f)]
public float CircleRotationSpeed { get; private set; }
[Serialize(5f, IsPropertySaveable.Yes), Editable(MinValueFloat = 1f, MaxValueFloat = 10f)]
[Serialize(false, IsPropertySaveable.Yes, description:"When enabled, the circle rotation speed can change when the target is far. When this setting is disabled (default), the character will head directly towards the target when it's too far."), Editable]
public bool DynamicCircleRotationSpeed { get; private set; }
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 1f)]
public float CircleRandomRotationFactor { get; private set; }
[Serialize(5f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 10f)]
public float CircleStrikeDistanceMultiplier { get; private set; }
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 50f)]
@@ -6,6 +6,7 @@ using System.Linq;
using Barotrauma.IO;
using System.Xml;
using Barotrauma.Extensions;
using FarseerPhysics;
#if CLIENT
using Barotrauma.SpriteDeformations;
#endif
@@ -621,13 +622,13 @@ namespace Barotrauma
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
public float SteerForce { get; set; }
[Serialize(0f, IsPropertySaveable.Yes, description: "Radius of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
[Serialize(0f, IsPropertySaveable.Yes, description: "Radius of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 2048)]
public float Radius { get; set; }
[Serialize(0f, IsPropertySaveable.Yes, description: "Height of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
[Serialize(0f, IsPropertySaveable.Yes, description: "Height of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 2048)]
public float Height { get; set; }
[Serialize(0f, IsPropertySaveable.Yes, description: "Width of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
[Serialize(0f, IsPropertySaveable.Yes, description: "Width of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 2048)]
public float Width { get; set; }
[Serialize(10f, IsPropertySaveable.Yes, description: "The more the density the heavier the limb is."), Editable(MinValueFloat = 0.01f, MaxValueFloat = 100, DecimalCount = 2)]
@@ -706,6 +707,15 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool OnlyBlinkInWater { get; set; }
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool UseTextureOffsetForBlinking { get; set; }
[Serialize("0.5, 0.5", IsPropertySaveable.Yes), Editable(DecimalCount = 2, MinValueFloat = 0f, MaxValueFloat = 1f)]
public Vector2 BlinkTextureOffsetIn { get; set; }
[Serialize("0.5, 0.5", IsPropertySaveable.Yes), Editable(DecimalCount = 2, MinValueFloat = 0f, MaxValueFloat = 1f)]
public Vector2 BlinkTextureOffsetOut { get; set; }
[Serialize(TransitionMode.Linear, IsPropertySaveable.Yes), Editable]
public TransitionMode BlinkTransitionIn { get; private set; }
@@ -1026,15 +1036,18 @@ namespace Barotrauma
}
}
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 2048)]
public float Radius { get; set; }
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 2048)]
public float Height { get; set; }
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 2048)]
public float Width { get; set; }
[Serialize(BodyType.Dynamic, IsPropertySaveable.Yes), Editable]
public BodyType BodyType { get; set; }
public ColliderParams(ContentXElement element, RagdollParams ragdoll, string name = null) : base(element, ragdoll)
{
Name = name;
@@ -14,45 +14,51 @@ namespace Barotrauma.Abilities
{
string[] missionTypeStrings = conditionElement.GetAttributeStringArray("missiontype", new []{ "None" })!;
HashSet<MissionType> missionTypes = new HashSet<MissionType>();
isAffiliated = conditionElement.GetAttributeBool("isaffiliated", false);
foreach (string missionTypeString in missionTypeStrings)
{
if (!Enum.TryParse(missionTypeString, out MissionType parsedMission) || parsedMission is MissionType.None)
{
DebugConsole.ThrowError($"Error in AbilityConditionMission \"{characterTalent.DebugIdentifier}\" - \"{missionTypeString}\" is not a valid mission type.");
return;
if (!isAffiliated)
{
DebugConsole.ThrowError($"Error in AbilityConditionMission \"{characterTalent.DebugIdentifier}\" - \"{missionTypeString}\" is not a valid mission type.");
}
continue;
}
missionTypes.Add(parsedMission);
}
missionType = missionTypes.ToImmutableHashSet();
isAffiliated = conditionElement.GetAttributeBool("isaffiliated", false);
}
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
if (abilityObject is IAbilityMission { Mission: { } mission })
{
if (isAffiliated)
if (!isAffiliated) { return CheckMissionType(); }
if (GameMain.GameSession?.Campaign?.Factions is not { } factions) { return false; }
foreach (var (factionIdentifier, amount) in mission.ReputationRewards)
{
if (GameMain.GameSession?.Campaign?.Factions is not { } factions) { return false; }
foreach (var (factionIdentifier, amount) in mission.ReputationRewards)
if (amount <= 0) { continue; }
if (GetMatchingFaction(factionIdentifier) is { } faction &&
Faction.GetPlayerAffiliationStatus(faction) is FactionAffiliation.Positive)
{
if (amount <= 0) { continue; }
Faction faction = factions.FirstOrDefault(faction => factionIdentifier == faction.Prefab.Identifier);
if (faction?.GetPlayerAffiliationStatus() is FactionAffiliation.Affiliated)
{
return true;
}
return CheckMissionType();
}
return false;
}
return missionType.Contains(mission.Prefab.Type);
return false;
Faction GetMatchingFaction(Identifier factionIdentifier) =>
factionIdentifier == "location"
? mission.OriginLocation?.Faction
: factions.FirstOrDefault(f => factionIdentifier == f.Prefab.Identifier);
bool CheckMissionType() => missionType.IsEmpty || missionType.Contains(mission.Prefab.Type);
}
LogAbilityConditionError(abilityObject, typeof(IAbilityMission));
@@ -23,7 +23,6 @@
protected override bool MatchesConditionSpecific()
{
Identifier identifier = CharacterAbilityGivePermanentStat.HandlePlaceholders(placeholder, statIdentifier);
return character.Info.GetSavedStatValue(statType, identifier) >= min;
}
}
@@ -4,6 +4,7 @@ using System;
using Barotrauma.Extensions;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
namespace Barotrauma.Abilities
{
@@ -29,11 +30,33 @@ namespace Barotrauma.Abilities
if (!TalentTree.JobTalentTrees.TryGet(apprentice.Identifier, out TalentTree? talentTree)) { return; }
ImmutableHashSet<Character> characters = GameSession.GetSessionCrewCharacters(CharacterType.Both);
HashSet<ImmutableHashSet<Identifier>> talentsTrees = new HashSet<ImmutableHashSet<Identifier>>();
foreach (TalentSubTree subTree in talentTree.TalentSubTrees)
{
if (subTree.Type != TalentTreeType.Specialization) { continue; }
talentsTrees.Add(subTree.AllTalentIdentifiers);
HashSet<Identifier> identifiers = new HashSet<Identifier>();
foreach (TalentOption option in subTree.TalentOptionStages)
{
foreach (Identifier identifier in option.TalentIdentifiers)
{
if (IsShowCaseTalent(identifier, option) || TalentTree.IsTalentLocked(identifier, characters)) { continue; }
identifiers.Add(identifier);
}
foreach (var (_, value) in option.ShowCaseTalents)
{
var ids = value.Where(i => !TalentTree.IsTalentLocked(i, characters)).ToImmutableHashSet();
if (ids.Count is 0) { continue; }
identifiers.Add(value.GetRandomUnsynced());
}
}
talentsTrees.Add(identifiers.ToImmutableHashSet());
}
ImmutableHashSet<Identifier> selectedTalentTree = talentsTrees.GetRandomUnsynced();
@@ -44,6 +67,16 @@ namespace Barotrauma.Abilities
Character.GiveTalent(identifier);
}
static bool IsShowCaseTalent(Identifier identifier, TalentOption option)
{
foreach (var (_, value) in option.ShowCaseTalents)
{
if (value.Contains(identifier)) { return true; }
}
return false;
}
}
protected override void ApplyEffect(AbilityObject abilityObject)
@@ -131,6 +131,8 @@ namespace Barotrauma
if (character.Info.GetTotalTalentPoints() - selectedTalents.Count <= 0) { return false; }
if (!JobTalentTrees.TryGet(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return false; }
if (IsTalentLocked(talentIdentifier)) { return false; }
foreach (var subTree in talentTree!.TalentSubTrees)
{
if (subTree.AllTalentIdentifiers.Contains(talentIdentifier) && subTree.HasMaxTalents(selectedTalents)) { return false; }
@@ -152,6 +154,18 @@ namespace Barotrauma
return false;
}
public static bool IsTalentLocked(Identifier talentIdentifier, ImmutableHashSet<Character> characterList = null)
{
characterList ??= GameSession.GetSessionCrewCharacters(CharacterType.Both);
foreach (Character c in characterList)
{
if (c.Info.GetSavedStatValue(StatTypes.LockedTalents, talentIdentifier) >= 1) { return true; }
}
return false;
}
public static List<Identifier> CheckTalentSelection(Character controlledCharacter, IEnumerable<Identifier> selectedTalents)
{
List<Identifier> viableTalents = new List<Identifier>();