Merge branch 'master' of https://github.com/Regalis11/Barotrauma into develop
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+11
-10
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -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));
|
||||
|
||||
+39
-29
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -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; }
|
||||
|
||||
+24
-19
@@ -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
|
||||
|
||||
+14
-10
@@ -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
|
||||
{
|
||||
|
||||
+18
-7
@@ -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: () =>
|
||||
|
||||
+55
-24
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-4
@@ -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)
|
||||
|
||||
+8
-3
@@ -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();
|
||||
}
|
||||
|
||||
+22
-12
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -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);
|
||||
|
||||
+75
-46
@@ -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
|
||||
{
|
||||
|
||||
+30
-29
@@ -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; }
|
||||
|
||||
+24
-5
@@ -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)
|
||||
|
||||
+4
-2
@@ -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);
|
||||
}
|
||||
|
||||
+32
-7
@@ -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)]
|
||||
|
||||
+19
-6
@@ -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;
|
||||
|
||||
+24
-18
@@ -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));
|
||||
|
||||
-1
@@ -23,7 +23,6 @@
|
||||
protected override bool MatchesConditionSpecific()
|
||||
{
|
||||
Identifier identifier = CharacterAbilityGivePermanentStat.HandlePlaceholders(placeholder, statIdentifier);
|
||||
|
||||
return character.Info.GetSavedStatValue(statType, identifier) >= min;
|
||||
}
|
||||
}
|
||||
|
||||
+34
-1
@@ -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>();
|
||||
|
||||
Reference in New Issue
Block a user