Build 1.1.4.0

This commit is contained in:
Markus Isberg
2023-03-31 18:40:44 +03:00
parent efba17e0ff
commit 9470edead3
483 changed files with 17487 additions and 8548 deletions
@@ -168,6 +168,7 @@ namespace Barotrauma
public void FaceTarget(ISpatialEntity target) => Character.AnimController.TargetDir = target.WorldPosition.X > Character.WorldPosition.X ? Direction.Right : Direction.Left;
public bool IsSteeringThroughGap { get; protected set; }
public bool IsTryingToSteerThroughGap { get; protected set; }
public virtual bool SteerThroughGap(Structure wall, WallSection section, Vector2 targetWorldPos, float deltaTime)
{
@@ -444,7 +445,7 @@ namespace Barotrauma
if (EscapeTarget != null)
{
var door = EscapeTarget.ConnectedDoor;
bool isClosedDoor = door != null && !door.IsOpen;
bool isClosedDoor = door != null && door.IsClosed;
Vector2 diff = EscapeTarget.WorldPosition - Character.WorldPosition;
float sqrDist = diff.LengthSquared();
bool isClose = sqrDist < MathUtils.Pow2(100);
@@ -245,11 +245,6 @@ namespace Barotrauma
{
throw new Exception($"Tried to create an enemy ai controller for human!");
}
if (Character.Params.Group == "human")
{
// Pet
Character.TeamID = CharacterTeamType.FriendlyNPC;
}
var mainElement = c.Params.OriginalElement.IsOverride() ? c.Params.OriginalElement.FirstElement() : c.Params.OriginalElement;
targetMemories = new Dictionary<AITarget, AITargetMemory>();
steeringManager = outsideSteering;
@@ -309,17 +304,20 @@ namespace Barotrauma
break;
}
}
//pets are friendly!
if (PetBehavior != null || Character.Group == "human")
{
Character.TeamID = CharacterTeamType.FriendlyNPC;
}
ReevaluateAttacks();
outsideSteering = new SteeringManager(this);
insideSteering = new IndoorsSteeringManager(this, Character.Params.AI.CanOpenDoors, canAttackDoors);
steeringManager = outsideSteering;
State = AIState.Idle;
requiredHoleCount = (int)Math.Ceiling(ConvertUnits.ToDisplayUnits(colliderWidth) / Structure.WallSectionSize);
myBodies = Character.AnimController.Limbs.Select(l => l.body.FarseerBody).ToList();
myBodies.Add(Character.AnimController.Collider.FarseerBody);
CreatureMetrics.UnlockInEditor(Character.SpeciesName);
}
private CharacterParams.AIParams _aiParams;
@@ -452,6 +450,7 @@ namespace Barotrauma
base.Update(deltaTime);
UpdateTriggers(deltaTime);
Character.ClearInputs();
IsTryingToSteerThroughGap = false;
Reverse = false;
bool ignorePlatforms = Character.AnimController.TargetMovement.Y < -0.5f && (-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X));
@@ -558,8 +557,9 @@ namespace Barotrauma
}
}
if (AIParams.CanOpenDoors)
if (Character.Params.UsePathFinding && Character.Params.AI.UsePathFindingToGetInside && AIParams.CanOpenDoors)
{
// Meant for monsters outside the player sub that target something inside the sub and can use the doors to access the sub (Husk).
bool IsCloseEnoughToTargetSub(float threshold) => SelectedAiTarget?.Entity?.Submarine is Submarine sub && sub != null && Vector2.DistanceSquared(Character.WorldPosition, sub.WorldPosition) < MathUtils.Pow(Math.Max(sub.Borders.Size.X, sub.Borders.Size.Y) / 2 + threshold, 2);
if (Character.Submarine != null || HasValidPath() && IsCloseEnoughToTargetSub(maxSteeringBuffer) || IsCloseEnoughToTargetSub(steeringBuffer))
@@ -584,6 +584,7 @@ namespace Barotrauma
}
else
{
// Normally the monsters only use pathing inside submarines, not outside.
if (Character.Submarine != null && Character.Params.UsePathFinding)
{
if (steeringManager != insideSteering)
@@ -848,7 +849,7 @@ namespace Barotrauma
IsSteeringThroughGap = false;
if (SwarmBehavior != null)
{
SwarmBehavior.IsActive = State == AIState.Idle && Character.CurrentHull == null;
SwarmBehavior.IsActive = SwarmBehavior.ForceActive || State == AIState.Idle && Character.CurrentHull == null;
SwarmBehavior.Refresh();
SwarmBehavior.UpdateSteering(deltaTime);
}
@@ -876,7 +877,7 @@ namespace Barotrauma
var pathSteering = SteeringManager as IndoorsSteeringManager;
if (pathSteering == null)
{
if (SimPosition.Y < ConvertUnits.ToSimUnits(Character.CharacterHealth.CrushDepth * 0.75f))
if (Level.Loaded != null && Level.Loaded.GetRealWorldDepth(WorldPosition.Y) > Character.CharacterHealth.CrushDepth * 0.75f)
{
// Steer straight up if very deep
SteeringManager.SteeringManual(deltaTime, Vector2.UnitY);
@@ -1144,7 +1145,6 @@ namespace Barotrauma
return;
}
}
attackLimbSelectionTimer -= deltaTime;
if (AttackLimb == null || attackLimbSelectionTimer <= 0)
{
@@ -1154,7 +1154,8 @@ namespace Barotrauma
AttackLimb = GetAttackLimb(attackWorldPos);
}
}
Character targetCharacter = SelectedAiTarget.Entity as Character;
IDamageable damageTarget = wallTarget != null ? wallTarget.Structure : SelectedAiTarget.Entity as IDamageable;
bool canAttack = true;
bool pursue = false;
if (IsCoolDownRunning && (_previousAttackLimb == null || AttackLimb == null || AttackLimb.attack.CoolDownTimer > 0))
@@ -1379,7 +1380,6 @@ namespace Barotrauma
float distance = 0;
Limb attackTargetLimb = null;
Character targetCharacter = SelectedAiTarget.Entity as Character;
if (canAttack)
{
if (!Character.AnimController.SimplePhysicsEnabled)
@@ -1400,29 +1400,29 @@ namespace Barotrauma
attackSimPos = Character.GetRelativeSimPosition(attackTargetLimb);
}
}
Vector2 attackLimbPos = Character.AnimController.SimplePhysicsEnabled ? Character.WorldPosition : AttackLimb.WorldPosition;
Vector2 toTarget = attackWorldPos - attackLimbPos;
Vector2 toTargetOffset = toTarget;
// Add a margin when the target is moving away, because otherwise it might be difficult to reach it if the attack takes some time to execute
if (wallTarget != null && Character.Submarine == null)
{
if (wallTarget.Structure.Submarine != null)
{
Vector2 margin = CalculateMargin(wallTarget.Structure.Submarine.Velocity);
toTarget += margin;
toTargetOffset += margin;
}
}
else if (targetCharacter != null)
{
Vector2 margin = CalculateMargin(targetCharacter.AnimController.Collider.LinearVelocity);
toTarget += margin;
toTargetOffset += margin;
}
else if (SelectedAiTarget.Entity is MapEntity e)
{
if (e.Submarine != null)
{
Vector2 margin = CalculateMargin(e.Submarine.Velocity);
toTarget += margin;
toTargetOffset += margin;
}
}
@@ -1430,7 +1430,7 @@ namespace Barotrauma
{
if (targetVelocity == Vector2.Zero) { return Vector2.Zero; }
float diff = AttackLimb.attack.Range - AttackLimb.attack.DamageRange;
if (diff <= 0 || toTarget.LengthSquared() <= MathUtils.Pow2(AttackLimb.attack.DamageRange)) { return Vector2.Zero; }
if (diff <= 0 || toTargetOffset.LengthSquared() <= MathUtils.Pow2(AttackLimb.attack.DamageRange)) { return Vector2.Zero; }
float dot = Vector2.Dot(Vector2.Normalize(targetVelocity), Vector2.Normalize(Character.AnimController.Collider.LinearVelocity));
if (dot <= 0 || !MathUtils.IsValid(dot)) { return Vector2.Zero; }
float distanceOffset = diff * AttackLimb.attack.Duration;
@@ -1439,7 +1439,7 @@ namespace Barotrauma
}
// Check that we can reach the target
distance = toTarget.Length();
distance = toTargetOffset.Length();
canAttack = distance < AttackLimb.attack.Range;
if (canAttack)
{
@@ -1523,20 +1523,18 @@ namespace Barotrauma
}
}
Limb steeringLimb = canAttack && !AttackLimb.attack.Ranged ? AttackLimb : null;
bool updateSteering = true;
if (steeringLimb == null)
{
// If the attacking limb is a hand or claw, for example, using it as the steering limb can end in the result where the character circles around the target.
steeringLimb = Character.AnimController.GetLimb(LimbType.Head) ?? Character.AnimController.GetLimb(LimbType.Torso);
}
if (steeringLimb == null)
{
State = AIState.Idle;
return;
}
var pathSteering = SteeringManager as IndoorsSteeringManager;
if (AttackLimb != null && AttackLimb.attack.Retreat)
{
UpdateFallBack(attackWorldPos, deltaTime, followThrough: false);
@@ -1603,7 +1601,7 @@ namespace Barotrauma
}
}
}
else
else if (!IsTryingToSteerThroughGap)
{
if (AttackLimb.attack.Ranged)
{
@@ -1624,6 +1622,10 @@ namespace Barotrauma
SteeringManager.Reset();
}
}
else
{
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(SelectedAiTarget.Entity.WorldPosition - Character.WorldPosition));
}
}
else
{
@@ -1662,40 +1664,60 @@ namespace Barotrauma
if (IsAttackRunning && CirclePhase != CirclePhase.Strike) { break; }
if (selectedTargetingParams == null) { break; }
var targetSub = SelectedAiTarget.Entity?.Submarine;
if (targetSub == null) { break; }
float subSize = Math.Max(targetSub.Borders.Width, targetSub.Borders.Height) / 2;
float sqrDistToSub = Vector2.DistanceSquared(WorldPosition, targetSub.WorldPosition);
ISpatialEntity spatialTarget = targetSub ?? SelectedAiTarget.Entity;
float targetSize = 0;
if (!selectedTargetingParams.IgnoreTargetSize)
{
targetSize =
targetSub != null ? Math.Max(targetSub.Borders.Width, targetSub.Borders.Height) / 2 :
targetCharacter != null ? ConvertUnits.ToDisplayUnits(targetCharacter.AnimController.Collider.GetSize().X) : 100;
}
float sqrDistToTarget = Vector2.DistanceSquared(WorldPosition, spatialTarget.WorldPosition);
bool isProgressive = AIParams.MaxAggression - AIParams.StartAggression > 0;
switch (CirclePhase)
{
case CirclePhase.Start:
currentAttackIntensity = MathUtils.InverseLerp(AIParams.StartAggression, AIParams.MaxAggression, aggressionIntensity * Rand.Range(0.9f, 1.1f));
currentAttackIntensity = MathUtils.InverseLerp(AIParams.StartAggression, AIParams.MaxAggression, ClampIntensity(aggressionIntensity));
inverseDir = false;
circleDir = GetDirFromHeadingInRadius();
circleRotation = 0;
strikeTimer = 0;
blockCheckTimer = 0;
breakCircling = false;
float minRotationSpeed = 0.01f * selectedTargetingParams.CircleRotationSpeed;
float maxRotationSpeed = 0.5f * selectedTargetingParams.CircleRotationSpeed;
float minFallBackDistance = selectedTargetingParams.CircleStartDistance * 0.5f;
float maxFallBackDistance = selectedTargetingParams.CircleStartDistance;
float maxRandomOffset = selectedTargetingParams.CircleMaxRandomOffset;
// The lower the rotation speed, the slower the progression. Also the distance to the target stays longer.
// So basically if the value is higher, the creature will strike the sub more quickly and with more precision.
circleRotationSpeed = MathHelper.Lerp(minRotationSpeed, maxRotationSpeed, currentAttackIntensity * Rand.Range(0.9f, 1.1f));
circleFallbackDistance = MathHelper.Lerp(maxFallBackDistance, minFallBackDistance, currentAttackIntensity * Rand.Range(0.9f, 1.1f));
circleOffset = Rand.Vector(MathHelper.Lerp(selectedTargetingParams.CircleMaxRandomOffset, 0, currentAttackIntensity * Rand.Range(0.9f, 1.1f)));
canAttack = false;
float ClampIntensity(float intensity) => MathHelper.Clamp(intensity * Rand.Range(0.9f, 1.1f), AIParams.StartAggression, AIParams.MaxAggression);
if (isProgressive)
{
float intensity = ClampIntensity(currentAttackIntensity);
float minRotationSpeed = 0.01f * selectedTargetingParams.CircleRotationSpeed;
float maxRotationSpeed = 0.5f * selectedTargetingParams.CircleRotationSpeed;
circleRotationSpeed = MathHelper.Lerp(minRotationSpeed, maxRotationSpeed, intensity);
circleFallbackDistance = MathHelper.Lerp(maxFallBackDistance, minFallBackDistance, intensity);
circleOffset = Rand.Vector(MathHelper.Lerp(maxRandomOffset, 0, intensity));
}
else
{
circleRotationSpeed = selectedTargetingParams.CircleRotationSpeed;
circleFallbackDistance = maxFallBackDistance;
circleOffset = Rand.Vector(maxRandomOffset);
}
circleRotationSpeed *= Rand.Range(1 - selectedTargetingParams.CircleRandomRotationFactor, 1 + selectedTargetingParams.CircleRandomRotationFactor);
aggressionIntensity = Math.Clamp(aggressionIntensity, AIParams.StartAggression, AIParams.MaxAggression);
if (targetSub.Borders.Width < 1000)
DisableAttacksIfLimbNotRanged();
if (targetSub != null && targetSub.Borders.Width < 1000 && AttackLimb?.attack is { Ranged: false })
{
breakCircling = true;
CirclePhase = CirclePhase.CloseIn;
}
else if (sqrDistToSub > MathUtils.Pow2(subSize + selectedTargetingParams.CircleStartDistance))
else if (sqrDistToTarget > MathUtils.Pow2(targetSize + selectedTargetingParams.CircleStartDistance))
{
CirclePhase = CirclePhase.CloseIn;
}
else if (sqrDistToSub < MathUtils.Pow2(subSize + circleFallbackDistance))
else if (sqrDistToTarget < MathUtils.Pow2(targetSize + circleFallbackDistance))
{
CirclePhase = CirclePhase.FallBack;
}
@@ -1705,52 +1727,76 @@ namespace Barotrauma
}
break;
case CirclePhase.CloseIn:
if (AttackLimb != null && distance > 0 && distance < AttackLimb.attack.Range * GetStrikeDistanceMultiplier(targetSub.Velocity))
Vector2 targetVelocity = GetTargetVelocity();
float targetDistance = selectedTargetingParams.IgnoreTargetSize ? selectedTargetingParams.CircleStartDistance * 0.9f:
targetSize + selectedTargetingParams.CircleStartDistance / 2;
if (AttackLimb != null && distance > 0 && distance < AttackLimb.attack.Range * GetStrikeDistanceMultiplier(targetVelocity))
{
strikeTimer = AttackLimb.attack.CoolDown;
CirclePhase = CirclePhase.Strike;
}
else if (!breakCircling && sqrDistToSub <= MathUtils.Pow2(subSize + selectedTargetingParams.CircleStartDistance / 2) && targetSub.Velocity.LengthSquared() <= MathUtils.Pow2(GetTargetMaxSpeed()))
else if (!breakCircling && sqrDistToTarget <= MathUtils.Pow2(targetDistance) && targetVelocity.LengthSquared() <= MathUtils.Pow2(GetTargetMaxSpeed()))
{
CirclePhase = CirclePhase.Advance;
}
canAttack = false;
DisableAttacksIfLimbNotRanged();
break;
case CirclePhase.FallBack:
updateSteering = false;
bool isBlocked = !UpdateFallBack(attackWorldPos, deltaTime, followThrough: false, checkBlocking: true);
if (isBlocked || sqrDistToSub > MathUtils.Pow2(subSize + circleFallbackDistance))
if (isBlocked || sqrDistToTarget > MathUtils.Pow2(targetSize + circleFallbackDistance))
{
CirclePhase = CirclePhase.Advance;
break;
}
return;
DisableAttacksIfLimbNotRanged();
break;
case CirclePhase.Advance:
Vector2 subSpeed = targetSub.Velocity;
float requiredDistMultiplier = 1;
// If the target sub is moving fast, just steer towards the target until close enough to strike
if (breakCircling || subSpeed.LengthSquared() > MathUtils.Pow2(GetTargetMaxSpeed()) || sqrDistToSub > MathUtils.Pow2(subSize + selectedTargetingParams.CircleStartDistance * 1.2f))
Vector2 targetVel = GetTargetVelocity();
// If the target is moving fast, just steer towards the target
if (breakCircling || targetVel.LengthSquared() > MathUtils.Pow2(GetTargetMaxSpeed()))
{
CirclePhase = CirclePhase.CloseIn;
}
else if (sqrDistToTarget > MathUtils.Pow2(targetSize + selectedTargetingParams.CircleStartDistance * 1.2f))
{
if (selectedTargetingParams.DynamicCircleRotationSpeed && circleRotationSpeed < 100)
{
circleRotationSpeed *= 1 + deltaTime;
}
else
{
CirclePhase = CirclePhase.CloseIn;
}
}
else
{
circleRotation += deltaTime * circleRotationSpeed * circleDir;
if (circleRotation < -360)
float rotationStep = circleRotationSpeed * deltaTime * circleDir;
if (isProgressive)
{
circleRotation += 360;
circleRotation += rotationStep;
}
else if (circleRotation > 360)
else
{
circleRotation -= 360;
circleRotation = rotationStep;
}
Vector2 targetPos = attackSimPos + circleOffset;
if (Vector2.DistanceSquared(SimPosition, targetPos) < 100)
float targetDist = targetSize;
if (targetDist <= 0)
{
targetDist = circleFallbackDistance;
}
if (targetSub != null && AttackLimb?.attack is { Ranged: true })
{
targetDist += circleFallbackDistance / 2;
}
if (Vector2.DistanceSquared(SimPosition, targetPos) < ConvertUnits.ToSimUnits(targetDist))
{
// Too close to the target point
// When the offset position is outside of the sub it happens that the creature sometimes reaches the target point,
// which makes it continue circling around the point (as supposed)
// But when there is some offset and the offset is too near, this is not what we want.
if (AttackLimb != null && sqrDistToSub < MathUtils.Pow2(subSize + circleFallbackDistance))
if (canAttack && AttackLimb?.attack is { Ranged: false } && sqrDistToTarget < MathUtils.Pow2(targetSize + circleFallbackDistance))
{
CirclePhase = CirclePhase.Strike;
strikeTimer = AttackLimb.attack.CoolDown;
@@ -1762,7 +1808,6 @@ namespace Barotrauma
break;
}
steerPos = MathUtils.RotatePointAroundTarget(SimPosition, targetPos, circleRotation);
requiredDistMultiplier = GetStrikeDistanceMultiplier(subSpeed);
if (IsBlocked(deltaTime, steerPos))
{
if (!inverseDir)
@@ -1774,7 +1819,7 @@ namespace Barotrauma
else if (circleRotationSpeed < 1)
{
// Then try increasing the rotation speed to change the movement curve
circleRotationSpeed *= 1.1f;
circleRotationSpeed *= 1 + deltaTime;
}
else if (circleOffset.LengthSquared() > 0.1f)
{
@@ -1784,16 +1829,24 @@ namespace Barotrauma
else
{
// If we still fail, just steer towards the target
breakCircling = true;
breakCircling = AttackLimb?.attack is { Ranged: false };
if (!breakCircling)
{
CirclePhase = CirclePhase.FallBack;
}
}
}
}
if (AttackLimb != null && distance > 0 && distance < AttackLimb.attack.Range * requiredDistMultiplier && IsFacing(margin: MathHelper.Lerp(0.5f, 0.9f, currentAttackIntensity)))
if (AttackLimb?.attack is { Ranged: false })
{
strikeTimer = AttackLimb.attack.CoolDown;
CirclePhase = CirclePhase.Strike;
canAttack = false;
float requiredDistMultiplier = GetStrikeDistanceMultiplier(targetVel);
if (distance > 0 && distance < AttackLimb.attack.Range * requiredDistMultiplier && IsFacing(margin: MathHelper.Lerp(0.5f, 0.9f, currentAttackIntensity)))
{
strikeTimer = AttackLimb.attack.CoolDown;
CirclePhase = CirclePhase.Strike;
}
}
canAttack = false;
break;
case CirclePhase.Strike:
strikeTimer -= deltaTime;
@@ -1815,18 +1868,19 @@ namespace Barotrauma
return Vector2.Dot(Vector2.Normalize(attackWorldPos - WorldPosition), forward) > margin;
}
float GetStrikeDistanceMultiplier(Vector2 subSpeed)
float GetStrikeDistanceMultiplier(Vector2 targetVelocity)
{
if (selectedTargetingParams.CircleStrikeDistanceMultiplier < 1) { return 0; }
float requiredDistMultiplier = 2;
bool isHeading = Steering != null && Vector2.Dot(Vector2.Normalize(attackWorldPos - WorldPosition), Vector2.Normalize(Steering)) > 0.9f;
bool isHeading = Vector2.Dot(Vector2.Normalize(attackWorldPos - WorldPosition), Vector2.Normalize(Steering)) > 0.9f;
if (isHeading)
{
requiredDistMultiplier = selectedTargetingParams.CircleStrikeDistanceMultiplier;
float subSpeedHorizontal = Math.Abs(subSpeed.X);
if (subSpeedHorizontal > 1)
float targetVelocityHorizontal = Math.Abs(targetVelocity.X);
if (targetVelocityHorizontal > 1)
{
// Reduce the required distance if the target is moving.
requiredDistMultiplier -= MathHelper.Lerp(0, Math.Max(selectedTargetingParams.CircleStrikeDistanceMultiplier - 1, 1), Math.Clamp(subSpeedHorizontal / 10, 0, 1));
requiredDistMultiplier -= MathHelper.Lerp(0, Math.Max(selectedTargetingParams.CircleStrikeDistanceMultiplier - 1, 1), Math.Clamp(targetVelocityHorizontal / 10, 0, 1));
if (requiredDistMultiplier < 2)
{
requiredDistMultiplier = 2;
@@ -1843,19 +1897,59 @@ namespace Barotrauma
return angle > MathHelper.Pi || angle < -MathHelper.Pi ? -1 : 1;
}
float GetTargetMaxSpeed() => Character.ApplyTemporarySpeedLimits(Character.AnimController.CurrentSwimParams.MovementSpeed * 0.3f);
Vector2 GetTargetVelocity()
{
if (targetSub != null)
{
return targetSub.Velocity;
}
else if (targetCharacter != null)
{
return targetCharacter.AnimController.Collider.LinearVelocity;
}
return Vector2.Zero;
}
float GetTargetMaxSpeed() => Character.ApplyTemporarySpeedLimits(Character.AnimController.SwimFastParams.MovementSpeed * (targetSub != null ? 0.3f : 0.5f));
}
}
if (selectedTargetingParams.AttackPattern == AttackPattern.Straight && AttackLimb is Limb attackLimb && attackLimb.attack.Ranged)
if (updateSteering)
{
bool advance = !canAttack && Character.CurrentHull == null || distance > attackLimb.attack.Range * 0.9f;
bool fallBack = canAttack && distance < Math.Min(250, attackLimb.attack.Range * 0.25f);
if (fallBack)
if (selectedTargetingParams.AttackPattern == AttackPattern.Straight && AttackLimb is Limb attackLimb && attackLimb.attack.Ranged)
{
Reverse = true;
UpdateFallBack(attackWorldPos, deltaTime, followThrough: false);
bool advance = !canAttack && Character.CurrentHull == null || distance > attackLimb.attack.Range * 0.9f;
bool fallBack = canAttack && distance < Math.Min(250, attackLimb.attack.Range * 0.25f);
if (fallBack)
{
Reverse = true;
UpdateFallBack(attackWorldPos, deltaTime, followThrough: false);
}
else if (advance)
{
if (pathSteering != null)
{
pathSteering.SteeringSeek(steerPos, weight: 10, minGapWidth: minGapSize);
}
else
{
SteeringManager.SteeringSeek(steerPos, 10);
}
}
else
{
if (Character.CurrentHull == null && !canAttack)
{
SteeringManager.SteeringWander(avoidWanderingOutsideLevel: true);
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 5);
}
else
{
SteeringManager.Reset();
FaceTarget(SelectedAiTarget.Entity);
}
}
}
else if (advance)
else if (!canAttack || distance > Math.Min(AttackLimb.attack.Range * 0.9f, 100))
{
if (pathSteering != null)
{
@@ -1866,41 +1960,18 @@ namespace Barotrauma
SteeringManager.SteeringSeek(steerPos, 10);
}
}
else
if (Character.CurrentHull == null && (SelectedAiTarget?.Entity is Character c && c.Submarine == null ||
distance == 0 ||
distance > ConvertUnits.ToDisplayUnits(avoidLookAheadDistance * 2) ||
AttackLimb != null && AttackLimb.attack.Ranged))
{
if (Character.CurrentHull == null && !canAttack)
{
SteeringManager.SteeringWander(avoidWanderingOutsideLevel: true);
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 5);
}
else
{
SteeringManager.Reset();
FaceTarget(SelectedAiTarget.Entity);
}
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 30);
}
}
else if (!canAttack || distance > Math.Min(AttackLimb.attack.Range * 0.9f, 100))
{
if (pathSteering != null)
{
pathSteering.SteeringSeek(steerPos, weight: 10, minGapWidth: minGapSize);
}
else
{
SteeringManager.SteeringSeek(steerPos, 10);
}
}
if (Character.CurrentHull == null && (SelectedAiTarget?.Entity is Character c && c.Submarine == null || distance == 0 || distance > ConvertUnits.ToDisplayUnits(avoidLookAheadDistance * 2)))
{
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 30);
}
}
}
Entity targetEntity = wallTarget?.Structure ?? SelectedAiTarget?.Entity;
IDamageable damageTarget = targetEntity as IDamageable;
if (AttackLimb?.attack is Attack { Ranged: true} attack)
if (AttackLimb?.attack is Attack { Ranged: true } attack)
{
AimRangedAttack(attack, targetEntity);
}
@@ -1915,12 +1986,21 @@ namespace Barotrauma
{
AttackLimb.attack.ResetAttackTimer();
}
void DisableAttacksIfLimbNotRanged()
{
if (AttackLimb?.attack is { Ranged: false })
{
canAttack = false;
}
}
}
public void AimRangedAttack(Attack attack, Entity targetEntity)
{
if (attack is not { Ranged: true } || targetEntity is not { Removed: false }) { return; }
Character.SetInput(InputType.Aim, false, true);
if (attack.AimRotationTorque <= 0) { return; }
Limb limb = GetLimbToRotate(attack);
if (limb != null)
{
@@ -2003,9 +2083,18 @@ namespace Barotrauma
float prio = 1 + limb.attack.Priority;
if (Character.AnimController.SimplePhysicsEnabled) { return prio; }
float dist = Vector2.Distance(limb.WorldPosition, attackPos);
// The limb is ignored if the target is not close. Prevents character going in reverse if very far away from it.
// We also need a max value that is more than the actual range.
float distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, limb.attack.Range * 3, dist));
float distanceFactor = 1;
if (limb.attack.Ranged)
{
float min = 100;
distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(min, Math.Max(limb.attack.Range / 2, min), dist));
}
else
{
// The limb is ignored if the target is not close. Prevents character going in reverse if very far away from it.
// We also need a max value that is more than the actual range.
distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, limb.attack.Range * 3, dist));
}
return prio * distanceFactor;
}
}
@@ -2164,7 +2253,9 @@ namespace Barotrauma
{
if (SelectedAiTarget?.Entity == null) { return false; }
if (AttackLimb?.attack == null) { return false; }
if (damageTarget == null) { return false; }
ISpatialEntity spatialTarget = wallTarget != null ? wallTarget.Structure : SelectedAiTarget.Entity as ISpatialEntity;
if (spatialTarget == null) { return false; }
ActiveAttack = AttackLimb.attack;
if (wallTarget != null)
{
// If the selected target is not the wall target, make the wall target the selected target.
@@ -2176,13 +2267,14 @@ namespace Barotrauma
return true;
}
}
if (damageTarget == null) { return false; }
ActiveAttack = AttackLimb.attack;
if (ActiveAttack.Ranged && ActiveAttack.RequiredAngleToShoot > 0)
{
Limb referenceLimb = GetLimbToRotate(ActiveAttack);
if (referenceLimb != null)
{
Vector2 toTarget = damageTarget.WorldPosition - referenceLimb.WorldPosition;
Vector2 toTarget = spatialTarget.WorldPosition - referenceLimb.WorldPosition;
float offset = referenceLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2;
Vector2 forward = VectorExtensions.Forward(referenceLimb.body.TransformedRotation - offset * referenceLimb.Dir);
float angle = MathHelper.ToDegrees(VectorExtensions.Angle(forward, toTarget));
@@ -2200,16 +2292,20 @@ namespace Barotrauma
{
if (item.RequireAimToUse)
{
if (!Aim(deltaTime, damageTarget as ISpatialEntity, item))
if (!Aim(deltaTime, spatialTarget, item))
{
// Valid target, but can't shoot -> return true so that it will not be ignored.
return true;
}
}
Character.SetInput(item.IsShootable ? InputType.Shoot : InputType.Use, false, true);
item.Use(deltaTime, Character);
if (damageTarget != null)
{
Character.SetInput(item.IsShootable ? InputType.Shoot : InputType.Use, false, true);
item.Use(deltaTime, Character);
}
}
}
if (damageTarget == null) { return true; }
//simulate attack input to get the character to attack client-side
Character.SetInput(InputType.Attack, true, true);
if (!ActiveAttack.IsRunning)
@@ -2224,20 +2320,11 @@ namespace Barotrauma
Character.PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3);
#endif
}
if (AttackLimb.UpdateAttack(deltaTime, attackSimPos, damageTarget, out AttackResult attackResult, distance, targetLimb))
{
if (ActiveAttack.CoolDownTimer > 0)
{
SetAimTimer(Math.Min(ActiveAttack.CoolDown, 1.5f));
// Managed to hit a living/non-destroyed target. Increase the priority more if the target is low in health -> dies easily/soon
float greed = AIParams.AggressionGreed;
if (damageTarget is not Barotrauma.Character)
{
// Halve the greed for attacking non-characters.
greed /= 2;
}
selectedTargetMemory.Priority += GetRelativeDamage(attackResult.Damage, damageTarget.Health) * greed;
}
if (LatchOntoAI != null && SelectedAiTarget.Entity is Character targetCharacter)
{
@@ -2269,10 +2356,19 @@ namespace Barotrauma
private float aimTimer;
private float visibilityCheckTimer;
private bool canSeeTarget;
private float sinTime;
private bool Aim(float deltaTime, ISpatialEntity target, Item weapon)
{
if (target == null || weapon == null) { return false; }
if (AttackLimb == null) { return false; }
Vector2 toTarget = target.WorldPosition - weapon.WorldPosition;
float dist = toTarget.Length();
Character.CursorPosition = target.WorldPosition;
if (AttackLimb.attack.SwayAmount > 0)
{
sinTime += deltaTime * AttackLimb.attack.SwayFrequency;
Character.CursorPosition += VectorExtensions.Forward(weapon.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, dist / 2 * AttackLimb.attack.SwayAmount);
}
if (Character.Submarine != null)
{
Character.CursorPosition -= Character.Submarine.Position;
@@ -2294,11 +2390,11 @@ namespace Barotrauma
aimTimer -= deltaTime;
return false;
}
Vector2 toTarget = target.WorldPosition - weapon.WorldPosition;
float angle = VectorExtensions.Angle(VectorExtensions.Forward(weapon.body.TransformedRotation), toTarget);
float distanceFactor = MathHelper.Lerp(1.0f, 0.1f, MathUtils.InverseLerp(100, 1000, toTarget.Length()));
float minDistance = 300;
float distanceFactor = MathHelper.Lerp(1.0f, 0.1f, MathUtils.InverseLerp(minDistance, 1000, dist));
float margin = MathHelper.PiOver4 * distanceFactor;
if (angle < margin)
if (angle < margin || dist < minDistance)
{
var collisionCategories = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel;
var pickedBody = Submarine.PickBody(weapon.SimPosition, Character.GetRelativeSimPosition(target), myBodies, collisionCategories, allowInsideFixture: true);
@@ -2591,13 +2687,19 @@ namespace Barotrauma
{
// Ignore all structures, items, and hulls inside these subs.
if (aiTarget.Entity.Submarine != null)
{
if (aiTarget.Entity.Submarine.Info.IsWreck ||
aiTarget.Entity.Submarine.Info.IsBeacon ||
{
if (aiTarget.Entity.Submarine.Info.IsWreck ||
aiTarget.Entity.Submarine.Info.IsBeacon ||
UnattackableSubmarines.Contains(aiTarget.Entity.Submarine))
{
continue;
}
//ignore the megaruin in end levels
if (aiTarget.Entity.Submarine.Info.OutpostGenerationParams != null &&
aiTarget.Entity.Submarine.Info.OutpostGenerationParams.ForceToEndLocationIndex > -1)
{
continue;
}
}
if (aiTarget.Entity is Hull hull)
{
@@ -2698,7 +2800,7 @@ namespace Barotrauma
}
else if (CanPassThroughHole(s, i))
{
valueModifier *= isInnerWall ? 1 : 0;
valueModifier *= isInnerWall ? 0.5f : 0;
}
else if (!canAttackWalls)
{
@@ -2968,7 +3070,8 @@ namespace Barotrauma
// In the attack state allow going into non-allowed zone only when chasing a target.
if (State == targetParams.State && SelectedAiTarget == aiTarget) { break; }
}
if (!IsPositionInsideAllowedZone(aiTarget.WorldPosition, out _))
bool insideSameSub = aiTarget?.Entity?.Submarine != null && aiTarget.Entity.Submarine == Character.Submarine;
if (!insideSameSub && !IsPositionInsideAllowedZone(aiTarget.WorldPosition, out _))
{
// If we have recently been damaged by the target (or another player/bot in the same team) allow targeting it even when we are in the idle state.
bool isTargetInPlayerTeam = IsTargetInPlayerTeam(aiTarget);
@@ -3401,10 +3504,10 @@ namespace Barotrauma
private readonly float stateResetCooldown = 10;
private float stateResetTimer;
private bool isStateChanged;
private readonly Dictionary<AITrigger, CharacterParams.TargetParams> activeTriggers = new Dictionary<AITrigger, CharacterParams.TargetParams>();
private readonly HashSet<AITrigger> inactiveTriggers = new HashSet<AITrigger>();
private readonly Dictionary<StatusEffect.AITrigger, CharacterParams.TargetParams> activeTriggers = new Dictionary<StatusEffect.AITrigger, CharacterParams.TargetParams>();
private readonly HashSet<StatusEffect.AITrigger> inactiveTriggers = new HashSet<StatusEffect.AITrigger>();
public void LaunchTrigger(AITrigger trigger)
public void LaunchTrigger(StatusEffect.AITrigger trigger)
{
if (trigger.IsTriggered) { return; }
if (activeTriggers.ContainsKey(trigger)) { return; }
@@ -3424,7 +3527,7 @@ namespace Barotrauma
{
foreach (var triggerObject in activeTriggers)
{
AITrigger trigger = triggerObject.Key;
StatusEffect.AITrigger trigger = triggerObject.Key;
if (trigger.IsPermanent) { continue; }
trigger.UpdateTimer(deltaTime);
if (!trigger.IsActive)
@@ -3434,7 +3537,7 @@ namespace Barotrauma
inactiveTriggers.Add(trigger);
}
}
foreach (AITrigger trigger in inactiveTriggers)
foreach (StatusEffect.AITrigger trigger in inactiveTriggers)
{
activeTriggers.Remove(trigger);
}
@@ -3588,6 +3691,11 @@ namespace Barotrauma
observeTimer = targetParams.Timer * Rand.Range(0.75f, 1.25f);
}
reachTimer = 0;
sinTime = 0;
if (breakCircling && strikeTimer <= 0)
{
CirclePhase = CirclePhase.Start;
}
}
protected override void OnStateChanged(AIState from, AIState to)
@@ -3609,6 +3717,11 @@ namespace Barotrauma
}
blockCheckTimer = 0;
reachTimer = 0;
sinTime = 0;
if (breakCircling && strikeTimer <= 0)
{
CirclePhase = CirclePhase.Start;
}
}
private void SetStateResetTimer() => stateResetTimer = stateResetCooldown * Rand.Range(0.75f, 1.25f);
@@ -3712,6 +3825,7 @@ namespace Barotrauma
{
targetDir = Vector2.Zero;
if (Level.Loaded == null) { return true; }
if (Level.Loaded.LevelData.Biome.IsEndBiome) { return true; }
if (AIParams.AvoidAbyss)
{
if (pos.Y < Level.Loaded.AbyssStart)
@@ -3771,6 +3885,7 @@ namespace Barotrauma
public override bool SteerThroughGap(Structure wall, WallSection section, Vector2 targetWorldPos, float deltaTime)
{
IsTryingToSteerThroughGap = true;
wallTarget = null;
LatchOntoAI?.DeattachFromBody(reset: true, cooldown: 2);
Character.AnimController.ReleaseStuckLimbs();
@@ -1,10 +1,10 @@
using Barotrauma.Networking;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
namespace Barotrauma
{
@@ -395,6 +395,8 @@ namespace Barotrauma
}
objectiveManager.UpdateObjectives(deltaTime);
UpdateDragged(deltaTime);
if (reportProblemsTimer > 0)
{
reportProblemsTimer -= deltaTime;
@@ -430,7 +432,7 @@ namespace Barotrauma
}
if (reportProblemsTimer <= 0.0f)
{
if (Character.Submarine != null && (Character.Submarine.TeamID == Character.TeamID || Character.IsEscorted) && !Character.Submarine.Info.IsWreck)
if (Character.Submarine != null && (Character.Submarine.TeamID == Character.TeamID || Character.Submarine.TeamID == Character.OriginalTeamID || Character.IsEscorted) && !Character.Submarine.Info.IsWreck)
{
ReportProblems();
}
@@ -444,7 +446,7 @@ namespace Barotrauma
if (objectiveManager.CurrentObjective == null) { return; }
objectiveManager.DoCurrentObjective(deltaTime);
bool run = objectiveManager.CurrentObjective.ForceRun || !objectiveManager.CurrentObjective.ForceWalk && objectiveManager.GetCurrentPriority() > AIObjectiveManager.RunPriority;
bool run = (objectiveManager.CurrentObjective.ForceRun && !objectiveManager.CurrentObjective.ForceWalk) || (!objectiveManager.CurrentObjective.ForceWalk && objectiveManager.GetCurrentPriority() > AIObjectiveManager.RunPriority);
if (ObjectiveManager.CurrentObjective is AIObjectiveGoTo goTo && goTo.Target != null)
{
if (Character.CurrentHull == null)
@@ -546,12 +548,11 @@ namespace Barotrauma
bool NeedsDivingGearOnPath(AIObjectiveGoTo gotoObjective)
{
if (!Character.NeedsAir) { return false; }
bool insideSteering = SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.IsPathDirty;
Hull targetHull = gotoObjective.GetTargetHull();
return gotoObjective.Target != null && targetHull == null ||
return (gotoObjective.Target != null && targetHull == null && !Character.IsImmuneToPressure) ||
NeedsDivingGear(targetHull, out _) ||
insideSteering && (PathSteering.CurrentPath.HasOutdoorsNodes || PathSteering.CurrentPath.Nodes.Any(n => NeedsDivingGear(n.CurrentHull, out _)));
(insideSteering && ((PathSteering.CurrentPath.HasOutdoorsNodes && !Character.IsImmuneToPressure) || PathSteering.CurrentPath.Nodes.Any(n => NeedsDivingGear(n.CurrentHull, out _))));
}
if (isCarrying)
@@ -584,7 +585,7 @@ namespace Barotrauma
Character.AnimController.InWater ||
Character.AnimController.HeadInWater ||
Character.Submarine == null ||
(Character.Submarine.TeamID != Character.TeamID && !Character.IsEscorted) ||
(!Character.IsOnFriendlyTeam(Character.TeamID, Character.Submarine.TeamID) && !Character.IsEscorted) ||
ObjectiveManager.CurrentOrders.Any(o => o.Objective.KeepDivingGearOnAlsoWhenInactive) ||
ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn) ||
Character.CurrentHull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 10 ||
@@ -621,9 +622,10 @@ namespace Barotrauma
}
else if (gotoObjective.Mimic)
{
bool targetHasDivingGear = HasDivingGear(gotoObjective.Target as Character, requireOxygenTank: false);
if (!removeSuit)
{
removeDivingSuit = !HasDivingSuit(gotoObjective.Target as Character);
removeDivingSuit = !targetHasDivingGear;
if (removeDivingSuit)
{
removeSuit = true;
@@ -631,7 +633,7 @@ namespace Barotrauma
}
if (!removeMask)
{
takeMaskOff = !HasDivingMask(gotoObjective.Target as Character);
takeMaskOff = !targetHasDivingGear;
if (takeMaskOff)
{
removeMask = true;
@@ -783,20 +785,23 @@ namespace Barotrauma
private void HandleRelocation(Item item)
{
if (item.Submarine?.TeamID == CharacterTeamType.FriendlyNPC)
if (item.SpawnedInCurrentOutpost) { return; }
if (item.Submarine == null) { return; }
// Only affects bots in the player team
if (!Character.IsOnPlayerTeam) { return; }
// Don't relocate if the item is on a sub of the same team
if (item.Submarine.TeamID == Character.TeamID) { return; }
if (itemsToRelocate.Contains(item)) { return; }
itemsToRelocate.Add(item);
if (item.Submarine.ConnectedDockingPorts.TryGetValue(Submarine.MainSub, out DockingPort myPort))
{
if (itemsToRelocate.Contains(item)) { return; }
itemsToRelocate.Add(item);
if (item.Submarine.ConnectedDockingPorts.TryGetValue(Submarine.MainSub, out DockingPort myPort))
{
myPort.OnUnDocked += Relocate;
}
var campaign = GameMain.GameSession.Campaign;
if (campaign != null)
{
// In the campaign mode, undocking happens after leaving the outpost, so we can't use that.
campaign.BeforeLevelLoading += Relocate;
}
myPort.OnUnDocked += Relocate;
}
var campaign = GameMain.GameSession.Campaign;
if (campaign != null)
{
// In the campaign mode, undocking happens after leaving the outpost, so we can't use that.
campaign.BeforeLevelLoading += Relocate;
}
void Relocate()
@@ -907,6 +912,35 @@ namespace Barotrauma
return false;
}
private float draggedTimer;
private float refuseDraggingTimer;
private const float RefuseDraggingAfter = 10.0f;
private const float RefuseDraggingDuration = 30.0f;
private void UpdateDragged(float deltaTime)
{
if (Character.HumanPrefab is { AllowDraggingIndefinitely: true }) { return; }
//don't allow player characters who aren't in the same team to drag us for more than x seconds
if (Character.SelectedBy == null ||
!Character.SelectedBy.IsPlayer ||
Character.SelectedBy.TeamID == Character.TeamID)
{
refuseDraggingTimer -= deltaTime;
return;
}
draggedTimer += deltaTime;
if (draggedTimer > RefuseDraggingAfter ||
(draggedTimer > 0.5f && refuseDraggingTimer > 0.0f))
{
draggedTimer = 0.0f;
refuseDraggingTimer = RefuseDraggingDuration;
Character.SelectedBy.DeselectCharacter();
Character.Speak(TextManager.Get("dialogrefusedragging").Value, delay: 0.5f, identifier: "refusedragging".ToIdentifier(), minDurationBetweenSimilar: 5.0f);
}
}
protected void ReportProblems()
{
Order newOrder = null;
@@ -989,8 +1023,8 @@ namespace Barotrauma
targetHull = hull;
}
}
}
foreach (Item item in Item.ItemList)
}
foreach (Item item in Item.RepairableItems)
{
if (item.CurrentHull != hull) { continue; }
if (AIObjectiveRepairItems.IsValidTarget(item, Character))
@@ -1209,7 +1243,7 @@ namespace Barotrauma
}
else
{
isAttackerInfected = attacker.CharacterHealth.GetAfflictionStrength("alieninfection") > 0;
isAttackerInfected = attacker.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.AlienInfectedType) > 0;
// Inform other NPCs
if (isAttackerInfected || cumulativeDamage > minorDamageThreshold || totalDamage > minorDamageThreshold)
{
@@ -1523,7 +1557,7 @@ namespace Barotrauma
{
margin *= 2;
}
float minCeilingDist = mainCollider.height / 2 + mainCollider.radius + margin;
float minCeilingDist = mainCollider.Height / 2 + mainCollider.Radius + margin;
shouldCrouch = Submarine.PickBody(startPos, startPos + Vector2.UnitY * minCeilingDist, null, Physics.CollisionWall, customPredicate: (fixture) => { return fixture.Body.UserData is not Submarine; }) != null;
}
@@ -1546,23 +1580,19 @@ namespace Barotrauma
public bool NeedsDivingGear(Hull hull, out bool needsSuit)
{
if (!Character.NeedsAir)
{
needsSuit = false;
return false;
}
needsSuit = false;
bool needsAir = Character.NeedsAir && Character.CharacterHealth.OxygenLowResistance < 1;
if (hull == null ||
hull.WaterPercentage > 90 ||
hull.LethalPressure > 0 ||
hull.ConnectedGaps.Any(gap => !gap.IsRoomToRoom && gap.Open > 0.9f))
{
needsSuit = !Character.HasAbilityFlag(AbilityFlags.ImmuneToPressure);
return true;
needsSuit = !Character.IsProtectedFromPressure;
return needsAir || needsSuit;
}
if (hull.WaterPercentage > 60 || hull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 1)
{
return true;
return needsAir;
}
return false;
}
@@ -1641,7 +1671,7 @@ namespace Barotrauma
{
if (otherCharacter == character || otherCharacter.TeamID == character.TeamID || otherCharacter.IsDead ||
otherCharacter.Info?.Job == null ||
!(otherCharacter.AIController is HumanAIController otherHumanAI) ||
otherCharacter.AIController is not HumanAIController otherHumanAI ||
!otherHumanAI.VisibleHulls.Contains(character.CurrentHull))
{
continue;
@@ -1654,10 +1684,10 @@ namespace Barotrauma
float accumulatedDamage = Math.Max(otherHumanAI.structureDamageAccumulator[character], maxAccumulatedDamage);
maxAccumulatedDamage = Math.Max(accumulatedDamage, maxAccumulatedDamage);
if (GameMain.GameSession?.Campaign?.Map?.CurrentLocation != null)
if (GameMain.GameSession?.Campaign?.Map?.CurrentLocation?.Reputation != null && character.IsPlayer)
{
var reputationLoss = damageAmount * Reputation.ReputationLossPerWallDamage;
GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.AddReputation(-reputationLoss);
GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.AddReputation(-reputationLoss, Reputation.MaxReputationLossFromWallDamage);
}
if (accumulatedDamage <= WarningThreshold) { return; }
@@ -1745,12 +1775,14 @@ namespace Barotrauma
}
if (!someoneSpoke)
{
if (!item.StolenDuringRound && GameMain.GameSession?.Campaign?.Map?.CurrentLocation != null)
if (!item.StolenDuringRound &&
Level.Loaded?.Type == LevelData.LevelType.Outpost &&
GameMain.GameSession?.Campaign?.Map?.CurrentLocation != null)
{
var reputationLoss = MathHelper.Clamp(
(item.Prefab.GetMinPrice() ?? 0) * Reputation.ReputationLossPerStolenItemPrice,
Reputation.MinReputationLossPerStolenItem, Reputation.MaxReputationLossPerStolenItem);
GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.AddReputation(-reputationLoss);
GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation?.AddReputation(-reputationLoss);
}
item.StolenDuringRound = true;
otherCharacter.Speak(TextManager.Get("dialogstealwarning").Value, null, Rand.Range(0.5f, 1.0f), "thief".ToIdentifier(), 10.0f);
@@ -1843,7 +1875,7 @@ namespace Barotrauma
}
break;
case "reportbrokendevices":
foreach (var item in Item.ItemList)
foreach (var item in Item.RepairableItems)
{
if (item.CurrentHull != hull) { continue; }
if (AIObjectiveRepairItems.IsValidTarget(item, character))
@@ -1924,11 +1956,12 @@ namespace Barotrauma
bool isCurrentHull = character == Character && character.CurrentHull == hull;
if (hull == null)
{
float hullSafety = character.IsProtectedFromPressure ? 0 : 100;
if (isCurrentHull)
{
CurrentHullSafety = character.NeedsAir ? 0 : 100;
CurrentHullSafety = hullSafety;
}
return CurrentHullSafety;
return hullSafety;
}
if (isCurrentHull && visibleHulls == null)
{
@@ -1936,10 +1969,9 @@ namespace Barotrauma
visibleHulls = VisibleHulls;
}
bool ignoreFire = objectiveManager.CurrentOrder is AIObjectiveExtinguishFires extinguishOrder && extinguishOrder.Priority > 0 || objectiveManager.HasActiveObjective<AIObjectiveExtinguishFire>();
bool ignoreWater = character.IsProtectedFromPressure();
bool ignoreOxygen = HasDivingGear(character);
bool ignoreOxygen = HasDivingGear(character);
bool ignoreEnemies = ObjectiveManager.IsCurrentOrder<AIObjectiveFightIntruders>() || ObjectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>();
float safety = CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies);
float safety = CalculateHullSafety(hull, visibleHulls, character, ignoreWater: false, ignoreOxygen, ignoreFire, ignoreEnemies);
if (isCurrentHull)
{
CurrentHullSafety = safety;
@@ -1949,15 +1981,33 @@ namespace Barotrauma
private static float CalculateHullSafety(Hull hull, IEnumerable<Hull> visibleHulls, Character character, bool ignoreWater = false, bool ignoreOxygen = false, bool ignoreFire = false, bool ignoreEnemies = false)
{
if (hull == null) { return character.NeedsAir ? 0 : 100; }
if (hull.LethalPressure > 0 && character.PressureProtection <= 0 && !character.HasAbilityFlag(AbilityFlags.ImmuneToPressure)) { return 0; }
bool isProtectedFromPressure = character.IsProtectedFromPressure;
if (hull == null) { return isProtectedFromPressure ? 100 : 0; }
if (hull.LethalPressure > 0 && !isProtectedFromPressure) { return 0; }
// Oxygen factor should be 1 with 70% oxygen or more and 0.1 when the oxygen level is 30% or lower.
// With insufficient oxygen, the safety of the hull should be 39, all the other factors aside. So, just below the HULL_SAFETY_THRESHOLD.
float oxygenFactor = ignoreOxygen ? 1 : MathHelper.Lerp((HULL_SAFETY_THRESHOLD - 1) / 100, 1, MathUtils.InverseLerp(HULL_LOW_OXYGEN_PERCENTAGE, 100 - HULL_LOW_OXYGEN_PERCENTAGE, hull.OxygenPercentage));
float waterFactor = ignoreWater ? 1 : MathHelper.Lerp(1, HULL_SAFETY_THRESHOLD / 2 / 100, hull.WaterPercentage / 100);
if (!character.NeedsAir)
float waterFactor = 1;
if (!ignoreWater)
{
if (visibleHulls != null)
{
// Take the visible hulls into account too, because otherwise multi-hull rooms on several floors (with platforms) will yield unexpected results.
float relativeWaterVolume = visibleHulls.Sum(s => s.WaterVolume) / visibleHulls.Sum(s => s.Volume);
waterFactor = MathHelper.Lerp(1, HULL_SAFETY_THRESHOLD / 2 / 100, relativeWaterVolume);
}
else
{
float relativeWaterVolume = hull.WaterVolume / hull.Volume;
waterFactor = MathHelper.Lerp(1, HULL_SAFETY_THRESHOLD / 2 / 100, relativeWaterVolume);
}
}
if (!character.NeedsOxygen || character.CharacterHealth.OxygenLowResistance >= 1)
{
oxygenFactor = 1;
}
if (isProtectedFromPressure)
{
waterFactor = 1;
}
float fireFactor = 1;
@@ -2005,6 +2055,10 @@ namespace Barotrauma
public float GetHullSafety(Hull hull, Character character, IEnumerable<Hull> visibleHulls = null)
{
if (hull == null)
{
return CalculateHullSafety(hull, character, visibleHulls);
}
if (!knownHulls.TryGetValue(hull, out HullSafety hullSafety))
{
hullSafety = new HullSafety(CalculateHullSafety(hull, character, visibleHulls));
@@ -2019,6 +2073,10 @@ namespace Barotrauma
public static float GetHullSafety(Hull hull, IEnumerable<Hull> visibleHulls, Character character, bool ignoreWater = false, bool ignoreOxygen = false, bool ignoreFire = false, bool ignoreEnemies = false)
{
if (hull == null)
{
return CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies);
}
HullSafety hullSafety;
if (character.AIController is HumanAIController controller)
{
@@ -2047,21 +2105,53 @@ namespace Barotrauma
bool sameTeam = me.TeamID == other.TeamID;
bool teamGood = sameTeam || !onlySameTeam && me.IsOnFriendlyTeam(other);
if (!teamGood) { return false; }
if (!me.IsSameSpeciesOrGroup(other)) { return false; }
if (me.TeamID == CharacterTeamType.FriendlyNPC && other.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign)
if (other.IsPet)
{
// Hostile NPCs are hostile to all pets, unless they are in the same team.
if (!sameTeam && me.TeamID == CharacterTeamType.None) { return false; }
}
else
{
if (!me.IsSameSpeciesOrGroup(other)) { return false; }
}
if (GameMain.GameSession?.GameMode is CampaignMode)
{
if ((me.TeamID == CharacterTeamType.FriendlyNPC && other.TeamID == CharacterTeamType.Team1) ||
(me.TeamID == CharacterTeamType.Team1 && other.TeamID == CharacterTeamType.FriendlyNPC))
{
Character npc = me.TeamID == CharacterTeamType.FriendlyNPC ? me : other;
//NPCs that allow some campaign interaction are not turned hostile by low reputation
if (npc.CampaignInteractionType != CampaignMode.InteractionType.None) { return true; }
if (!npc.IsEscorted && npc.AIController is HumanAIController npcAI)
{
return !npcAI.IsInHostileFaction();
}
}
}
return true;
}
public bool IsInHostileFaction()
{
if (GameMain.GameSession?.GameMode is not CampaignMode campaign) { return false; }
Identifier npcFaction = Character.Faction;
Identifier currentLocationFaction = campaign.Map?.CurrentLocation?.Faction?.Prefab.Identifier ?? Identifier.Empty;
if (npcFaction.IsEmpty)
{
//if faction identifier is not specified, assume the NPC is a member of the faction that owns the outpost
npcFaction = currentLocationFaction;
}
if (!currentLocationFaction.IsEmpty && npcFaction == currentLocationFaction)
{
var reputation = campaign.Map?.CurrentLocation?.Reputation;
if (reputation != null && reputation.NormalizedValue < Reputation.HostileThreshold)
{
return false;
return true;
}
}
if (!sameTeam && me.TeamID == CharacterTeamType.None && other.IsPet)
{
// Hostile NPCs are hostile to all pets, unless they are in the same team.
return false;
}
return true;
return false;
}
public static bool IsActive(Character other) => other != null && !other.Removed && !other.IsDead && !other.IsUnconscious;
@@ -26,7 +26,7 @@ namespace Barotrauma
private float findPathTimer;
private const float buttonPressCooldown = 3;
private const float ButtonPressCooldown = 1;
private float checkDoorsTimer;
private float buttonPressTimer;
@@ -96,7 +96,7 @@ namespace Barotrauma
base.Update(speed);
float step = 1.0f / 60.0f;
checkDoorsTimer -= step;
if (lastDoor.door == null || !lastDoor.shouldBeOpen || lastDoor.door.IsOpen)
if (lastDoor.door == null || !lastDoor.shouldBeOpen || lastDoor.door.IsFullyOpen)
{
buttonPressTimer = 0;
}
@@ -211,7 +211,7 @@ namespace Barotrauma
currentTarget = target;
Vector2 currentPos = host.SimPosition;
pathFinder.InsideSubmarine = character.Submarine != null && !character.Submarine.Info.IsRuin;
pathFinder.ApplyPenaltyToOutsideNodes = character.Submarine != null && character.PressureProtection <= 0;
pathFinder.ApplyPenaltyToOutsideNodes = character.Submarine != null && !character.IsProtectedFromPressure;
var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", minGapSize, startNodeFilter, endNodeFilter, nodeFilter, checkVisibility: checkVisibility);
bool useNewPath = needsNewPath || currentPath == null || currentPath.CurrentNode == null || character.Submarine != null && findPathTimer < -1 && Math.Abs(character.AnimController.TargetMovement.Combine()) <= 0;
if (!useNewPath && currentPath?.CurrentNode != null && newPath.Nodes.Any() && !newPath.Unreachable)
@@ -310,7 +310,7 @@ namespace Barotrauma
// Only humanoids can climb ladders
bool canClimb = character.AnimController is HumanoidAnimController;
//if not in water and the waypoint is between the top and bottom of the collider, no need to move vertically
if (canClimb && !character.AnimController.InWater && !character.IsClimbing && diff.Y < collider.height / 2 + collider.radius)
if (canClimb && !character.AnimController.InWater && !character.IsClimbing && diff.Y < collider.Height / 2 + collider.Radius)
{
diff.Y = 0.0f;
}
@@ -342,7 +342,7 @@ namespace Barotrauma
CheckDoorsInPath();
doorsChecked = true;
}
if (buttonPressTimer > 0 && lastDoor.door != null && lastDoor.shouldBeOpen && !lastDoor.door.IsOpen)
if (buttonPressTimer > 0 && lastDoor.door != null && lastDoor.shouldBeOpen && !lastDoor.door.IsFullyOpen)
{
// We have pressed the button and are waiting for the door to open -> Hold still until we can press the button again.
Reset();
@@ -395,7 +395,7 @@ namespace Barotrauma
}
//at the same height as the waypoint
float heightDiff = Math.Abs(collider.SimPosition.Y - currentPath.CurrentNode.SimPosition.Y);
float colliderSize = (collider.height / 2 + collider.radius) * 1.25f;
float colliderSize = (collider.Height / 2 + collider.Radius) * 1.25f;
if (heightDiff < colliderSize)
{
float heightFromFloor = character.AnimController.GetHeightFromFloor();
@@ -510,7 +510,7 @@ namespace Barotrauma
private bool CanAccessDoor(Door door, Func<Controller, bool> buttonFilter = null)
{
if (door.IsBroken) { return true; }
if (!door.IsOpen)
if (door.IsClosed)
{
if (!door.Item.IsInteractable(character)) { return false; }
if (!ShouldBreakDoor(door))
@@ -536,7 +536,7 @@ namespace Barotrauma
}
foreach (var linked in door.Item.linkedTo)
{
if (!(linked is Item linkedItem)) { continue; }
if (linked is not Item linkedItem) { continue; }
var button = linkedItem.GetComponent<Controller>();
if (button == null) { continue; }
if (button.HasAccess(character) && (buttonFilter == null || buttonFilter(button)))
@@ -694,7 +694,7 @@ namespace Barotrauma
if (door.Item.TryInteract(character, forceSelectKey: true))
{
lastDoor = (door, shouldBeOpen);
buttonPressTimer = shouldBeOpen ? buttonPressCooldown : 0;
buttonPressTimer = shouldBeOpen ? ButtonPressCooldown : 0;
}
else
{
@@ -712,7 +712,7 @@ namespace Barotrauma
if (closestButton.Item.TryInteract(character, forceSelectKey: true))
{
lastDoor = (door, shouldBeOpen);
buttonPressTimer = shouldBeOpen ? buttonPressCooldown : 0;
buttonPressTimer = shouldBeOpen ? ButtonPressCooldown : 0;
}
else
{
@@ -785,7 +785,7 @@ namespace Barotrauma
{
if (hull.WaterVolume / hull.Rect.Width > 100.0f)
{
if (!HumanAIController.HasDivingSuit(character))
if (!HumanAIController.HasDivingSuit(character) && character.CharacterHealth.OxygenLowResistance < 1)
{
penalty += 500.0f;
}
@@ -808,7 +808,7 @@ namespace Barotrauma
private float? GetSingleNodePenalty(PathNode node)
{
if (node.Waypoint.isObstructed) { return null; }
if (!node.Waypoint.IsTraversable) { return null; }
if (node.IsBlocked()) { return null; }
float penalty = 0.0f;
if (node.Waypoint.ConnectedGap != null && node.Waypoint.ConnectedGap.Open < 0.9f)
@@ -244,7 +244,7 @@ namespace Barotrauma
else
{
float squaredDistance = Vector2.DistanceSquared(character.SimPosition, _attachPos);
float targetDistance = Math.Max(Math.Max(character.AnimController.Collider.radius, character.AnimController.Collider.width), character.AnimController.Collider.height) * 1.2f;
float targetDistance = Math.Max(Math.Max(character.AnimController.Collider.Radius, character.AnimController.Collider.Width), character.AnimController.Collider.Height) * 1.2f;
if (squaredDistance < targetDistance * targetDistance)
{
//close enough to a wall -> attach
@@ -107,7 +107,6 @@ namespace Barotrauma
{
return MentalType.Normal;
}
// test this later
int psychosisIndex = (int)(affliction.Strength / (affliction.Prefab.MaxStrength / MentalTypeCount) * Rand.Range(1f, 1.2f));
psychosisIndex = Math.Clamp(psychosisIndex, 0, 4);
MentalType mentalType = psychosisIndex switch
@@ -83,11 +83,16 @@ namespace Barotrauma
{
if (GameMain.GameSession.RoundDuration < 120.0f &&
speaker?.CurrentHull != null &&
GameMain.GameSession.Map?.CurrentLocation?.Reputation?.Value >= 0.0f &&
(speaker.TeamID == CharacterTeamType.FriendlyNPC || speaker.TeamID == CharacterTeamType.None) &&
Character.CharacterList.Any(c => c.TeamID != speaker.TeamID && c.CurrentHull == speaker.CurrentHull))
{
currentFlags.Add("EnterOutpost".ToIdentifier());
}
if (Level.Loaded.IsEndBiome)
{
currentFlags.Add("EndLevel".ToIdentifier());
}
}
if (GameMain.GameSession.EventManager.CurrentIntensity <= 0.2f)
{
@@ -117,7 +122,7 @@ namespace Barotrauma
foreach (Affliction affliction in afflictions)
{
var currentEffect = affliction.GetActiveEffect();
if (currentEffect != null && !string.IsNullOrEmpty(currentEffect.DialogFlag.Value) && !currentFlags.Contains(currentEffect.DialogFlag))
if (currentEffect is { DialogFlag.IsEmpty: false } && !currentFlags.Contains(currentEffect.DialogFlag))
{
currentFlags.Add(currentEffect.DialogFlag);
}
@@ -126,6 +131,10 @@ namespace Barotrauma
if (speaker.TeamID == CharacterTeamType.FriendlyNPC && speaker.Submarine != null && speaker.Submarine.Info.IsOutpost)
{
currentFlags.Add("OutpostNPC".ToIdentifier());
if (GameMain.GameSession?.Level?.StartLocation?.Faction is Faction faction)
{
currentFlags.Add($"OutpostNPC{faction.Prefab.Identifier}".ToIdentifier());
}
}
if (speaker.CampaignInteractionType != CampaignMode.InteractionType.None)
{
@@ -256,7 +256,9 @@ namespace Barotrauma
if (!AllowOutsideSubmarine && character.Submarine == null) { return false; }
if (AllowInAnySub) { return true; }
if ((AllowInFriendlySubs && character.Submarine.TeamID == CharacterTeamType.FriendlyNPC) || character.IsEscorted) { return true; }
return character.Submarine.TeamID == character.TeamID || character.Submarine.DockedTo.Any(sub => sub.TeamID == character.TeamID);
return character.Submarine.TeamID == character.TeamID ||
character.Submarine.TeamID == character.OriginalTeamID ||
character.Submarine.DockedTo.Any(sub => sub.TeamID == character.TeamID || sub.TeamID == character.OriginalTeamID);
}
}
@@ -648,11 +648,11 @@ namespace Barotrauma
{
statusEffects = statusEffects.Concat(hitEffects);
}
float afflictionsStun = attack.Afflictions.Keys.Sum(a => a.Identifier == "stun" ? a.Strength : 0);
float afflictionsStun = attack.Afflictions.Keys.Sum(a => a.Identifier == AfflictionPrefab.StunType ? a.Strength : 0);
float effectsStun = statusEffects.None() ? 0 : statusEffects.Max(se =>
{
float stunAmount = 0;
var stunAffliction = se.Afflictions.Find(a => a.Identifier == "stun");
var stunAffliction = se.Afflictions.Find(a => a.Identifier == AfflictionPrefab.StunType);
if (stunAffliction != null)
{
stunAmount = stunAffliction.Strength;
@@ -1176,30 +1176,31 @@ namespace Barotrauma
if (sqrDistance > repairTool.Range * repairTool.Range) { return; }
}
float aimFactor = MathHelper.PiOver2 * (1 - AimAccuracy);
if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.Position - Weapon.Position) < MathHelper.PiOver4 + aimFactor)
if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.WorldPosition - Weapon.WorldPosition) < MathHelper.PiOver4 + aimFactor)
{
if (myBodies == null)
{
myBodies = character.AnimController.Limbs.Select(l => l.body.FarseerBody);
}
var collisionCategories = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel;
var pickedBody = Submarine.PickBody(Weapon.SimPosition, Enemy.SimPosition, myBodies, collisionCategories, allowInsideFixture: true);
if (pickedBody != null)
// Check that we don't hit friendlies. No need to check the walls, because there's a separate check for that at 1096 (which intentionally has a small delay)
var pickedBodies = Submarine.PickBodies(Weapon.SimPosition, Character.GetRelativeSimPosition(from: Weapon, to: Enemy), myBodies, Physics.CollisionCharacter);
foreach (var body in pickedBodies)
{
Character target = null;
if (pickedBody.UserData is Character c)
if (body.UserData is Character c)
{
target = c;
}
else if (pickedBody.UserData is Limb limb)
else if (body.UserData is Limb limb)
{
target = limb.character;
}
if (target != null && (target == Enemy || !HumanAIController.IsFriendly(target)))
if (target != null && target != Enemy && HumanAIController.IsFriendly(target))
{
UseWeapon(deltaTime);
return;
}
}
UseWeapon(deltaTime);
}
}
}
@@ -4,6 +4,7 @@ using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
@@ -200,7 +201,8 @@ namespace Barotrauma
(container.Item.GetRootContainer()?.OwnInventory?.Locked ?? false) ||
ItemToContain == null || ItemToContain.Removed ||
!ItemToContain.IsOwnedBy(character) || container.Item.GetRootInventoryOwner() is Character c && c != character,
SpeakIfFails = !objectiveManager.IsCurrentOrder<AIObjectiveCleanupItems>()
SpeakIfFails = !objectiveManager.IsCurrentOrder<AIObjectiveCleanupItems>(),
endNodeFilter = n => Vector2.DistanceSquared(n.Waypoint.WorldPosition, container.Item.WorldPosition) <= MathUtils.Pow2(AIObjectiveGetItem.DefaultReach)
},
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref goToObjective));
@@ -19,7 +19,6 @@ namespace Barotrauma
private AIObjectiveGetItem getExtinguisherObjective;
private AIObjectiveGoTo gotoObjective;
private float useExtinquisherTimer;
public AIObjectiveExtinguishFire(Character character, Hull targetHull, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
@@ -44,7 +43,8 @@ namespace Barotrauma
}
else
{
float yDist = Math.Abs(character.WorldPosition.Y - targetHull.WorldPosition.Y);
float characterY = character.CurrentHull?.WorldPosition.Y ?? character.WorldPosition.Y;
float yDist = Math.Abs(characterY - targetHull.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 3 : 0;
float dist = Math.Abs(character.WorldPosition.X - targetHull.WorldPosition.X) + yDist;
float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, dist));
@@ -119,24 +119,18 @@ namespace Barotrauma
Abandon = true;
break;
}
float xDist = Math.Abs(character.WorldPosition.X - fs.WorldPosition.X) - fs.DamageRange;
float yDist = Math.Abs(character.WorldPosition.Y - fs.WorldPosition.Y);
bool inRange = xDist + yDist < extinguisher.Range;
// Use the hull position, because the fire x pos is sometimes inside a wall -> the bot can't ever see it and continues running towards the wall.
ISpatialEntity lookTarget = character.CurrentHull == targetHull || character.CurrentHull.linkedTo.Contains(targetHull) ? targetHull : fs as ISpatialEntity;
bool move = !inRange || !character.CanSeeTarget(lookTarget);
if ((inRange && character.CanSeeTarget(lookTarget)) || useExtinquisherTimer > 0)
float xDist = Math.Abs(character.WorldPosition.X - fs.WorldPosition.X);
float yDist = Math.Abs(character.CurrentHull.WorldPosition.Y - targetHull.WorldPosition.Y);
float dist = xDist + yDist;
bool inRange = dist < extinguisher.Range;
bool isInDamageRange = fs.IsInDamageRange(character, fs.DamageRange) && character.CanSeeTarget(targetHull);
bool moveCloser = !isInDamageRange && (!inRange || !character.CanSeeTarget(targetHull));
bool operateExtinguisher = !moveCloser || (dist < extinguisher.Range * 1.2f && character.CanSeeTarget(targetHull));
if (operateExtinguisher)
{
useExtinquisherTimer += deltaTime;
if (useExtinquisherTimer > 2.0f)
{
useExtinquisherTimer = 0.0f;
}
// Aim
character.CursorPosition = fs.Position;
Vector2 fromCharacterToFireSource = fs.WorldPosition - character.WorldPosition;
float dist = fromCharacterToFireSource.Length();
character.CursorPosition += VectorExtensions.Forward(extinguisherItem.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, dist / 2);
character.CursorPosition += VectorExtensions.Forward(extinguisherItem.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, fromCharacterToFireSource.Length() / 2);
if (extinguisherItem.RequireAimToUse)
{
character.SetInput(InputType.Aim, false, true);
@@ -148,25 +142,29 @@ namespace Barotrauma
{
character.Speak(TextManager.GetWithVariable("DialogPutOutFire", "[roomname]", targetHull.DisplayName, FormatCapitals.Yes).Value, null, 0, "putoutfire".ToIdentifier(), 10.0f);
}
// Prevents running into the flames.
objectiveManager.CurrentObjective.ForceWalk = true;
}
if (move)
if (moveCloser)
{
//go to the first firesource
if (TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(fs, character, objectiveManager, closeEnough: Math.Max(fs.DamageRange, extinguisher.Range * 0.7f))
{
DialogueIdentifier = "dialogcannotreachfire".ToIdentifier(),
TargetName = fs.Hull.DisplayName
},
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref gotoObjective)))
if (TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(fs, character, objectiveManager, closeEnough: extinguisher.Range * 0.8f)
{
DialogueIdentifier = "dialogcannotreachfire".ToIdentifier(),
TargetName = fs.Hull.DisplayName,
},
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref gotoObjective)))
{
gotoObjective.requiredCondition = () => character.CanSeeTarget(targetHull);
}
}
else
else if (!operateExtinguisher || isInDamageRange)
{
character.AIController.SteeringManager.Reset();
// Don't walk into the flames.
RemoveSubObjective(ref gotoObjective);
SteeringManager.Reset();
}
// Only target one fire source at the time.
break;
}
}
@@ -177,8 +175,20 @@ namespace Barotrauma
base.Reset();
getExtinguisherObjective = null;
gotoObjective = null;
useExtinquisherTimer = 0;
sinTime = 0;
SteeringManager.Reset();
}
protected override void OnCompleted()
{
base.OnCompleted();
SteeringManager.Reset();
}
protected override void OnAbandon()
{
base.OnAbandon();
SteeringManager.Reset();
}
}
}
@@ -24,7 +24,7 @@ namespace Barotrauma
protected override float TargetEvaluation()
{
if (Targets.None()) { return 0; }
if (!character.IsOnPlayerTeam) { return 100; }
if (!character.IsOnPlayerTeam && !character.IsOriginallyOnPlayerTeam) { return 100; }
if (character.IsSecurity) { return 100; }
if (objectiveManager.IsOrder(this)) { return 100; }
// If there's any security officers onboard, leave fighting for them.
@@ -66,7 +66,13 @@ namespace Barotrauma
if (target.CurrentHull == null) { return false; }
if (HumanAIController.IsFriendly(character, target)) { return false; }
if (!character.Submarine.IsConnectedTo(target.Submarine)) { return false; }
if (!targetCharactersInOtherSubs && character.Submarine.TeamID != target.Submarine.TeamID) { return false; }
if (!targetCharactersInOtherSubs)
{
if (character.Submarine.TeamID != target.Submarine.TeamID && character.OriginalTeamID != target.Submarine.TeamID)
{
return false;
}
}
if (target.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI)) { return false; }
if (target.IsArrested) { return false; }
if (EnemyAIController.IsLatchedToSomeoneElse(target, character)) { return false; }
@@ -47,19 +47,12 @@ namespace Barotrauma
}
if (character.CurrentHull == null)
{
if (!character.NeedsAir)
{
Priority = 0;
}
else
{
Priority = (
objectiveManager.HasOrder<AIObjectiveGoTo>(o => o.Priority > 0) ||
objectiveManager.HasOrder<AIObjectiveReturn>(o => o.Priority > 0) ||
objectiveManager.HasActiveObjective<AIObjectiveRescue>() ||
objectiveManager.Objectives.Any(o => o is AIObjectiveCombat && o.Priority > 0))
&& HumanAIController.HasDivingSuit(character) ? 0 : 100;
}
Priority = (
objectiveManager.HasOrder<AIObjectiveGoTo>(o => o.Priority > 0) ||
objectiveManager.HasOrder<AIObjectiveReturn>(o => o.Priority > 0) ||
objectiveManager.HasActiveObjective<AIObjectiveRescue>() ||
objectiveManager.Objectives.Any(o => o is AIObjectiveCombat && o.Priority > 0))
&& ((character.IsImmuneToPressure && !character.IsLowInOxygen)|| HumanAIController.HasDivingSuit(character)) ? 0 : 100;
}
else
{
@@ -118,6 +111,11 @@ namespace Barotrauma
if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD)
{
Priority -= priorityDecrease * deltaTime;
if (currenthullSafety >= 100)
{
// Reduce the priority to zero so that the bot can get switch to other objectives immediately, e.g. when entering the airlock.
Priority = 0;
}
}
else
{
@@ -140,8 +138,8 @@ namespace Barotrauma
{
if (resetPriority) { return; }
var currentHull = character.CurrentHull;
bool dangerousPressure = !character.IsProtectedFromPressure && (currentHull == null || currentHull.LethalPressure > 0);
bool shouldActOnSuffocation = character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false);
bool dangerousPressure = currentHull == null || currentHull.LethalPressure > 0 && character.PressureProtection <= 0;
if (!character.LockHands && (!dangerousPressure || shouldActOnSuffocation || cannotFindSafeHull))
{
bool needsDivingGear = HumanAIController.NeedsDivingGear(currentHull, out bool needsDivingSuit);
@@ -221,7 +219,11 @@ namespace Barotrauma
TryAddSubObjective(ref goToObjective,
constructor: () => new AIObjectiveGoTo(currentSafeHull, character, objectiveManager, getDivingGearIfNeeded: true)
{
AllowGoingOutside = HumanAIController.HasDivingSuit(character, conditionPercentage: 50)
AllowGoingOutside =
character.IsProtectedFromPressure ||
character.CurrentHull == null ||
character.CurrentHull.IsTaggedAirlock() ||
character.CurrentHull.LeadsOutside(character)
},
onCompleted: () =>
{
@@ -352,8 +354,8 @@ namespace Barotrauma
//tends to make the method much faster, because we find a potential hull earlier and can discard further-away hulls more easily
//(for instance, an NPC in an outpost might otherwise go through all the hulls in the main sub first and do tons of expensive
//path calculations, only to discard all of them when going through the hulls in the outpost)
float hullSuitability = EstimateHullSuitability(character, hull);
if (!hulls.Any())
float hullSuitability = EstimateHullSuitability(character, hull);
if (hulls.None())
{
hulls.Add(hull);
}
@@ -448,9 +450,12 @@ namespace Barotrauma
{
hullSafety = 100;
}
float characterY = character.CurrentHull?.WorldPosition.Y ?? character.WorldPosition.Y;
float yDist = Math.Abs(characterY - potentialHull.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 3 : 0;
float distance = Math.Abs(character.WorldPosition.X - potentialHull.WorldPosition.X) + yDist;
// Huge preference for closer targets
float distance = Vector2.DistanceSquared(character.WorldPosition, potentialHull.WorldPosition);
float distanceFactor = MathHelper.Lerp(1, 0.2f, MathUtils.InverseLerp(0, MathUtils.Pow(100000, 2), distance));
float distanceFactor = MathHelper.Lerp(1, 0.2f, MathUtils.InverseLerp(0, 10000, distance));
hullSafety *= distanceFactor;
// If the target is not inside a friendly submarine, considerably reduce the hull safety.
// Intentionally exclude wrecks from this check
@@ -155,17 +155,21 @@ namespace Barotrauma
bool canOperate = toLeak.LengthSquared() < reach * reach;
if (canOperate)
{
TryAddSubObjective(ref operateObjective, () => new AIObjectiveOperateItem(repairTool, character, objectiveManager, option: Identifier.Empty, requireEquip: true, operateTarget: Leak),
onAbandon: () => Abandon = true,
onCompleted: () =>
TryAddSubObjective(ref operateObjective, () => new AIObjectiveOperateItem(repairTool, character, objectiveManager, option: Identifier.Empty, requireEquip: true, operateTarget: Leak)
{
// Use an empty filter to override the default
EndNodeFilter = n => true
},
onAbandon: () => Abandon = true,
onCompleted: () =>
{
if (CheckObjectiveSpecific()) { IsCompleted = true; }
else
{
if (CheckObjectiveSpecific()) { IsCompleted = true; }
else
{
// Failed to operate. Probably too far.
Abandon = true;
}
});
// Failed to operate. Probably too far.
Abandon = true;
}
});
}
else
{
@@ -123,6 +123,11 @@ namespace Barotrauma
return ignoredTags;
}
public static Func<PathNode, bool> CreateEndNodeFilter(ISpatialEntity targetEntity)
{
return n => (n.Waypoint.Ladders == null || n.Waypoint.IsInWater) && Vector2.DistanceSquared(n.Waypoint.WorldPosition, targetEntity.WorldPosition) <= MathUtils.Pow2(DefaultReach);
}
private bool CheckInventory()
{
if (IdentifiersOrTags == null) { return false; }
@@ -155,11 +160,6 @@ namespace Barotrauma
Abandon = true;
return;
}
if (character.Submarine == null)
{
Abandon = true;
return;
}
if (IdentifiersOrTags != null && !isDoneSeeking)
{
if (checkInventory)
@@ -171,9 +171,14 @@ namespace Barotrauma
}
if (!isDoneSeeking)
{
if (character.Submarine == null)
{
Abandon = true;
return;
}
if (!AllowDangerousPressure)
{
bool dangerousPressure = character.CurrentHull == null || character.CurrentHull.LethalPressure > 0 && character.PressureProtection <= 0;
bool dangerousPressure = !character.IsProtectedFromPressure && (character.CurrentHull == null || character.CurrentHull.LethalPressure > 0);
if (dangerousPressure)
{
#if DEBUG
@@ -192,6 +197,11 @@ namespace Barotrauma
return;
}
}
else if (character.Submarine == null)
{
Abandon = true;
return;
}
if (targetItem == null || targetItem.Removed)
{
#if DEBUG
@@ -307,7 +317,8 @@ namespace Barotrauma
{
// If the root container changes, the item is no longer where it was (taken by someone -> need to find another item)
AbortCondition = obj => targetItem == null || targetItem.GetRootInventoryOwner() != moveToTarget,
SpeakIfFails = false
SpeakIfFails = false,
endNodeFilter = CreateEndNodeFilter(moveToTarget)
};
},
onAbandon: () =>
@@ -33,6 +33,11 @@ namespace Barotrauma
public bool DebugLogWhenFails { get; set; } = true;
public bool UsePathingOutside { get; set; } = true;
/// <summary>
/// Which event action created this objective (if any)
/// </summary>
public EventAction SourceEventAction;
public float ExtraDistanceWhileSwimming;
public float ExtraDistanceOutsideSub;
private float _closeEnoughMultiplier = 1;
@@ -45,6 +50,7 @@ namespace Barotrauma
private readonly float minDistance = 50;
private readonly float seekGapsInterval = 1;
private float seekGapsTimer;
private bool cantFindDivingGear;
/// <summary>
/// Display units
@@ -85,7 +91,7 @@ namespace Barotrauma
/// </summary>
public bool UseDistanceRelativeToAimSourcePos { get; set; } = false;
public override bool AbandonWhenCannotCompleteSubjectives => !repeat;
public override bool AbandonWhenCannotCompleteSubjectives => false;
public override bool AllowOutsideSubmarine => AllowGoingOutside;
public override bool AllowInAnySub => true;
@@ -258,48 +264,73 @@ namespace Barotrauma
}
if (!Abandon)
{
if (getDivingGearIfNeeded && !character.LockHands)
if (getDivingGearIfNeeded)
{
Character followTarget = Target as Character;
bool needsDivingSuit = (!isInside || hasOutdoorNodes) && character.NeedsAir && !character.HasAbilityFlag(AbilityFlags.ImmuneToPressure);
bool needsDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit);
if (Mimic)
bool needsDivingSuit = (!isInside || hasOutdoorNodes) && !character.IsImmuneToPressure;
bool tryToGetDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit);
bool tryToGetDivingSuit = needsDivingSuit;
if (Mimic && !character.IsImmuneToPressure)
{
if (HumanAIController.HasDivingSuit(followTarget))
{
needsDivingGear = true;
needsDivingSuit = true;
tryToGetDivingGear = true;
tryToGetDivingSuit = true;
}
else if (HumanAIController.HasDivingMask(followTarget))
else if (HumanAIController.HasDivingMask(followTarget) && character.CharacterHealth.OxygenLowResistance < 1)
{
needsDivingGear = true;
tryToGetDivingGear = true;
}
}
bool needsEquipment = false;
float minOxygen = AIObjectiveFindDivingGear.GetMinOxygen(character);
if (needsDivingSuit)
if (tryToGetDivingSuit)
{
needsEquipment = !HumanAIController.HasDivingSuit(character, minOxygen);
}
else if (needsDivingGear)
else if (tryToGetDivingGear)
{
needsEquipment = !HumanAIController.HasDivingGear(character, minOxygen);
}
if (needsEquipment)
if (character.LockHands)
{
cantFindDivingGear = true;
}
if (cantFindDivingGear && needsDivingSuit)
{
// Don't try to reach the target without a suit because it's lethal.
Abandon = true;
return;
}
if (needsEquipment && !cantFindDivingGear)
{
SteeringManager.Reset();
if (findDivingGear != null && !findDivingGear.CanBeCompleted)
{
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager),
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref findDivingGear));
}
else
{
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager),
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref findDivingGear));
}
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: tryToGetDivingSuit, objectiveManager),
onAbandon: () =>
{
cantFindDivingGear = true;
if (needsDivingSuit)
{
// Shouldn't try to reach the target without a suit, because it's lethal.
Abandon = true;
}
else
{
// Try again without requiring the diving suit
RemoveSubObjective(ref findDivingGear);
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager),
onAbandon: () =>
{
Abandon = character.CurrentHull != null && (objectiveManager.CurrentOrder != this || Target.Submarine == null);
RemoveSubObjective(ref findDivingGear);
},
onCompleted: () =>
{
RemoveSubObjective(ref findDivingGear);
});
}
},
onCompleted: () => RemoveSubObjective(ref findDivingGear));
return;
}
}
@@ -593,7 +624,7 @@ namespace Barotrauma
}
else if (target is Character c)
{
return c.CurrentHull;
return c.CurrentHull ?? c.AnimController.CurrentHull;
}
else if (target is Structure structure)
{
@@ -170,7 +170,8 @@ namespace Barotrauma
TargetHull = character.CurrentHull;
}
if (behavior == BehaviorType.StayInHull)
bool currentTargetIsInvalid = currentTarget == null || IsForbidden(currentTarget) || (PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull)));
if (behavior == BehaviorType.StayInHull && !currentTargetIsInvalid)
{
currentTarget = TargetHull;
bool stayInHull = character.CurrentHull == currentTarget && IsSteeringFinished() && !character.IsClimbing;
@@ -190,9 +191,6 @@ namespace Barotrauma
}
else
{
bool currentTargetIsInvalid = currentTarget == null || IsForbidden(currentTarget) ||
(PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull)));
if (currentTarget != null && !currentTargetIsInvalid)
{
if (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted)
@@ -98,7 +98,7 @@ namespace Barotrauma
{
foreach (var item in itemContainer.ContainableItems)
{
if (CheckStatusEffects(item.statusEffects) == CheckStatus.Finished)
if (CheckStatusEffects(item.StatusEffects) == CheckStatus.Finished)
{
return CheckStatus.Finished;
}
@@ -23,6 +23,11 @@ namespace Barotrauma
private AIObjectiveGoTo goToObjective;
private AIObjectiveGetItem getItemObjective;
/// <summary>
/// If undefined, a default filter will be used.
/// </summary>
public Func<PathNode, bool> EndNodeFilter;
public bool Override { get; set; } = true;
public override bool CanBeCompleted => base.CanBeCompleted && (!useController || controller != null);
@@ -222,7 +227,7 @@ namespace Barotrauma
{
target.Item.TryInteract(character, forceSelectKey: true);
}
if (component.AIOperate(deltaTime, character, this))
if (component.CrewAIOperate(deltaTime, character, this))
{
isDoneOperating = completionCondition == null || completionCondition();
}
@@ -232,7 +237,7 @@ namespace Barotrauma
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(target.Item, character, objectiveManager, closeEnough: 50)
{
TargetName = target.Item.Name,
endNodeFilter = node => node.Waypoint.Ladders == null
endNodeFilter = EndNodeFilter ?? AIObjectiveGetItem.CreateEndNodeFilter(target.Item)
},
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref goToObjective));
@@ -290,7 +295,7 @@ namespace Barotrauma
}
return;
}
if (component.AIOperate(deltaTime, character, this))
if (component.CrewAIOperate(deltaTime, character, this))
{
isDoneOperating = completionCondition == null || completionCondition();
}
@@ -17,7 +17,6 @@ namespace Barotrauma
private AIObjectiveGoTo goToObjective;
private AIObjectiveContainItem refuelObjective;
private float previousCondition = -1;
private RepairTool repairTool;
private const float WaitTimeBeforeRepair = 0.5f;
@@ -196,15 +195,7 @@ namespace Barotrauma
Abandon = true;
}
}
if (previousCondition == -1)
{
previousCondition = Item.Condition;
}
else if (Item.Condition < previousCondition)
{
// If the current condition is less than the previous condition, we can't complete the task, so let's abandon it. The item is probably deteriorating at a greater speed than we can repair it.
Abandon = true;
}
CheckPreviousCondition(deltaTime);
}
if (Abandon)
{
@@ -229,7 +220,6 @@ namespace Barotrauma
TryAddSubObjective(ref goToObjective,
constructor: () =>
{
previousCondition = -1;
var objective = new AIObjectiveGoTo(Item, character, objectiveManager)
{
TargetName = Item.Name
@@ -251,6 +241,27 @@ namespace Barotrauma
}
}
private const float conditionCheckDelay = 1;
private float conditionCheckTimer;
private float previousCondition;
private void CheckPreviousCondition(float deltaTime)
{
if (Item == null || Item.Removed) { return; }
conditionCheckTimer -= deltaTime;
if (conditionCheckTimer > 0) { return; }
conditionCheckTimer = conditionCheckDelay;
if (previousCondition > -1 && Item.Condition < previousCondition)
{
// If the current condition is less than the previous condition, we can't complete the task, so let's abandon it. The item is probably deteriorating at a greater speed than we can repair it.
Abandon = true;
}
else
{
// If the previous condition is not yet stored or if it's valid (greater or equal to current condition), save the condition for the next check here.
previousCondition = Item.Condition;
}
}
private void FindRepairTool()
{
foreach (Repairable repairable in Item.Repairables)
@@ -303,7 +314,6 @@ namespace Barotrauma
base.Reset();
goToObjective = null;
refuelObjective = null;
previousCondition = -1;
repairTool = null;
}
}
@@ -139,14 +139,14 @@ namespace Barotrauma
recursive: true);
}
}
if (character.Submarine != null)
if (character.Submarine != null && targetCharacter.CurrentHull != null)
{
if (HumanAIController.GetHullSafety(targetCharacter.CurrentHull, targetCharacter) < HumanAIController.HULL_SAFETY_THRESHOLD)
{
// Incapacitated target is not in a safe place -> Move to a safe place first
if (character.SelectedCharacter != targetCharacter)
{
if (targetCharacter.CurrentHull != null && HumanAIController.VisibleHulls.Contains(targetCharacter.CurrentHull) && targetCharacter.CurrentHull.DisplayName != null)
if (HumanAIController.VisibleHulls.Contains(targetCharacter.CurrentHull) && targetCharacter.CurrentHull.DisplayName != null)
{
character.Speak(TextManager.GetWithVariables("DialogFoundUnconsciousTarget",
("[targetname]", targetCharacter.Name, FormatCapitals.No),
@@ -71,11 +71,11 @@ namespace Barotrauma
{
float strength = character.CharacterHealth.GetPredictedStrength(affliction, predictFutureDuration: 10.0f);
vitality -= affliction.GetVitalityDecrease(character.CharacterHealth, strength) / character.MaxVitality * 100;
if (affliction.Prefab.AfflictionType == "paralysis")
if (affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType)
{
vitality -= affliction.Strength;
}
else if (affliction.Prefab.AfflictionType == "poison")
else if (affliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType)
{
vitality -= affliction.Strength;
}
@@ -12,6 +12,9 @@ namespace Barotrauma
private AIObjectiveGoTo moveInsideObjective, moveOutsideObjective;
private bool usingEscapeBehavior, isSteeringThroughGap;
public override bool AllowOutsideSubmarine => true;
public override bool AllowInAnySub => true;
public AIObjectiveReturn(Character character, Character orderGiver, AIObjectiveManager objectiveManager, float priorityModifier = 1.0f) : base(character, objectiveManager, priorityModifier)
{
ReturnTarget = GetReturnTarget(Submarine.MainSubs) ?? GetReturnTarget(Submarine.Loaded);
@@ -465,6 +465,10 @@ namespace Barotrauma
public readonly OrderTarget TargetPosition;
private ISpatialEntity targetSpatialEntity;
/// <summary>
/// Note this property doesn't return the follow target of the Follow objective, as expected!
/// </summary>
public ISpatialEntity TargetSpatialEntity
{
get
@@ -348,6 +348,7 @@ namespace Barotrauma
{
if (body.UserData is Submarine) { return false; }
if (body.UserData is Structure s && !s.IsPlatform) { return false; }
if (body.UserData is Voronoi2.VoronoiCell) { return false; }
if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { return false; }
}
}
@@ -47,7 +47,8 @@ namespace Barotrauma
public void SetOrder(Character orderedCharacter)
{
OrderedCharacter = orderedCharacter;
if (OrderedCharacter.AIController is HumanAIController humanAI && humanAI.ObjectiveManager.CurrentOrders.None(o => o.MatchesOrder(SuggestedOrder.Identifier, Option)))
if (OrderedCharacter.AIController is HumanAIController humanAI &&
humanAI.ObjectiveManager.CurrentOrders.None(o => o.MatchesOrder(SuggestedOrder.Identifier, Option) && o.TargetEntity == TargetItem))
{
if (orderedCharacter != CommandingCharacter)
{
@@ -17,7 +17,20 @@ namespace Barotrauma
float GetTargetingImportance(Entity entity)
{
float currentDistanceToEnemy = Vector2.Distance(entity.WorldPosition, TargetItem.WorldPosition);
return MathHelper.Clamp(100 - (currentDistanceToEnemy / 100f), MinImportance, MaxImportance);
float importance = MathHelper.Clamp(100 - (currentDistanceToEnemy / 100f), MinImportance, MaxImportance * 0.5f);
if (TargetItem.Submarine != null)
{
Vector2 dir = entity.WorldPosition - TargetItem.WorldPosition;
Vector2 submarineDir = TargetItem.WorldPosition - TargetItem.Submarine.WorldPosition;
if (Vector2.Dot(dir, submarineDir) < 0)
{
//direction from the weapon to the target is opposite to the direction from the sub to the weapon
// = the turret is most likely on the wrong side of the sub, reduce importance
importance *= 0.1f;
}
}
return importance;
}
public override void CalculateImportanceSpecific()
@@ -229,7 +229,7 @@ namespace Barotrauma
#if DEBUG
ShipCommandLog("Current importance for " + shipIssueWorker + " was " + importance + " and it was already being attended by " + shipIssueWorker.OrderedCharacter);
#endif
attendedIssues.Add(shipIssueWorker);
InsertIssue(shipIssueWorker, attendedIssues);
}
else
{
@@ -237,12 +237,19 @@ namespace Barotrauma
ShipCommandLog("Current importance for " + shipIssueWorker + " was " + importance + " and it is not attended to");
#endif
shipIssueWorker.RemoveOrder();
availableIssues.Add(shipIssueWorker);
InsertIssue(shipIssueWorker, availableIssues);
}
}
availableIssues.Sort((x, y) => y.Importance.CompareTo(x.Importance));
attendedIssues.Sort((x, y) => x.Importance.CompareTo(y.Importance));
static void InsertIssue(ShipIssueWorker issue, List<ShipIssueWorker> list)
{
int index = 0;
while (index < list.Count && list[index].Importance > issue.Importance)
{
index++;
}
list.Insert(index, issue);
}
ShipIssueWorker mostImportantIssue = availableIssues.FirstOrDefault();
@@ -13,6 +13,7 @@ namespace Barotrauma
private readonly float minDistFromClosest;
private readonly float maxDistFromCenter;
private readonly float cohesion;
public bool ForceActive { get; private set; }
public List<AICharacter> Members { get; private set; } = new List<AICharacter>();
public HashSet<AICharacter> ActiveMembers { get; private set; } = new HashSet<AICharacter>();
@@ -26,9 +27,10 @@ namespace Barotrauma
public SwarmBehavior(XElement element, EnemyAIController ai)
{
this.ai = ai;
minDistFromClosest = ConvertUnits.ToSimUnits(element.GetAttributeFloat("mindistfromclosest", 10.0f));
maxDistFromCenter = ConvertUnits.ToSimUnits(element.GetAttributeFloat("maxdistfromcenter", 1000.0f));
cohesion = element.GetAttributeFloat("cohesion", 1) / 10;
minDistFromClosest = ConvertUnits.ToSimUnits(element.GetAttributeFloat(nameof(minDistFromClosest), 10.0f));
maxDistFromCenter = ConvertUnits.ToSimUnits(element.GetAttributeFloat(nameof(maxDistFromCenter), 1000.0f));
cohesion = element.GetAttributeFloat(nameof(cohesion), 1) / 10;
ForceActive = element.GetAttributeBool(nameof(ForceActive), false);
}
public static void CreateSwarm(IEnumerable<AICharacter> swarm)
@@ -8,16 +8,90 @@ using System;
namespace Barotrauma
{
partial class WreckAI : IServerSerializable
internal class SubmarineTurretAI
{
public Submarine Wreck { get; private set; }
public Submarine Submarine { get; protected set; }
protected readonly List<Turret> turrets = new List<Turret>();
public Identifier FriendlyTag;
public SubmarineTurretAI(Submarine submarine, Identifier friendlyTag = default)
{
FriendlyTag = friendlyTag;
Submarine = submarine;
foreach (Item item in Item.ItemList)
{
if (item.Submarine != Submarine) { continue; }
var turret = item.GetComponent<Turret>();
if (turret != null)
{
turrets.Add(turret);
// Set false, because we manage the turrets in the Update method.
turret.AutoOperate = false;
// Set to full condition, because items don't work when they are broken.
turret.Item.Condition = turret.Item.MaxCondition;
foreach (MapEntity linkedEntity in turret.Item.linkedTo)
{
if (linkedEntity is Item linkedItem)
{
linkedItem.Condition = linkedItem.MaxCondition;
}
}
}
}
LoadAllTurrets();
}
public virtual void Update(float deltaTime)
{
if (Submarine == null || Submarine.Removed) { return; }
OperateTurrets(deltaTime, FriendlyTag);
}
protected virtual void LoadAllTurrets()
{
foreach (var turret in turrets)
{
LoadTurret(turret);
}
}
protected void LoadTurret(Turret turret, Func<ItemPrefab, bool> ammoFilter = null)
{
foreach (var linkedItem in turret.Item.GetLinkedEntities<Item>())
{
var container = linkedItem.GetComponent<ItemContainer>();
if (container == null) { continue; }
for (int i = 0; i < container.Inventory.Capacity; i++)
{
if (container.Inventory.GetItemAt(i) != null) { continue; }
if (MapEntityPrefab.List.GetRandom(e => e is ItemPrefab ip && container.CanBeContained(ip, i) && (ammoFilter == null || ammoFilter(ip)), Rand.RandSync.ServerAndClient) is ItemPrefab ammoPrefab)
{
Item ammo = new Item(ammoPrefab, container.Item.WorldPosition, Submarine);
if (!container.Inventory.TryPutItem(ammo, i, allowSwapping: false, allowCombine: false, user: null, createNetworkEvent: false))
{
turret.Item.Remove();
}
}
}
}
}
protected void OperateTurrets(float deltaTime, Identifier friendlyTag)
{
foreach (var turret in turrets)
{
turret.UpdateAutoOperate(deltaTime, friendlyTag);
}
}
}
partial class WreckAI : SubmarineTurretAI, IServerSerializable
{
public bool IsAlive { get; private set; }
private readonly List<Item> allItems;
private readonly List<Item> thalamusItems;
private readonly List<Structure> thalamusStructures;
private readonly List<Turret> turrets = new List<Turret>();
private readonly List<WayPoint> wayPoints = new List<WayPoint>();
private readonly List<Hull> hulls = new List<Hull>();
private readonly List<Item> spawnOrgans = new List<Item>();
@@ -25,7 +99,7 @@ namespace Barotrauma
private bool initialCellsSpawned;
public readonly WreckAIConfig Config;
public WreckAIConfig Config { get; private set; }
private bool IsClient => GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient;
@@ -44,15 +118,10 @@ namespace Barotrauma
return wreckAI;
}
private WreckAI(Submarine wreck)
private WreckAI(Submarine wreck) : base(wreck)
{
Wreck = wreck;
Config = WreckAIConfig.GetRandom();
if (Config == null)
{
DebugConsole.ThrowError("WreckAI: No wreck AI config found!");
return;
}
GetConfig();
if (Config == null) { return; }
var thalamusPrefabs = ItemPrefab.Prefabs.Where(p => IsThalamus(p));
var brainPrefab = thalamusPrefabs.GetRandom(i => i.Tags.Contains(Config.Brain), Rand.RandSync.ServerAndClient);
if (brainPrefab == null)
@@ -60,20 +129,20 @@ namespace Barotrauma
DebugConsole.ThrowError($"WreckAI: Could not find any brain prefab with the tag {Config.Brain}! Cannot continue. Failed to create wreck AI.");
return;
}
allItems = Wreck.GetItems(false);
allItems = wreck.GetItems(false);
thalamusItems = allItems.FindAll(i => IsThalamus(((MapEntity)i).Prefab));
hulls.AddRange(Wreck.GetHulls(false));
hulls.AddRange(wreck.GetHulls(false));
var potentialBrainHulls = new List<(Hull hull, float weight)>();
brain = new Item(brainPrefab, Vector2.Zero, Wreck);
brain = new Item(brainPrefab, Vector2.Zero, wreck);
thalamusItems.Add(brain);
Point minSize = brain.Rect.Size.Multiply(brain.Scale);
// Bigger hulls are allowed, but not preferred more than what's sufficent.
Vector2 sufficentSize = new Vector2(minSize.X * 2, minSize.Y * 1.1f);
// Shrink the horizontal axis so that the brain is not placed in the left or right side, where we often have curved walls.
Rectangle shrinkedBounds = ToolBox.GetWorldBounds(Wreck.WorldPosition.ToPoint(), new Point(Wreck.Borders.Width - 500, Wreck.Borders.Height));
Rectangle shrinkedBounds = ToolBox.GetWorldBounds(wreck.WorldPosition.ToPoint(), new Point(wreck.Borders.Width - 500, wreck.Borders.Height));
foreach (Hull hull in hulls)
{
float distanceFromCenter = Vector2.Distance(Wreck.WorldPosition, hull.WorldPosition);
float distanceFromCenter = Vector2.Distance(wreck.WorldPosition, hull.WorldPosition);
float distanceFactor = MathHelper.Lerp(1.0f, 0.5f, MathUtils.InverseLerp(0, Math.Max(shrinkedBounds.Width, shrinkedBounds.Height) / 2, distanceFromCenter));
float horizontalSizeFactor = MathHelper.Lerp(0.5f, 1.0f, MathUtils.InverseLerp(minSize.X, sufficentSize.X, hull.Rect.Width));
float verticalSizeFactor = MathHelper.Lerp(0.5f, 1.0f, MathUtils.InverseLerp(minSize.Y, sufficentSize.Y, hull.Rect.Height));
@@ -121,7 +190,7 @@ namespace Barotrauma
var backgroundPrefab = thalamusStructurePrefabs.GetRandom(i => i.Tags.Contains(Config.BrainRoomBackground), Rand.RandSync.ServerAndClient);
if (backgroundPrefab != null)
{
new Structure(brainHull.Rect, backgroundPrefab, Wreck);
new Structure(brainHull.Rect, backgroundPrefab, wreck);
}
var horizontalWallPrefab = thalamusStructurePrefabs.GetRandom(p => p.Tags.Contains(Config.BrainRoomHorizontalWall), Rand.RandSync.ServerAndClient);
if (horizontalWallPrefab != null)
@@ -129,8 +198,8 @@ namespace Barotrauma
int height = (int)horizontalWallPrefab.Size.Y;
int halfHeight = height / 2;
int quarterHeight = halfHeight / 2;
new Structure(new Rectangle(brainHull.Rect.Left, brainHull.Rect.Top + quarterHeight, brainHull.Rect.Width, height), horizontalWallPrefab, Wreck);
new Structure(new Rectangle(brainHull.Rect.Left, brainHull.Rect.Top - brainHull.Rect.Height + halfHeight + quarterHeight, brainHull.Rect.Width, height), horizontalWallPrefab, Wreck);
new Structure(new Rectangle(brainHull.Rect.Left, brainHull.Rect.Top + quarterHeight, brainHull.Rect.Width, height), horizontalWallPrefab, wreck);
new Structure(new Rectangle(brainHull.Rect.Left, brainHull.Rect.Top - brainHull.Rect.Height + halfHeight + quarterHeight, brainHull.Rect.Width, height), horizontalWallPrefab, wreck);
}
var verticalWallPrefab = thalamusStructurePrefabs.GetRandom(p => p.Tags.Contains(Config.BrainRoomVerticalWall), Rand.RandSync.ServerAndClient);
if (verticalWallPrefab != null)
@@ -138,50 +207,13 @@ namespace Barotrauma
int width = (int)verticalWallPrefab.Size.X;
int halfWidth = width / 2;
int quarterWidth = halfWidth / 2;
new Structure(new Rectangle(brainHull.Rect.Left - quarterWidth, brainHull.Rect.Top, width, brainHull.Rect.Height), verticalWallPrefab, Wreck);
new Structure(new Rectangle(brainHull.Rect.Right - halfWidth - quarterWidth, brainHull.Rect.Top, width, brainHull.Rect.Height), verticalWallPrefab, Wreck);
new Structure(new Rectangle(brainHull.Rect.Left - quarterWidth, brainHull.Rect.Top, width, brainHull.Rect.Height), verticalWallPrefab, wreck);
new Structure(new Rectangle(brainHull.Rect.Right - halfWidth - quarterWidth, brainHull.Rect.Top, width, brainHull.Rect.Height), verticalWallPrefab, wreck);
}
foreach (Item item in allItems)
foreach (Item item in thalamusItems)
{
if (thalamusItems.Contains(item))
{
// Ensure that thalamus items are visible
item.HiddenInGame = false;
}
else
{
// Load regular turrets
var turret = item.GetComponent<Turret>();
if (turret != null)
{
foreach (var linkedItem in item.GetLinkedEntities<Item>())
{
var container = linkedItem.GetComponent<ItemContainer>();
if (container == null) { continue; }
for (int i = 0; i < container.Inventory.Capacity; i++)
{
if (container.Inventory.GetItemAt(i) != null) { continue; }
if (MapEntityPrefab.List.GetRandom(e => e is ItemPrefab ip && container.CanBeContained(ip, i) &&
Config.ForbiddenAmmunition.None(id => id == ip.Identifier), Rand.RandSync.ServerAndClient) is ItemPrefab ammoPrefab)
{
Item ammo = new Item(ammoPrefab, container.Item.WorldPosition, Wreck);
if (!container.Inventory.TryPutItem(ammo, i, allowSwapping: false, allowCombine: false, user: null, createNetworkEvent: false))
{
item.Remove();
}
}
}
}
}
}
}
foreach (var item in allItems)
{
var turret = item.GetComponent<Turret>();
if (turret != null)
{
turrets.Add(turret);
}
// Ensure that thalamus items are visible
item.HiddenInGame = false;
if (item.HasTag(Config.Spawner))
{
if (!spawnOrgans.Contains(item))
@@ -195,16 +227,34 @@ namespace Barotrauma
}
}
}
wayPoints.AddRange(Wreck.GetWaypoints(false));
wayPoints.AddRange(wreck.GetWaypoints(false));
IsAlive = true;
thalamusStructures = GetThalamusEntities<Structure>(Wreck, Config.Entity).ToList();
thalamusStructures = GetThalamusEntities<Structure>(wreck, Config.Entity).ToList();
}
private void GetConfig()
{
Config ??= WreckAIConfig.GetRandom();
if (Config == null)
{
DebugConsole.ThrowError("WreckAI: No wreck AI config found!");
}
}
protected override void LoadAllTurrets()
{
GetConfig();
foreach (var turret in turrets)
{
LoadTurret(turret, ip => Config.ForbiddenAmmunition.None(id => id == ip.Identifier));
}
}
private readonly List<Item> destroyedOrgans = new List<Item>();
public void Update(float deltaTime)
public override void Update(float deltaTime)
{
if (!IsAlive) { return; }
if (Wreck == null || Wreck.Removed)
if (Submarine == null || Submarine.Removed)
{
Remove();
return;
@@ -223,34 +273,60 @@ namespace Barotrauma
}
}
destroyedOrgans.ForEach(o => spawnOrgans.Remove(o));
bool someoneNearby = false;
bool isSomeoneNearby = false;
float minDist = Sonar.DefaultSonarRange * 2.0f;
foreach (Submarine submarine in Submarine.Loaded)
#if SERVER
foreach (var client in GameMain.Server.ConnectedClients)
{
if (submarine.Info.Type != SubmarineType.Player) { continue; }
if (Vector2.DistanceSquared(submarine.WorldPosition, Wreck.WorldPosition) < minDist * minDist)
var spectatePos = client.SpectatePos;
if (spectatePos.HasValue)
{
someoneNearby = true;
break;
if (IsCloseEnough(spectatePos.Value, minDist))
{
isSomeoneNearby = true;
break;
}
}
}
foreach (Character c in Character.CharacterList)
#else
if (IsCloseEnough(GameMain.GameScreen.Cam.Position, minDist))
{
if (c != Character.Controlled && !c.IsRemotePlayer) { continue; }
if (Vector2.DistanceSquared(c.WorldPosition, Wreck.WorldPosition) < minDist * minDist)
isSomeoneNearby = true;
}
#endif
if (!isSomeoneNearby)
{
foreach (Submarine submarine in Submarine.Loaded)
{
someoneNearby = true;
break;
if (submarine.Info.Type != SubmarineType.Player) { continue; }
if (IsCloseEnough(submarine.WorldPosition, minDist))
{
isSomeoneNearby = true;
break;
}
}
}
if (!someoneNearby) { return; }
OperateTurrets(deltaTime);
if (!isSomeoneNearby)
{
foreach (Character c in Character.CharacterList)
{
if (!c.IsPlayer && !c.IsOnPlayerTeam) { continue; }
if (IsCloseEnough(c.WorldPosition, minDist))
{
isSomeoneNearby = true;
break;
}
}
}
if (!isSomeoneNearby) { return; }
OperateTurrets(deltaTime, Config.Entity);
if (!IsClient)
{
if (!initialCellsSpawned) { SpawnInitialCells(); }
UpdateReinforcements(deltaTime);
}
}
private bool IsCloseEnough(Vector2 targetPos, float minDist) => Vector2.DistanceSquared(targetPos, Submarine.WorldPosition) < minDist * minDist;
private void SpawnInitialCells()
{
@@ -287,7 +363,7 @@ namespace Barotrauma
// Snap all tendons
foreach (Item item in turret.ActiveProjectiles)
{
if (item.GetComponent<Projectile>()?.IsStuckToTarget ?? false)
if (item.GetComponent<Projectile>() is { IsStuckToTarget: true })
{
item.Condition = 0;
}
@@ -314,7 +390,7 @@ namespace Barotrauma
{
// Sonar distance is used also for wreck positioning. No wreck should be closer to each other than this.
float maxDistance = Sonar.DefaultSonarRange;
if (Vector2.DistanceSquared(character.WorldPosition, Wreck.WorldPosition) < maxDistance * maxDistance)
if (Vector2.DistanceSquared(character.WorldPosition, Submarine.WorldPosition) < maxDistance * maxDistance)
{
character.Kill(CauseOfDeathType.Unknown, null);
}
@@ -333,7 +409,7 @@ namespace Barotrauma
public void Remove()
{
Kill();
RemoveThalamusItems(Wreck);
RemoveThalamusItems(Submarine);
thalamusItems?.Clear();
thalamusStructures?.Clear();
}
@@ -387,7 +463,7 @@ namespace Barotrauma
return MathHelper.Lerp(max, min, MathUtils.InverseLerp(0, 100, t));
}
void UpdateReinforcements(float deltaTime)
private void UpdateReinforcements(float deltaTime)
{
if (spawnOrgans.Count == 0) { return; }
cellSpawnTimer -= deltaTime;
@@ -398,7 +474,7 @@ namespace Barotrauma
}
}
bool TrySpawnCell(out Character cell, ISpatialEntity targetEntity = null)
private bool TrySpawnCell(out Character cell, ISpatialEntity targetEntity = null)
{
cell = null;
if (protectiveCells.Count >= MaxCellCount) { return false; }
@@ -424,19 +500,6 @@ namespace Barotrauma
cellSpawnTimer = GetSpawnTime();
return true;
}
void OperateTurrets(float deltaTime)
{
foreach (var turret in turrets)
{
// Never target other creatures than humans with the turrets.
turret.ThalamusOperate(this, deltaTime,
!turret.Item.HasTag("ignorecharacters"),
targetOtherCreatures: false,
!turret.Item.HasTag("ignoresubmarines"),
turret.Item.HasTag("ignoreaimdelay"));
}
}
void OnCellDeath(Character character, CauseOfDeath causeOfDeath)
{
@@ -541,11 +541,11 @@ namespace Barotrauma
float wobbleStrength = 0.0f;
if (character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand) == heldItem)
{
wobbleStrength += Character.CharacterHealth.GetLimbDamage(rightHand, afflictionType: "damage");
wobbleStrength += Character.CharacterHealth.GetLimbDamage(rightHand, afflictionType: AfflictionPrefab.DamageType);
}
if (character.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand) == heldItem)
{
wobbleStrength += Character.CharacterHealth.GetLimbDamage(leftHand, afflictionType: "damage");
wobbleStrength += Character.CharacterHealth.GetLimbDamage(leftHand, afflictionType: AfflictionPrefab.DamageType);
}
if (wobbleStrength <= 0.1f) { return 0.0f; }
wobbleStrength = (float)Math.Min(wobbleStrength, 1.0f);
@@ -135,8 +135,14 @@ namespace Barotrauma
public override void UpdateAnim(float deltaTime)
{
if (Frozen) return;
if (MainLimb == null) { return; }
//wait a bit for the ragdoll to "settle" (for joints to force the limbs to appropriate positions) before starting to animate
if (Timing.TotalTime - character.SpawnTime < 0.1f) { return; }
if (Frozen) { return; }
if (MainLimb == null)
{
ResetState();
return;
}
var mainLimb = MainLimb;
levitatingCollider = !IsHanging;
@@ -164,6 +170,7 @@ namespace Barotrauma
//cannot walk but on dry land -> wiggle around
UpdateDying(deltaTime);
}
ResetState();
return;
}
else
@@ -176,11 +183,17 @@ namespace Barotrauma
{
var lowestLimb = FindLowestLimb();
Collider.SetTransform(new Vector2(
Collider.SimPosition.X,
Math.Max(lowestLimb.SimPosition.Y + (Collider.radius + Collider.height / 2), Collider.SimPosition.Y)),
0.0f);
if (InWater)
{
Collider.SetTransform(new Vector2(Collider.SimPosition.X, MainLimb.SimPosition.Y), 0.0f);
}
else
{
Collider.SetTransform(new Vector2(
Collider.SimPosition.X,
Math.Max(lowestLimb.SimPosition.Y + (Collider.Radius + Collider.Height / 2), Collider.SimPosition.Y)),
0.0f);
}
Collider.Enabled = true;
}
@@ -223,6 +236,7 @@ namespace Barotrauma
if (character.SelectedCharacter != null)
{
DragCharacter(character.SelectedCharacter, deltaTime);
ResetState();
return;
}
if (character.AnimController.AnimationTestPose)
@@ -230,7 +244,11 @@ namespace Barotrauma
ApplyTestPose();
}
//don't flip when simply physics is enabled
if (SimplePhysicsEnabled) { return; }
if (SimplePhysicsEnabled)
{
ResetState();
return;
}
if (!character.IsRemotelyControlled && (character.AIController == null || character.AIController.CanFlip) && !Aiming)
{
@@ -264,43 +282,47 @@ namespace Barotrauma
}
}
if (!CurrentFishAnimation.Flip) { return; }
if (IsStuck) { return; }
if (character.AIController != null && !character.AIController.CanFlip) { return; }
flipCooldown -= deltaTime;
if (TargetDir != Direction.None && TargetDir != dir)
if (!IsStuck && CurrentFishAnimation.Flip && character.AIController is not { CanFlip: false })
{
flipTimer += deltaTime;
// Speed reductions are not taken into account here. It's intentional: an ai character cannot flip if it's heavily paralyzed (for example).
float requiredSpeed = CurrentAnimationParams.MovementSpeed / 2;
if (CurrentHull != null)
flipCooldown -= deltaTime;
if (TargetDir != Direction.None && TargetDir != dir)
{
// Enemy movement speeds are halved inside submarines
requiredSpeed /= 2;
}
bool isMovingFastEnough = Math.Abs(MainLimb.LinearVelocity.X) > requiredSpeed;
bool isTryingToMoveHorizontally = Math.Abs(TargetMovement.X) > Math.Abs(TargetMovement.Y);
if ((flipTimer > CurrentFishAnimation.FlipDelay && flipCooldown <= 0.0f && ((isMovingFastEnough && isTryingToMoveHorizontally) || IsMovingBackwards))
|| character.IsRemotePlayer)
{
Flip();
if (!inWater || (CurrentSwimParams != null && CurrentSwimParams.Mirror))
flipTimer += deltaTime;
// Speed reductions are not taken into account here. It's intentional: an ai character cannot flip if it's heavily paralyzed (for example).
float requiredSpeed = CurrentAnimationParams.MovementSpeed / 2;
if (CurrentHull != null)
{
Mirror(CurrentSwimParams != null ? CurrentSwimParams.MirrorLerp : true);
// Enemy movement speeds are halved inside submarines
requiredSpeed /= 2;
}
bool isMovingFastEnough = Math.Abs(MainLimb.LinearVelocity.X) > requiredSpeed;
bool isTryingToMoveHorizontally = Math.Abs(TargetMovement.X) > Math.Abs(TargetMovement.Y);
if ((flipTimer > CurrentFishAnimation.FlipDelay && flipCooldown <= 0.0f && ((isMovingFastEnough && isTryingToMoveHorizontally) || IsMovingBackwards))
|| character.IsRemotePlayer)
{
Flip();
if (!inWater || (CurrentSwimParams != null && CurrentSwimParams.Mirror))
{
Mirror(CurrentSwimParams != null ? CurrentSwimParams.MirrorLerp : true);
}
flipTimer = 0.0f;
flipCooldown = CurrentFishAnimation.FlipCooldown;
}
}
else
{
flipTimer = 0.0f;
flipCooldown = CurrentFishAnimation.FlipCooldown;
}
}
else
ResetState();
void ResetState()
{
flipTimer = 0.0f;
wasAiming = aiming;
aiming = false;
wasAimingMelee = aimingMelee;
aimingMelee = false;
}
wasAiming = aiming;
aiming = false;
wasAimingMelee = aimingMelee;
aimingMelee = false;
}
private bool CanDrag(Character target)
@@ -458,24 +480,34 @@ namespace Barotrauma
t = MathHelper.Clamp((1 + dot) / 10, 0.01f, 0.1f);
}
}
Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, t);
if (Collider.BodyType == BodyType.Dynamic)
{
Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, t);
}
//limbs are disabled when simple physics is enabled, no need to move them
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
{
@@ -688,9 +720,12 @@ namespace Barotrauma
{
movement = MathUtils.SmoothStep(movement, TargetMovement, 0.2f);
Collider.LinearVelocity = new Vector2(
movement.X,
Collider.LinearVelocity.Y > 0.0f ? Collider.LinearVelocity.Y * 0.5f : Collider.LinearVelocity.Y);
if (Collider.BodyType == BodyType.Dynamic)
{
Collider.LinearVelocity = new Vector2(
movement.X,
Collider.LinearVelocity.Y > 0.0f ? Collider.LinearVelocity.Y * 0.5f : Collider.LinearVelocity.Y);
}
//limbs are disabled when simple physics is enabled, no need to move them
if (SimplePhysicsEnabled) { return; }
@@ -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);
}
}
}
@@ -840,14 +841,11 @@ namespace Barotrauma
if (head == null) { return; }
if (torso == null) { return; }
const float DisableMovementAboveSurfaceThreshold = 50.0f;
if (currentHull != null && character.CurrentHull != null)
{
float surfacePos = GetSurfaceY();
float surfaceThreshold = ConvertUnits.ToDisplayUnits(Collider.SimPosition.Y + 1.0f);
surfaceLimiter = Math.Max(1.0f, surfaceThreshold - surfacePos);
if (surfaceLimiter > DisableMovementAboveSurfaceThreshold) { return; }
}
Limb leftHand = GetLimb(LimbType.LeftHand);
@@ -917,6 +915,7 @@ namespace Barotrauma
RotateHead(head);
}
const float DisableMovementAboveSurfaceThreshold = 50.0f;
//dont try to move upwards if head is already out of water
if (surfaceLimiter > 1.0f && TargetMovement.Y > 0.0f)
{
@@ -936,8 +935,8 @@ namespace Barotrauma
//turn head above the water
head.body.ApplyTorque(Dir);
}
movement.Y *= Math.Max(0, 1.0f - ((surfaceLimiter - 1.0f) / DisableMovementAboveSurfaceThreshold));
movement.Y = movement.Y * (1.0f - ((surfaceLimiter - 1.0f) / DisableMovementAboveSurfaceThreshold));
}
bool isNotRemote = true;
@@ -956,7 +955,13 @@ namespace Barotrauma
t = MathHelper.Clamp((1 + dot) / 10, 0.01f, 0.1f);
}
}
Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, t);
Vector2 targetVelocity = movement;
//if we're too high above the surface, don't touch the vertical velocity of the collider unless we're heading down
if (surfaceLimiter > DisableMovementAboveSurfaceThreshold)
{
targetVelocity.Y = Math.Min(Collider.LinearVelocity.Y, movement.Y);
};
Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, targetVelocity, t);
}
WalkPos += movement.Length();
@@ -1130,7 +1135,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 +1230,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) &&
@@ -1524,13 +1529,15 @@ namespace Barotrauma
Limb leftHand = GetLimb(LimbType.LeftHand);
Limb rightHand = GetLimb(LimbType.RightHand);
Limb targetLeftHand = target.AnimController.GetLimb(LimbType.LeftForearm);
if (targetLeftHand == null) { targetLeftHand = target.AnimController.GetLimb(LimbType.Torso); }
if (targetLeftHand == null) { targetLeftHand = target.AnimController.MainLimb; }
Limb targetLeftHand =
target.AnimController.GetLimb(LimbType.LeftForearm) ??
target.AnimController.GetLimb(LimbType.Torso) ??
target.AnimController.MainLimb;
Limb targetRightHand = target.AnimController.GetLimb(LimbType.RightForearm);
if (targetRightHand == null) { targetRightHand = target.AnimController.GetLimb(LimbType.Torso); }
if (targetRightHand == null) { targetRightHand = target.AnimController.MainLimb; }
Limb targetRightHand =
target.AnimController.GetLimb(LimbType.RightForearm) ??
target.AnimController.GetLimb(LimbType.Torso) ??
target.AnimController.MainLimb;
if (!target.AllowInput)
{
@@ -1546,10 +1553,7 @@ namespace Barotrauma
return;
}
Limb targetTorso = target.AnimController.GetLimb(LimbType.Torso);
if (targetTorso == null)
{
targetTorso = target.AnimController.MainLimb;
}
targetTorso ??= target.AnimController.MainLimb;
if (target.AnimController.Dir != Dir)
{
target.AnimController.Flip();
@@ -173,18 +173,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;
@@ -575,6 +575,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;
@@ -680,6 +684,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)
@@ -1189,7 +1197,7 @@ namespace Barotrauma
{
inWater = false;
headInWater = false;
RefreshFloorY(ignoreStairs: Stairs == null);
RefreshFloorY(deltaTime, ignoreStairs: Stairs == null);
}
//ragdoll isn't in any room -> it's in the water
else if (currentHull == null)
@@ -1201,10 +1209,12 @@ namespace Barotrauma
{
headInWater = false;
inWater = false;
RefreshFloorY(ignoreStairs: Stairs == null);
RefreshFloorY(deltaTime, 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();
@@ -1214,13 +1224,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;
}
}
}
}
@@ -1281,21 +1299,32 @@ namespace Barotrauma
limb.Update(deltaTime);
}
if (!inWater && character.AllowInput && levitatingCollider && Collider.LinearVelocity.Y > -ImpactTolerance && onGround)
if (!inWater && character.AllowInput && levitatingCollider)
{
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 (onGround && Collider.LinearVelocity.Y > -ImpactTolerance)
{
if (Stairs != null)
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)
{
Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X,
(targetY < Collider.SimPosition.Y ? Math.Sign(targetY - Collider.SimPosition.Y) : (targetY - Collider.SimPosition.Y)) * 5.0f);
if (Stairs != null)
{
Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X,
(targetY < Collider.SimPosition.Y ? Math.Sign(targetY - Collider.SimPosition.Y) : (targetY - Collider.SimPosition.Y)) * 5.0f);
}
else
{
Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X, (targetY - Collider.SimPosition.Y) * 5.0f);
}
}
else
}
else
{
// Falling -> ragdoll briefly if we are not moving at all, because we are probably stuck.
if (Collider.LinearVelocity == Vector2.Zero)
{
Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X, (targetY - Collider.SimPosition.Y) * 5.0f);
character.IsRagdolled = true;
}
}
}
}
UpdateProjSpecific(deltaTime, cam);
forceNotStanding = false;
@@ -1525,15 +1554,24 @@ namespace Barotrauma
lastFloorCheckPos = Vector2.Zero;
}
private void RefreshFloorY(Limb refLimb = null, bool ignoreStairs = false)
// Force check floor y at least once a second so that we'll drop through gaps that we are standing upon.
private const float FloorYStaleTime = 1;
private float floorYCheckTimer;
private void RefreshFloorY(float deltaTime, Limb refLimb = null, bool ignoreStairs = false)
{
floorYCheckTimer -= deltaTime;
PhysicsBody refBody = refLimb == null ? Collider : refLimb.body;
if (Vector2.DistanceSquared(lastFloorCheckPos, refBody.SimPosition) > 0.1f * 0.1f || lastFloorCheckIgnoreStairs != ignoreStairs || lastFloorCheckIgnorePlatforms != IgnorePlatforms)
if (floorYCheckTimer < 0 ||
lastFloorCheckIgnoreStairs != ignoreStairs ||
lastFloorCheckIgnorePlatforms != IgnorePlatforms ||
Vector2.DistanceSquared(lastFloorCheckPos, refBody.SimPosition) > 0.1f * 0.1f)
{
floorY = GetFloorY(refBody.SimPosition, ignoreStairs);
lastFloorCheckPos = refBody.SimPosition;
lastFloorCheckIgnoreStairs = ignoreStairs;
lastFloorCheckIgnorePlatforms = IgnorePlatforms;
// Add some randomness to prevent all stationary characters doing the checks at the same frame.
floorYCheckTimer = FloorYStaleTime * Rand.Range(0.9f, 1.1f);
}
}
@@ -1616,7 +1654,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)
@@ -1655,22 +1693,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)
@@ -1681,6 +1731,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;
}
@@ -1689,15 +1740,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)
@@ -1803,22 +1855,36 @@ namespace Barotrauma
private bool collisionsDisabled;
private double lastObstacleRayCastTime;
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;
float obstacleCheckDist = 0.3f;
Vector2 diff = Collider.SimPosition - MainLimb.SimPosition;
float distSqrd = diff.LengthSquared();
if (distSqrd > resetDist * resetDist)
bool shouldReset = distSqrd > resetDist * resetDist;
if (!shouldReset && distSqrd > obstacleCheckDist * obstacleCheckDist)
{
if (Timing.TotalTime > lastObstacleRayCastTime + 1 &&
Submarine.PickBody(Collider.SimPosition, MainLimb.SimPosition, collisionCategory: Physics.CollisionWall) != null)
{
shouldReset = true;
lastObstacleRayCastTime = Timing.TotalTime;
}
}
if (shouldReset)
{
//ragdoll way too far, reset position
SetPosition(Collider.SimPosition, lerp: true, forceMainLimbToCollider: true);
}
if (distSqrd > allowedDist * allowedDist)
else if (distSqrd > allowedDist * allowedDist)
{
//ragdoll too far from the collider, disable collisions until it's close enough
//(in case the ragdoll has gotten stuck somewhere)
@@ -1840,7 +1906,7 @@ namespace Barotrauma
collisionsDisabled = false;
//force collision categories to be updated
prevCollisionCategory = Category.None;
}
}
}
partial void UpdateNetPlayerPositionProjSpecific(float deltaTime, float lowestSubPos);
@@ -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)
@@ -421,13 +428,7 @@ namespace Barotrauma
}
break;
case "conditional":
foreach (XAttribute attribute in subElement.Attributes())
{
if (PropertyConditional.IsValid(attribute))
{
Conditionals.Add(new PropertyConditional(attribute));
}
}
Conditionals.AddRange(PropertyConditional.FromXElement(subElement));
break;
}
}
@@ -136,6 +136,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
{
@@ -173,6 +179,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
{
@@ -184,6 +197,13 @@ namespace Barotrauma
}
}
private CharacterTeamType? originalTeamID;
public CharacterTeamType OriginalTeamID
{
get { return originalTeamID ?? teamID; }
}
private Wallet wallet;
public Wallet Wallet
@@ -205,7 +225,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()
{
@@ -220,20 +240,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;
@@ -277,7 +293,7 @@ namespace Barotrauma
{
if (currentTeamChange == removedTeamChange)
{
currentTeamChange = activeTeamChanges[OriginalTeamIdentifier];
currentTeamChange = activeTeamChanges[OriginalChangeTeamIdentifier];
}
}
return activeTeamChanges.Remove(identifier);
@@ -311,7 +327,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;
@@ -360,7 +378,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;
@@ -458,10 +476,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;
}
}
}
@@ -521,8 +544,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);
}
}
}
@@ -728,7 +756,7 @@ namespace Barotrauma
/// </summary>
public bool InPressure
{
get { return CurrentHull == null || CurrentHull.LethalPressure > 5.0f; }
get { return CurrentHull == null || CurrentHull.LethalPressure > 0.0f; }
}
/// <summary>
@@ -752,7 +780,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);
}
}
@@ -836,7 +864,7 @@ namespace Barotrauma
public float Bleeding
{
get { return CharacterHealth.GetAfflictionStrength("bleeding", true); }
get { return CharacterHealth.GetAfflictionStrength(AfflictionPrefab.BleedingType, true); }
}
private bool speechImpedimentSet;
@@ -1041,7 +1069,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;
@@ -1099,6 +1127,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;
@@ -1619,7 +1653,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;
}
@@ -1691,7 +1725,7 @@ namespace Barotrauma
if (wearable.SkillModifiers.TryGetValue(skillIdentifier, out float skillValue))
{
skillLevel += skillValue;
break;
break;
}
}
@@ -1700,9 +1734,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
@@ -1791,20 +1823,8 @@ namespace Barotrauma
public void StackSpeedMultiplier(float val)
{
if (val < 1f)
{
if (val < greatestNegativeSpeedMultiplier)
{
greatestNegativeSpeedMultiplier = val;
}
}
else
{
if (val > greatestPositiveSpeedMultiplier)
{
greatestPositiveSpeedMultiplier = val;
}
}
greatestNegativeSpeedMultiplier = Math.Min(val, greatestNegativeSpeedMultiplier);
greatestPositiveSpeedMultiplier = Math.Max(val, greatestPositiveSpeedMultiplier);
}
public void ResetSpeedMultiplier()
@@ -1827,20 +1847,8 @@ namespace Barotrauma
public void StackHealthMultiplier(float val)
{
if (val < 1f)
{
if (val < greatestNegativeHealthMultiplier)
{
greatestNegativeHealthMultiplier = val;
}
}
else
{
if (val > greatestPositiveHealthMultiplier)
{
greatestPositiveHealthMultiplier = val;
}
}
greatestNegativeHealthMultiplier = Math.Min(val, greatestNegativeHealthMultiplier);
greatestPositiveHealthMultiplier = Math.Max(val, greatestPositiveHealthMultiplier);
}
private void CalculateHealthMultiplier()
@@ -1900,7 +1908,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);
}
@@ -2196,24 +2204,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)
@@ -2695,7 +2686,7 @@ namespace Barotrauma
//character is outside but cursor position inside
if (cursorPosition.Y > Level.Loaded.Size.Y)
{
var sub = Submarine.FindContaining(cursorPosition);
var sub = Submarine.FindContainingInLocalCoordinates(cursorPosition);
if (sub != null) cursorPosition += sub.Position;
}
}
@@ -2872,7 +2863,7 @@ namespace Barotrauma
}
#endif
}
else
else if (!IsClimbing)
{
#if CLIENT
if (Controlled == this)
@@ -2920,9 +2911,9 @@ namespace Barotrauma
CharacterHealth.OpenHealthWindow = null;
#endif
}
else if (IsKeyHit(InputType.Health) && (SelectedItem != null || SelectedSecondaryItem != null))
else if (IsKeyHit(InputType.Health) && SelectedItem != null)
{
SelectedItem = SelectedSecondaryItem = null;
SelectedItem = null;
}
else if (focusedItem != null)
{
@@ -3017,7 +3008,9 @@ namespace Barotrauma
for (int i = 0; i < CharacterList.Count; i++)
{
CharacterList[i].Update(deltaTime, cam);
var character = CharacterList[i];
System.Diagnostics.Debug.Assert(character != null && !character.Removed);
character.Update(deltaTime, cam);
}
}
@@ -3108,8 +3101,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)
{
@@ -3136,15 +3128,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; }
}
}
}
@@ -4064,17 +4058,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)
@@ -4109,13 +4093,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)
{
@@ -4157,13 +4134,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;
}
@@ -4181,7 +4169,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)
{
@@ -4261,7 +4250,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;
}
@@ -4288,7 +4277,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))
@@ -4298,7 +4287,7 @@ namespace Barotrauma
if (statusEffect.type == ActionType.OnDamaged)
{
if (!statusEffect.HasRequiredAfflictions(LastDamage)) { continue; }
if (statusEffect.OnlyPlayerTriggered)
if (statusEffect.OnlyWhenDamagedByPlayer)
{
if (LastAttacker == null || !LastAttacker.IsPlayer)
{
@@ -4356,6 +4345,10 @@ namespace Barotrauma
{
statusEffect.Apply(actionType, deltaTime, this, this);
}
if (statusEffect.HasTargetType(StatusEffect.TargetType.Hull) && CurrentHull != null)
{
statusEffect.Apply(actionType, deltaTime, this, CurrentHull);
}
}
if (actionType != ActionType.OnDamaged && actionType != ActionType.OnSevered)
{
@@ -4494,9 +4487,12 @@ namespace Barotrauma
OnDeath?.Invoke(this, CauseOfDeath);
var abilityCharacterKiller = new AbilityCharacterKiller(CauseOfDeath.Killer);
CheckTalents(AbilityEffectType.OnDieToCharacter, abilityCharacterKiller);
CauseOfDeath.Killer?.RecordKill(this);
if (CauseOfDeath.Type != CauseOfDeathType.Disconnected)
{
var abilityCharacterKiller = new AbilityCharacterKiller(CauseOfDeath.Killer);
CheckTalents(AbilityEffectType.OnDieToCharacter, abilityCharacterKiller);
CauseOfDeath.Killer?.RecordKill(this);
}
if (GameMain.GameSession != null && Screen.Selected == GameMain.GameScreen)
{
@@ -4517,6 +4513,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);
}
}
@@ -4563,6 +4562,11 @@ namespace Barotrauma
SetStun(0.0f, true);
isDead = false;
if (info != null)
{
info.CauseOfDeath = null;
}
foreach (LimbJoint joint in AnimController.LimbJoints)
{
var revoluteJoint = joint.revoluteJoint;
@@ -4586,10 +4590,7 @@ namespace Barotrauma
limb.IsSevered = false;
}
if (GameMain.GameSession != null)
{
GameMain.GameSession.ReviveCharacter(this);
}
GameMain.GameSession?.ReviveCharacter(this);
}
public override void Remove()
@@ -4657,6 +4658,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();
@@ -4707,7 +4709,7 @@ namespace Barotrauma
if (!MathUtils.NearlyEqual(newItem.Condition, newItem.MaxCondition) &&
GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
newItem.CreateStatusEvent();
newItem.CreateStatusEvent(loadingRound: true);
}
#if SERVER
newItem.GetComponent<Terminal>()?.SyncHistory();
@@ -4903,34 +4905,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;
}
}
@@ -4952,13 +4956,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()
@@ -5032,6 +5037,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>
@@ -5058,24 +5106,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)
@@ -5085,27 +5115,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));
@@ -5169,7 +5178,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.
@@ -5345,7 +5353,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()
{
@@ -4,6 +4,7 @@ using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using Barotrauma.IO;
using System.Linq;
using System.Xml.Linq;
@@ -11,6 +12,31 @@ using Barotrauma.Abilities;
namespace Barotrauma
{
[NetworkSerialize]
internal readonly record struct NetJobVariant(Identifier Identifier, byte Variant) : INetSerializableStruct
{
[return: MaybeNull]
public JobVariant ToJobVariant()
{
if (!JobPrefab.Prefabs.TryGet(Identifier, out JobPrefab jobPrefab) || jobPrefab.HiddenJob) { return null; }
return new JobVariant(jobPrefab, Variant);
}
public static NetJobVariant FromJobVariant(JobVariant jobVariant) => new NetJobVariant(jobVariant.Prefab.Identifier, (byte)jobVariant.Variant);
}
[NetworkSerialize(ArrayMaxSize = byte.MaxValue)]
internal readonly record struct NetCharacterInfo(string NewName,
ImmutableArray<Identifier> Tags,
byte HairIndex,
byte BeardIndex,
byte MoustacheIndex,
byte FaceAttachmentIndex,
Color SkinColor,
Color HairColor,
Color FacialHairColor,
ImmutableArray<NetJobVariant> JobVariants) : INetSerializableStruct;
class CharacterInfoPrefab
{
public readonly ImmutableArray<CharacterInfo.HeadPreset> Heads;
@@ -315,6 +341,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 +536,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 +698,6 @@ namespace Barotrauma
{
Name = GetRandomName(randSync);
}
TryLoadNameAndTitle(npcIdentifier);
SetPersonalityTrait();
@@ -824,6 +854,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 +951,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 +1192,7 @@ namespace Barotrauma
partial void LoadAttachmentSprites();
private int CalculateSalary()
public int CalculateSalary()
{
if (Name == null || Job == null) { return 0; }
@@ -1394,6 +1434,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)
@@ -1471,7 +1518,10 @@ namespace Barotrauma
break;
}
}
targetAvailableInNextLevel = !isOutside && GameMain.GameSession?.Campaign?.PendingSubmarineSwitch == null && (isOnConnectedLinkedSub || entitySub == Submarine.MainSub);
targetAvailableInNextLevel =
!isOutside &&
GameMain.GameSession?.Campaign is not { SwitchedSubsThisRound: true } &&
(isOnConnectedLinkedSub || entitySub == Submarine.MainSub);
if (!targetAvailableInNextLevel)
{
if (!order.Prefab.CanBeGeneralized)
@@ -1502,7 +1552,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 +1646,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)
{
@@ -1661,6 +1711,7 @@ namespace Barotrauma
{
targetId = GetOffsetId(parentSub, targetId);
targetEntity = Entity.FindEntityByID(targetId);
return targetEntity != null;
}
else
{
@@ -1674,8 +1725,8 @@ namespace Barotrauma
{
DebugConsole.AddWarning($"Trying to load a previously saved order ({orderIdentifier}). Can't find the parent sub of the target entity. The order doesn't require a target so a more generic version of the order will be loaded instead.");
}
return true;
}
return true;
}
}
return orders;
@@ -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; }
@@ -1,4 +1,5 @@
using Microsoft.Xna.Framework;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -14,11 +15,15 @@ namespace Barotrauma
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; set; }
public float PendingAdditionStrength { get; set; }
public float AdditionStrength { get; set; }
public float PendingGrainEffectStrength { get; set; }
public float GrainEffectStrength { get; set; }
private float fluctuationTimer;
private AfflictionPrefab.Effect activeEffect;
private float prevActiveEffectStrength;
protected bool activeEffectDirty = true;
protected float _strength;
[Serialize(0f, IsPropertySaveable.Yes), Editable]
@@ -42,10 +47,11 @@ namespace Barotrauma
float newValue = MathHelper.Clamp(value, 0.0f, Prefab.MaxStrength);
if (newValue > _strength)
{
PendingAdditionStrength = Prefab.GrainBurst;
PendingGrainEffectStrength = Prefab.GrainBurst;
Duration = Prefab.Duration;
}
_strength = newValue;
activeEffectDirty = true;
}
}
@@ -68,8 +74,7 @@ namespace Barotrauma
public float DamagePerSecondTimer;
public float PreviousVitalityDecrease;
public float StrengthDiminishMultiplier = 1.0f;
public Affliction MultiplierSource;
public (float Value, Affliction Source) StrengthDiminishMultiplier = (1.0f, null);
public readonly Dictionary<AfflictionPrefab.PeriodicEffect, float> PeriodicEffectTimers = new Dictionary<AfflictionPrefab.PeriodicEffect, float>();
@@ -95,7 +100,7 @@ namespace Barotrauma
prefab?.ReloadSoundsIfNeeded();
#endif
Prefab = prefab;
PendingAdditionStrength = Prefab.GrainBurst;
PendingGrainEffectStrength = Prefab.GrainBurst;
_strength = strength;
Identifier = prefab.Identifier;
@@ -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,14 +172,14 @@ 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; }
float currVitalityDecrease = MathHelper.Lerp(
currentEffect.MinVitalityDecrease,
currentEffect.MaxVitalityDecrease,
(strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
currentEffect.GetStrengthFactor(this));
if (currentEffect.MultiplyByMaxVitality)
{
@@ -186,11 +200,11 @@ namespace Barotrauma
float amount = MathHelper.Lerp(
currentEffect.MinGrainStrength,
currentEffect.MaxGrainStrength,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)) * GetScreenEffectFluctuation(currentEffect);
currentEffect.GetStrengthFactor(this)) * GetScreenEffectFluctuation(currentEffect);
if (Prefab.GrainBurst > 0 && AdditionStrength > amount)
if (Prefab.GrainBurst > 0 && GrainEffectStrength > amount)
{
return Math.Min(AdditionStrength, 1.0f);
return Math.Min(GrainEffectStrength, 1.0f);
}
return amount;
@@ -206,7 +220,7 @@ namespace Barotrauma
return MathHelper.Lerp(
currentEffect.MinScreenDistort,
currentEffect.MaxScreenDistort,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)) * GetScreenEffectFluctuation(currentEffect);
currentEffect.GetStrengthFactor(this)) * GetScreenEffectFluctuation(currentEffect);
}
public float GetRadialDistortStrength()
@@ -219,7 +233,7 @@ namespace Barotrauma
return MathHelper.Lerp(
currentEffect.MinRadialDistort,
currentEffect.MaxRadialDistort,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)) * GetScreenEffectFluctuation(currentEffect);
currentEffect.GetStrengthFactor(this)) * GetScreenEffectFluctuation(currentEffect);
}
public float GetChromaticAberrationStrength()
@@ -232,7 +246,7 @@ namespace Barotrauma
return MathHelper.Lerp(
currentEffect.MinChromaticAberration,
currentEffect.MaxChromaticAberration,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)) * GetScreenEffectFluctuation(currentEffect);
currentEffect.GetStrengthFactor(this)) * GetScreenEffectFluctuation(currentEffect);
}
public float GetAfflictionOverlayMultiplier()
@@ -247,7 +261,7 @@ namespace Barotrauma
return MathHelper.Lerp(
currentEffect.MinAfflictionOverlayAlphaMultiplier,
currentEffect.MaxAfflictionOverlayAlphaMultiplier,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
currentEffect.GetStrengthFactor(this));
}
public Color GetFaceTint()
@@ -259,7 +273,7 @@ namespace Barotrauma
return Color.Lerp(
currentEffect.MinFaceTint,
currentEffect.MaxFaceTint,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
currentEffect.GetStrengthFactor(this));
}
public Color GetBodyTint()
@@ -271,7 +285,7 @@ namespace Barotrauma
return Color.Lerp(
currentEffect.MinBodyTint,
currentEffect.MaxBodyTint,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
currentEffect.GetStrengthFactor(this));
}
public float GetScreenBlurStrength()
@@ -284,7 +298,7 @@ namespace Barotrauma
return MathHelper.Lerp(
currentEffect.MinScreenBlur,
currentEffect.MaxScreenBlur,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)) * GetScreenEffectFluctuation(currentEffect);
currentEffect.GetStrengthFactor(this)) * GetScreenEffectFluctuation(currentEffect);
}
private float GetScreenEffectFluctuation(AfflictionPrefab.Effect currentEffect)
@@ -302,7 +316,7 @@ namespace Barotrauma
float amount = MathHelper.Lerp(
currentEffect.MinSkillMultiplier,
currentEffect.MaxSkillMultiplier,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
currentEffect.GetStrengthFactor(this));
return amount;
}
@@ -333,7 +347,7 @@ namespace Barotrauma
return MathHelper.Lerp(
currentEffect.MinResistance,
currentEffect.MaxResistance,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
currentEffect.GetStrengthFactor(this));
}
public float GetSpeedMultiplier()
@@ -344,26 +358,21 @@ namespace Barotrauma
return MathHelper.Lerp(
currentEffect.MinSpeedMultiplier,
currentEffect.MaxSpeedMultiplier,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
currentEffect.GetStrengthFactor(this));
}
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))
{
return MathHelper.Lerp(
value.minValue,
value.maxValue,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
}
return 0.0f;
if (!currentEffect.AfflictionStatValues.TryGetValue(statType, out var appliedStat)) { return 0.0f; }
return MathHelper.Lerp(appliedStat.MinValue, appliedStat.MaxValue, currentEffect.GetStrengthFactor(this));
}
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);
}
@@ -401,13 +410,16 @@ namespace Barotrauma
fluctuationTimer += deltaTime * currentEffect.ScreenEffectFluctuationFrequency;
fluctuationTimer %= 1.0f;
if (currentEffect.StrengthChange < 0) // Reduce diminishing of buffs if boosted
if (currentEffect.StrengthChange < 0) // Only apply StrengthDiminish.Multiplier if affliction is being weakened
{
float durationMultiplier = 1 / (1 + (Prefab.IsBuff ? characterHealth.Character.GetStatValue(StatTypes.BuffDurationMultiplier)
: characterHealth.Character.GetStatValue(StatTypes.DebuffDurationMultiplier)));
float stat = characterHealth.Character.GetStatValue(
Prefab.IsBuff
? StatTypes.BuffDurationMultiplier
: StatTypes.DebuffDurationMultiplier);
_strength += currentEffect.StrengthChange * deltaTime * StrengthDiminishMultiplier * durationMultiplier;
float durationMultiplier = 1f / (1f + stat);
_strength += currentEffect.StrengthChange * deltaTime * StrengthDiminishMultiplier.Value * durationMultiplier;
}
else if (currentEffect.StrengthChange > 0) // Reduce strengthening of afflictions if resistant
{
@@ -415,6 +427,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)
{
@@ -426,14 +439,14 @@ namespace Barotrauma
{
amount /= Prefab.GrainBurst;
}
if (PendingAdditionStrength >= 0)
if (PendingGrainEffectStrength >= 0)
{
AdditionStrength += amount;
PendingAdditionStrength -= deltaTime;
GrainEffectStrength += amount;
PendingGrainEffectStrength -= deltaTime;
}
else if (AdditionStrength > 0)
else if (GrainEffectStrength > 0)
{
AdditionStrength -= amount;
GrainEffectStrength -= amount;
}
}
@@ -442,7 +455,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);
}
}
}
@@ -481,6 +497,7 @@ namespace Barotrauma
{
_nonClampedStrength = strength;
_strength = _nonClampedStrength;
activeEffectDirty |= !MathUtils.NearlyEqual(_strength, prevActiveEffectStrength);
}
public bool ShouldShowIcon(Character afflictedCharacter)
@@ -1,5 +1,8 @@
namespace Barotrauma
{
/// <summary>
/// A special affliction type that increases the character's Bloodloss affliction with a rate relative to the strength of the bleeding.
/// </summary>
class AfflictionBleeding : Affliction
{
public AfflictionBleeding(AfflictionPrefab prefab, float strength) :
@@ -7,6 +7,10 @@ using Microsoft.Xna.Framework;
namespace Barotrauma
{
/// <summary>
/// A special affliction type that gradually makes the character turn into another type of character.
/// See <see cref="AfflictionPrefabHusk"/> for more details.
/// </summary>
partial class AfflictionHusk : Affliction
{
public enum InfectionState
@@ -22,7 +26,7 @@ namespace Barotrauma
private Character character;
private bool stun = true;
private bool stun = false;
private readonly List<Affliction> huskInfection = new List<Affliction>();
@@ -43,6 +47,7 @@ namespace Barotrauma
DeactivateHusk();
highestStrength = 0;
}
activeEffectDirty = true;
}
}
private float highestStrength;
@@ -62,6 +67,7 @@ namespace Barotrauma
private float DormantThreshold => HuskPrefab.DormantThreshold;
private float ActiveThreshold => HuskPrefab.ActiveThreshold;
private float TransitionThreshold => HuskPrefab.TransitionThreshold;
private float TransformThresholdOnDeath => HuskPrefab.TransformThresholdOnDeath;
public AfflictionHusk(AfflictionPrefab prefab, float strength) : base(prefab, strength)
@@ -216,7 +222,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);
}
@@ -6,6 +6,7 @@ using System.Reflection;
using System.Xml.Linq;
using Barotrauma.Extensions;
using System.Collections.Immutable;
using Barotrauma.Items.Components;
namespace Barotrauma
{
@@ -56,8 +57,77 @@ namespace Barotrauma
public override void Dispose() { }
}
/// <summary>
/// AfflictionPrefabHusk is a special type of affliction that has added functionality for husk infection.
/// </summary>
class AfflictionPrefabHusk : AfflictionPrefab
{
// Use any of these to define which limb the appendage is attached to.
// If multiple are defined, the order of preference is: id, name, type.
public readonly int AttachLimbId;
public readonly string AttachLimbName;
public readonly LimbType AttachLimbType;
/// <summary>
/// The minimum strength at which husk infection will be in the dormant stage.
/// It must be less than or equal to ActiveThreshold.
/// </summary>
public readonly float DormantThreshold;
/// <summary>
/// The minimum strength at which husk infection will be in the active stage.
/// It must be greater than or equal to DormantThreshold and less than or equal to TransitionThreshold.
/// </summary>
public readonly float ActiveThreshold;
/// <summary>
/// The minimum strength at which husk infection will be in its final stage.
/// It must be greater than or equal to ActiveThreshold.
/// </summary>
public readonly float TransitionThreshold;
/// <summary>
/// The minimum strength the affliction must have for the affected character
/// to transform into a husk upon death.
/// </summary>
public readonly float TransformThresholdOnDeath;
/// <summary>
/// The species of husk to convert the affected character to
/// once husk infection reaches its final stage.
/// </summary>
public readonly Identifier HuskedSpeciesName;
/// <summary>
/// If set to true, all buffs are transferred to the converted
/// character after husk transformation is complete.
/// </summary>
public readonly bool TransferBuffs;
/// <summary>
/// If set to true, the affected player will see on-screen messages describing husk infection symptoms
/// and affected bots will speak about their current husk infection stage.
/// </summary>
public readonly bool SendMessages;
/// <summary>
/// If set to true, affected characters will have their speech impeded once the affliction
/// reaches the dormant stage.
/// </summary>
public readonly bool CauseSpeechImpediment;
/// <summary>
/// If not set to true, affected characters will no longer require air
/// once the affliction reaches the active stage.
/// </summary>
public readonly bool NeedsAir;
/// <summary>
/// If set to true, affected players will retain control of their character
/// after transforming into a husk.
/// </summary>
public readonly bool ControlHusk;
public AfflictionPrefabHusk(ContentXElement element, AfflictionsFile file, Type type = null) : base(element, file, type)
{
HuskedSpeciesName = element.GetAttributeIdentifier("huskedspeciesname", Identifier.Empty);
@@ -68,7 +138,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);
@@ -79,7 +148,7 @@ namespace Barotrauma
{
AttachLimbId = attachElement.GetAttributeInt("id", -1);
AttachLimbName = attachElement.GetAttributeString("name", null);
AttachLimbType = Enum.TryParse(attachElement.GetAttributeString("type", "none"), true, out LimbType limbType) ? limbType : LimbType.None;
AttachLimbType = attachElement.GetAttributeEnum("type", LimbType.None);
}
else
{
@@ -97,175 +166,282 @@ namespace Barotrauma
DormantThreshold = element.GetAttributeFloat("dormantthreshold", MaxStrength * 0.5f);
ActiveThreshold = element.GetAttributeFloat("activethreshold", MaxStrength * 0.75f);
TransitionThreshold = element.GetAttributeFloat("transitionthreshold", MaxStrength);
if (DormantThreshold > ActiveThreshold)
{
DebugConsole.ThrowError($"Error in \"{Identifier}\": {nameof(DormantThreshold)} is greater than {nameof(ActiveThreshold)} ({DormantThreshold} > {ActiveThreshold})");
}
if (ActiveThreshold > TransitionThreshold)
{
DebugConsole.ThrowError($"Error in \"{Identifier}\": {nameof(ActiveThreshold)} is greater than {nameof(TransitionThreshold)} ({ActiveThreshold} > {TransitionThreshold})");
}
TransformThresholdOnDeath = element.GetAttributeFloat("transformthresholdondeath", ActiveThreshold);
}
// Use any of these to define which limb the appendage is attached to.
// If multiple are defined, the order of preference is: id, name, type.
public readonly int AttachLimbId;
public readonly string AttachLimbName;
public readonly LimbType AttachLimbType;
public float ActiveThreshold, DormantThreshold, TransitionThreshold;
public float TransformThresholdOnDeath;
public readonly Identifier HuskedSpeciesName;
public readonly Identifier[] TargetSpecies;
public readonly bool TransferBuffs;
public readonly bool SendMessages;
public readonly bool CauseSpeechImpediment;
public readonly bool NeedsAir;
public readonly bool ControlHusk;
}
/// <summary>
/// AfflictionPrefab is a prefab that defines a type of affliction that can be applied to a character.
/// There are multiple sub-types of afflictions such as AfflictionPrefabHusk, AfflictionPsychosis and AfflictionBleeding that can be used for additional functionality.
///
/// When defining a new affliction, the type will be determined by the element name.
/// </summary>
/// <example>
/// <code language="xml">
/// <Afflictions>
/// <!-- Defines a regular affliction. -->
/// <Affliction identifier="mycoolaffliction1" />
///
/// <!-- Defines an AfflictionPrefabHusk affliction. -->
/// <AfflictionPrefabHusk identifier="mycoolaffliction2"/>
///
/// <!-- Defines an AfflictionBleeding affliction. -->
/// <AfflictionBleeding identifier="mycoolaffliction3"/>
/// </Afflictions>
/// </code>
/// </example>
class AfflictionPrefab : PrefabWithUintIdentifier
{
public class Effect
/// <summary>
/// Effects are the primary way to add functionality to afflictions.
/// </summary>
/// <doc>
/// <Ignore type="SubElement" identifier="AbilityFlag" />
/// <SubElement identifier="abilityflag" type="AppliedAbilityFlag">
/// Enables the specified flag on the character as long as the effect is active.
/// </SubElement>
/// <Type identifier="AppliedAbilityFlag">
/// <Summary>
/// Flag that will be enabled for the character as long as the effect is active.
/// <example>
/// <code language="xml">
/// <Effect minstrength="0" maxstrength="100">
/// <!-- Grants pressure immunity to the character while the effect is active. -->
/// <AbilityFlag flagtype="ImmuneToPressure" />
/// </Effect>
/// </code>
/// </example>
/// </Summary>
/// <Field identifier="FlagType" type="AbilityFlags" defaultValue="None">
/// Which ability flag to enable.
/// </Field>
/// </Type>
/// </doc>
public sealed class Effect
{
//this effect is applied when the strength is within this range
[Serialize(0.0f, IsPropertySaveable.No)]
[Serialize(0.0f, IsPropertySaveable.No, description: "Minimum affliction strength required for this effect to be active.")]
public float MinStrength { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No)]
[Serialize(0.0f, IsPropertySaveable.No, description: "Maximum affliction strength for which this effect will be active.")]
public float MaxStrength { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No)]
[Serialize(0.0f, IsPropertySaveable.No, description: "The amount of vitality that is lost at this effect's lowest strength.")]
public float MinVitalityDecrease { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No)]
[Serialize(0.0f, IsPropertySaveable.No, description: "The amount of vitality that is lost at this effect's highest strength.")]
public float MaxVitalityDecrease { get; private set; }
//how much the strength of the affliction changes per second
[Serialize(0.0f, IsPropertySaveable.No)]
[Serialize(0.0f, IsPropertySaveable.No, description: "How much the affliction's strength changes every second while this effect is active.")]
public float StrengthChange { get; private set; }
[Serialize(false, IsPropertySaveable.No)]
[Serialize(false, IsPropertySaveable.No, description:
"If set to true, MinVitalityDecrease and MaxVitalityDecrease represent a fraction of the affected character's maximum " +
"vilatily, with 1 meaning 100%, instead of the same amount for all species.")]
public bool MultiplyByMaxVitality { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No)]
[Serialize(0.0f, IsPropertySaveable.No, description: "Blur effect strength at this effect's lowest strength.")]
public float MinScreenBlur { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No)]
[Serialize(0.0f, IsPropertySaveable.No, description: "Blur effect strength at this effect's highest strength.")]
public float MaxScreenBlur { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No)]
[Serialize(0.0f, IsPropertySaveable.No, description: "Generic distortion effect strength at this effect's lowest strength.")]
public float MinScreenDistort { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No)]
[Serialize(0.0f, IsPropertySaveable.No, description: "Generic distortion effect strength at this effect's highest strength.")]
public float MaxScreenDistort { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No)]
[Serialize(0.0f, IsPropertySaveable.No, description: "Radial distortion effect strength at this effect's lowest strength.")]
public float MinRadialDistort { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No)]
[Serialize(0.0f, IsPropertySaveable.No, description: "Radial distortion effect strength at this effect's highest strength.")]
public float MaxRadialDistort { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No)]
[Serialize(0.0f, IsPropertySaveable.No, description: "Chromatic aberration effect strength at this effect's lowest strength.")]
public float MinChromaticAberration { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No)]
[Serialize(0.0f, IsPropertySaveable.No, description: "Chromatic aberration effect strength at this effect's highest strength.")]
public float MaxChromaticAberration { get; private set; }
[Serialize("255,255,255,255", IsPropertySaveable.No)]
[Serialize("255,255,255,255", IsPropertySaveable.No, description: "Radiation grain effect color.")]
public Color GrainColor { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No)]
[Serialize(0.0f, IsPropertySaveable.No, description: "Radiation grain effect strength at this effect's lowest strength.")]
public float MinGrainStrength { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No)]
[Serialize(0.0f, IsPropertySaveable.No, description: "Radiation grain effect strength at this effect's highest strength.")]
public float MaxGrainStrength { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No)]
[Serialize(0.0f, IsPropertySaveable.No, description:
"The maximum rate of fluctuation to apply to visual effects caused by this affliction effect. " +
"Effective fluctuation is proportional to the affliction's current strength.")]
public float ScreenEffectFluctuationFrequency { get; private set; }
[Serialize(1.0f, IsPropertySaveable.No)]
[Serialize(1.0f, IsPropertySaveable.No, description:
"Multiplier for the affliction overlay's opacity at this effect's lowest strength. " +
"See the list of elements for more details.")]
public float MinAfflictionOverlayAlphaMultiplier { get; private set; }
[Serialize(1.0f, IsPropertySaveable.No)]
[Serialize(1.0f, IsPropertySaveable.No, description:
"Multiplier for the affliction overlay's opacity at this effect's highest strength. " +
"See the list of elements for more details.")]
public float MaxAfflictionOverlayAlphaMultiplier { get; private set; }
[Serialize(1.0f, IsPropertySaveable.No)]
[Serialize(1.0f, IsPropertySaveable.No, description:
"Multiplier for every buff's decay rate at this effect's lowest strength. " +
"Only applies to afflictions of class BuffDurationIncrease.")]
public float MinBuffMultiplier { get; private set; }
[Serialize(1.0f, IsPropertySaveable.No)]
[Serialize(1.0f, IsPropertySaveable.No, description:
"Multiplier for every buff's decay rate at this effect's highest strength. " +
"Only applies to afflictions of class BuffDurationIncrease.")]
public float MaxBuffMultiplier { get; private set; }
[Serialize(1.0f, IsPropertySaveable.No)]
[Serialize(1.0f, IsPropertySaveable.No, description: "Multiplier to apply to the affected character's speed at this effect's lowest strength.")]
public float MinSpeedMultiplier { get; private set; }
[Serialize(1.0f, IsPropertySaveable.No)]
[Serialize(1.0f, IsPropertySaveable.No, description: "Multiplier to apply to the affected character's speed at this effect's highest strength.")]
public float MaxSpeedMultiplier { get; private set; }
[Serialize(1.0f, IsPropertySaveable.No)]
[Serialize(1.0f, IsPropertySaveable.No, description: "Multiplier to apply to all of the affected character's skill levels at this effect's lowest strength.")]
public float MinSkillMultiplier { get; private set; }
[Serialize(1.0f, IsPropertySaveable.No)]
[Serialize(1.0f, IsPropertySaveable.No, description: "Multiplier to apply to all of the affected character's skill levels at this effect's highest strength.")]
public float MaxSkillMultiplier { get; private set; }
private readonly Identifier[] resistanceFor;
public IReadOnlyList<Identifier> ResistanceFor => resistanceFor;
/// <summary>
/// A list of identifiers of afflictions that the affected character will be
/// resistant to when this effect is active.
/// </summary>
public readonly ImmutableArray<Identifier> ResistanceFor;
[Serialize(0.0f, IsPropertySaveable.No)]
[Serialize(0.0f, IsPropertySaveable.No,
description: "The amount of resistance to the afflictions specified by ResistanceFor to apply at this effect's lowest strength.")]
public float MinResistance { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No)]
[Serialize(0.0f, IsPropertySaveable.No,
description: "The amount of resistance to the afflictions specified by ResistanceFor to apply at this effect's highest strength.")]
public float MaxResistance { get; private set; }
[Serialize("", IsPropertySaveable.No)]
[Serialize("", IsPropertySaveable.No, description: "Identifier used by AI to determine conversation lines to say when this effect is active.")]
public Identifier DialogFlag { get; private set; }
[Serialize("", IsPropertySaveable.No)]
[Serialize("", IsPropertySaveable.No, description: "Tag that enemy AI may use to target the affected character when this effect is active.")]
public Identifier Tag { get; private set; }
[Serialize("0,0,0,0", IsPropertySaveable.No)]
[Serialize("0,0,0,0", IsPropertySaveable.No,
description: "Color to tint the affected character's face with at this effect's lowest strength. The alpha channel is used to determine how much to tint the character's face.")]
public Color MinFaceTint { get; private set; }
[Serialize("0,0,0,0", IsPropertySaveable.No)]
[Serialize("0,0,0,0", IsPropertySaveable.No,
description: "Color to tint the affected character's face with at this effect's highest strength. The alpha channel is used to determine how much to tint the character's face.")]
public Color MaxFaceTint { get; private set; }
[Serialize("0,0,0,0", IsPropertySaveable.No)]
[Serialize("0,0,0,0", IsPropertySaveable.No,
description: "Color to tint the affected character's entire body with at this effect's lowest strength. The alpha channel is used to determine how much to tint the character.")]
public Color MinBodyTint { get; private set; }
[Serialize("0,0,0,0", IsPropertySaveable.No)]
[Serialize("0,0,0,0", IsPropertySaveable.No,
description: "Color to tint the affected character's entire body with at this effect's highest strength. The alpha channel is used to determine how much to tint the character.")]
public Color MaxBodyTint { get; private set; }
/// <summary>
/// Prevents AfflictionHusks with the specified identifier(s) from transforming the character into an AI-controlled character
/// StatType that will be applied to the affected character when the effect is active that is proportional to the effect's strength.
/// </summary>
public Identifier[] BlockTransformation { get; private set; }
/// <example>
/// <code language="xml">
/// <Effect minstrength="0" maxstrength="100">
/// <!-- Walking speed will be increased by 10% at strength 0, 20% at 50 and 30% at 100 -->
/// <StatValue stattype="WalkingSpeed" minvalue="0.1" maxvalue="0.3" />
/// <!-- Maximum health will be increased by 20% regardless of the effect strength -->
/// <StatValue stattype="MaximumHealthMultiplier" value="0.2" />
/// </Effect>
/// </code>
/// </example>
public readonly struct AppliedStatValue
{
/// <summary>
/// Which StatType to apply
/// </summary>
public readonly StatTypes StatType;
public readonly Dictionary<StatTypes, (float minValue, float maxValue)> AfflictionStatValues = new Dictionary<StatTypes, (float minValue, float maxValue)>();
public AbilityFlags AfflictionAbilityFlags;
/// <summary>
/// Minimum value to apply
/// </summary>
public readonly float MinValue;
/// <summary>
/// Minimum value to apply
/// </summary>
public readonly float MaxValue;
/// <summary>
/// Constant value to apply, will be ignored if MinValue or MaxValue are set
/// </summary>
private readonly float Value;
public AppliedStatValue(ContentXElement element)
{
Value = element.GetAttributeFloat("value", 0.0f);
StatType = element.GetAttributeEnum("stattype", StatTypes.None);
MinValue = element.GetAttributeFloat("minvalue", Value);
MaxValue = element.GetAttributeFloat("maxvalue", Value);
}
}
/// <summary>
/// Prevents AfflictionHusks with the specified identifier(s) from transforming the character into an AI-controlled character.
/// </summary>
public readonly ImmutableArray<Identifier> BlockTransformation;
/// <summary>
/// StatType that will be applied to the affected character when the effect is active that is proportional to the effect's strength.
/// </summary>
public readonly ImmutableDictionary<StatTypes, AppliedStatValue> AfflictionStatValues;
public readonly AbilityFlags AfflictionAbilityFlags;
//statuseffects applied on the character when the affliction is active
public readonly List<StatusEffect> StatusEffects = new List<StatusEffect>();
public readonly ImmutableArray<StatusEffect> StatusEffects;
public Effect(ContentXElement element, string parentDebugName)
{
SerializableProperty.DeserializeProperties(this, element);
resistanceFor = element.GetAttributeIdentifierArray("resistancefor", Array.Empty<Identifier>());
BlockTransformation = element.GetAttributeIdentifierArray("blocktransformation", Array.Empty<Identifier>());
ResistanceFor = element.GetAttributeIdentifierArray("resistancefor", Array.Empty<Identifier>())!.ToImmutableArray();
BlockTransformation = element.GetAttributeIdentifierArray("blocktransformation", Array.Empty<Identifier>())!.ToImmutableArray();
var afflictionStatValues = new Dictionary<StatTypes, AppliedStatValue>();
var statusEffects = new List<StatusEffect>();
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "statuseffect":
StatusEffects.Add(StatusEffect.Load(subElement, parentDebugName));
statusEffects.Add(StatusEffect.Load(subElement, parentDebugName));
break;
case "statvalue":
var statType = CharacterAbilityGroup.ParseStatType(subElement.GetAttributeString("stattype", ""), parentDebugName);
float defaultValue = subElement.GetAttributeFloat("value", 0f);
float minValue = subElement.GetAttributeFloat("minvalue", defaultValue);
float maxValue = subElement.GetAttributeFloat("maxvalue", defaultValue);
AfflictionStatValues.TryAdd(statType, (minValue, maxValue));
var newStatValue = new AppliedStatValue(subElement);
afflictionStatValues.Add(newStatValue.StatType, newStatValue);
break;
case "abilityflag":
var flagType = CharacterAbilityGroup.ParseFlagType(subElement.GetAttributeString("flagtype", ""), parentDebugName);
AbilityFlags flagType = subElement.GetAttributeEnum("flagtype", AbilityFlags.None);
if (flagType is AbilityFlags.None)
{
DebugConsole.ThrowError($"Error in affliction \"{parentDebugName}\" - invalid ability flag type \"{subElement.GetAttributeString("flagtype", "")}\".");
continue;
}
AfflictionAbilityFlags |= flagType;
break;
case "affliction":
@@ -273,21 +449,71 @@ namespace Barotrauma
break;
}
}
AfflictionStatValues = afflictionStatValues.ToImmutableDictionary();
StatusEffects = statusEffects.ToImmutableArray();
}
/// <summary>
/// Returns 0 if affliction.Strength is MinStrength,
/// 1 if affliction.Strength is MaxStrength
/// </summary>
public float GetStrengthFactor(Affliction affliction)
=> MathUtils.InverseLerp(
MinStrength,
MaxStrength,
affliction.Strength);
}
public class Description
/// <summary>
/// Description element can be used to define descriptions for the affliction that are shown at specific conditions.
/// For example a description that only shows to other players or only at certain strength levels.
/// </summary>
/// <doc>
/// <Field identifier="Text" type="string" defaultValue="&quot;&quot;">
/// Raw text for the description.
/// </Field>
/// </doc>
public sealed class Description
{
public enum TargetType
{
/// <summary>
/// Everyone can see the description.
/// </summary>
Any,
/// <summary>
/// Only the affected character can see the description.
/// </summary>
Self,
/// <summary>
/// The affected character cannot see the description but others can.
/// </summary>
OtherCharacter
}
/// <summary>
/// Raw text for the description.
/// </summary>
public readonly LocalizedString Text;
/// <summary>
/// Text tag used to set the text from the localization files.
/// </summary>
public readonly Identifier TextTag;
public readonly float MinStrength, MaxStrength;
/// <summary>
/// Minimum strength required for the description to be shown.
/// </summary>
public readonly float MinStrength;
/// <summary>
/// Maximum strength required for the description to be shown.
/// </summary>
public readonly float MaxStrength;
/// <summary>
/// Who can see the description.
/// </summary>
public readonly TargetType Target;
public Description(ContentXElement element, AfflictionPrefab affliction)
@@ -317,7 +543,23 @@ namespace Barotrauma
}
}
public class PeriodicEffect
/// <summary>
/// PeriodicEffect applies StatusEffects to the character periodically.
/// </summary>
/// <doc>
/// <SubElement identifier="StatusEffect" type="StatusEffect" />
/// <Field identifier="Interval" type="float" defaultValue="1.0">
/// How often the status effect is applied in seconds.
/// Setting this attribute will set both the min and max interval to the specified value.
/// </Field>
/// <Field identifier="MinInterval" type="float" defaultValue="1.0">
/// Minimum interval between applying the status effect in seconds.
/// </Field>
/// <Field identifier="MaxInterval" type="float" defaultValue="1.0">
/// Maximum interval between applying the status effect in seconds.
/// </Field>
/// </doc>
public sealed class PeriodicEffect
{
public readonly List<StatusEffect> StatusEffects = new List<StatusEffect>();
public readonly float MinInterval, MaxInterval;
@@ -344,65 +586,151 @@ 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() { }
public static IEnumerable<AfflictionPrefab> List => Prefabs;
// Arbitrary string that is used to identify the type of the affliction.
public readonly Identifier AfflictionType;
public override void Dispose() { }
private readonly ContentXElement configElement;
//Does the affliction affect a specific limb or the whole character
public readonly bool LimbSpecific;
//If not a limb-specific affliction, which limb is the indicator shown on in the health menu
//(e.g. mental health problems on head, lack of oxygen on torso...)
public readonly LimbType IndicatorLimb;
public readonly LocalizedString Name;
public readonly Identifier TranslationIdentifier;
public readonly bool IsBuff;
public readonly bool AffectMachines;
public readonly bool HealableInMedicalClinic;
public readonly float HealCostMultiplier;
public readonly int BaseHealCost;
public readonly bool ShowBarInHealthMenu;
public readonly LocalizedString CauseOfDeathDescription, SelfCauseOfDeathDescription;
private readonly LocalizedString defaultDescription;
public readonly ImmutableList<Description> Descriptions;
/// <summary>
/// Arbitrary string that is used to identify the type of the affliction.
/// </summary>
public readonly Identifier AfflictionType;
/// <summary>
/// If set to true, the affliction affects individual limbs. Otherwise, it affects the whole character.
/// </summary>
public readonly bool LimbSpecific;
/// <summary>
/// If the affliction doesn't affect individual limbs, this attribute determines
/// where the game will render the affliction's indicator when viewed in the
/// in-game health UI.
///
/// For example, the psychosis indicator is rendered on the head, and low oxygen
/// is rendered on the torso.
/// </summary>
public readonly LimbType IndicatorLimb;
/// <summary>
/// Can be set to the identifier of another affliction to make this affliction
/// reuse the same name and description.
/// </summary>
public readonly Identifier TranslationIdentifier;
/// <summary>
/// If set to true, the game will recognize this affliction as a buff.
/// This means, among other things, that bots won't attempt to treat it,
/// and the health UI will render the affected limb in green rather than red.
/// </summary>
public readonly bool IsBuff;
/// <summary>
/// If set to true, this affliction can affect characters that are marked as
/// machines, such as the Fractal Guardian.
/// </summary>
public readonly bool AffectMachines;
/// <summary>
/// If set to true, this affliction can be healed at the medical clinic.
/// </summary>
/// <doc>
/// <override type="DefaultValue">
/// false if the affliction is a buff or has the type "geneticmaterialbuff" or "geneticmaterialdebuff", true otherwise.
/// </override>
/// </doc>
public readonly bool HealableInMedicalClinic;
/// <summary>
/// How much each unit of this affliction's strength will add
/// to the cost of healing at the medical clinic.
/// </summary>
public readonly float HealCostMultiplier;
/// <summary>
/// The minimum cost of healing this affliction at the medical clinic.
/// </summary>
public readonly int BaseHealCost;
/// <summary>
/// If set to false, the health UI will not show the strength of the affliction
/// as a bar under its indicator.
/// </summary>
public readonly bool ShowBarInHealthMenu;
/// <summary>
/// If set to true, this affliction's icon will be hidden from the HUD
/// after 5 seconds.
/// </summary>
public readonly bool HideIconAfterDelay;
//how high the strength has to be for the affliction to take affect
/// <summary>
/// How high the strength has to be for the affliction to take effect
/// </summary>
public readonly float ActivationThreshold = 0.0f;
//how high the strength has to be for the affliction icon to be shown in the UI
/// <summary>
/// How high the strength has to be for the affliction icon to be shown in the UI
/// </summary>
public readonly float ShowIconThreshold = 0.05f;
//how high the strength has to be for the affliction icon to be shown to others with a health scanner or via the health interface
/// <summary>
/// How high the strength has to be for the affliction icon to be shown to others with a health scanner or via the health interface
/// </summary>
public readonly float ShowIconToOthersThreshold = 0.05f;
/// <summary>
/// The maximum strength this affliction can have.
/// </summary>
public readonly float MaxStrength = 100.0f;
/// <summary>
/// The strength of the radiation grain effect to apply
/// when the strength of this affliction increases.
/// </summary>
public readonly float GrainBurst;
//how high the strength has to be for the affliction icon to be shown with a health scanner
/// <summary>
/// How high the strength has to be for the affliction icon to be shown with a health scanner
/// </summary>
public readonly float ShowInHealthScannerThreshold = 0.05f;
//how strong the affliction needs to be before bots attempt to treat it
/// <summary>
/// How strong the affliction needs to be before bots attempt to treat it
/// </summary>
public readonly float TreatmentThreshold = 5.0f;
/// <summary>
@@ -411,25 +739,57 @@ namespace Barotrauma
public ImmutableHashSet<Identifier> IgnoreTreatmentIfAfflictedBy;
/// <summary>
/// The affliction is automatically removed after this time. 0 = unlimited
/// The duration of the affliction, in seconds. If set to 0, the affliction does not expire.
/// </summary>
public readonly float Duration;
//how much karma changes when a player applies this affliction to someone (per strength of the affliction)
/// <summary>
/// How much karma changes when a player applies this affliction to someone (per strength of the affliction)
/// </summary>
public float KarmaChangeOnApplied;
/// <summary>
/// Opacity of the burn effect (darker tint) on limbs affected by this affliction. 1 = full strength.
/// </summary>
public readonly float BurnOverlayAlpha;
/// <summary>
/// Opacity of the bloody damage overlay on limbs affected by this affliction. 1 = full strength.
/// </summary>
public readonly float DamageOverlayAlpha;
//steam achievement given when the affliction is removed from the controlled character
/// <summary>
/// Steam achievement given when the controlled character receives the affliction
/// </summary>
public readonly Identifier AchievementOnReceived;
/// <summary>
/// Steam achievement given when the affliction is removed from the controlled character
/// </summary>
public readonly Identifier AchievementOnRemoved;
public readonly Sprite Icon;
/// <summary>
/// A gradient that defines which color to render this affliction's icon
/// with, based on the affliction's current strength.
/// </summary>
public readonly Color[] IconColors;
public readonly Sprite AfflictionOverlay;
/// <summary>
/// If set to true and the affliction has an AfflictionOverlay element,
/// the overlay's opacity will be strictly proportional to its strength.
/// Otherwise, the overlay's opacity will be determined based on its
/// activation threshold and effects.
/// </summary>
public readonly bool AfflictionOverlayAlphaIsLinear;
/// <summary>
/// If set to true, this affliction will not persist between rounds.
/// </summary>
public readonly bool ResetBetweenRounds;
/// <summary>
/// Should damage particles be emitted when a character receives this affliction? Only relevant if the affliction is of the type "bleeding" or "damage".
/// </summary>
public readonly bool DamageParticles;
/// <summary>
@@ -444,7 +804,20 @@ namespace Barotrauma
/// </summary>
public readonly float WeaponsSkillGain;
/// <summary>
/// A list of species this affliction is allowed to affect.
/// </summary>
public Identifier[] TargetSpecies { get; protected set; }
/// <summary>
/// Effects to apply at various strength levels.
/// Only one effect can be applied at any given moment, so their ranges should be defined with no overlap.
/// </summary>
private readonly List<Effect> effects = new List<Effect>();
/// <summary>
/// PeriodicEffect applies StatusEffects to the character periodically.
/// </summary>
private readonly List<PeriodicEffect> periodicEffects = new List<PeriodicEffect>();
public IEnumerable<Effect> Effects => effects;
@@ -453,7 +826,17 @@ namespace Barotrauma
private readonly ConstructorInfo constructor;
public readonly bool ResetBetweenRounds;
/// <summary>
/// Icon that's used in UI to represent this affliction.
/// </summary>
public readonly Sprite Icon;
/// <summary>
/// A sprite that covers the affected player's entire screen when this affliction is active.
/// Its opacity is controlled by the active effect's MinAfflictionOverlayAlphaMultiplier
/// and MaxAfflictionOverlayAlphaMultiplier
/// </summary>
public readonly Sprite AfflictionOverlay;
public IEnumerable<KeyValuePair<Identifier, float>> TreatmentSuitability
{
@@ -481,7 +864,7 @@ namespace Barotrauma
if (!string.IsNullOrEmpty(fallbackName))
{
Name = Name.Fallback(fallbackName);
}
}
defaultDescription = TextManager.Get($"AfflictionDescription.{TranslationIdentifier}");
string fallbackDescription = element.GetAttributeString("description", "");
if (!string.IsNullOrEmpty(fallbackDescription))
@@ -536,13 +919,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);
@@ -1,5 +1,8 @@
namespace Barotrauma
{
/// <summary>
/// A special affliction type that makes the character see and hear things that aren't there.
/// </summary>
partial class AfflictionPsychosis : Affliction
{
@@ -1,11 +1,12 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Barotrauma
{
/// <summary>
/// A special affliction type that periodically inverts the character's controls and stuns the character.
/// The frequency and duration of the effects increases the higher the strength of the affliction is.
/// </summary>
class AfflictionSpaceHerpes : Affliction
{
private float invertControlsCooldown = 60.0f;
@@ -3,6 +3,10 @@ using System;
namespace Barotrauma
{
/// <summary>
/// A special affliction type that increases the duration of buffs (afflictions of the type "buff"). The increase is defined using the
/// <see cref="AfflictionPrefab.Effect.MinBuffMultiplier"/> and <see cref="AfflictionPrefab.Effect.MaxBuffMultiplier"/> attributes of the affliction effect.
/// </summary>
class BuffDurationIncrease : Affliction
{
public BuffDurationIncrease(AfflictionPrefab prefab, float strength) : base(prefab, strength)
@@ -20,9 +24,9 @@ namespace Barotrauma
{
foreach (Affliction affliction in afflictions)
{
if (!affliction.Prefab.IsBuff || affliction == this || affliction.MultiplierSource != this) { continue; }
affliction.MultiplierSource = null;
affliction.StrengthDiminishMultiplier = 1f;
if (!affliction.Prefab.IsBuff || affliction == this || affliction.StrengthDiminishMultiplier.Source != this) { continue; }
affliction.StrengthDiminishMultiplier.Source = null;
affliction.StrengthDiminishMultiplier.Value = 1f;
}
}
else
@@ -31,10 +35,10 @@ namespace Barotrauma
{
if (!affliction.Prefab.IsBuff || affliction == this) { continue; }
float multiplier = GetDiminishMultiplier();
if (affliction.StrengthDiminishMultiplier < multiplier && affliction.MultiplierSource != this) { continue; }
if (affliction.StrengthDiminishMultiplier.Value < multiplier && affliction.StrengthDiminishMultiplier.Source != this) { continue; }
affliction.MultiplierSource = this;
affliction.StrengthDiminishMultiplier = multiplier;
affliction.StrengthDiminishMultiplier.Source = this;
affliction.StrengthDiminishMultiplier.Value = multiplier;
}
}
}
@@ -48,7 +52,7 @@ namespace Barotrauma
float multiplier = MathHelper.Lerp(
currentEffect.MinBuffMultiplier,
currentEffect.MaxBuffMultiplier,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
currentEffect.GetStrengthFactor(this));
return 1.0f / Math.Max(multiplier, 0.001f);
}
}
@@ -184,7 +184,7 @@ namespace Barotrauma
}
}
public Color DefaultFaceTint = Color.TransparentBlack;
public static readonly Color DefaultFaceTint = Color.TransparentBlack;
public Color FaceTint
{
@@ -339,12 +339,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)
{
@@ -380,7 +380,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; }
@@ -401,7 +401,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)
@@ -449,7 +449,11 @@ namespace Barotrauma
var affliction = kvp.Key;
resistance += affliction.GetResistance(afflictionPrefab.Identifier);
}
return 1 - ((1 - resistance) * Character.GetAbilityResistance(afflictionPrefab));
resistance = 1 - ((1 - resistance) * Character.GetAbilityResistance(afflictionPrefab));
if (resistance > 1f) { resistance = 1f; }
return resistance;
}
public float GetStatValue(StatTypes statType)
@@ -483,16 +487,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);
}
@@ -509,18 +516,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);
}
@@ -622,7 +632,7 @@ namespace Barotrauma
KillIfOutOfVitality();
}
public float GetLimbDamage(Limb limb, string afflictionType = null)
public float GetLimbDamage(Limb limb, Identifier afflictionType)
{
float damageStrength;
if (limb.IsSevered)
@@ -635,16 +645,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;
}
@@ -701,22 +711,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; }
Affliction existingAffliction = null;
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
@@ -753,7 +758,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();
@@ -826,10 +833,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));
@@ -883,6 +896,11 @@ namespace Barotrauma
}
}
/// <summary>
/// 0-1.
/// </summary>
public float OxygenLowResistance => !Character.NeedsOxygen ? 1 : GetResistance(oxygenLowAffliction.Prefab);
private void UpdateOxygen(float deltaTime)
{
if (!Character.NeedsOxygen)
@@ -1137,16 +1155,14 @@ namespace Barotrauma
}
}
public IEnumerable<Identifier> GetActiveAfflictionTags() => GetActiveAfflictionTags(afflictions.Keys);
private readonly HashSet<Identifier> afflictionTags = new HashSet<Identifier>();
public IEnumerable<Identifier> GetActiveAfflictionTags(IEnumerable<Affliction> afflictions)
public IEnumerable<Identifier> GetActiveAfflictionTags()
{
afflictionTags.Clear();
foreach (Affliction affliction in afflictions)
foreach (Affliction affliction in afflictions.Keys)
{
var currentEffect = affliction.GetActiveEffect();
if (currentEffect != null && !currentEffect.Tag.IsEmpty)
if (currentEffect is { Tag.IsEmpty: false })
{
afflictionTags.Add(currentEffect.Tag);
}
@@ -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,15 @@ namespace Barotrauma
public Identifier[] PreferredOutpostModuleTypes { get; protected set; }
[Serialize("", IsPropertySaveable.No)]
public Identifier Faction { get; set; }
[Serialize("", IsPropertySaveable.No)]
public Identifier Group { get; set; }
[Serialize(false, IsPropertySaveable.No)]
public bool AllowDraggingIndefinitely { get; set; }
public XElement Element { get; protected set; }
@@ -97,6 +132,11 @@ namespace Barotrauma
this.NpcSetIdentifier = npcSetIdentifier;
}
public IEnumerable<Identifier> GetTags()
{
return tags;
}
public IEnumerable<Identifier> GetModuleFlags()
{
return moduleFlags;
@@ -148,7 +188,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 +199,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 +217,27 @@ 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 +281,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 +304,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);
}
}
}
@@ -50,7 +50,7 @@ namespace Barotrauma
public override void Dispose() { }
}
class JobVariant
internal class JobVariant
{
public JobPrefab Prefab;
public int Variant;
@@ -113,7 +113,7 @@ namespace Barotrauma
public readonly LocalizedString Name;
[Serialize(AIObjectiveIdle.BehaviorType.Passive, IsPropertySaveable.No)]
[Serialize(AIObjectiveIdle.BehaviorType.Passive, IsPropertySaveable.No, description: "How should the character behave when idling (not doing any particular task)?")]
public AIObjectiveIdle.BehaviorType IdleBehavior
{
get;
@@ -122,78 +122,63 @@ namespace Barotrauma
public readonly LocalizedString Description;
[Serialize(false, IsPropertySaveable.No)]
[Serialize(false, IsPropertySaveable.No, description: "Can the character speak any random lines, or just ones specifically meant for the job?")]
public bool OnlyJobSpecificDialog
{
get;
private set;
}
//the number of these characters in the crew the player starts with in the single player campaign
[Serialize(0, IsPropertySaveable.No)]
[Serialize(0, IsPropertySaveable.No, description: "The number of these characters in the crew the player starts with in the single player campaign.")]
public int InitialCount
{
get;
private set;
}
//if set to true, a client that has chosen this as their preferred job will get it no matter what
[Serialize(false, IsPropertySaveable.No)]
[Serialize(false, IsPropertySaveable.No, description: "If set to true, a client that has chosen this as their preferred job will get it regardless of the maximum number or the amount of spawnpoints in the sub.")]
public bool AllowAlways
{
get;
private set;
}
//how many crew members can have the job (only one captain etc)
[Serialize(100, IsPropertySaveable.No)]
[Serialize(100, IsPropertySaveable.No, description: "How many crew members can have the job (e.g. only one captain etc).")]
public int MaxNumber
{
get;
private set;
}
//how many crew members are REQUIRED to have the job
//(i.e. if one captain is required, one captain is chosen even if all the players have set captain to lowest preference)
[Serialize(0, IsPropertySaveable.No)]
[Serialize(0, IsPropertySaveable.No, description: "How many crew members are required to have the job. I.e. if one captain is required, one captain is chosen even if all the players have set captain to lowest preference.")]
public int MinNumber
{
get;
private set;
}
[Serialize(0.0f, IsPropertySaveable.No)]
[Serialize(0.0f, IsPropertySaveable.No, description: "Minimum amount of karma a player must have to get assigned this job.")]
public float MinKarma
{
get;
private set;
}
[Serialize(1.0f, IsPropertySaveable.No)]
[Serialize(1.0f, IsPropertySaveable.No, description: "Multiplier on the base hiring cost when hiring the character from an outpost.")]
public float PriceMultiplier
{
get;
private set;
}
// TODO: not used
[Serialize(10.0f, IsPropertySaveable.No)]
public float Commonness
{
get;
private set;
}
//how much the vitality of the character is increased/reduced from the default value
[Serialize(0.0f, IsPropertySaveable.No)]
[Serialize(0.0f, IsPropertySaveable.No, description: "How much the vitality of the character is increased/reduced from the default value (e.g. 10 = 110 total vitality if the default vitality is 100.).")]
public float VitalityModifier
{
get;
private set;
}
//whether the job should be available to NPCs
[Serialize(false, IsPropertySaveable.No)]
[Serialize(false, IsPropertySaveable.No, description: "Hidden jobs are not selectable by players, but can be used by e.g. outpost NPCs.")]
public bool HiddenJob
{
get;
@@ -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)
@@ -1225,7 +1226,7 @@ namespace Barotrauma
if (statusEffect.type == ActionType.OnDamaged)
{
if (!statusEffect.HasRequiredAfflictions(character.LastDamage)) { continue; }
if (statusEffect.OnlyPlayerTriggered)
if (statusEffect.OnlyWhenDamagedByPlayer)
{
if (character.LastAttacker == null || !character.LastAttacker.IsPlayer)
{
@@ -1303,7 +1304,8 @@ namespace Barotrauma
}
private float blinkTimer;
private float blinkPhase;
public float BlinkPhase;
public bool FreezeBlinkState;
private float TotalBlinkDurationOut => Params.BlinkDurationOut + Params.BlinkHoldTime;
@@ -1316,16 +1318,25 @@ namespace Barotrauma
{
if (blinkTimer > -TotalBlinkDurationOut)
{
blinkPhase -= deltaTime;
if (blinkPhase > 0)
if (!FreezeBlinkState)
{
BlinkPhase -= deltaTime;
}
if (BlinkPhase > 0)
{
// in
float t = ToolBox.GetEasing(Params.BlinkTransitionIn, MathUtils.InverseLerp(1, 0, blinkPhase / Params.BlinkDurationIn));
float t = ToolBox.GetEasing(Params.BlinkTransitionIn, MathUtils.InverseLerp(1, 0, BlinkPhase / Params.BlinkDurationIn));
body.SmoothRotate(referenceRotation + MathHelper.ToRadians(Params.BlinkRotationIn) * Dir, Mass * Params.BlinkForce * t, wrapAngle: true);
if (Params.UseTextureOffsetForBlinking)
{
#if CLIENT
ActiveSprite.RelativeOrigin = Vector2.Lerp(Params.BlinkTextureOffsetOut, Params.BlinkTextureOffsetIn, t);
#endif
}
}
else
{
if (Math.Abs(blinkPhase) < Params.BlinkHoldTime)
if (Math.Abs(BlinkPhase) < Params.BlinkHoldTime)
{
// hold
body.SmoothRotate(referenceRotation + MathHelper.ToRadians(Params.BlinkRotationIn) * Dir, Mass * Params.BlinkForce, wrapAngle: true);
@@ -1333,15 +1344,25 @@ namespace Barotrauma
else
{
// out
float t = ToolBox.GetEasing(Params.BlinkTransitionOut, MathUtils.InverseLerp(0, 1, -blinkPhase / TotalBlinkDurationOut));
//float t = ToolBox.GetEasing(Params.BlinkTransitionOut, MathUtils.InverseLerp(0, 1, -blinkPhase / TotalBlinkDurationOut));
float t = ToolBox.GetEasing(Params.BlinkTransitionOut, MathUtils.InverseLerp(0, 1, (-BlinkPhase - Params.BlinkHoldTime) / Params.BlinkDurationOut));
body.SmoothRotate(referenceRotation + MathHelper.ToRadians(Params.BlinkRotationOut) * Dir, Mass * Params.BlinkForce * t, wrapAngle: true);
if (Params.UseTextureOffsetForBlinking)
{
#if CLIENT
ActiveSprite.RelativeOrigin = Vector2.Lerp(Params.BlinkTextureOffsetIn, Params.BlinkTextureOffsetOut, t);
#endif
}
}
}
}
else
{
// out
blinkPhase = Params.BlinkDurationIn;
if (!FreezeBlinkState)
{
BlinkPhase = Params.BlinkDurationIn;
}
body.SmoothRotate(referenceRotation + MathHelper.ToRadians(Params.BlinkRotationOut) * Dir, Mass * Params.BlinkForce, wrapAngle: true);
}
}
@@ -177,6 +177,9 @@ namespace Barotrauma
set => SetFootAngles(FootAnglesInRadians, value);
}
[Serialize(false, IsPropertySaveable.Yes, description: "Should the animation be updated even if the character is not moving?"), Editable]
public bool UpdateAnimationWhenNotMoving { get; set; }
/// <summary>
/// Key = limb id, value = angle in radians
/// </summary>
@@ -56,6 +56,9 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.No), Editable]
public bool CanSpeak { get; set; }
[Serialize(true, IsPropertySaveable.Yes), Editable]
public bool ShowHealthBar { get; private set; }
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool UseBossHealthBar { get; private set; }
@@ -110,6 +113,12 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool DrawLast { get; set; }
[Serialize(1.0f, IsPropertySaveable.Yes, "Tells the bots how much they should prefer targeting this character with submarine weapons. Defaults to 1. Set 0 to tell the bots not to target this character at all. Distance to the target affects the decision making."), Editable]
public float AITurretPriority { get; set; }
[Serialize(1.0f, IsPropertySaveable.Yes, "Tells the bots how much they should prefer targeting this character with submarine weapons tagged as \"slowturret\", like railguns. The tag is arbitrary and can be added to any turrets, just like the priority. Defaults to 1. Not used if AITurretPriority is 0. Distance to the target affects the decision making."), Editable]
public float AISlowTurretPriority { get; set; }
public readonly CharacterFile File;
public XDocument VariantFile { get; private set; }
@@ -214,7 +223,7 @@ namespace Barotrauma
return true;
}
public bool CompareGroup(Identifier group) => group != Identifier.Empty && Group != Identifier.Empty && group == Group;
public static bool CompareGroup(Identifier group1, Identifier group2) => group1 != Identifier.Empty && group2 != Identifier.Empty && group1 == group2;
protected void CreateSubParams()
{
@@ -476,7 +485,7 @@ namespace Barotrauma
[Serialize(true, IsPropertySaveable.Yes), Editable]
public bool DoesBleed { get; set; }
[Serialize(float.NegativeInfinity, IsPropertySaveable.Yes), Editable(minValue: float.NegativeInfinity, maxValue: 0)]
[Serialize(float.PositiveInfinity, IsPropertySaveable.Yes), Editable(minValue: 0, maxValue: float.PositiveInfinity)]
public float CrushDepth { get; set; }
// Make editable?
@@ -512,7 +521,20 @@ namespace Barotrauma
// TODO: limbhealths, sprite?
public HealthParams(ContentXElement element, CharacterParams character) : base(element, character) { }
public HealthParams(ContentXElement element, CharacterParams character) : base(element, character)
{
//backwards compatibility
if (CrushDepth < 0)
{
//invert y, convert to meters, and add 1000 to be on the safe side (previously the value would be from the bottom of the level)
float newCrushDepth = -CrushDepth * Physics.DisplayToRealWorldRatio + 1000;
DebugConsole.AddWarning($"Character \"{character.SpeciesName}\" has a negative crush depth. "+
"Previously the crush depths were defined as display units (e.g. -30000 would correspond to 300 meters below the level), "+
"but now they're in meters (e.g. 3000 would correspond to a depth of 3000 meters displayed on the nav terminal). "+
$"Changing the crush depth from {CrushDepth} to {newCrushDepth}.");
CrushDepth = newCrushDepth;
}
}
}
public class InventoryParams : SubParam
@@ -615,6 +637,9 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.Yes, description:"Does the creature know how to open doors (still requires a proper ID card). Humans can always open doors (They don't use this AI definition)."), Editable]
public bool CanOpenDoors { get; private set; }
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool UsePathFindingToGetInside { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Does the creature close the doors behind it. Humans don't use this AI definition."), Editable]
public bool KeepDoorsClosed { get; private set; }
@@ -823,10 +848,19 @@ namespace Barotrauma
[Serialize(5000f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 20000f)]
public float CircleStartDistance { get; private set; }
[Serialize(1f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.5f, MaxValueFloat = 2f)]
[Serialize(false, IsPropertySaveable.Yes, description:"Normally the target size is taken into account when calculating the distance to the target. Set this true to skip that.")]
public bool IgnoreTargetSize { get; private set; }
[Serialize(1f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 100f)]
public float CircleRotationSpeed { get; private set; }
[Serialize(5f, IsPropertySaveable.Yes), Editable(MinValueFloat = 1f, MaxValueFloat = 10f)]
[Serialize(false, IsPropertySaveable.Yes, description:"When enabled, the circle rotation speed can change when the target is far. When this setting is disabled (default), the character will head directly towards the target when it's too far."), Editable]
public bool DynamicCircleRotationSpeed { get; private set; }
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 1f)]
public float CircleRandomRotationFactor { get; private set; }
[Serialize(5f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 10f)]
public float CircleStrikeDistanceMultiplier { get; private set; }
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 50f)]
@@ -6,6 +6,7 @@ using System.Linq;
using Barotrauma.IO;
using System.Xml;
using Barotrauma.Extensions;
using FarseerPhysics;
#if CLIENT
using Barotrauma.SpriteDeformations;
#endif
@@ -621,13 +622,13 @@ namespace Barotrauma
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
public float SteerForce { get; set; }
[Serialize(0f, IsPropertySaveable.Yes, description: "Radius of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
[Serialize(0f, IsPropertySaveable.Yes, description: "Radius of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 2048)]
public float Radius { get; set; }
[Serialize(0f, IsPropertySaveable.Yes, description: "Height of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
[Serialize(0f, IsPropertySaveable.Yes, description: "Height of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 2048)]
public float Height { get; set; }
[Serialize(0f, IsPropertySaveable.Yes, description: "Width of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
[Serialize(0f, IsPropertySaveable.Yes, description: "Width of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 2048)]
public float Width { get; set; }
[Serialize(10f, IsPropertySaveable.Yes, description: "The more the density the heavier the limb is."), Editable(MinValueFloat = 0.01f, MaxValueFloat = 100, DecimalCount = 2)]
@@ -706,6 +707,15 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool OnlyBlinkInWater { get; set; }
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool UseTextureOffsetForBlinking { get; set; }
[Serialize("0.5, 0.5", IsPropertySaveable.Yes), Editable(DecimalCount = 2, MinValueFloat = 0f, MaxValueFloat = 1f)]
public Vector2 BlinkTextureOffsetIn { get; set; }
[Serialize("0.5, 0.5", IsPropertySaveable.Yes), Editable(DecimalCount = 2, MinValueFloat = 0f, MaxValueFloat = 1f)]
public Vector2 BlinkTextureOffsetOut { get; set; }
[Serialize(TransitionMode.Linear, IsPropertySaveable.Yes), Editable]
public TransitionMode BlinkTransitionIn { get; private set; }
@@ -1026,15 +1036,18 @@ namespace Barotrauma
}
}
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 2048)]
public float Radius { get; set; }
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 2048)]
public float Height { get; set; }
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 2048)]
public float Width { get; set; }
[Serialize(BodyType.Dynamic, IsPropertySaveable.Yes), Editable]
public BodyType BodyType { get; set; }
public ColliderParams(ContentXElement element, RagdollParams ragdoll, string name = null) : base(element, ragdoll)
{
Name = name;
@@ -19,15 +19,9 @@ namespace Barotrauma.Abilities
foreach (XElement subElement in conditionElement.Elements())
{
if (subElement.Name.ToString().Equals("conditional", StringComparison.OrdinalIgnoreCase))
if (subElement.NameAsIdentifier() == "conditional")
{
foreach (XAttribute attribute in subElement.Attributes())
{
if (PropertyConditional.IsValid(attribute))
{
conditionals.Add(new PropertyConditional(attribute));
}
}
conditionals.AddRange(PropertyConditional.FromXElement(subElement));
}
}
@@ -14,45 +14,51 @@ namespace Barotrauma.Abilities
{
string[] missionTypeStrings = conditionElement.GetAttributeStringArray("missiontype", new []{ "None" })!;
HashSet<MissionType> missionTypes = new HashSet<MissionType>();
isAffiliated = conditionElement.GetAttributeBool("isaffiliated", false);
foreach (string missionTypeString in missionTypeStrings)
{
if (!Enum.TryParse(missionTypeString, out MissionType parsedMission) || parsedMission is MissionType.None)
{
DebugConsole.ThrowError($"Error in AbilityConditionMission \"{characterTalent.DebugIdentifier}\" - \"{missionTypeString}\" is not a valid mission type.");
return;
if (!isAffiliated)
{
DebugConsole.ThrowError($"Error in AbilityConditionMission \"{characterTalent.DebugIdentifier}\" - \"{missionTypeString}\" is not a valid mission type.");
}
continue;
}
missionTypes.Add(parsedMission);
}
missionType = missionTypes.ToImmutableHashSet();
isAffiliated = conditionElement.GetAttributeBool("isaffiliated", false);
}
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
if (abilityObject is IAbilityMission { Mission: { } mission })
{
if (isAffiliated)
if (!isAffiliated) { return CheckMissionType(); }
if (GameMain.GameSession?.Campaign?.Factions is not { } factions) { return false; }
foreach (var (factionIdentifier, amount) in mission.ReputationRewards)
{
if (GameMain.GameSession?.Campaign?.Factions is not { } factions) { return false; }
foreach (var (factionIdentifier, amount) in mission.ReputationRewards)
if (amount <= 0) { continue; }
if (GetMatchingFaction(factionIdentifier) is { } faction &&
Faction.GetPlayerAffiliationStatus(faction) is FactionAffiliation.Positive)
{
if (amount <= 0) { continue; }
Faction faction = factions.FirstOrDefault(faction => factionIdentifier == faction.Prefab.Identifier);
if (faction?.GetPlayerAffiliationStatus() is FactionAffiliation.Affiliated)
{
return true;
}
return CheckMissionType();
}
return false;
}
return missionType.Contains(mission.Prefab.Type);
return false;
Faction GetMatchingFaction(Identifier factionIdentifier) =>
factionIdentifier == "location"
? mission.OriginLocation?.Faction
: factions.FirstOrDefault(f => factionIdentifier == f.Prefab.Identifier);
bool CheckMissionType() => missionType.IsEmpty || missionType.Contains(mission.Prefab.Type);
}
LogAbilityConditionError(abilityObject, typeof(IAbilityMission));
@@ -23,7 +23,6 @@
protected override bool MatchesConditionSpecific()
{
Identifier identifier = CharacterAbilityGivePermanentStat.HandlePlaceholders(placeholder, statIdentifier);
return character.Info.GetSavedStatValue(statType, identifier) >= min;
}
}
@@ -4,6 +4,7 @@ using System;
using Barotrauma.Extensions;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
namespace Barotrauma.Abilities
{
@@ -29,11 +30,33 @@ namespace Barotrauma.Abilities
if (!TalentTree.JobTalentTrees.TryGet(apprentice.Identifier, out TalentTree? talentTree)) { return; }
ImmutableHashSet<Character> characters = GameSession.GetSessionCrewCharacters(CharacterType.Both);
HashSet<ImmutableHashSet<Identifier>> talentsTrees = new HashSet<ImmutableHashSet<Identifier>>();
foreach (TalentSubTree subTree in talentTree.TalentSubTrees)
{
if (subTree.Type != TalentTreeType.Specialization) { continue; }
talentsTrees.Add(subTree.AllTalentIdentifiers);
HashSet<Identifier> identifiers = new HashSet<Identifier>();
foreach (TalentOption option in subTree.TalentOptionStages)
{
foreach (Identifier identifier in option.TalentIdentifiers)
{
if (IsShowCaseTalent(identifier, option) || TalentTree.IsTalentLocked(identifier, characters)) { continue; }
identifiers.Add(identifier);
}
foreach (var (_, value) in option.ShowCaseTalents)
{
var ids = value.Where(i => !TalentTree.IsTalentLocked(i, characters)).ToImmutableHashSet();
if (ids.Count is 0) { continue; }
identifiers.Add(value.GetRandomUnsynced());
}
}
talentsTrees.Add(identifiers.ToImmutableHashSet());
}
ImmutableHashSet<Identifier> selectedTalentTree = talentsTrees.GetRandomUnsynced();
@@ -44,6 +67,16 @@ namespace Barotrauma.Abilities
Character.GiveTalent(identifier);
}
static bool IsShowCaseTalent(Identifier identifier, TalentOption option)
{
foreach (var (_, value) in option.ShowCaseTalents)
{
if (value.Contains(identifier)) { return true; }
}
return false;
}
}
protected override void ApplyEffect(AbilityObject abilityObject)
@@ -131,6 +131,8 @@ namespace Barotrauma
if (character.Info.GetTotalTalentPoints() - selectedTalents.Count <= 0) { return false; }
if (!JobTalentTrees.TryGet(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return false; }
if (IsTalentLocked(talentIdentifier)) { return false; }
foreach (var subTree in talentTree!.TalentSubTrees)
{
if (subTree.AllTalentIdentifiers.Contains(talentIdentifier) && subTree.HasMaxTalents(selectedTalents)) { return false; }
@@ -152,6 +154,18 @@ namespace Barotrauma
return false;
}
public static bool IsTalentLocked(Identifier talentIdentifier, ImmutableHashSet<Character> characterList = null)
{
characterList ??= GameSession.GetSessionCrewCharacters(CharacterType.Both);
foreach (Character c in characterList)
{
if (c.Info.GetSavedStatValue(StatTypes.LockedTalents, talentIdentifier) >= 1) { return true; }
}
return false;
}
public static List<Identifier> CheckTalentSelection(Character controlledCharacter, IEnumerable<Identifier> selectedTalents)
{
List<Identifier> viableTalents = new List<Identifier>();
@@ -0,0 +1,15 @@
namespace Barotrauma
{
sealed class SlideshowsFile : GenericPrefabFile<SlideshowPrefab>
{
protected override PrefabCollection<SlideshowPrefab> Prefabs => SlideshowPrefab.Prefabs;
public SlideshowsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
protected override bool MatchesSingular(Identifier identifier) => identifier == "Slideshow";
protected override bool MatchesPlural(Identifier identifier) => identifier == "Slideshows";
protected override SlideshowPrefab CreatePrefab(ContentXElement element) => new SlideshowPrefab(this, element);
}
}
@@ -116,9 +116,12 @@ namespace Barotrauma
public static void ThrowIfDuplicates(IEnumerable<ContentPackage> pkgs)
{
var contentPackages = pkgs as IList<ContentPackage> ?? pkgs.ToArray();
if (contentPackages.Any(p1 => contentPackages.AtLeast(2, p2 => p1 == p2)))
foreach (ContentPackage cp in contentPackages)
{
throw new InvalidOperationException($"Input contains duplicate packages");
if (contentPackages.AtLeast(2, cp2 => cp == cp2))
{
throw new InvalidOperationException($"Input contains duplicate packages (\"{cp.Name}\", hash: {cp.Hash?.ShortRepresentation ?? "none"})");
}
}
}
@@ -1,12 +1,11 @@
#nullable enable
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection.Metadata.Ecma335;
using System.Xml.Linq;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
@@ -64,9 +63,14 @@ namespace Barotrauma
public Identifier GetAttributeIdentifier(string key, string def) => Element.GetAttributeIdentifier(key, def);
public Identifier GetAttributeIdentifier(string key, Identifier def) => Element.GetAttributeIdentifier(key, def);
public Identifier[]? GetAttributeIdentifierArray(string key, Identifier[] def, bool trim = true) => Element.GetAttributeIdentifierArray(key, def, trim);
[return:NotNullIfNotNull("def")]
public ImmutableHashSet<Identifier>? GetAttributeIdentifierImmutableHashSet(string key, ImmutableHashSet<Identifier>? def, bool trim = true) => Element.GetAttributeIdentifierImmutableHashSet(key, def, trim);
[return: NotNullIfNotNull("def")]
public Identifier[] GetAttributeIdentifierArray(Identifier[] def, params string[] keys) => Element.GetAttributeIdentifierArray(def, keys);
[return: NotNullIfNotNull("def")]
public Identifier[] GetAttributeIdentifierArray(string key, Identifier[] def, bool trim = true) => Element.GetAttributeIdentifierArray(key, def, trim);
[return: NotNullIfNotNull("def")]
public ImmutableHashSet<Identifier> GetAttributeIdentifierImmutableHashSet(string key, ImmutableHashSet<Identifier>? def, bool trim = true) => Element.GetAttributeIdentifierImmutableHashSet(key, def, trim);
public string? GetAttributeString(string key, string? def) => Element.GetAttributeString(key, def);
public string GetAttributeStringUnrestricted(string key, string def) => Element.GetAttributeStringUnrestricted(key, def);
public string[]? GetAttributeStringArray(string key, string[]? def, bool convertToLowerInvariant = false) => Element.GetAttributeStringArray(key, def, convertToLowerInvariant);
@@ -384,7 +384,7 @@ namespace Barotrauma
return new string[][]
{
GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray(),
PermissionPreset.List.Select(pp => pp.Name.Value).ToArray()
PermissionPreset.List.Select(pp => pp.DisplayName.Value).ToArray()
};
}));
@@ -737,7 +737,7 @@ namespace Barotrauma
commands.Add(new Command("revive", "revive [character name]: Bring the specified character back from the dead. If the name parameter is omitted, the controlled character will be revived.", (string[] args) =>
{
Character revivedCharacter = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args);
if (revivedCharacter == null) return;
if (revivedCharacter == null) { return; }
revivedCharacter.Revive();
#if SERVER
@@ -745,7 +745,7 @@ namespace Barotrauma
{
foreach (Client c in GameMain.Server.ConnectedClients)
{
if (c.Character != revivedCharacter) continue;
if (c.Character != revivedCharacter) { continue; }
//clients stop controlling the character when it dies, force control back
GameMain.Server.SetClientCharacter(c, revivedCharacter);
@@ -889,7 +889,15 @@ namespace Barotrauma
ThrowError("Please specify an identifier and a value.");
return;
}
SetDataAction.PerformOperation(campaign.CampaignMetadata, args[0].ToIdentifier(), args[1], SetDataAction.OperationType.Set);
if (float.TryParse(args[1], out float floatVal))
{
SetDataAction.PerformOperation(campaign.CampaignMetadata, args[0].ToIdentifier(), floatVal, SetDataAction.OperationType.Set);
}
else
{
SetDataAction.PerformOperation(campaign.CampaignMetadata, args[0].ToIdentifier(), args[1], SetDataAction.OperationType.Set);
}
}, isCheat: true));
commands.Add(new Command("setskill", "setskill [all/identifier] [max/level] [character]: Set your skill level.", (string[] args) =>
@@ -1091,11 +1099,6 @@ namespace Barotrauma
commands.Add(new Command("teleportsub", "teleportsub [start/end/cursor]: Teleport the submarine to the position of the cursor, or the start or end of the level. WARNING: does not take outposts into account, so often leads to physics glitches. Only use for debugging.", (string[] args) =>
{
if (Submarine.MainSub == null) { return; }
if (Level.Loaded?.Type == LevelData.LevelType.Outpost && GameMain.GameSession != null)
{
NewMessage("The teleportsub command is unavailable in outpost levels!", Color.Red);
return;
}
if (args.Length == 0 || args[0].Equals("cursor", StringComparison.OrdinalIgnoreCase))
{
@@ -1260,6 +1263,22 @@ namespace Barotrauma
}
#endif
commands.Add(new Command("showreputation", "showreputation: List the current reputation values.", (string[] args) =>
{
if (GameMain.GameSession?.GameMode is CampaignMode campaign)
{
NewMessage("Reputation:");
foreach (var faction in campaign.Factions)
{
NewMessage($" - {faction.Prefab.Name}: {faction.Reputation.Value}");
}
}
else
{
ThrowError("Could not show reputation (no active campaign).");
}
}, null));
commands.Add(new Command("setlocationreputation", "setlocationreputation [value]: Set the reputation in the current location to the specified value.", (string[] args) =>
{
if (GameMain.GameSession?.GameMode is CampaignMode campaign)
@@ -1267,7 +1286,7 @@ namespace Barotrauma
if (args.Length == 0) { return; }
if (float.TryParse(args[0], NumberStyles.Any, CultureInfo.InvariantCulture, out float reputation))
{
campaign.Map.CurrentLocation.Reputation.SetReputation(reputation);
campaign.Map.CurrentLocation.Reputation?.SetReputation(reputation);
}
else
{
@@ -1424,7 +1443,7 @@ namespace Barotrauma
commands.Add(new Command("kill", "kill [character]: Immediately kills the specified character.", (string[] args) =>
{
Character killedCharacter = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args);
killedCharacter?.SetAllDamage(200.0f, 0.0f, 0.0f);
killedCharacter?.Kill(CauseOfDeathType.Unknown, causeOfDeathAffliction: null);
},
() =>
{
@@ -1860,6 +1879,7 @@ namespace Barotrauma
commands.Add(new Command("ambientlight", "ambientlight [color]: Change the color of the ambient light in the level.", null, isCheat: true));
commands.Add(new Command("debugdraw", "Toggle the debug drawing mode on/off (client-only).", null, isCheat: true));
commands.Add(new Command("debugdrawlocalization", "Toggle the localization debug drawing mode on/off (client-only). Colors all text that hasn't been fetched from a localization file magenta, making it easier to spot hard-coded or missing texts.", null, isCheat: false));
commands.Add(new Command("debugdrawlos", "Toggle the los debug drawing mode on/off (client-only).", null, isCheat: true));
commands.Add(new Command("togglevoicechatfilters", "Toggle the radio/muffle filters in the voice chat (client-only).", null, isCheat: false));
commands.Add(new Command("togglehud|hud", "Toggle the character HUD (inventories, icons, buttons, etc) on/off (client-only).", null));
commands.Add(new Command("toggleupperhud", "Toggle the upper part of the ingame HUD (chatbox, crewmanager) on/off (client-only).", null));
@@ -1869,6 +1889,8 @@ namespace Barotrauma
commands.Add(new Command("toggleaitargets|aitargets", "Toggle the visibility of AI targets (= targets that enemies can detect and attack/escape from) (client-only).", null, isCheat: true));
commands.Add(new Command("debugai", "Toggle the ai debug mode on/off (works properly only in single player).", null, isCheat: true));
commands.Add(new Command("devmode", "Toggle the dev mode on/off (client-only).", null, isCheat: true));
commands.Add(new Command("showmonsters", "Permanently unlocks all the monsters in the character editor. Use \"hidemonsters\" to undo.", null, isCheat: true));
commands.Add(new Command("hidemonsters", "Permanently hides in the character editor all the monsters that haven't been encountered in the game. Use \"showmonsters\" to undo.", null, isCheat: true));
InitProjectSpecific();
+473 -22
View File
@@ -12,21 +12,112 @@ namespace Barotrauma
Exponential
}
/// <summary>
/// ActionTypes define when a <see cref="StatusEffect"/> is executed.
/// </summary>
public enum ActionType
{
Always = 0, OnPicked = 1, OnUse = 2, OnSecondaryUse = 3,
OnWearing = 4, OnContaining = 5, OnContained = 6, OnNotContained = 7,
OnActive = 8, OnFailure = 9, OnBroken = 10,
OnFire = 11, InWater = 12, NotInWater = 13,
/// <summary>
/// Executes every frame regardless of the state of the entity.
/// </summary>
Always = 0,
/// <summary>
/// Executes when the item is picked up. Only valid for items.
/// </summary>
OnPicked = 1,
/// <summary>
/// Executes when the item is used. The meaning of "using" an item depends on the item, but generally it means the action that happens when holding the item and clicking LMB. Only valid for items.
/// </summary>
OnUse = 2,
/// <summary>
/// Executes when an item is held and the aim key is held. Only valid for items.
/// </summary>
OnSecondaryUse = 3,
/// <summary>
/// Executes continuously while the item is being worn. Only valid for wearable items.
/// </summary>
OnWearing = 4,
/// <summary>
/// Executes continuously when a specific Containable is inside an ItemContainer. Only valid for Containables defined in an ItemContainer component.
/// </summary>
OnContaining = 5,
/// <summary>
/// Executes continuously when the item is contained in some inventory. Only valid for items.
/// </summary>
OnContained = 6,
/// <summary>
/// Executes continuously when the item is NOT contained in an inventory. Only valid for items.
/// </summary>
OnNotContained = 7,
/// <summary>
/// Executes continuously when the item is active. The meaning of "active" depends on the item, but generally means the item is on, powered, and doing the thing it's intended for. Only valid for items.
/// </summary>
OnActive = 8,
/// <summary>
/// Executes when using the item fails due to a failed skill check. Only valid for items.
/// </summary>
OnFailure = 9,
/// <summary>
/// Executes when using the item's condition drops to 0. Only valid for items.
/// </summary>
OnBroken = 10,
/// <summary>
/// Executes continuously when the entity is within the damage range of fire. Valid for items and characters.
/// </summary>
OnFire = 11,
/// <summary>
/// Executes continuously when the entity is submerged. Valid for items and characters.
/// </summary>
InWater = 12,
/// <summary>
/// Executes continuously when the entity is NOT submerged. Valid for items and characters.
/// </summary>
NotInWater = 13,
/// <summary>
/// Executes when the entity hits something hard enough. For items, the threshold is determined by <see cref="ItemPrefab.ImpactTolerance"/>,
/// for characters by <see cref="Ragdoll.ImpactTolerance"/>. Valid for items and characters.
/// </summary>
OnImpact = 14,
/// <summary>
/// Executes continuously when the character is eating another character. Only valid for characters.
/// </summary>
OnEating = 15,
/// <summary>
/// Executes when the entity receives damage from an external source (i.e. an affliction that increases in severity, or an item degrading by itself don't count).
/// Valid for items and characters.
/// </summary>
OnDamaged = 16,
/// <summary>
/// Executes when the limb gets severed. Only valid for limbs.
/// </summary>
OnSevered = 17,
/// <summary>
/// Executes when a <see cref="Items.Components.Growable"/> produces an item (e.g. when a plant grows a fruit). Only valid for Growable items.
/// </summary>
OnProduceSpawned = 18,
OnOpen = 19, OnClose = 20,
/// <summary>
/// Executes when a <see cref="Items.Components.Door"/> is opened. Only valid for doors.
/// </summary>
OnOpen = 19,
/// <summary>
/// Executes when a <see cref="Items.Components.Door"/> is closed. Only valid for doors.
/// </summary>
OnClose = 20,
/// <summary>
/// Executes when the entity spawns. Only valid for doors.
/// </summary>
OnSpawn = 21,
/// <summary>
/// Executes when using the item succeeds based on a skill check. Only valid for items.
/// </summary>
OnSuccess = 22,
/// <summary>
/// Executes when an Ability (an effect from a talent) triggers the status effect. Only valid in Abilities, the target can be either a character or an item depending on the type of Ability.
/// </summary>
OnAbility = 23,
/// <summary>
/// Executes when the character dies. Only valid for characters.
/// </summary>
OnDeath = OnBroken
}
@@ -75,86 +166,391 @@ namespace Barotrauma
OnStatusEffectIdentifier,
}
/// <summary>
/// StatTypes are used to alter several traits of a character. They are mostly used by talents.
///
/// A lot of StatTypes use a "percentage" value. The way this works is that the value is 0 by default and 1 is added to the value of the stat type to get the final multiplier.
/// For example if the value is set to 0.2 then 1 is added to it making it 1.2 and that is used as a multiplier.
/// This makes it so values between -100% and +100% can be easily represented as -1 and 1 respectively. For example 0.5 would translate to 1.5 for +50% and -0.2 would translate to 0.8 for -20% multiplier.
/// </summary>
public enum StatTypes
{
/// <summary>
/// Used to indicate an invalid stat type. Should not be used.
/// </summary>
None,
// Skills
/// <summary>
/// Boosts electrical skill by a flat amount.
/// </summary>
ElectricalSkillBonus,
/// <summary>
/// Boosts helm skill by a flat amount.
/// </summary>
HelmSkillBonus,
HelmSkillOverride,
MedicalSkillOverride,
WeaponsSkillOverride,
ElectricalSkillOverride,
MechanicalSkillOverride,
/// <summary>
/// Boosts mechanical skill by a flat amount.
/// </summary>
MechanicalSkillBonus,
/// <summary>
/// Boosts medical skill by a flat amount.
/// </summary>
MedicalSkillBonus,
/// <summary>
/// Boosts weapons skill by a flat amount.
/// </summary>
WeaponsSkillBonus,
// Character attributes
/// <summary>
/// Boosts the character's helm skill to the given value if it's lower than the given value.
/// </summary>
HelmSkillOverride,
/// <summary>
/// Boosts the character's medical skill to the given value if it's lower than the given value.
/// </summary>
MedicalSkillOverride,
/// <summary>
/// Boosts the character's weapons skill to the given value if it's lower than the given value.
/// </summary>
WeaponsSkillOverride,
/// <summary>
/// Boosts the character's electrical skill to the given value if it's lower than the given value.
/// </summary>
ElectricalSkillOverride,
/// <summary>
/// Boosts the character's mechanical skill to the given value if it's lower than the given value.
/// </summary>
MechanicalSkillOverride,
/// <summary>
/// Increases character's maximum vitality by a percentage.
/// </summary>
MaximumHealthMultiplier,
/// <summary>
/// Increases both walking and swimming speed of the character by a percentage.
/// </summary>
MovementSpeed,
/// <summary>
/// Increases the character's walking speed by a percentage.
/// </summary>
WalkingSpeed,
/// <summary>
/// Increases the character's swimming speed by a percentage.
/// </summary>
SwimmingSpeed,
/// <summary>
/// Decreases how long it takes for buffs applied to the character decay over time by a percentage.
/// Buffs are afflictions that have isBuff set to true.
/// </summary>
BuffDurationMultiplier,
/// <summary>
/// Decreases how long it takes for debuff applied to the character decay over time by a percentage.
/// Debuffs are afflictions that have isBuff set to false.
/// </summary>
DebuffDurationMultiplier,
/// <summary>
/// Increases the strength of afflictions that are applied to the character by a percentage.
/// Medicines are items that have the "medical" tag.
/// </summary>
MedicalItemEffectivenessMultiplier,
/// <summary>
/// Increases the resistance to pushing force caused by flowing water by a percentage. The resistance cannot be below 0% or higher than 100%.
/// </summary>
FlowResistance,
// Combat
/// <summary>
/// Increases how much damage the character deals via all attacks by a percentage.
/// </summary>
AttackMultiplier,
/// <summary>
/// Increases how much damage the character deals to other characters on the same team by a percentage.
/// </summary>
TeamAttackMultiplier,
/// <summary>
/// Decreases the reload time of ranged weapons held by the character by a percentage.
/// </summary>
RangedAttackSpeed,
/// <summary>
/// Decreases the reload time of submarine turrets operated by the character by a percentage.
/// </summary>
TurretAttackSpeed,
/// <summary>
/// Decreases the power consumption of submarine turrets operated by the character by a percentage.
/// </summary>
TurretPowerCostReduction,
/// <summary>
/// Increases how fast submarine turrets operated by the character charge up by a percentage. Affects turrets like pulse laser.
/// </summary>
TurretChargeSpeed,
/// <summary>
/// Increases how fast the character can swing melee weapons by a percentage.
/// </summary>
MeleeAttackSpeed,
/// <summary>
/// Increases the damage dealt by melee weapons held by the character by a percentage.
/// </summary>
MeleeAttackMultiplier,
RangedAttackMultiplier,
/// <summary>
/// Decreases the spread of ranged weapons held by the character by a percentage.
/// </summary>
RangedSpreadReduction,
// Utility
/// <summary>
/// Increases the repair speed of the character by a percentage.
/// </summary>
RepairSpeed,
/// <summary>
/// Increases the repair speed of the character when repairing mechanical items by a percentage.
/// </summary>
MechanicalRepairSpeed,
/// <summary>
/// Increase deconstruction speed of deconstructor operated by the character by a percentage.
/// </summary>
DeconstructorSpeedMultiplier,
/// <summary>
/// Increases the repair speed of repair tools that fix submarine walls by a percentage.
/// </summary>
RepairToolStructureRepairMultiplier,
/// <summary>
/// Increases the wall damage of tools that destroy submarine walls like plasma cutter by a percentage.
/// </summary>
RepairToolStructureDamageMultiplier,
/// <summary>
/// Increase the detach speed of items like minerals that require a tool to detach from the wall by a percentage.
/// </summary>
RepairToolDeattachTimeMultiplier,
/// <summary>
/// Allows the character to repair mechanical items past the maximum condition by a flat percentage amount. For example setting this to 0.1 allows the character to repair mechanical items to 110% condition.
/// </summary>
MaxRepairConditionMultiplierMechanical,
/// <summary>
/// Allows the character to repair electrical items past the maximum condition by a flat percentage amount. For example setting this to 0.1 allows the character to repair electrical items to 110% condition.
/// </summary>
MaxRepairConditionMultiplierElectrical,
/// <summary>
/// Increase the the quality of items crafted by the character by a flat amount.
/// Can be made to only affect certain item with a given tag types by specifying a tag via CharacterAbilityGivePermanentStat, when no tag is specified the ability affects all items.
/// </summary>
IncreaseFabricationQuality,
/// <summary>
/// Boosts the condition of genes combined by the character by a flat amount.
/// </summary>
GeneticMaterialRefineBonus,
/// <summary>
/// Reduces the chance to taint a gene when combining genes by a percentage. Tainting probability can not go below 0% or above 100%.
/// </summary>
GeneticMaterialTaintedProbabilityReductionOnCombine,
/// <summary>
/// Increases the speed at which the character gains skills by a percentage.
/// </summary>
SkillGainSpeed,
/// <summary>
/// Whenever the character's skill level up add a flat amount of more skill levels to the character.
/// </summary>
ExtraLevelGain,
/// <summary>
/// Increases the speed at which the character gains helm skill by a percentage.
/// </summary>
HelmSkillGainSpeed,
/// <summary>
/// Increases the speed at which the character gains weapons skill by a percentage.
/// </summary>
WeaponsSkillGainSpeed,
/// <summary>
/// Increases the speed at which the character gains medical skill by a percentage.
/// </summary>
MedicalSkillGainSpeed,
/// <summary>
/// Increases the speed at which the character gains electrical skill by a percentage.
/// </summary>
ElectricalSkillGainSpeed,
/// <summary>
/// Increases the speed at which the character gains mechanical skill by a percentage.
/// </summary>
MechanicalSkillGainSpeed,
/// <summary>
/// Increases the strength of afflictions the character applies to other characters via medicine by a percentage.
/// Medicines are items that have the "medical" tag.
/// </summary>
MedicalItemApplyingMultiplier,
MedicalItemDurationMultiplier,
/// <summary>
/// Increases the strength of afflictions the character applies to other characters via medicine by a percentage.
/// Works only for afflictions that have isBuff set to true.
/// </summary>
BuffItemApplyingMultiplier,
/// <summary>
/// Increases the strength of afflictions the character applies to other characters via medicine by a percentage.
/// Works only for afflictions that have "poison" type.
/// </summary>
PoisonMultiplier,
// Tinker
/// <summary>
/// Increases how long the character can tinker with items by a flat amount where 1 = 1 second.
/// </summary>
TinkeringDuration,
/// <summary>
/// Increases the effectiveness of the character's tinkerings by a percentage.
/// Tinkering strength affects the speed and effectiveness of the item that is being tinkered with.
/// </summary>
TinkeringStrength,
/// <summary>
/// Increases how much condition tinkered items lose when the character tinkers with them by a percentage.
/// </summary>
TinkeringDamage,
// Misc
/// <summary>
/// Increases how much reputation the character gains by a percentage.
/// Can be made to only affect certain factions with a given tag types by specifying a tag via CharacterAbilityGivePermanentStat, when no tag is specified the ability affects all factions.
/// </summary>
ReputationGainMultiplier,
/// <summary>
/// Increases how much reputation the character loses by a percentage.
/// Can be made to only affect certain factions with a given tag types by specifying a tag via CharacterAbilityGivePermanentStat, when no tag is specified the ability affects all factions.
/// </summary>
ReputationLossMultiplier,
/// <summary>
/// Increases how much money the character gains from missions by a percentage.
/// </summary>
MissionMoneyGainMultiplier,
/// <summary>
/// Increases how much talent experience the character gains from all sources by a percentage.
/// </summary>
ExperienceGainMultiplier,
/// <summary>
/// Increases how much talent experience the character gains from missions by a percentage.
/// </summary>
MissionExperienceGainMultiplier,
/// <summary>
/// Increases how many missions the characters crew can have at the same time by a flat amount.
/// </summary>
ExtraMissionCount,
/// <summary>
/// Increases how many items are in stock in special sales in the store by a flat amount.
/// </summary>
ExtraSpecialSalesCount,
/// <summary>
/// Increases how much money is gained from selling items to the store by a percentage.
/// </summary>
StoreSellMultiplier,
/// <summary>
/// Decreases the prices of items in affiliated store by a percentage.
/// </summary>
StoreBuyMultiplierAffiliated,
/// <summary>
/// Decreases the prices of items in all stores by a percentage.
/// </summary>
StoreBuyMultiplier,
/// <summary>
/// Decreases the price of upgrades and submarines in affiliated outposts by a percentage.
/// </summary>
ShipyardBuyMultiplierAffiliated,
/// <summary>
/// Decreases the price of upgrades and submarines in all outposts by a percentage.
/// </summary>
ShipyardBuyMultiplier,
/// <summary>
/// Limits how many of a certain item can be attached to the wall in the submarine at the same time.
/// Has to be used with CharacterAbilityGivePermanentStat to specify the tag of the item that is affected. Does nothing if no tag is specified.
/// </summary>
MaxAttachableCount,
/// <summary>
/// Increase the radius of explosions caused by the character by a percentage.
/// </summary>
ExplosionRadiusMultiplier,
/// <summary>
/// Increases the damage of explosions caused by the character by a percentage.
/// </summary>
ExplosionDamageMultiplier,
/// <summary>
/// Decreases the time it takes to fabricate items on fabricators operated by the character by a percentage.
/// </summary>
FabricationSpeed,
/// <summary>
/// Increases how much damage the character deals to ballast flora by a percentage.
/// </summary>
BallastFloraDamageMultiplier,
/// <summary>
/// Increases the time it takes for the character to pass out when out of oxygen.
/// </summary>
HoldBreathMultiplier,
/// <summary>
/// Used to set the character's apprencticeship to a certain job.
/// Used by the "apprenticeship" talent and requires a job to be specified via CharacterAbilityGivePermanentStat.
/// </summary>
Apprenticeship,
Affiliation,
CPRBoost
/// <summary>
/// Increases the revival chance of the character when performing CPR by a percentage.
/// </summary>
CPRBoost,
/// <summary>
/// Can be used to prevent certain talents from being unlocked by specifying the talent's identifier via CharacterAbilityGivePermanentStat.
/// </summary>
LockedTalents
}
internal enum ItemTalentStats
@@ -172,22 +568,77 @@ namespace Barotrauma
FabricationSpeed
}
/// <summary>
/// AbilityFlags are a set of toggleable flags that can be applied to characters.
/// </summary>
[Flags]
public enum AbilityFlags
{
/// <summary>
/// Used to indicate an erroneous ability flag. Should not be used.
/// </summary>
None = 0,
/// <summary>
/// Character will not be able to run.
/// </summary>
MustWalk = 0x1,
/// <summary>
/// Character is immune to pressure.
/// </summary>
ImmuneToPressure = 0x2,
/// <summary>
/// Character won't be targeted by enemy AI.
/// </summary>
IgnoredByEnemyAI = 0x4,
/// <summary>
/// Character can drag corpses without a movement speed penalty.
/// </summary>
MoveNormallyWhileDragging = 0x8,
/// <summary>
/// Character is able to tinker with items.
/// </summary>
CanTinker = 0x10,
/// <summary>
/// Character is able to tinker with fabricators and deconstructors.
/// </summary>
CanTinkerFabricatorsAndDeconstructors = 0x20,
/// <summary>
/// Allows items tinkered by the character to consume no power.
/// </summary>
TinkeringPowersDevices = 0x40,
/// <summary>
/// Allows the character to gain skills past 100.
/// </summary>
GainSkillPastMaximum = 0x80,
/// <summary>
/// Allows the character to retain experience when respawning as a new character.
/// </summary>
RetainExperienceForNewCharacter = 0x100,
/// <summary>
/// Allows CharacterAbilityApplyStatusEffectsToLastOrderedCharacter to affect the last 2 characters ordered.
/// </summary>
AllowSecondOrderedTarget = 0x200,
/// <summary>
/// Character will stay conscious even if their vitality drops below 0.
/// </summary>
AlwaysStayConscious = 0x400,
CanNotDieToAfflictions = 0x800,
/// <summary>
/// Prevents afflictions on the character from dropping the characters vitality below the kill threshold.
/// The character can still die from sources like getting crushed by pressure or if their head is severed.
/// </summary>
CanNotDieToAfflictions = 0x800
}
[Flags]
@@ -225,4 +676,4 @@ namespace Barotrauma
Local,
Radio
}
}
}
@@ -110,7 +110,7 @@ namespace Barotrauma
state = 1;
break;
case 1:
if (!Submarine.MainSub.AtEndExit && !Submarine.MainSub.AtStartExit) return;
if (!Submarine.MainSub.AtEitherExit) { return; }
Finish();
state = 2;
@@ -9,6 +9,8 @@ namespace Barotrauma
public event Action Finished;
protected bool isFinished;
public int RandomSeed;
protected readonly EventPrefab prefab;
public EventPrefab Prefab => prefab;
@@ -1,3 +1,4 @@
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
@@ -15,20 +16,13 @@ namespace Barotrauma
{
DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckConditionalAction with no target tag! This will cause the check to automatically succeed.");
}
foreach (var attribute in element.Attributes())
{
if (PropertyConditional.IsValid(attribute) && !IsTargetTagAttribute(attribute))
{
Conditional = new PropertyConditional(attribute);
break;
}
}
Conditional = PropertyConditional.FromXElement(element, IsNotTargetTagAttribute).FirstOrDefault();
if (Conditional == null)
{
DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckConditionalAction with no valid PropertyConditional! This will cause the check to automatically succeed.");
}
static bool IsTargetTagAttribute(XAttribute attribute) => attribute.NameAsIdentifier() == "targettag";
static bool IsNotTargetTagAttribute(XAttribute attribute) => attribute.NameAsIdentifier() != "targettag";
}
private string GetEventName()
@@ -21,7 +21,7 @@ namespace Barotrauma
protected object? value2;
protected object? value1;
protected PropertyConditional.OperatorType Operator { get; set; }
protected PropertyConditional.ComparisonOperatorType Operator { get; set; }
public CheckDataAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
@@ -56,23 +56,13 @@ namespace Barotrauma
{
if (GameMain.GameSession?.GameMode is not CampaignMode campaignMode) { return false; }
string[] splitString = Condition.Split(' ');
string value;
if (splitString.Length > 0)
(Operator, string value) = PropertyConditional.ExtractComparisonOperatorFromConditionString(Condition);
if (Operator == PropertyConditional.ComparisonOperatorType.None)
{
//the first part of the string is the operator, skip it
value = string.Join(" ", splitString.Skip(1));
}
else
{
DebugConsole.ThrowError($"{Condition} is too short, it should start with an operator followed by a boolean or a floating point value.");
DebugConsole.ThrowError($"{Condition} is invalid, it should start with an operator followed by a boolean or a floating point value.");
return false;
}
string op = splitString[0];
Operator = PropertyConditional.GetOperatorType(op);
if (Operator == PropertyConditional.OperatorType.None) { return false; }
if (CheckAgainstMetadata)
{
object? metadata1 = campaignMode.CampaignMetadata.GetValue(Identifier);
@@ -82,8 +72,8 @@ namespace Barotrauma
{
return Operator switch
{
PropertyConditional.OperatorType.Equals => metadata1 == metadata2,
PropertyConditional.OperatorType.NotEquals => metadata1 != metadata2,
PropertyConditional.ComparisonOperatorType.Equals => metadata1 == metadata2,
PropertyConditional.ComparisonOperatorType.NotEquals => metadata1 != metadata2,
_ => false
};
}
@@ -139,9 +129,9 @@ namespace Barotrauma
value2 = val2;
switch (Operator)
{
case PropertyConditional.OperatorType.Equals:
case PropertyConditional.ComparisonOperatorType.Equals:
return val1 == val2;
case PropertyConditional.OperatorType.NotEquals:
case PropertyConditional.ComparisonOperatorType.NotEquals:
return val1 != val2;
default:
DebugConsole.Log($"Only \"Equals\" and \"Not equals\" operators are allowed for a boolean (was {Operator} for {val2}).");
@@ -166,17 +156,17 @@ namespace Barotrauma
value2 = val2;
switch (Operator)
{
case PropertyConditional.OperatorType.Equals:
case PropertyConditional.ComparisonOperatorType.Equals:
return MathUtils.NearlyEqual(val1, val2);
case PropertyConditional.OperatorType.GreaterThan:
case PropertyConditional.ComparisonOperatorType.GreaterThan:
return val1 > val2;
case PropertyConditional.OperatorType.GreaterThanEquals:
case PropertyConditional.ComparisonOperatorType.GreaterThanEquals:
return val1 >= val2;
case PropertyConditional.OperatorType.LessThan:
case PropertyConditional.ComparisonOperatorType.LessThan:
return val1 < val2;
case PropertyConditional.OperatorType.LessThanEquals:
case PropertyConditional.ComparisonOperatorType.LessThanEquals:
return val1 <= val2;
case PropertyConditional.OperatorType.NotEquals:
case PropertyConditional.ComparisonOperatorType.NotEquals:
return !MathUtils.NearlyEqual(val1, val2);
}
@@ -195,9 +185,9 @@ namespace Barotrauma
bool equals = string.Equals(val1, val2, StringComparison.OrdinalIgnoreCase);
switch (Operator)
{
case PropertyConditional.OperatorType.Equals:
case PropertyConditional.ComparisonOperatorType.Equals:
return equals;
case PropertyConditional.OperatorType.NotEquals:
case PropertyConditional.ComparisonOperatorType.NotEquals:
return !equals;
default:
DebugConsole.Log($"Only \"Equals\" and \"Not equals\" operators are allowed for a string (was {Operator} for {val2}).");
@@ -33,7 +33,7 @@ namespace Barotrauma
public int ItemContainerIndex { get; set; }
private readonly IReadOnlyList<PropertyConditional> conditionals;
private readonly Identifier[] itemIdentifierSplit;
private readonly Identifier[] itemTags;
@@ -44,13 +44,7 @@ namespace Barotrauma
var conditionalList = new List<PropertyConditional>();
foreach (ContentXElement subElement in element.GetChildElements("conditional"))
{
foreach (XAttribute attribute in subElement.Attributes())
{
if (PropertyConditional.IsValid(attribute))
{
conditionalList.Add(new PropertyConditional(attribute));
}
}
conditionalList.AddRange(PropertyConditional.FromXElement(subElement));
break;
}
conditionals = conditionalList;
@@ -200,7 +200,7 @@ namespace Barotrauma
}
}
private int[] GetEndingOptions()
public int[] GetEndingOptions()
{
List<int> endings = Options.Where(group => !group.Actions.Any() || group.EndConversation).Select(group => Options.IndexOf(group)).ToList();
if (!ContinueConversation) { endings.Add(-1); }
@@ -1,12 +1,13 @@
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Xml.Linq;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System.Collections.Immutable;
using System.Linq;
namespace Barotrauma
{
class MissionAction : EventAction
partial class MissionAction : EventAction
{
[Serialize("", IsPropertySaveable.Yes)]
public Identifier MissionIdentifier { get; set; }
@@ -14,8 +15,10 @@ namespace Barotrauma
[Serialize("", IsPropertySaveable.Yes)]
public Identifier MissionTag { get; set; }
[Serialize("", IsPropertySaveable.Yes, description: "The type of the location the mission will be unlocked in (if empty, any location can be selected).")]
public string LocationType { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier RequiredFaction { get; set; }
public ImmutableArray<Identifier> LocationTypes { get; }
[Serialize(0, IsPropertySaveable.Yes, description: "Minimum distance to the location the mission is unlocked in (1 = one path between locations).")]
public int MinLocationDistance { get; set; }
@@ -28,6 +31,8 @@ namespace Barotrauma
private bool isFinished;
private readonly Random random;
public MissionAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
if (MissionIdentifier.IsEmpty && MissionTag.IsEmpty)
@@ -38,6 +43,8 @@ namespace Barotrauma
{
DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": both MissionIdentifier or MissionTag have been configured. The tag will be ignored.");
}
LocationTypes = element.GetAttributeIdentifierArray("locationtype", Array.Empty<Identifier>()).ToImmutableArray();
random = new MTRandom(parentEvent.RandomSeed);
}
public override bool IsFinished(ref string goTo)
@@ -56,14 +63,14 @@ namespace Barotrauma
if (GameMain.GameSession.GameMode is CampaignMode campaign)
{
Mission unlockedMission = null;
var unlockLocation = FindUnlockLocation();
var unlockLocation = FindUnlockLocation(MinLocationDistance, UnlockFurtherOnMap, LocationTypes);
if (unlockLocation == null && CreateLocationIfNotFound)
{
//find an empty location at least 3 steps away, further on the map
var emptyLocation = FindUnlockLocationRecursive(campaign.Map.CurrentLocation, Math.Max(MinLocationDistance, 3), "none", true, new HashSet<Location>());
var emptyLocation = FindUnlockLocation(Math.Max(MinLocationDistance, 3), unlockFurtherOnMap: true, "none".ToIdentifier().ToEnumerable());
if (emptyLocation != null)
{
emptyLocation.ChangeType(Barotrauma.LocationType.Prefabs[LocationType]);
emptyLocation.ChangeType(campaign, LocationType.Prefabs[LocationTypes[0]]);
unlockLocation = emptyLocation;
}
}
@@ -72,11 +79,11 @@ namespace Barotrauma
{
if (!MissionIdentifier.IsEmpty)
{
unlockedMission = unlockLocation.UnlockMissionByIdentifier(MissionIdentifier);
unlockedMission = unlockLocation.UnlockMissionByIdentifier(MissionIdentifier);
}
else if (!MissionTag.IsEmpty)
{
unlockedMission = unlockLocation.UnlockMissionByTag(MissionTag);
unlockedMission = unlockLocation.UnlockMissionByTag(MissionTag, random);
}
if (campaign is MultiPlayerCampaign mpCampaign)
{
@@ -84,7 +91,9 @@ namespace Barotrauma
}
if (unlockedMission != null)
{
if (unlockedMission.Locations[0] == unlockedMission.Locations[1] || unlockedMission.Locations[1] ==null)
unlockedMission.OriginLocation = campaign.Map.CurrentLocation;
campaign.Map.Discover(unlockLocation, checkTalents: false);
if (unlockedMission.Locations[0] == unlockedMission.Locations[1] || unlockedMission.Locations[1] == null)
{
DebugConsole.NewMessage($"Unlocked mission \"{unlockedMission.Name}\" in the location \"{unlockLocation.Name}\".");
}
@@ -99,66 +108,86 @@ namespace Barotrauma
IconColor = unlockedMission.Prefab.IconColor
};
#else
missionsUnlockedThisRound.Add(unlockedMission);
NotifyMissionUnlock(unlockedMission);
#endif
#endif
}
}
else
{
DebugConsole.AddWarning($"Failed to find a suitable location to unlock a mission in (LocationType: {LocationType}, MinLocationDistance: {MinLocationDistance}, UnlockFurtherOnMap: {UnlockFurtherOnMap})");
DebugConsole.AddWarning($"Failed to find a suitable location to unlock a mission in (LocationType: {LocationTypes}, MinLocationDistance: {MinLocationDistance}, UnlockFurtherOnMap: {UnlockFurtherOnMap})");
}
}
isFinished = true;
}
private Location FindUnlockLocation()
private Location FindUnlockLocation(int minDistance, bool unlockFurtherOnMap, IEnumerable<Identifier> locationTypes)
{
var campaign = GameMain.GameSession.GameMode as CampaignMode;
if (string.IsNullOrEmpty(LocationType) && MinLocationDistance <= 1)
if (LocationTypes.Length == 0 && minDistance <= 1)
{
return campaign.Map.CurrentLocation;
}
return FindUnlockLocationRecursive(campaign.Map.CurrentLocation, 0, LocationType, UnlockFurtherOnMap, new HashSet<Location>());
var currentLocation = campaign.Map.CurrentLocation;
int distance = 0;
HashSet<Location> checkedLocations = new HashSet<Location>();
HashSet<Location> pendingLocations = new HashSet<Location>() { currentLocation };
do
{
List<Location> currentLocations = pendingLocations.ToList();
pendingLocations.Clear();
foreach (var location in currentLocations)
{
checkedLocations.Add(location);
if (IsLocationValid(currentLocation, location, unlockFurtherOnMap, distance, minDistance, locationTypes))
{
return location;
}
else
{
foreach (LocationConnection connection in location.Connections)
{
var otherLocation = connection.OtherLocation(location);
if (checkedLocations.Contains(otherLocation)) { continue; }
pendingLocations.Add(otherLocation);
}
}
}
distance++;
} while (pendingLocations.Any());
return null;
}
private Location FindUnlockLocationRecursive(Location currLocation, int currDistance, string locationType, bool unlockFurtherOnMap, HashSet<Location> checkedLocations)
private bool IsLocationValid(Location currLocation, Location location, bool unlockFurtherOnMap, int distance, int minDistance, IEnumerable<Identifier> locationTypes)
{
var campaign = GameMain.GameSession.GameMode as CampaignMode;
if (currLocation.Type.Identifier == locationType && currDistance >= MinLocationDistance &&
(!unlockFurtherOnMap || currLocation.MapPosition.X > campaign.Map.CurrentLocation.MapPosition.X))
if (!RequiredFaction.IsEmpty)
{
return currLocation;
if (location.Faction?.Prefab.Identifier != RequiredFaction &&
location.SecondaryFaction?.Prefab.Identifier != RequiredFaction)
{
return false;
}
}
checkedLocations.Add(currLocation);
foreach (LocationConnection connection in currLocation.Connections)
if (!locationTypes.Contains(location.Type.Identifier) && !(location.HasOutpost() && locationTypes.Contains("AnyOutpost".ToIdentifier())))
{
var otherLocation = connection.OtherLocation(currLocation);
if (checkedLocations.Contains(otherLocation)) { continue; }
var unlockLocation = FindUnlockLocationRecursive(otherLocation, ++currDistance, locationType, unlockFurtherOnMap, checkedLocations);
if (unlockLocation != null) { return unlockLocation; }
return false;
}
return null;
if (distance < minDistance)
{
return false;
}
if (unlockFurtherOnMap && location.MapPosition.X < currLocation.MapPosition.X)
{
return false;
}
return true;
}
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(MissionAction)} -> ({(MissionIdentifier.IsEmpty ? MissionTag : MissionIdentifier)})";
}
#if SERVER
private void NotifyMissionUnlock(Mission mission)
{
foreach (Client client in GameMain.Server.ConnectedClients)
{
IWriteMessage outmsg = new WriteOnlyMessage();
outmsg.WriteByte((byte)ServerPacketHeader.EVENTACTION);
outmsg.WriteByte((byte)EventManager.NetworkEventType.MISSION);
outmsg.WriteIdentifier(mission.Prefab.Identifier);
outmsg.WriteString(mission.Name.Value);
GameMain.Server.ServerPeer.Send(outmsg, client.Connection, DeliveryMethod.Reliable);
}
}
#endif
}
}
@@ -0,0 +1,66 @@
namespace Barotrauma
{
class MissionStateAction : EventAction
{
[Serialize("", IsPropertySaveable.Yes)]
public Identifier MissionIdentifier { get; set; }
public enum OperationType
{
Set,
Add
}
[Serialize(OperationType.Set, IsPropertySaveable.Yes)]
public OperationType Operation { get; set; }
[Serialize(0, IsPropertySaveable.Yes)]
public int State { get; set; }
private bool isFinished;
public MissionStateAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
State = element.GetAttributeInt("value", State);
if (MissionIdentifier.IsEmpty)
{
DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": MissionIdentifier has not been configured.");
}
}
public override bool IsFinished(ref string goTo)
{
return isFinished;
}
public override void Reset()
{
isFinished = false;
}
public override void Update(float deltaTime)
{
if (isFinished) { return; }
foreach (Mission mission in GameMain.GameSession.Missions)
{
if (mission.Prefab.Identifier != MissionIdentifier) { continue; }
switch (Operation)
{
case OperationType.Set:
mission.State = State;
break;
case OperationType.Add:
mission.State += 1;
break;
}
}
isFinished = true;
}
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(MissionStateAction)} -> ({(Operation == OperationType.Set ? State : '+' + State)})";
}
}
}
@@ -0,0 +1,91 @@
namespace Barotrauma
{
class ModifyLocationAction : EventAction
{
[Serialize("", IsPropertySaveable.Yes)]
public Identifier Faction { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier SecondaryFaction { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier Type { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public string Name { get; set; }
private bool isFinished;
public ModifyLocationAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
}
public override bool IsFinished(ref string goTo)
{
return isFinished;
}
public override void Reset()
{
isFinished = false;
}
public override void Update(float deltaTime)
{
if (isFinished) { return; }
if (GameMain.GameSession.GameMode is CampaignMode campaign)
{
var location = campaign.Map.CurrentLocation;
if (location != null)
{
if (!Faction.IsEmpty)
{
var faction = campaign.Factions.Find(f => f.Prefab.Identifier == Faction);
if (faction == null)
{
DebugConsole.ThrowError($"Error in ModifyLocationAction ({ParentEvent.Prefab.Identifier}): could not find a faction with the identifier \"{Faction}\".");
}
else
{
location.Faction = faction;
}
}
if (!SecondaryFaction.IsEmpty)
{
var secondaryFaction = campaign.Factions.Find(f => f.Prefab.Identifier == SecondaryFaction);
if (secondaryFaction == null)
{
DebugConsole.ThrowError($"Error in ModifyLocationAction ({ParentEvent.Prefab.Identifier}): could not find a faction with the identifier \"{SecondaryFaction}\".");
}
else
{
location.SecondaryFaction = secondaryFaction;
}
}
if (!Type.IsEmpty)
{
var locationType = LocationType.Prefabs.Find(lt => lt.Identifier == Type);
if (locationType == null)
{
DebugConsole.ThrowError($"Error in ModifyLocationAction ({ParentEvent.Prefab.Identifier}): could not find a location type with the identifier \"{Type}\".");
}
else if (!location.LocationTypeChangesBlocked)
{
location.ChangeType(campaign, locationType);
}
}
if (!string.IsNullOrEmpty(Name))
{
location.ForceName(TextManager.Get(Name).Fallback(Name).Value);
}
}
}
isFinished = true;
}
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(ModifyLocationAction)}";
}
}
}
@@ -1,8 +1,6 @@
using Barotrauma.Networking;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
@@ -12,7 +10,7 @@ namespace Barotrauma
public Identifier NPCTag { get; set; }
[Serialize(CharacterTeamType.None, IsPropertySaveable.Yes)]
public CharacterTeamType TeamTag { get; set; }
public CharacterTeamType TeamID { get; set; }
[Serialize(false, IsPropertySaveable.Yes)]
public bool AddToCrew { get; set; }
@@ -24,10 +22,13 @@ namespace Barotrauma
public NPCChangeTeamAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
//backwards compatibility
TeamID = element.GetAttributeEnum("teamtag", element.GetAttributeEnum<CharacterTeamType>("team", TeamID));
var enums = Enum.GetValues(typeof(CharacterTeamType)).Cast<CharacterTeamType>();
if (!enums.Contains(TeamTag))
if (!enums.Contains(TeamID))
{
DebugConsole.ThrowError($"Error in {nameof(NPCChangeTeamAction)} in the event {ParentEvent.Prefab.Identifier}. \"{TeamTag}\" is not a valid Team ID. Valid values are {string.Join(',', Enum.GetNames(typeof(CharacterTeamType)))}.");
DebugConsole.ThrowError($"Error in {nameof(NPCChangeTeamAction)} in the event {ParentEvent.Prefab.Identifier}. \"{TeamID}\" is not a valid Team ID. Valid values are {string.Join(',', Enum.GetNames(typeof(CharacterTeamType)))}.");
}
}
@@ -41,27 +42,34 @@ namespace Barotrauma
foreach (var npc in affectedNpcs)
{
// characters will still remain on friendlyNPC team for rest of the tick
npc.SetOriginalTeam(TeamTag);
if (AddToCrew && (TeamTag == CharacterTeamType.Team1 || TeamTag == CharacterTeamType.Team2))
npc.SetOriginalTeam(TeamID);
foreach (Item item in npc.Inventory.AllItems)
{
var idCard = item.GetComponent<Items.Components.IdCard>();
if (idCard != null)
{
idCard.TeamID = TeamID;
}
}
if (AddToCrew && (TeamID == CharacterTeamType.Team1 || TeamID == CharacterTeamType.Team2))
{
npc.Info.StartItemsGiven = true;
GameMain.GameSession.CrewManager.AddCharacter(npc);
ChangeItemTeam(Submarine.MainSub, true);
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
GameMain.NetworkMember.CreateEntityEvent(npc, new Character.AddToCrewEventData(TeamTag, npc.Inventory.AllItems));
GameMain.NetworkMember.CreateEntityEvent(npc, new Character.AddToCrewEventData(TeamID, npc.Inventory.AllItems));
}
}
else if (RemoveFromCrew && (npc.TeamID == CharacterTeamType.Team1 || npc.TeamID == CharacterTeamType.Team2))
{
npc.Info.StartItemsGiven = true;
GameMain.GameSession.CrewManager.RemoveCharacter(npc, removeInfo: true);
var sub = Submarine.Loaded.FirstOrDefault(s => s.TeamID == TeamTag);
var sub = Submarine.Loaded.FirstOrDefault(s => s.TeamID == TeamID);
ChangeItemTeam(sub, false);
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
GameMain.NetworkMember.CreateEntityEvent(npc, new Character.RemoveFromCrewEventData(TeamTag, npc.Inventory.AllItems));
GameMain.NetworkMember.CreateEntityEvent(npc, new Character.RemoveFromCrewEventData(TeamID, npc.Inventory.AllItems));
}
}
@@ -72,11 +80,10 @@ namespace Barotrauma
item.AllowStealing = allowStealing;
if (item.GetComponent<Items.Components.WifiComponent>() is { } wifiComponent)
{
wifiComponent.TeamID = TeamTag;
wifiComponent.TeamID = TeamID;
}
if (item.GetComponent<Items.Components.IdCard>() is { } idCard)
{
idCard.TeamID = TeamTag;
idCard.SubmarineSpecificID = 0;
}
}
@@ -39,13 +39,14 @@ namespace Barotrauma
affectedNpcs = ParentEvent.GetTargets(NPCTag).Where(c => c is Character).Select(c => c as Character).ToList();
foreach (var npc in affectedNpcs)
{
if (!(npc.AIController is HumanAIController humanAiController)) { continue; }
if (npc.AIController is not HumanAIController humanAiController) { continue; }
if (Follow)
{
var newObjective = new AIObjectiveGoTo(target, npc, humanAiController.ObjectiveManager, repeat: true)
{
OverridePriority = 100.0f
OverridePriority = 100.0f,
IsFollowOrderObjective = true
};
humanAiController.ObjectiveManager.AddObjective(newObjective);
humanAiController.ObjectiveManager.WaitTimer = 0.0f;
@@ -19,8 +19,6 @@ namespace Barotrauma
private IEnumerable<Character> affectedNpcs;
private AIObjectiveGoTo gotoObjective;
public override void Update(float deltaTime)
{
if (isFinished) { return; }
@@ -33,19 +31,18 @@ namespace Barotrauma
if (Wait)
{
gotoObjective = new AIObjectiveGoTo(npc, npc, humanAiController.ObjectiveManager, repeat: true)
var gotoObjective = new AIObjectiveGoTo(
AIObjectiveGoTo.GetTargetHull(npc) as ISpatialEntity ?? npc, npc, humanAiController.ObjectiveManager, repeat: true)
{
OverridePriority = 100.0f
OverridePriority = 100.0f,
SourceEventAction = this
};
humanAiController.ObjectiveManager.AddObjective(gotoObjective);
humanAiController.ObjectiveManager.WaitTimer = 0.0f;
}
else
{
if (gotoObjective != null)
{
gotoObjective.Abandon = true;
}
AbandonGoToObjectives(humanAiController);
}
}
isFinished = true;
@@ -62,17 +59,25 @@ namespace Barotrauma
{
foreach (var npc in affectedNpcs)
{
if (npc.Removed || npc.AIController is not HumanAIController) { continue; }
if (gotoObjective != null)
{
gotoObjective.Abandon = true;
}
if (npc.Removed || npc.AIController is not HumanAIController aiController) { continue; }
AbandonGoToObjectives(aiController);
}
affectedNpcs = null;
}
isFinished = false;
}
private void AbandonGoToObjectives(HumanAIController aiController)
{
foreach (var objective in aiController.ObjectiveManager.Objectives)
{
if (objective is AIObjectiveGoTo gotoObjective && gotoObjective.SourceEventAction?.ParentEvent == ParentEvent)
{
gotoObjective.Abandon = true;
}
}
}
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(NPCWaitAction)} -> (NPCTag: {NPCTag.ColorizeObject()}, Wait: {Wait.ColorizeObject()})";
@@ -1,7 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using System.Collections.Immutable;
namespace Barotrauma
{
@@ -11,17 +9,20 @@ namespace Barotrauma
public Identifier TargetTag { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier ItemIdentifier { get; set; }
public string ItemIdentifiers { get; set; }
[Serialize(1, IsPropertySaveable.Yes)]
public int Amount { get; set; }
public RemoveItemAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
if (ItemIdentifier.IsEmpty)
private readonly ImmutableHashSet<Identifier> itemIdentifierSplit;
public RemoveItemAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
if (string.IsNullOrEmpty(ItemIdentifiers))
{
ItemIdentifier = element.GetAttributeIdentifier("itemidentifiers", element.GetAttributeIdentifier("identifier", Identifier.Empty));
ItemIdentifiers = element.GetAttributeString("itemidentifier", element.GetAttributeString("identifier", string.Empty));
}
itemIdentifierSplit = ItemIdentifiers.Split(',').ToIdentifiers().ToImmutableHashSet();
}
private bool isFinished = false;
@@ -62,7 +63,7 @@ namespace Barotrauma
var item = inventory.FindItem(it =>
it != null &&
!removedItems.Contains(it) &&
(ItemIdentifier.IsEmpty || it.Prefab.Identifier == ItemIdentifier), recursive: true);
(itemIdentifierSplit.Count == 0 || itemIdentifierSplit.Contains(it.Prefab.Identifier)), recursive: true);
if (item == null) { break; }
Entity.Spawner.AddItemToRemoveQueue(item);
removedItems.Add(item);
@@ -70,7 +71,7 @@ namespace Barotrauma
}
else if (target is Item item)
{
if (ItemIdentifier.IsEmpty || item.Prefab.Identifier == ItemIdentifier)
if (itemIdentifierSplit.Count == 0 || itemIdentifierSplit.Contains(item.Prefab.Identifier))
{
Entity.Spawner.AddItemToRemoveQueue(item);
removedItems.Add(item);
@@ -46,43 +46,29 @@ namespace Barotrauma
switch (TargetType)
{
case ReputationType.Faction:
{
Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier == Identifier);
if (faction != null)
{
faction.Reputation.AddReputation(Increase);
}
else
{
DebugConsole.ThrowError($"Faction with the identifier \"{Identifier}\" was not found.");
}
break;
}
case ReputationType.Location:
{
Location location = campaign.Map.CurrentLocation;
if (location != null)
{
location.Reputation.AddReputation(Increase);
IEnumerable<Location> locations = location.Connections.SelectMany(c => c.Locations).Distinct().Where(l => l != null && l != location);
foreach (Location connectedLocation in locations)
Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier == Identifier);
if (faction != null)
{
Debug.Assert(connectedLocation.Reputation != null, "connectedLocation.Reputation != null");
if (connectedLocation.Reputation != null)
{
connectedLocation.Reputation.AddReputation(Increase / 4);
}
faction.Reputation.AddReputation(Increase);
}
else
{
DebugConsole.ThrowError($"Faction with the identifier \"{Identifier}\" was not found.");
}
}
break;
}
break;
}
case ReputationType.Location:
{
campaign.Map.CurrentLocation?.Reputation?.AddReputation(Increase);
break;
}
default:
{
DebugConsole.ThrowError("ReputationAction requires a \"TargetType\" but none were specified.");
break;
}
{
DebugConsole.ThrowError("ReputationAction requires a \"TargetType\" but none were specified.");
break;
}
}
}
@@ -50,7 +50,7 @@ namespace Barotrauma
public Identifier SpawnPointTag { get; set; }
[Serialize(CharacterTeamType.FriendlyNPC, IsPropertySaveable.Yes)]
public CharacterTeamType Team { get; protected set; }
public CharacterTeamType TeamID { get; protected set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Should we spawn the entity even when no spawn points with matching tags were found?")]
public bool RequireSpawnPointTag { get; set; }
@@ -92,6 +92,14 @@ namespace Barotrauma
public SpawnAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
ignoreSpawnPointType = element.GetAttribute("spawnpointtype") == null;
//backwards compatibility
TeamID = element.GetAttributeEnum("teamtag", element.GetAttributeEnum("team", TeamID));
if (element.GetAttribute("submarinetype") != null)
{
DebugConsole.ThrowError(
$"Error in even \"{(parentEvent.Prefab?.Identifier.ToString() ?? "unknown")}\". " +
$"The attribute \"submarinetype\" is not valid in {nameof(SpawnAction)}. Did you mean {nameof(SpawnLocation)}?");
}
}
public override bool IsFinished(ref string goTo)
@@ -118,7 +126,28 @@ namespace Barotrauma
if (!NPCSetIdentifier.IsEmpty && !NPCIdentifier.IsEmpty)
{
HumanPrefab humanPrefab = NPCSet.Get(NPCSetIdentifier, NPCIdentifier);
HumanPrefab humanPrefab = null;
if (Level.Loaded?.StartLocation is Location startLocation)
{
humanPrefab =
TryFindHumanPrefab(startLocation.Faction) ??
TryFindHumanPrefab(startLocation.SecondaryFaction);
}
HumanPrefab TryFindHumanPrefab(Faction faction)
{
if (faction == null) { return null; }
return
NPCSet.Get(NPCSetIdentifier,
NPCIdentifier.Replace("[faction]".ToIdentifier(), faction.Prefab.Identifier),
logError: false) ??
//try to spawn a coalition NPC if a correct one can't be found
NPCSet.Get(NPCSetIdentifier,
NPCIdentifier.Replace("[faction]".ToIdentifier(), "coalition".ToIdentifier()),
logError: false);
}
humanPrefab ??= NPCSet.Get(NPCSetIdentifier, NPCIdentifier, logError: true);
if (humanPrefab != null)
{
if (!AllowDuplicates &&
@@ -130,13 +159,13 @@ namespace Barotrauma
ISpatialEntity spawnPos = GetSpawnPos();
if (spawnPos != null)
{
Entity.Spawner.AddCharacterToSpawnQueue(CharacterPrefab.HumanSpeciesName, OffsetSpawnPos(spawnPos.WorldPosition, Offset), humanPrefab.CreateCharacterInfo(), onSpawn: newCharacter =>
Entity.Spawner.AddCharacterToSpawnQueue(CharacterPrefab.HumanSpeciesName, OffsetSpawnPos(spawnPos.WorldPosition, Rand.Range(0.0f, Offset)), humanPrefab.CreateCharacterInfo(), onSpawn: newCharacter =>
{
if (newCharacter == null) { return; }
newCharacter.HumanPrefab = humanPrefab;
newCharacter.TeamID = Team;
newCharacter.TeamID = TeamID;
newCharacter.EnableDespawn = false;
humanPrefab.GiveItems(newCharacter, newCharacter.Submarine);
humanPrefab.GiveItems(newCharacter, newCharacter.Submarine, spawnPos as WayPoint);
if (LootingIsStealing)
{
foreach (Item item in newCharacter.Inventory.FindAllItems(recursive: true))
@@ -151,6 +180,18 @@ namespace Barotrauma
ParentEvent.AddTarget(TargetTag, newCharacter);
}
spawnedEntity = newCharacter;
if (Level.Loaded?.StartOutpost?.Info is { } outPostInfo)
{
outPostInfo.AddOutpostNPCIdentifierOrTag(newCharacter, humanPrefab.Identifier);
foreach (Identifier tag in humanPrefab.GetTags())
{
outPostInfo.AddOutpostNPCIdentifierOrTag(newCharacter, tag);
}
}
#if SERVER
newCharacter.LoadTalents();
GameMain.NetworkMember.CreateEntityEvent(newCharacter, new Character.UpdateTalentsEventData());
#endif
});
}
}
@@ -165,7 +206,7 @@ namespace Barotrauma
ISpatialEntity spawnPos = GetSpawnPos();
if (spawnPos != null)
{
Entity.Spawner.AddCharacterToSpawnQueue(SpeciesName, OffsetSpawnPos(spawnPos.WorldPosition, Offset), onSpawn: newCharacter =>
Entity.Spawner.AddCharacterToSpawnQueue(SpeciesName, OffsetSpawnPos(spawnPos.WorldPosition, Rand.Range(0.0f, Offset)), onSpawn: newCharacter =>
{
if (!TargetTag.IsEmpty && newCharacter != null)
{
@@ -211,7 +252,7 @@ namespace Barotrauma
ISpatialEntity spawnPos = GetSpawnPos();
if (spawnPos != null)
{
Entity.Spawner.AddItemToSpawnQueue(itemPrefab, OffsetSpawnPos(spawnPos.WorldPosition, Offset), onSpawned: onSpawned);
Entity.Spawner.AddItemToSpawnQueue(itemPrefab, OffsetSpawnPos(spawnPos.WorldPosition, Rand.Range(0.0f, Offset)), onSpawned: onSpawned);
}
}
else
@@ -239,10 +280,10 @@ namespace Barotrauma
spawned = true;
}
public static Vector2 OffsetSpawnPos(Vector2 pos, float offsetAmount)
public static Vector2 OffsetSpawnPos(Vector2 pos, float offset)
{
Hull hull = Hull.FindHull(pos);
pos += Rand.Vector(offsetAmount);
Hull hull = Hull.FindHull(pos);
pos += Rand.Vector(offset);
if (hull != null)
{
float margin = 50.0f;
@@ -289,30 +330,24 @@ namespace Barotrauma
public static WayPoint GetSpawnPos(SpawnLocationType spawnLocation, SpawnType? spawnPointType, IEnumerable<Identifier> moduleFlags = null, IEnumerable<Identifier> spawnpointTags = null, bool asFarAsPossibleFromAirlock = false, bool requireTaggedSpawnPoint = false)
{
bool requireHull = spawnLocation == SpawnLocationType.MainSub || spawnLocation == SpawnLocationType.Outpost;
List<WayPoint> potentialSpawnPoints = WayPoint.WayPointList.FindAll(wp => IsValidSubmarineType(spawnLocation, wp.Submarine) && (wp.CurrentHull != null || !requireHull));
potentialSpawnPoints = potentialSpawnPoints.FindAll(wp => wp.ConnectedDoor == null && wp.Ladders == null && !wp.isObstructed);
List<WayPoint> potentialSpawnPoints = WayPoint.WayPointList.FindAll(wp => IsValidSubmarineType(spawnLocation, wp.Submarine) && (wp.CurrentHull != null || !requireHull));
potentialSpawnPoints = potentialSpawnPoints.FindAll(wp => wp.ConnectedDoor == null && wp.Ladders == null && wp.IsTraversable);
if (moduleFlags != null && moduleFlags.Any())
{
List<WayPoint> spawnPoints = potentialSpawnPoints.Where(wp => wp.CurrentHull?.OutpostModuleTags.Any(moduleFlags.Contains) ?? false).ToList();
var spawnPoints = potentialSpawnPoints.Where(wp => wp.CurrentHull is Hull h && h.OutpostModuleTags.Any(moduleFlags.Contains));
if (spawnPoints.Any())
{
potentialSpawnPoints = spawnPoints;
potentialSpawnPoints = spawnPoints.ToList();
}
}
if (spawnpointTags != null && spawnpointTags.Any())
{
var spawnPoints = potentialSpawnPoints
.Where(wp => spawnpointTags.Any(tag => wp.Tags.Contains(tag) && wp.ConnectedDoor == null && !wp.isObstructed));
var spawnPoints = potentialSpawnPoints.Where(wp => spawnpointTags.Any(tag => wp.Tags.Contains(tag) && wp.ConnectedDoor == null && wp.IsTraversable));
if (requireTaggedSpawnPoint || spawnPoints.Any())
{
potentialSpawnPoints = spawnPoints.ToList();
}
}
if (potentialSpawnPoints.None())
{
if (requireTaggedSpawnPoint && spawnpointTags != null && spawnpointTags.Any())
@@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma
{
@@ -7,7 +6,7 @@ namespace Barotrauma
{
private readonly List<StatusEffect> effects = new List<StatusEffect>();
private int actionIndex;
private readonly int actionIndex;
[Serialize("", IsPropertySaveable.Yes)]
public Identifier TargetTag { get; set; }
@@ -46,25 +45,40 @@ namespace Barotrauma
public override void Update(float deltaTime)
{
if (isFinished) { return; }
var targets = ParentEvent.GetTargets(TargetTag);
var eventTargets = ParentEvent.GetTargets(TargetTag);
foreach (StatusEffect effect in effects)
{
foreach (var target in targets)
foreach (var target in eventTargets)
{
if (target is Item targetItem)
if (effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
effect.Apply(effect.type, deltaTime, target, targetItem.AllPropertyObjects);
}
else
{
effect.Apply(effect.type, deltaTime, target, target as ISerializableEntity);
List<ISerializableEntity> nearbyTargets = new List<ISerializableEntity>();
effect.AddNearbyTargets(target.WorldPosition, nearbyTargets);
foreach (var nearbyTarget in nearbyTargets)
{
ApplyOnTarget(nearbyTarget as Entity, effect);
}
continue;
}
ApplyOnTarget(target, effect);
}
}
#if SERVER
ServerWrite(targets);
ServerWrite(eventTargets);
#endif
isFinished = true;
void ApplyOnTarget(Entity target, StatusEffect effect)
{
if (target is Item targetItem)
{
effect.Apply(effect.type, deltaTime, target, targetItem.AllPropertyObjects);
}
else
{
effect.Apply(effect.type, deltaTime, target, target as ISerializableEntity);
}
}
}
public override string ToDebugString()
@@ -21,6 +21,9 @@ namespace Barotrauma
[Serialize(true, IsPropertySaveable.Yes)]
public bool IgnoreIncapacitatedCharacters { get; set; }
[Serialize(false, IsPropertySaveable.Yes)]
public bool AllowHiddenItems { get; set; }
private bool isFinished = false;
public TagAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
@@ -119,12 +122,12 @@ namespace Barotrauma
private void TagItemsByIdentifier(Identifier identifier)
{
ParentEvent.AddTargetPredicate(Tag, e => e is Item it && SubmarineTypeMatches(it.Submarine) && it.Prefab.Identifier == identifier);
ParentEvent.AddTargetPredicate(Tag, e => e is Item it && IsValidItem(it) && it.Prefab.Identifier == identifier);
}
private void TagItemsByTag(Identifier tag)
{
ParentEvent.AddTargetPredicate(Tag, e => e is Item it && SubmarineTypeMatches(it.Submarine) && it.HasTag(tag));
ParentEvent.AddTargetPredicate(Tag, e => e is Item it && IsValidItem(it) && it.HasTag(tag));
}
private void TagHullsByName(Identifier name)
@@ -137,6 +140,11 @@ namespace Barotrauma
ParentEvent.AddTargetPredicate(Tag, e => e is Submarine s && SubmarineTypeMatches(s) && (type.IsEmpty || type == s.Info?.Type.ToIdentifier()));
}
private bool IsValidItem(Item it)
{
return (!it.HiddenInGame || AllowHiddenItems) && SubmarineTypeMatches(it.Submarine);
}
private bool SubmarineTypeMatches(Submarine sub)
{
if (SubmarineType == SubType.Any) { return true; }
@@ -1,12 +1,13 @@
using System.Xml.Linq;
namespace Barotrauma
namespace Barotrauma
{
class TriggerEventAction : EventAction
{
[Serialize("", IsPropertySaveable.Yes)]
public Identifier Identifier { get; set; }
[Serialize(false, IsPropertySaveable.Yes)]
public bool NextRound { get; set; }
private bool isFinished;
public TriggerEventAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
@@ -26,17 +27,24 @@ namespace Barotrauma
if (GameMain.GameSession?.EventManager != null)
{
var eventPrefab = EventSet.GetEventPrefab(Identifier);
if (eventPrefab == null)
if (NextRound)
{
DebugConsole.ThrowError($"Error in TriggerEventAction - could not find an event with the identifier {Identifier}.");
GameMain.GameSession.EventManager.QueuedEventsForNextRound.Enqueue(Identifier);
}
else
{
var ev = eventPrefab.CreateInstance();
if (ev != null)
var eventPrefab = EventSet.GetEventPrefab(Identifier);
if (eventPrefab == null)
{
GameMain.GameSession.EventManager.QueuedEvents.Enqueue(ev);
DebugConsole.ThrowError($"Error in TriggerEventAction - could not find an event with the identifier {Identifier}.");
}
else
{
var ev = eventPrefab.CreateInstance();
if (ev != null)
{
GameMain.GameSession.EventManager.QueuedEvents.Enqueue(ev);
}
}
}
}
@@ -1,6 +1,3 @@
using System;
using System.Xml.Linq;
namespace Barotrauma
{
class WaitAction : EventAction
@@ -1,9 +1,11 @@
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
@@ -12,6 +14,7 @@ namespace Barotrauma
public enum NetworkEventType
{
CONVERSATION,
CONVERSATION_SELECTED_OPTION,
STATUSEFFECT,
MISSION,
UNLOCKPATH
@@ -72,8 +75,7 @@ namespace Barotrauma
private readonly List<Event> activeEvents = new List<Event>();
private readonly HashSet<Event> finishedEvents = new HashSet<Event>();
private readonly HashSet<EventPrefab> nonRepeatableEvents = new HashSet<EventPrefab>();
private readonly HashSet<EventSet> usedUniqueSets = new HashSet<EventSet>();
private readonly HashSet<Identifier> nonRepeatableEvents = new HashSet<Identifier>();
#if DEBUG && SERVER
@@ -100,7 +102,9 @@ namespace Barotrauma
public readonly Queue<Event> QueuedEvents = new Queue<Event>();
private struct TimeStamp
public readonly Queue<Identifier> QueuedEventsForNextRound = new Queue<Identifier>();
private readonly struct TimeStamp
{
public readonly double Time;
public readonly Event Event;
@@ -122,7 +126,8 @@ namespace Barotrauma
public bool Enabled = true;
private MTRandom rand;
private MTRandom random;
private int randomSeed;
public void StartRound(Level level)
{
@@ -134,7 +139,9 @@ namespace Barotrauma
pendingEventSets.Clear();
selectedEvents.Clear();
activeEvents.Clear();
#if SERVER
MissionAction.ResetMissionsUnlockedThisRound();
#endif
pathFinder = new PathFinder(WayPoint.WayPointList, false);
totalPathLength = 0.0f;
if (level != null)
@@ -144,23 +151,22 @@ namespace Barotrauma
}
SelectSettings();
int seed = 0;
if (level != null)
{
seed = ToolBox.StringToInt(level.Seed);
randomSeed = ToolBox.StringToInt(level.Seed);
foreach (var previousEvent in level.LevelData.EventHistory)
{
seed ^= ToolBox.IdentifierToInt(previousEvent.Identifier);
randomSeed ^= ToolBox.IdentifierToInt(previousEvent);
}
}
rand = new MTRandom(seed);
random = new MTRandom(randomSeed);
bool playingCampaign = GameMain.GameSession?.GameMode is CampaignMode;
EventSet initialEventSet = SelectRandomEvents(
EventSet.Prefabs.ToList(),
requireCampaignSet: playingCampaign,
random: rand);
random: random);
EventSet additiveSet = null;
if (initialEventSet != null && initialEventSet.Additive)
{
@@ -168,7 +174,7 @@ namespace Barotrauma
initialEventSet = SelectRandomEvents(
EventSet.Prefabs.Where(e => !e.Additive).ToList(),
requireCampaignSet: playingCampaign,
random: rand);
random: random);
}
if (initialEventSet != null)
{
@@ -188,14 +194,7 @@ namespace Barotrauma
//if the outpost is connected to a locked connection, create an event to unlock it
if (level.StartLocation?.Connections.Any(c => c.Locked && level.StartLocation.MapPosition.X < c.OtherLocation(level.StartLocation).MapPosition.X) ?? false)
{
var unlockPathPrefabs = EventPrefab.Prefabs.Where(e => e.UnlockPathEvent);
var unlockPathPrefabsForBiome = unlockPathPrefabs.Where(e =>
e.BiomeIdentifier.IsEmpty ||
e.BiomeIdentifier == level.LevelData.Biome.Identifier);
var unlockPathEventPrefab = unlockPathPrefabsForBiome.Any() ?
ToolBox.SelectWeightedRandom(unlockPathPrefabsForBiome, b => b.Commonness, rand) :
ToolBox.SelectWeightedRandom(unlockPathPrefabs, b => b.Commonness, rand);
var unlockPathEventPrefab = EventPrefab.GetUnlockPathEvent(level.LevelData.Biome.Identifier, level.StartLocation.Faction);
if (unlockPathEventPrefab != null)
{
var newEvent = unlockPathEventPrefab.CreateInstance();
@@ -216,7 +215,7 @@ namespace Barotrauma
{
foreach (EventPrefab ep in eventSet.EventPrefabs.SelectMany(e => e.EventPrefabs))
{
nonRepeatableEvents.Add(ep);
nonRepeatableEvents.Add(ep.Identifier);
}
}
foreach (EventSet childSet in eventSet.ChildSets)
@@ -226,6 +225,21 @@ namespace Barotrauma
}
}
while (QueuedEventsForNextRound.Count > 0 && QueuedEventsForNextRound.Dequeue() is Identifier id)
{
var eventPrefab = EventSet.GetEventPrefab(id);
if (eventPrefab == null)
{
DebugConsole.ThrowError($"Error in EventManager.StartRound - could not find an event with the identifier {id}.");
continue;
}
var ev = eventPrefab.CreateInstance();
if (ev != null)
{
QueuedEvents.Enqueue(ev);
}
}
PreloadContent(GetFilesToPreload());
roundDuration = 0.0f;
@@ -358,7 +372,6 @@ namespace Barotrauma
QueuedEvents.Clear();
finishedEvents.Clear();
nonRepeatableEvents.Clear();
usedUniqueSets.Clear();
preloadedSprites.ForEach(s => s.Remove());
preloadedSprites.Clear();
@@ -370,20 +383,49 @@ namespace Barotrauma
/// <summary>
/// Registers the exhaustible events in the level as exhausted, and adds the current events to the event history
/// </summary>
public void RegisterEventHistory()
public void RegisterEventHistory(bool registerFinishedOnly = false)
{
if (level?.LevelData == null) { return; }
level.LevelData.EventsExhausted = true;
level.LevelData.EventsExhausted = !registerFinishedOnly;
if (level.LevelData.Type == LevelData.LevelType.Outpost)
{
level.LevelData.EventHistory.AddRange(selectedEvents.Values.SelectMany(v => v).Select(e => e.Prefab).Where(e => !level.LevelData.EventHistory.Contains(e)));
if (registerFinishedOnly)
{
foreach (var finishedEvent in finishedEvents)
{
var key = finishedEvent.ParentSet;
if (key == null) { continue; }
if (level.LevelData.FinishedEvents.ContainsKey(key))
{
level.LevelData.FinishedEvents[key] += 1;
}
else
{
level.LevelData.FinishedEvents.Add(key, 1);
}
}
}
level.LevelData.EventHistory.AddRange(selectedEvents.Values
.SelectMany(v => v)
.Select(e => e.Prefab.Identifier)
.Where(eventId => Register(eventId) && !level.LevelData.EventHistory.Contains(eventId)));
if (level.LevelData.EventHistory.Count > MaxEventHistory)
{
level.LevelData.EventHistory.RemoveRange(0, level.LevelData.EventHistory.Count - MaxEventHistory);
}
}
level.LevelData.NonRepeatableEvents.AddRange(nonRepeatableEvents.Where(e => !level.LevelData.NonRepeatableEvents.Contains(e)));
level.LevelData.NonRepeatableEvents.AddRange(nonRepeatableEvents.Where(eventId => Register(eventId) && !level.LevelData.NonRepeatableEvents.Contains(eventId)));
if (!registerFinishedOnly)
{
level.LevelData.FinishedEvents.Clear();
}
bool Register(Identifier eventId) => !registerFinishedOnly || finishedEvents.Any(fe => fe.Prefab.Identifier == eventId);
}
public void SkipEventCooldown()
@@ -393,9 +435,9 @@ namespace Barotrauma
private float CalculateCommonness(EventPrefab eventPrefab, float baseCommonness)
{
if (level.LevelData.NonRepeatableEvents.Contains(eventPrefab)) { return 0.0f; }
if (level.LevelData.NonRepeatableEvents.Contains(eventPrefab.Identifier)) { return 0.0f; }
float retVal = baseCommonness;
if (level.LevelData.EventHistory.Contains(eventPrefab)) { retVal *= 0.1f; }
if (level.LevelData.EventHistory.Contains(eventPrefab.Identifier)) { retVal *= 0.1f; }
return retVal;
}
@@ -436,9 +478,13 @@ namespace Barotrauma
}
}
bool isPrefabSuitable(EventPrefab e)
=> (e.BiomeIdentifier.IsEmpty || e.BiomeIdentifier == level.LevelData?.Biome?.Identifier) &&
!level.LevelData.NonRepeatableEvents.Contains(e);
bool isPrefabSuitable(EventPrefab e) =>
(e.BiomeIdentifier.IsEmpty || e.BiomeIdentifier == level.LevelData?.Biome?.Identifier) &&
!level.LevelData.NonRepeatableEvents.Contains(e.Identifier) &&
isFactionSuitable(e.Faction);
bool isFactionSuitable(Identifier factionId) =>
factionId.IsEmpty || factionId == level.StartLocation?.Faction?.Prefab.Identifier || factionId == level.StartLocation?.SecondaryFaction?.Prefab.Identifier;
foreach (var subEventPrefab in eventSet.EventPrefabs)
{
@@ -447,9 +493,9 @@ namespace Barotrauma
DebugConsole.ThrowError($"Error in event set \"{eventSet.Identifier}\" ({eventSet.ContentFile?.ContentPackage?.Name ?? "null"}) - could not find an event prefab with the identifier \"{missingId}\".");
}
}
var suitablePrefabSubsets = eventSet.EventPrefabs.Where(
e => e.EventPrefabs.Any(isPrefabSuitable)).ToArray();
e => isFactionSuitable(e.Faction) && e.EventPrefabs.Any(isPrefabSuitable)).ToArray();
for (int i = 0; i < applyCount; i++)
{
@@ -462,14 +508,14 @@ namespace Barotrauma
for (int j = 0; j < eventCount; j++)
{
if (unusedEvents.All(e => e.EventPrefabs.All(p => CalculateCommonness(p, e.Commonness) <= 0.0f))) { break; }
EventSet.SubEventPrefab subEventPrefab = ToolBox.SelectWeightedRandom(unusedEvents, e => e.EventPrefabs.Max(p => CalculateCommonness(p, e.Commonness)), rand);
EventSet.SubEventPrefab subEventPrefab = ToolBox.SelectWeightedRandom(unusedEvents, e => e.EventPrefabs.Max(p => CalculateCommonness(p, e.Commonness)), random);
(IEnumerable<EventPrefab> eventPrefabs, float commonness, float probability) = subEventPrefab;
if (eventPrefabs != null && rand.NextDouble() <= probability)
if (eventPrefabs != null && random.NextDouble() <= probability)
{
var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(isPrefabSuitable), e => e.Commonness, rand);
var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(isPrefabSuitable), e => e.Commonness, random);
var newEvent = eventPrefab.CreateInstance();
if (newEvent == null) { continue; }
newEvent.RandomSeed = randomSeed;
if (i < spawnPosFilter.Count) { newEvent.SpawnPosFilter = spawnPosFilter[i]; }
DebugConsole.NewMessage($"Initialized event {newEvent}", debugOnly: true);
if (!selectedEvents.ContainsKey(eventSet))
@@ -483,7 +529,7 @@ namespace Barotrauma
}
if (eventSet.ChildSets.Any())
{
var newEventSet = SelectRandomEvents(eventSet.ChildSets, random: rand);
var newEventSet = SelectRandomEvents(eventSet.ChildSets, random: random);
if (newEventSet != null)
{
CreateEvents(newEventSet);
@@ -494,9 +540,9 @@ namespace Barotrauma
{
foreach ((IEnumerable<EventPrefab> eventPrefabs, float commonness, float probability) in suitablePrefabSubsets)
{
if (rand.NextDouble() > probability) { continue; }
if (random.NextDouble() > probability) { continue; }
var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(isPrefabSuitable), e => e.Commonness, rand);
var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(isPrefabSuitable), e => e.Commonness, random);
var newEvent = eventPrefab.CreateInstance();
if (newEvent == null) { continue; }
if (!selectedEvents.ContainsKey(eventSet))
@@ -601,6 +647,10 @@ namespace Barotrauma
private bool IsValidForLocation(EventSet eventSet, Location location)
{
if (location is null) { return true; }
if (!eventSet.Faction.IsEmpty)
{
if (eventSet.Faction != location.Faction?.Prefab.Identifier && eventSet.Faction != location.SecondaryFaction?.Prefab.Identifier) { return false; }
}
var locationType = location.GetLocationType();
bool includeGenericEvents = level.Type == LevelData.LevelType.LocationConnection || !locationType.IgnoreGenericEvents;
if (includeGenericEvents && eventSet.LocationTypeIdentifiers == null) { return true; }
@@ -728,53 +778,50 @@ namespace Barotrauma
calculateDistanceTraveledTimer = CalculateDistanceTraveledInterval;
}
if (currentIntensity < eventThreshold)
bool recheck = false;
do
{
bool recheck = false;
do
recheck = false;
//activate pending event sets that can be activated
for (int i = pendingEventSets.Count - 1; i >= 0; i--)
{
recheck = false;
//activate pending event sets that can be activated
for (int i = pendingEventSets.Count - 1; i >= 0; i--)
var eventSet = pendingEventSets[i];
if (eventCoolDown > 0.0f && !eventSet.IgnoreCoolDown) { continue; }
if (currentIntensity > eventThreshold && !eventSet.IgnoreIntensity) { continue; }
if (!CanStartEventSet(eventSet)) { continue; }
pendingEventSets.RemoveAt(i);
if (selectedEvents.ContainsKey(eventSet))
{
var eventSet = pendingEventSets[i];
if (eventCoolDown > 0.0f && !eventSet.IgnoreCoolDown) { continue; }
if (!CanStartEventSet(eventSet)) { continue; }
pendingEventSets.RemoveAt(i);
if (selectedEvents.ContainsKey(eventSet))
//start events in this set
foreach (Event ev in selectedEvents[eventSet])
{
//start events in this set
foreach (Event ev in selectedEvents[eventSet])
activeEvents.Add(ev);
eventThreshold = settings.DefaultEventThreshold;
if (eventSet.TriggerEventCooldown && selectedEvents[eventSet].Any(e => e.Prefab.TriggerEventCooldown))
{
activeEvents.Add(ev);
eventThreshold = settings.DefaultEventThreshold;
if (eventSet.TriggerEventCooldown && selectedEvents[eventSet].Any(e => e.Prefab.TriggerEventCooldown))
eventCoolDown = settings.EventCooldown;
}
if (eventSet.ResetTime > 0)
{
ev.Finished += () =>
{
eventCoolDown = settings.EventCooldown;
}
if (eventSet.ResetTime > 0)
{
ev.Finished += () =>
{
pendingEventSets.Add(eventSet);
CreateEvents(eventSet);
};
}
pendingEventSets.Add(eventSet);
CreateEvents(eventSet);
};
}
}
//add child event sets to pending
foreach (EventSet childEventSet in eventSet.ChildSets)
{
pendingEventSets.Add(childEventSet);
recheck = true;
}
}
} while (recheck);
}
//add child event sets to pending
foreach (EventSet childEventSet in eventSet.ChildSets)
{
pendingEventSets.Add(childEventSet);
recheck = true;
}
}
} while (recheck);
foreach (Event ev in activeEvents)
{
@@ -782,11 +829,11 @@ namespace Barotrauma
{
ev.Update(deltaTime);
}
else if (!finishedEvents.Contains(ev))
else if (ev.Prefab != null && !finishedEvents.Any(e => e.Prefab == ev.Prefab))
{
if (level?.LevelData != null && level.LevelData.Type == LevelData.LevelType.Outpost)
{
if (!level.LevelData.EventHistory.Contains(ev.Prefab)) { level.LevelData.EventHistory.Add(ev.Prefab); }
if (!level.LevelData.EventHistory.Contains(ev.Prefab.Identifier)) { level.LevelData.EventHistory.Add(ev.Prefab.Identifier); }
}
finishedEvents.Add(ev);
}
@@ -832,30 +879,43 @@ namespace Barotrauma
monsterStrength = 0;
foreach (Character character in Character.CharacterList)
{
if (character.IsIncapacitated || !character.Enabled || character.IsPet || character.Params.CompareGroup(CharacterPrefab.HumanSpeciesName)) { continue; }
if (character.IsIncapacitated || !character.Enabled || character.IsPet) { continue; }
if (!(character.AIController is EnemyAIController enemyAI)) { continue; }
if (character.AIController is EnemyAIController enemyAI)
{
if (!enemyAI.AIParams.StayInAbyss)
{
// Ignore abyss monsters because they can stay active for quite great distances. They'll be taken into account when they target the sub.
monsterStrength += enemyAI.CombatStrength;
}
if (!enemyAI.AIParams.StayInAbyss)
if (character.CurrentHull?.Submarine?.Info != null &&
(character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine)) &&
character.CurrentHull.Submarine.Info.Type == SubmarineType.Player)
{
// Enemy onboard -> Crawler inside the sub adds 0.2 to enemy danger, Mudraptor 0.42
enemyDanger += enemyAI.CombatStrength / 500.0f;
}
else if (enemyAI.SelectedAiTarget?.Entity?.Submarine != null)
{
// Enemy outside targeting the sub or something in it
// -> One Crawler adds 0.02, a Mudraptor 0.042, a Hammerhead 0.1, and a Moloch 0.25.
enemyDanger += enemyAI.CombatStrength / 5000.0f;
}
}
else if (character.AIController is HumanAIController humanAi && !character.IsOnFriendlyTeam(CharacterTeamType.Team1))
{
// Ignore abyss monsters because they can stay active for quite great distances. They'll be taken into account when they target the sub.
monsterStrength += enemyAI.CombatStrength;
}
if (character.CurrentHull?.Submarine?.Info != null &&
(character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine)) &&
character.CurrentHull.Submarine.Info.Type == SubmarineType.Player)
{
// Enemy onboard -> Crawler inside the sub adds 0.2 to enemy danger, Mudraptor 0.42
enemyDanger += enemyAI.CombatStrength / 500.0f;
}
else if (enemyAI.SelectedAiTarget?.Entity?.Submarine != null)
{
// Enemy outside targeting the sub or something in it
// -> One Crawler adds 0.02, a Mudraptor 0.042, a Hammerhead 0.1, and a Moloch 0.25.
enemyDanger += enemyAI.CombatStrength / 5000.0f;
if (character.Submarine != null &&
Vector2.DistanceSquared(character.Submarine.WorldPosition, Submarine.MainSub.WorldPosition) < Sonar.DefaultSonarRange * Sonar.DefaultSonarRange)
{
//we have no easy way to define the strength of a human enemy (depends more on the sub and it's state than the character),
//so let's just go with a fixed value.
//5 living enemy characters in an enemy sub in sonar range is enough to bump the intensity to max
enemyDanger += 0.2f;
}
}
}
// Add a portion of the total strength of active monsters to the enemy danger so that we don't spawn too many monsters around the sub.
// On top of the existing value, so if 10 crawlers are targeting the sub simultaneously from outside, the final value would be: 0.02 x 10 + 0.2 = 0.4.
// And if they get inside, we add 0.1 per crawler on that.
@@ -1108,5 +1168,20 @@ namespace Barotrauma
return false;
}
public void Load(XElement element)
{
foreach (var id in element.GetAttributeIdentifierArray(nameof(QueuedEventsForNextRound), Array.Empty<Identifier>()))
{
QueuedEventsForNextRound.Enqueue(id);
}
}
public XElement Save()
{
return new XElement("eventmanager",
new XAttribute(nameof(QueuedEventsForNextRound),
string.Join(',', QueuedEventsForNextRound)));
}
}
}
@@ -1,6 +1,7 @@
using System;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
using System.Reflection.Emit;
namespace Barotrauma
{
@@ -14,12 +15,12 @@ namespace Barotrauma
public readonly bool TriggerEventCooldown;
public readonly float Commonness;
public readonly Identifier BiomeIdentifier;
public readonly Identifier Faction;
public readonly float SpawnDistance;
public readonly bool UnlockPathEvent;
public readonly string UnlockPathTooltip;
public readonly int UnlockPathReputation;
public readonly string UnlockPathFaction;
public EventPrefab(ContentXElement element, RandomEventsFile file, Identifier fallbackIdentifier = default)
: base(file, element.GetAttributeIdentifier("identifier", fallbackIdentifier))
@@ -40,6 +41,7 @@ namespace Barotrauma
}
BiomeIdentifier = ConfigElement.GetAttributeIdentifier("biome", Identifier.Empty);
Faction = ConfigElement.GetAttributeIdentifier("faction", Identifier.Empty);
Commonness = element.GetAttributeFloat("commonness", 1.0f);
Probability = Math.Clamp(element.GetAttributeFloat(1.0f, "probability", "spawnprobability"), 0, 1);
TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", EventType != typeof(ScriptedEvent));
@@ -47,7 +49,6 @@ namespace Barotrauma
UnlockPathEvent = element.GetAttributeBool("unlockpathevent", false);
UnlockPathTooltip = element.GetAttributeString("unlockpathtooltip", "lockedpathtooltip");
UnlockPathReputation = element.GetAttributeInt("unlockpathreputation", 0);
UnlockPathFaction = element.GetAttributeString("unlockpathfaction", "");
SpawnDistance = element.GetAttributeFloat("spawndistance", 0);
}
@@ -80,5 +81,17 @@ namespace Barotrauma
{
return $"EventPrefab ({Identifier})";
}
public static EventPrefab GetUnlockPathEvent(Identifier biomeIdentifier, Faction faction)
{
var unlockPathEvents = Prefabs.OrderBy(p => p.Identifier).Where(e => e.UnlockPathEvent);
if (faction != null && unlockPathEvents.Any(e => e.Faction == faction.Prefab.Identifier))
{
unlockPathEvents = unlockPathEvents.Where(e => e.Faction == faction.Prefab.Identifier);
}
return
unlockPathEvents.FirstOrDefault(ep => ep.BiomeIdentifier == biomeIdentifier) ??
unlockPathEvents.FirstOrDefault(ep => ep.BiomeIdentifier == Identifier.Empty);
}
}
}
@@ -1,10 +1,9 @@
using System;
using Barotrauma.Extensions;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
@@ -89,7 +88,9 @@ namespace Barotrauma
public readonly LevelData.LevelType LevelType;
public readonly ImmutableArray<Identifier> LocationTypeIdentifiers;
public readonly Identifier Faction;
public readonly bool ChooseRandom;
private readonly int eventCount = 1;
@@ -110,6 +111,8 @@ namespace Barotrauma
public readonly bool IgnoreCoolDown;
public readonly bool IgnoreIntensity;
public readonly bool PerRuin, PerCave, PerWreck;
public readonly bool DisableInHuntingGrounds;
@@ -143,11 +146,12 @@ namespace Barotrauma
public readonly struct SubEventPrefab
{
public SubEventPrefab(Either<Identifier[], EventPrefab> prefabOrIdentifiers, float? commonness, float? probability)
public SubEventPrefab(Either<Identifier[], EventPrefab> prefabOrIdentifiers, float? commonness, float? probability, Identifier factionId)
{
PrefabOrIdentifier = prefabOrIdentifiers;
SelfCommonness = commonness;
SelfProbability = probability;
Faction = factionId;
}
public readonly Either<Identifier[], EventPrefab> PrefabOrIdentifier;
@@ -178,6 +182,8 @@ namespace Barotrauma
public readonly float? SelfProbability;
public float Probability => SelfProbability ?? EventPrefabs.MaxOrNull(p => p.Probability) ?? 0.0f;
public readonly Identifier Faction;
public void Deconstruct(out IEnumerable<EventPrefab> eventPrefabs, out float commonness, out float probability)
{
eventPrefabs = EventPrefabs;
@@ -260,6 +266,8 @@ namespace Barotrauma
DebugConsole.ThrowError($"Error in event set \"{Identifier}\". \"{levelTypeStr}\" is not a valid level type.");
}
Faction = element.GetAttributeIdentifier(nameof(Faction), Identifier.Empty);
Identifier[] locationTypeStr = element.GetAttributeIdentifierArray("locationtype", null);
if (locationTypeStr != null)
{
@@ -282,6 +290,7 @@ namespace Barotrauma
PerWreck = element.GetAttributeBool("perwreck", false);
DisableInHuntingGrounds = element.GetAttributeBool("disableinhuntinggrounds", false);
IgnoreCoolDown = element.GetAttributeBool("ignorecooldown", parentSet?.IgnoreCoolDown ?? (PerRuin || PerCave || PerWreck));
IgnoreIntensity = element.GetAttributeBool("ignoreintensity", parentSet?.IgnoreIntensity ?? false);
DelayWhenCrewAway = element.GetAttributeBool("delaywhencrewaway", !PerRuin && !PerCave && !PerWreck);
OncePerLevel = element.GetAttributeBool("onceperlevel", element.GetAttributeBool("onceperoutpost", false));
TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", true);
@@ -332,15 +341,17 @@ namespace Barotrauma
Identifier[] identifiers = subElement.GetAttributeIdentifierArray("identifier", Array.Empty<Identifier>());
float commonness = subElement.GetAttributeFloat("commonness", -1f);
float probability = subElement.GetAttributeFloat("probability", -1f);
Identifier factionId = subElement.GetAttributeIdentifier(nameof(Faction), Identifier.Empty);
eventPrefabs.Add(new SubEventPrefab(
identifiers,
commonness >= 0f ? commonness : (float?)null,
probability >= 0f ? probability : (float?)null));
probability >= 0f ? probability : (float?)null,
factionId));
}
else
{
var prefab = new EventPrefab(subElement, file, $"{Identifier}-{subElement.ElementsBeforeSelf().Count()}".ToIdentifier());
eventPrefabs.Add(new SubEventPrefab(prefab, prefab.Commonness, prefab.Probability));
eventPrefabs.Add(new SubEventPrefab(prefab, prefab.Commonness, prefab.Probability, prefab.Faction));
}
break;
}
@@ -365,14 +376,36 @@ namespace Barotrauma
public float GetCommonness(Level level)
{
Identifier key = level.GenerationParams?.Identifier ?? Identifier.Empty;
return OverrideCommonness.ContainsKey(key) ? OverrideCommonness[key] : DefaultCommonness;
if (level.GenerationParams?.Identifier != null &&
OverrideCommonness.TryGetValue(level.GenerationParams.Identifier, out float generationParamsCommonness))
{
return generationParamsCommonness;
}
else if (level.StartOutpost?.Info.OutpostGenerationParams?.Identifier != null &&
OverrideCommonness.TryGetValue(level.StartOutpost.Info.OutpostGenerationParams.Identifier, out float startOutpostParamsCommonness))
{
return startOutpostParamsCommonness;
}
else if (level.EndOutpost?.Info.OutpostGenerationParams?.Identifier != null &&
OverrideCommonness.TryGetValue(level.EndOutpost.Info.OutpostGenerationParams.Identifier, out float endOutpostParamsCommonness))
{
return endOutpostParamsCommonness;
}
return DefaultCommonness;
}
public int GetEventCount(Level level)
{
if (level?.StartLocation == null || !overrideEventCount.TryGetValue(level.StartLocation.Type.Identifier, out int count)) { return eventCount; }
return count;
int finishedEventCount = 0;
if (level is not null)
{
level.LevelData.FinishedEvents.TryGetValue(this, out finishedEventCount);
}
if (level.StartLocation == null || !overrideEventCount.TryGetValue(level.StartLocation.Type.Identifier, out int count))
{
return eventCount - finishedEventCount;
}
return count - finishedEventCount;
}
public static List<string> GetDebugStatistics(int simulatedRoundCount = 100, Func<MonsterEvent, bool> filter = null, bool fullLog = false)
@@ -1,5 +1,4 @@
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
@@ -28,6 +27,8 @@ namespace Barotrauma
private const float EndDelay = 5.0f;
private float endTimer;
private bool allowOrderingRescuees;
public override bool AllowRespawn => false;
public override bool AllowUndocking
@@ -39,17 +40,17 @@ namespace Barotrauma
}
}
public override IEnumerable<Vector2> SonarPositions
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
{
get
{
if (State > 0)
if (State == 0)
{
return Enumerable.Empty<Vector2>();
return Targets.Select(t => (Prefab.SonarLabel, t.WorldPosition));
}
else
{
return Targets.Select(t => t.WorldPosition);
return Enumerable.Empty<(LocalizedString Label, Vector2 Position)>();
}
}
}
@@ -83,6 +84,8 @@ namespace Barotrauma
{
characterConfig = prefab.ConfigElement.GetChildElement("Characters");
allowOrderingRescuees = prefab.ConfigElement.GetAttributeBool(nameof(allowOrderingRescuees), true);
string msgTag = prefab.ConfigElement.GetAttributeString("hostageskilledmessage", "");
hostagesKilledMessage = TextManager.Get(msgTag).Fallback(msgTag);
@@ -144,10 +147,7 @@ namespace Barotrauma
ISpatialEntity spawnPoint = SpawnAction.GetSpawnPos(
SpawnAction.SpawnLocationType.Outpost, SpawnType.Human | SpawnType.Enemy,
moduleFlags, spawnPointTags, element.GetAttributeBool("asfaraspossible", false));
if (spawnPoint == null)
{
spawnPoint = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced();
}
spawnPoint ??= submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced();
Vector2 spawnPos = spawnPoint.WorldPosition;
if (spawnPoint is WayPoint wp && wp.CurrentHull != null && wp.CurrentHull.Rect.Width > 100)
{
@@ -186,7 +186,12 @@ namespace Barotrauma
if (element.Attribute("identifier") != null && element.Attribute("from") != null)
{
HumanPrefab humanPrefab = GetHumanPrefabFromElement(element);
HumanPrefab humanPrefab = GetHumanPrefabFromElement(element);
if (humanPrefab == null)
{
DebugConsole.ThrowError($"Couldn't spawn a human character for abandoned outpost mission: human prefab \"{element.GetAttributeString("identifier", string.Empty)}\" not found");
continue;
}
for (int i = 0; i < count; i++)
{
LoadHuman(humanPrefab, element, submarine);
@@ -198,7 +203,7 @@ namespace Barotrauma
var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName);
if (characterPrefab == null)
{
DebugConsole.ThrowError("Couldn't spawn a character for abandoned outpost mission: character prefab \"" + speciesName + "\" not found");
DebugConsole.ThrowError($"Couldn't spawn a character for abandoned outpost mission: character prefab \"{speciesName}\" not found");
continue;
}
for (int i = 0; i < count; i++)
@@ -214,19 +219,25 @@ namespace Barotrauma
{
Identifier[] moduleFlags = element.GetAttributeIdentifierArray("moduleflags", null);
Identifier[] spawnPointTags = element.GetAttributeIdentifierArray("spawnpointtags", null);
var spawnPointType = element.GetAttributeEnum("spawnpointtype", SpawnType.Human);
ISpatialEntity spawnPos = SpawnAction.GetSpawnPos(
SpawnAction.SpawnLocationType.Outpost, SpawnType.Human,
SpawnAction.SpawnLocationType.Outpost, spawnPointType,
moduleFlags ?? humanPrefab.GetModuleFlags(),
spawnPointTags ?? humanPrefab.GetSpawnPointTags(),
element.GetAttributeBool("asfaraspossible", false));
if (spawnPos == null)
{
spawnPos = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced();
}
spawnPos ??= submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced();
bool requiresRescue = element.GetAttributeBool("requirerescue", false);
Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, requiresRescue ? CharacterTeamType.FriendlyNPC : CharacterTeamType.None, spawnPos);
var teamId = element.GetAttributeEnum("teamid", requiresRescue ? CharacterTeamType.FriendlyNPC : CharacterTeamType.None);
Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, teamId, spawnPos);
if (Level.Loaded?.StartOutpost?.Info is { } outPostInfo)
{
outPostInfo.AddOutpostNPCIdentifierOrTag(spawnedCharacter, humanPrefab.Identifier);
foreach (Identifier tag in humanPrefab.GetTags())
{
outPostInfo.AddOutpostNPCIdentifierOrTag(spawnedCharacter, tag);
}
}
if (spawnPos is WayPoint wp)
{
@@ -237,9 +248,19 @@ namespace Barotrauma
{
requireRescue.Add(spawnedCharacter);
#if CLIENT
GameMain.GameSession.CrewManager.AddCharacterToCrewList(spawnedCharacter);
if (allowOrderingRescuees)
{
GameMain.GameSession.CrewManager.AddCharacterToCrewList(spawnedCharacter);
}
#endif
}
else if (TimesAttempted > 0 && spawnedCharacter.AIController is HumanAIController humanAi)
{
var order = OrderPrefab.Prefabs["fightintruders"]
.CreateInstance(OrderPrefab.OrderTargetType.Entity, orderGiver: spawnedCharacter)
.WithManualPriority(CharacterInfo.HighestManualOrderPriority);
spawnedCharacter.SetOrder(order, isNewOrder: true, speak: false);
}
if (element.GetAttributeBool("requirekill", false))
{
@@ -252,10 +273,7 @@ namespace Barotrauma
Identifier[] moduleFlags = element.GetAttributeIdentifierArray("moduleflags", null);
Identifier[] spawnPointTags = element.GetAttributeIdentifierArray("spawnpointtags", null);
ISpatialEntity spawnPos = SpawnAction.GetSpawnPos(SpawnAction.SpawnLocationType.Outpost, SpawnType.Enemy, moduleFlags, spawnPointTags, element.GetAttributeBool("asfaraspossible", false));
if (spawnPos == null)
{
spawnPos = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced();
}
spawnPos ??= submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced();
Character spawnedCharacter = Character.Create(monsterPrefab.Identifier, spawnPos.WorldPosition, ToolBox.RandomSeed(8), createNetworkEvent: false);
characters.Add(spawnedCharacter);
if (element.GetAttributeBool("requirekill", false))
@@ -18,17 +18,19 @@ namespace Barotrauma
private Ruin TargetRuin { get; set; }
public override IEnumerable<Vector2> SonarPositions
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
{
get
{
if (State == 0)
{
return allTargets.Where(t => (t is Item i && !IsItemDestroyed(i)) || (t is Character c && !IsEnemyDefeated(c))).Select(t => t.WorldPosition);
return allTargets
.Where(t => (t is Item i && !IsItemDestroyed(i)) || (t is Character c && !IsEnemyDefeated(c)))
.Select(t => (Prefab.SonarLabel, t.WorldPosition));
}
else
{
return Enumerable.Empty<Vector2>();
return Enumerable.Empty<(LocalizedString Label, Vector2 Position)>();
}
}
}
@@ -164,7 +166,7 @@ namespace Barotrauma
{
bool exitingLevel = GameMain.GameSession?.GameMode is CampaignMode campaign ?
campaign.GetAvailableTransition() != CampaignMode.TransitionType.None :
Submarine.MainSub is { } sub && (sub.AtEndExit || sub.AtStartExit);
Submarine.MainSub is { } sub && sub.AtEitherExit;
return State > 0 && exitingLevel;
}
@@ -69,15 +69,7 @@ namespace Barotrauma
}
}
public override LocalizedString SonarLabel
{
get
{
return base.SonarLabel.IsNullOrEmpty() ? sonarLabel : base.SonarLabel;
}
}
public override IEnumerable<Vector2> SonarPositions
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
{
get
{
@@ -85,7 +77,12 @@ namespace Barotrauma
{
yield break;
}
yield return level.BeaconStation.WorldPosition;
else
{
yield return (
Prefab.SonarLabel.IsNullOrEmpty() ? sonarLabel : Prefab.SonarLabel,
level.BeaconStation.WorldPosition);
}
}
}
@@ -0,0 +1,295 @@
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
namespace Barotrauma
{
partial class EndMission : Mission
{
enum MissionPhase
{
Initial,
NoItemsDestroyed,
SomeItemsDestroyed,
AllItemsDestroyed,
BossKilled
}
private readonly CharacterPrefab bossPrefab;
private readonly CharacterPrefab minionPrefab;
private readonly Identifier spawnPointTag;
private readonly Identifier destructibleItemTag;
private readonly string endCinematicSound;
private ImmutableArray<Character> minions;
private readonly int minionCount;
private readonly float minionScatter;
private Character boss;
private readonly ItemPrefab projectilePrefab;
private float projectileTimer = 30.0f;
private readonly float startCinematicDistance = 30.0f;
private float endCinematicTimer;
private readonly List<Item> destructibleItems = new List<Item>();
protected readonly float wakeUpCinematicDelay = 5.0f;
protected readonly float bossWakeUpDelay = 7.0f;
protected readonly float cameraWaitDuration = 7.0f;
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
{
get { return destructibleItems.Where(it => it.Condition > 0.0f).Select(it => (Prefab.SonarLabel, it.WorldPosition)); }
}
public override int State
{
get { return base.State; }
set
{
if (state != value)
{
base.State = value;
OnStateChangedProjSpecific();
if (Phase == MissionPhase.AllItemsDestroyed)
{
CoroutineManager.Invoke(() =>
{
if (boss != null && !boss.Removed)
{
boss.AnimController.ColliderIndex = 1;
}
}, delay: wakeUpCinematicDelay + bossWakeUpDelay + 2);
}
}
}
}
private MissionPhase Phase
{
get
{
//state 0: nothing happens yet, play a cinematic and skip to the next state when close enough to the boss
//state 1: start cinematic played
//state 2: first destructibleItems destroyed
//state 3: 2nd destructibleItems destroyed
//state 4: all destructibleItems destroyed
//state 5: boss killed
if (state == 0) { return MissionPhase.Initial; }
if (state == 1) { return MissionPhase.NoItemsDestroyed; }
if (state < destructibleItems.Count + 1) { return MissionPhase.SomeItemsDestroyed; }
if (state < destructibleItems.Count + 2) { return MissionPhase.AllItemsDestroyed; }
return MissionPhase.BossKilled;
}
}
public EndMission(MissionPrefab prefab, Location[] locations, Submarine sub)
: base(prefab, locations, sub)
{
Identifier speciesName = prefab.ConfigElement.GetAttributeIdentifier("bossfile", Identifier.Empty);
if (!speciesName.IsEmpty)
{
bossPrefab = CharacterPrefab.FindBySpeciesName(speciesName);
if (bossPrefab == null)
{
DebugConsole.ThrowError($"Error in end mission \"{prefab.Identifier}\". Could not find a character prefab with the name \"{speciesName}\".");
}
}
else
{
DebugConsole.ThrowError($"Error in end mission \"{prefab.Identifier}\". Monster file not set.");
}
Identifier minionName = prefab.ConfigElement.GetAttributeIdentifier("minionfile", Identifier.Empty);
if (!minionName.IsEmpty)
{
minionPrefab = CharacterPrefab.FindBySpeciesName(minionName);
if (minionPrefab == null)
{
DebugConsole.ThrowError($"Error in end mission \"{prefab.Identifier}\". Could not find a character prefab with the name \"{speciesName}\".");
}
}
minionCount = Math.Min(prefab.ConfigElement.GetAttributeInt(nameof(minionCount), 0), 255);
minionScatter = Math.Min(prefab.ConfigElement.GetAttributeFloat(nameof(minionScatter), 0), 10000);
Identifier projectileId = prefab.ConfigElement.GetAttributeIdentifier("projectile", Identifier.Empty);
if (!projectileId.IsEmpty)
{
projectilePrefab = MapEntityPrefab.FindByIdentifier(projectileId) as ItemPrefab;
if (projectilePrefab == null)
{
DebugConsole.ThrowError($"Error in end mission \"{prefab.Identifier}\". Could not find an item prefab with the name \"{projectileId}\".");
}
}
spawnPointTag = prefab.ConfigElement.GetAttributeIdentifier(nameof(spawnPointTag), Identifier.Empty);
destructibleItemTag = prefab.ConfigElement.GetAttributeIdentifier(nameof(destructibleItemTag), Identifier.Empty);
endCinematicSound = prefab.ConfigElement.GetAttributeString(nameof(endCinematicSound), string.Empty);
startCinematicDistance = prefab.ConfigElement.GetAttributeFloat(nameof(startCinematicDistance), 0);
}
protected override void StartMissionSpecific(Level level)
{
var spawnPoint = WayPoint.WayPointList.FirstOrDefault(wp => wp.Tags.Contains(spawnPointTag));
if (spawnPoint == null)
{
DebugConsole.ThrowError($"Error in end mission \"{Prefab.Identifier}\". Could not find a spawn point \"{spawnPointTag}\".");
return;
}
if (!IsClient)
{
boss = Character.Create(bossPrefab.Identifier, spawnPoint.WorldPosition, ToolBox.RandomSeed(8), createNetworkEvent: false);
var minionList = new List<Character>();
float angle = 0;
float angleStep = MathHelper.TwoPi / Math.Max(minionCount, 1);
for (int i = 0; i < minionCount; i++)
{
minionList.Add(Character.Create(minionPrefab.Identifier, MathUtils.GetPointOnCircumference(spawnPoint.WorldPosition, minionScatter, angle), ToolBox.RandomSeed(8), createNetworkEvent: false));
angle += angleStep;
}
SwarmBehavior.CreateSwarm(minionList.Cast<AICharacter>());
minions = minionList.ToImmutableArray();
}
if (destructibleItemTag.IsEmpty)
{
DebugConsole.ThrowError($"Error in end mission \"{Prefab.Identifier}\". Destructible item tag not set.");
return;
}
destructibleItems.Clear();
destructibleItems.AddRange(Item.ItemList.FindAll(it => it.HasTag(destructibleItemTag)));
if (destructibleItems.None())
{
DebugConsole.ThrowError($"Error in end mission \"{Prefab.Identifier}\". Could not find any destructible items with the tag \"{spawnPointTag}\".");
return;
}
}
protected override void UpdateMissionSpecific(float deltaTime)
{
UpdateProjSpecific();
if (state == 0)
{
if (startCinematicDistance <= 0.0f ||
boss == null || Submarine.MainSub == null ||
Vector2.DistanceSquared(Submarine.MainSub.WorldPosition, boss.WorldPosition) <= startCinematicDistance * startCinematicDistance)
{
State = 1;
}
return;
}
if (!IsClient && State > 0)
{
State = Math.Max(State, destructibleItems.Count(it => it.Condition <= 0.0f) + 1);
}
if (Phase == MissionPhase.AllItemsDestroyed)
{
if (projectilePrefab != null && boss != null && !boss.IsDead && !boss.Removed)
{
projectileTimer -= deltaTime;
if (projectileTimer <= 0.0f)
{
float dist = Vector2.Distance(Submarine.MainSub.WorldPosition, boss.WorldPosition);
float distanceFactor = Math.Min(dist / 10000.0f, 1.0f);
int projectileAmount = Rand.Range(3, 6);
//more concentrated shots the further the sub is
float spread = MathHelper.ToRadians(Rand.Range(20.0f, 180.0f)) * Math.Max(1.0f - distanceFactor, 0.2f);
for (int i = 0; i < projectileAmount; i++)
{
int index = i;
Entity.Spawner.AddItemToSpawnQueue(projectilePrefab, boss.WorldPosition, onSpawned: it =>
{
var projectile = it.GetComponent<Projectile>();
float angle = MathUtils.VectorToAngle(Submarine.MainSub.WorldPosition - boss.WorldPosition);
if (projectileAmount > 1)
{
angle += (index / (float)(projectileAmount - 1) - 0.5f) * spread;
}
it.body.SetTransform(it.SimPosition, angle);
it.UpdateTransform();
//faster launch velocity the further the sub is
projectile.Use(launchImpulseModifier: MathHelper.Lerp(0, 5, distanceFactor));
});
}
//the closer the sub is, more likely it is to shoot frequently
float shortIntervalProbability = MathHelper.Lerp(0.9f, 0.05f, distanceFactor);
if (Rand.Range(0.0f, 1.0f) < shortIntervalProbability)
{
projectileTimer = Rand.Range(3.0f, 5.0f);
}
else
{
projectileTimer = Rand.Range(15f, 30f);
}
}
}
else
{
State = Math.Max(destructibleItems.Count + 2, State);
}
}
else if (Phase == MissionPhase.BossKilled)
{
const float EndCinematicDuration = 20.0f;
endCinematicTimer += deltaTime;
#if CLIENT
Screen.Selected.Cam.Shake = MathHelper.Clamp(MathF.Pow(endCinematicTimer, 3), 5.0f, 200.0f);
Screen.Selected.Cam.Rotation =
Math.Max((endCinematicTimer - 5.0f) * 0.05f, 0.0f)
+ (PerlinNoise.GetPerlin(endCinematicTimer * 0.1f, endCinematicTimer * 0.05f) - 0.5f) * 0.5f * (endCinematicTimer / EndCinematicDuration);
if (Rand.Range(0.0f, 100.0f) < endCinematicTimer)
{
Level.Loaded.Renderer.Flash();
}
Level.Loaded.Renderer.ChromaticAberrationStrength = endCinematicTimer * 5;
Level.Loaded.Renderer.CollapseEffectOrigin = boss.WorldPosition;
Level.Loaded.Renderer.CollapseEffectStrength = endCinematicTimer / EndCinematicDuration;
#endif
if (endCinematicTimer > 5 && !IsClient)
{
foreach (Character c in Character.CharacterList)
{
if (c.AIController is EnemyAIController enemyAI && enemyAI.PetBehavior == null)
{
c.SetAllDamage(200.0f, 0.0f, 0.0f);
}
}
}
if (endCinematicTimer > EndCinematicDuration && !IsClient)
{
//endCinematicTimer = 0;
GameMain.GameSession.Campaign?.LoadNewLevel();
}
}
}
partial void UpdateProjSpecific();
partial void OnStateChangedProjSpecific();
protected override bool DetermineCompleted()
{
return Phase == MissionPhase.BossKilled;
}
}
}
@@ -10,11 +10,12 @@ namespace Barotrauma
{
partial class EscortMission : Mission
{
private readonly XElement characterConfig;
private readonly XElement itemConfig;
private readonly ContentXElement characterConfig;
private readonly ContentXElement itemConfig;
private readonly List<Character> characters = new List<Character>();
private readonly Dictionary<Character, List<Item>> characterItems = new Dictionary<Character, List<Item>>();
private readonly Dictionary<HumanPrefab, List<StatusEffect>> characterStatusEffects = new Dictionary<HumanPrefab, List<StatusEffect>>();
private readonly int baseEscortedCharacters;
private readonly float scalingEscortedCharacters;
@@ -28,7 +29,8 @@ namespace Barotrauma
private readonly List<Character> terroristCharacters = new List<Character>();
private bool terroristsShouldAct = false;
private float terroristDistanceSquared;
private const string TerroristTeamChangeIdentifier = "terrorist";
private const string TerroristTeamChangeIdentifier = "terrorist";
private readonly string terroristAnnounceDialogTag = string.Empty;
public EscortMission(MissionPrefab prefab, Location[] locations, Submarine sub)
: base(prefab, locations, sub)
@@ -39,6 +41,7 @@ namespace Barotrauma
scalingEscortedCharacters = prefab.ConfigElement.GetAttributeFloat("scalingescortedcharacters", 0);
terroristChance = prefab.ConfigElement.GetAttributeFloat("terroristchance", 0);
itemConfig = prefab.ConfigElement.GetChildElement("TerroristItems");
terroristAnnounceDialogTag = prefab.ConfigElement.GetAttributeString("terroristannouncedialogtag", string.Empty);
CalculateReward();
}
@@ -96,14 +99,27 @@ namespace Barotrauma
}
List<HumanPrefab> humanPrefabsToSpawn = new List<HumanPrefab>();
foreach (XElement element in characterConfig.Elements())
foreach (ContentXElement characterElement in characterConfig.Elements())
{
int count = CalculateScalingEscortedCharacterCount(inMission: true);
var humanPrefab = GetHumanPrefabFromElement(element);
var humanPrefab = GetHumanPrefabFromElement(characterElement);
for (int i = 0; i < count; i++)
{
humanPrefabsToSpawn.Add(humanPrefab);
}
foreach (var element in characterElement.Elements())
{
if (element.NameAsIdentifier() == "statuseffect")
{
var newEffect = StatusEffect.Load(element, parentDebugName: Prefab.Name.Value);
if (newEffect == null) { continue; }
if (!characterStatusEffects.ContainsKey(humanPrefab))
{
characterStatusEffects[humanPrefab] = new List<StatusEffect> { newEffect };
}
characterStatusEffects[humanPrefab].Add(newEffect);
}
}
}
//if any of the escortees have a job defined, try to use a spawnpoint designated for that job
@@ -128,6 +144,13 @@ namespace Barotrauma
{
humanAI.InitMentalStateManager();
}
if (characterStatusEffects.TryGetValue(humanPrefab, out var statusEffectList))
{
foreach (var statusEffect in statusEffectList)
{
statusEffect.Apply(statusEffect.type, 1.0f, spawnedCharacter, spawnedCharacter);
}
}
}
@@ -162,7 +185,7 @@ namespace Barotrauma
}
int i = 0;
foreach (XElement element in characterConfig.Elements())
foreach (ContentXElement element in characterConfig.Elements())
{
string escortIdentifier = element.GetAttributeString("escortidentifier", string.Empty);
string colorIdentifier = element.GetAttributeString("color", string.Empty);
@@ -231,7 +254,10 @@ namespace Barotrauma
if (IsAlive(character) && !character.IsIncapacitated && !character.LockHands)
{
character.TryAddNewTeamChange(TerroristTeamChangeIdentifier, new ActiveTeamChange(CharacterTeamType.None, ActiveTeamChange.TeamChangePriorities.Willful, aggressiveBehavior: true));
character.Speak(TextManager.Get("dialogterroristannounce").Value, null, Rand.Range(0.5f, 3f));
if (!string.IsNullOrEmpty(terroristAnnounceDialogTag))
{
character.Speak(TextManager.Get("dialogterroristannounce").Value, null, Rand.Range(0.5f, 3f));
}
XElement randomElement = itemConfig.Elements().GetRandomUnsynced(e => e.GetAttributeFloat(0f, "mindifficulty") <= Level.Loaded.Difficulty);
if (randomElement != null)
{
@@ -1,4 +1,6 @@
namespace Barotrauma
using System;
namespace Barotrauma
{
partial class GoToMission : Mission
{
@@ -11,7 +13,7 @@
{
if (Level.Loaded?.Type == LevelData.LevelType.Outpost)
{
State = 1;
State = Math.Max(1, State);
}
}
@@ -11,20 +11,7 @@ namespace Barotrauma
{
partial class MineralMission : Mission
{
private struct ResourceCluster
{
public int Amount;
public float Rotation;
public ResourceCluster(int amount, float rotation)
{
Amount = amount;
Rotation = rotation;
}
public static implicit operator ResourceCluster((int amount, float rotation) tuple) => new ResourceCluster(tuple.amount, tuple.rotation);
}
private readonly Dictionary<Identifier, ResourceCluster> resourceClusters = new Dictionary<Identifier, ResourceCluster>();
private readonly Dictionary<Identifier, int> resourceAmounts = new Dictionary<Identifier, int>();
private readonly Dictionary<Identifier, List<Item>> spawnedResources = new Dictionary<Identifier, List<Item>>();
private readonly Dictionary<Identifier, Item[]> relevantLevelResources = new Dictionary<Identifier, Item[]>();
private readonly List<(Identifier Identifier, Vector2 Position)> missionClusterPositions = new List<(Identifier Identifier, Vector2 Position)>();
@@ -50,13 +37,13 @@ namespace Barotrauma
/// </summary>
private readonly float resourceHandoverAmount;
public override IEnumerable<Vector2> SonarPositions
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
{
get
{
return missionClusterPositions
.Where(p => spawnedResources.ContainsKey(p.Item1) && AnyAreUncollected(spawnedResources[p.Item1]))
.Select(p => p.Item2);
.Where(p => spawnedResources.ContainsKey(p.Identifier) && AnyAreUncollected(spawnedResources[p.Identifier]))
.Select(p => (ModifyMessage(Prefab.SonarLabel, color: false), p.Position));
}
}
@@ -64,7 +51,6 @@ namespace Barotrauma
public override LocalizedString FailureMessage => ModifyMessage(base.FailureMessage);
public override LocalizedString Description => ModifyMessage(description);
public override LocalizedString Name => ModifyMessage(base.Name, false);
public override LocalizedString SonarLabel => ModifyMessage(base.SonarLabel, false);
public MineralMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub)
{
@@ -82,13 +68,13 @@ namespace Barotrauma
{
var identifier = c.GetAttributeIdentifier("identifier", Identifier.Empty);
if (identifier.IsEmpty) { continue; }
if (resourceClusters.ContainsKey(identifier))
if (resourceAmounts.ContainsKey(identifier))
{
resourceClusters[identifier] = (resourceClusters[identifier].Amount + 1, resourceClusters[identifier].Rotation);
resourceAmounts[identifier]++;
}
else
{
resourceClusters.Add(identifier, (1, 0.0f));
resourceAmounts.Add(identifier, 1);
}
}
}
@@ -129,7 +115,7 @@ namespace Barotrauma
if (IsClient) { return; }
foreach ((Identifier identifier, ResourceCluster cluster) in resourceClusters)
foreach ((Identifier identifier, int amount) in resourceAmounts)
{
if (MapEntityPrefab.FindByIdentifier(identifier) is not ItemPrefab prefab)
{
@@ -137,10 +123,10 @@ namespace Barotrauma
continue;
}
var spawnedResources = level.GenerateMissionResources(prefab, cluster.Amount, positionType, out float rotation, caves);
if (spawnedResources.Count < cluster.Amount)
var spawnedResources = level.GenerateMissionResources(prefab, amount, positionType, caves);
if (spawnedResources.Count < amount)
{
DebugConsole.ThrowError($"Error in MineralMission: spawned only {spawnedResources.Count}/{cluster.Amount} of {prefab.Name}");
DebugConsole.ThrowError($"Error in MineralMission: spawned only {spawnedResources.Count}/{amount} of {prefab.Name}");
}
if (spawnedResources.None()) { continue; }
@@ -175,7 +161,7 @@ namespace Barotrauma
State = 1;
break;
case 1:
if (!Submarine.MainSub.AtEndExit && !Submarine.MainSub.AtStartExit) { return; }
if (!Submarine.MainSub.AtEitherExit) { return; }
State = 2;
break;
}
@@ -195,7 +181,7 @@ namespace Barotrauma
{
// When mission is completed successfully, half of the resources will be removed from the player (i.e. given to the outpost as a part of the mission)
var handoverResources = new List<Item>();
foreach (Identifier identifier in resourceClusters.Keys)
foreach (Identifier identifier in resourceAmounts.Keys)
{
if (relevantLevelResources.TryGetValue(identifier, out var availableResources))
{
@@ -232,11 +218,11 @@ namespace Barotrauma
private void FindRelevantLevelResources()
{
relevantLevelResources.Clear();
foreach (var identifier in resourceClusters.Keys)
foreach (var identifier in resourceAmounts.Keys)
{
var items = Item.ItemList.Where(i => i.Prefab.Identifier == identifier &&
i.Submarine == null && i.ParentInventory == null &&
(!(i.GetComponent<Holdable>() is Holdable h) || (h.Attachable && h.Attached)))
(i.GetComponent<Holdable>() is not Holdable h || (h.Attachable && h.Attached)))
.ToArray();
relevantLevelResources.Add(identifier, items);
}
@@ -244,12 +230,12 @@ namespace Barotrauma
private bool EnoughHaveBeenCollected()
{
foreach (var kvp in resourceClusters)
foreach (var kvp in resourceAmounts)
{
if (relevantLevelResources.TryGetValue(kvp.Key, out var availableResources))
{
var collected = availableResources.Count(HasBeenCollected);
var needed = kvp.Value.Amount;
var needed = kvp.Value;
if (collected < needed) { return false; }
}
else
@@ -300,10 +286,10 @@ namespace Barotrauma
protected override LocalizedString ModifyMessage(LocalizedString message, bool color = true)
{
int i = 1;
foreach ((Identifier identifier, ResourceCluster cluster) in resourceClusters)
foreach ((Identifier identifier, int amount) in resourceAmounts)
{
Replace($"[resourcename{i}]", ItemPrefab.FindByIdentifier(identifier)?.Name.Value ?? "");
Replace($"[resourcequantity{i}]", cluster.Amount.ToString());
Replace($"[resourcequantity{i}]", amount.ToString());
i++;
}
Replace("[handoverpercentage]", ToolBox.GetFormattedPercentage(resourceHandoverAmount));
@@ -22,7 +22,7 @@ namespace Barotrauma
public virtual int State
{
get { return state; }
protected set
set
{
if (state != value)
{
@@ -30,6 +30,11 @@ namespace Barotrauma
TryTriggerEvents(state);
#if SERVER
GameMain.Server?.UpdateMissionState(this);
#elif CLIENT
if (Prefab.ShowProgressBar)
{
CharacterHUD.ShowMissionProgressBar(this);
}
#endif
ShowMessage(State);
OnMissionStateChanged?.Invoke(this);
@@ -37,6 +42,8 @@ namespace Barotrauma
}
}
public int TimesAttempted { get; set; }
protected static bool IsClient => GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient;
private readonly CheckDataAction completeCheckDataAction;
@@ -44,6 +51,12 @@ namespace Barotrauma
public readonly ImmutableArray<LocalizedString> Headers;
public readonly ImmutableArray<LocalizedString> Messages;
/// <summary>
/// The reward that was actually given from completing the mission, taking any talent bonuses into account
/// (some of which may not be possible to determine in advance)
/// </summary>
private int? finalReward;
public virtual LocalizedString Name => Prefab.Name;
private readonly LocalizedString successMessage;
@@ -113,15 +126,19 @@ namespace Barotrauma
get { return null; }
}
public virtual IEnumerable<Vector2> SonarPositions
public virtual IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
{
get { return Enumerable.Empty<Vector2>(); }
get { return Enumerable.Empty<(LocalizedString Label, Vector2 Position)>(); }
}
public virtual LocalizedString SonarLabel => Prefab.SonarLabel;
public Identifier SonarIconIdentifier => Prefab.SonarIconIdentifier;
/// <summary>
/// Where was this mission received from? Affects which faction we give reputation for if the mission is configured to give reputation for the faction that gave the mission.
/// Defaults to Locations[0]
/// </summary>
public Location OriginLocation;
public readonly Location[] Locations;
public int? Difficulty
@@ -141,7 +158,7 @@ namespace Barotrauma
}
}
private List<DelayedTriggerEvent> delayedTriggerEvents = new List<DelayedTriggerEvent>();
private readonly List<DelayedTriggerEvent> delayedTriggerEvents = new List<DelayedTriggerEvent>();
public Action<Mission> OnMissionStateChanged;
@@ -157,12 +174,13 @@ namespace Barotrauma
Headers = prefab.Headers;
var messages = prefab.Messages.ToArray();
OriginLocation = locations[0];
Locations = locations;
var endConditionElement = prefab.ConfigElement.GetChildElement(nameof(completeCheckDataAction));
if (endConditionElement != null)
{
completeCheckDataAction = new CheckDataAction(endConditionElement, $"Mission ({prefab.Identifier.ToString()})");
completeCheckDataAction = new CheckDataAction(endConditionElement, $"Mission ({prefab.Identifier})");
}
for (int n = 0; n < 2; n++)
@@ -307,7 +325,7 @@ namespace Barotrauma
private void TryTriggerEvent(MissionPrefab.TriggerEvent trigger)
{
if (trigger.CampaignOnly && GameMain.GameSession?.Campaign == null) { return; }
if (trigger.Delay > 0)
if (trigger.Delay > 0 || trigger.State == 0)
{
if (!delayedTriggerEvents.Any(t => t.TriggerEvent == trigger))
{
@@ -357,6 +375,8 @@ namespace Barotrauma
GiveReward();
}
TimesAttempted++;
EndMissionSpecific(completed);
}
@@ -364,6 +384,27 @@ namespace Barotrauma
protected virtual void EndMissionSpecific(bool completed) { }
/// <summary>
/// Get the final reward, taking talent bonuses into account if the mission has concluded and the talents modified the reward accordingly.
/// </summary>
public int GetFinalReward(Submarine sub)
{
return finalReward ?? GetReward(sub);
}
/// <summary>
/// Calculates the final reward after talent bonuses have been applied. Note that this triggers talent effects of the type OnGainMissionMoney,
/// and should only be called once when the mission is completed!
/// </summary>
private void CalculateFinalReward(Submarine sub)
{
int reward = GetReward(sub);
IEnumerable<Character> crewCharacters = GameSession.GetSessionCrewCharacters(CharacterType.Both);
var missionMoneyGainMultiplier = new AbilityMissionMoneyGainMultiplier(this, 1f);
crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnGainMissionMoney, missionMoneyGainMultiplier));
crewCharacters.ForEach(c => missionMoneyGainMultiplier.Value += c.GetStatValue(StatTypes.MissionMoneyGainMultiplier));
finalReward = (int)(reward * missionMoneyGainMultiplier.Value);
}
private void GiveReward()
{
@@ -407,39 +448,35 @@ namespace Barotrauma
info?.GiveExperience((int)((experienceGain * experienceGainMultiplier.Value) * experienceGainMultiplierIndividual.Value));
}
// apply money gains afterwards to prevent them from affecting XP gains
var missionMoneyGainMultiplier = new AbilityMissionMoneyGainMultiplier(this, 1f);
crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnGainMissionMoney, missionMoneyGainMultiplier));
crewCharacters.ForEach(c => missionMoneyGainMultiplier.Value += c.GetStatValue(StatTypes.MissionMoneyGainMultiplier));
int totalReward = (int)(reward * missionMoneyGainMultiplier.Value);
GameAnalyticsManager.AddMoneyGainedEvent(totalReward, GameAnalyticsManager.MoneySource.MissionReward, Prefab.Identifier.Value);
CalculateFinalReward(Submarine.MainSub);
#if SERVER
totalReward = DistributeRewardsToCrew(GameSession.GetSessionCrewCharacters(CharacterType.Player), totalReward);
finalReward = DistributeRewardsToCrew(GameSession.GetSessionCrewCharacters(CharacterType.Player), finalReward.Value);
#endif
bool isSingleplayerOrServer = GameMain.IsSingleplayer || GameMain.NetworkMember is { IsServer: true };
if (isSingleplayerOrServer && totalReward > 0)
if (isSingleplayerOrServer)
{
campaign.Bank.Give(totalReward);
}
foreach (Character character in crewCharacters)
{
character.Info.MissionsCompletedSinceDeath++;
}
foreach (KeyValuePair<Identifier, float> reputationReward in ReputationRewards)
{
if (reputationReward.Key == "location")
if (finalReward > 0)
{
Locations[0].Reputation.AddReputation(reputationReward.Value);
Locations[1].Reputation.AddReputation(reputationReward.Value);
campaign.Bank.Give(finalReward.Value);
}
else
foreach (Character character in crewCharacters)
{
Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier == reputationReward.Key);
if (faction != null) { faction.Reputation.AddReputation(reputationReward.Value); }
character.Info.MissionsCompletedSinceDeath++;
}
foreach (KeyValuePair<Identifier, float> reputationReward in ReputationRewards)
{
if (reputationReward.Key == "location")
{
OriginLocation.Reputation?.AddReputation(reputationReward.Value);
}
else
{
Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier == reputationReward.Key);
float prevValue = faction.Reputation.Value;
faction?.Reputation.AddReputation(reputationReward.Value);
}
}
}
@@ -484,18 +521,15 @@ namespace Barotrauma
float rewardWeight = sum > 100 ? rewardDistribution / sum : rewardDistribution / 100f;
int rewardPercentage = (int)(rewardWeight * 100);
return reward switch
{
Some<int> { Value: var amount } => ((int)(amount * rewardWeight), rewardPercentage, sum),
None<int> _ => (0, rewardPercentage, sum),
_ => throw new ArgumentOutOfRangeException()
};
int amount = reward.TryUnwrap(out var a) ? a : 0;
return ((int)(amount * rewardWeight), rewardPercentage, sum);
}
protected void ChangeLocationType(LocationTypeChange change)
{
if (change == null) { throw new ArgumentException(); }
if (GameMain.GameSession.GameMode is CampaignMode && !IsClient)
if (GameMain.GameSession.GameMode is CampaignMode campaign && !IsClient)
{
int srcIndex = -1;
for (int i = 0; i < Locations.Length; i++)
@@ -509,13 +543,15 @@ namespace Barotrauma
if (srcIndex == -1) { return; }
var location = Locations[srcIndex];
if (location.LocationTypeChangesBlocked) { return; }
if (change.RequiredDurationRange.X > 0)
{
location.PendingLocationTypeChange = (change, Rand.Range(change.RequiredDurationRange.X, change.RequiredDurationRange.Y), Prefab);
}
else
{
location.ChangeType(LocationType.Prefabs[change.ChangeToType]);
location.ChangeType(campaign, LocationType.Prefabs[change.ChangeToType]);
location.LocationTypeChangeCooldown = change.CooldownAfterChange;
}
}
@@ -529,7 +565,6 @@ namespace Barotrauma
if (element.Attribute("name") != null)
{
DebugConsole.ThrowError("Error in mission \"" + Name + "\" - use character identifiers instead of names to configure the characters.");
return null;
}
@@ -538,7 +573,7 @@ namespace Barotrauma
HumanPrefab humanPrefab = NPCSet.Get(characterFrom, characterIdentifier);
if (humanPrefab == null)
{
DebugConsole.ThrowError("Couldn't spawn character for mission: character prefab \"" + characterIdentifier + "\" not found");
DebugConsole.ThrowError($"Couldn't spawn character for mission: character prefab \"{characterIdentifier}\" not found in the NPC set \"{characterFrom}\".");
return null;
}
@@ -557,8 +592,7 @@ namespace Barotrauma
Character spawnedCharacter = Character.Create(characterInfo.SpeciesName, positionToStayIn.WorldPosition, ToolBox.RandomSeed(8), characterInfo, createNetworkEvent: false);
spawnedCharacter.HumanPrefab = humanPrefab;
humanPrefab.InitializeCharacter(spawnedCharacter, positionToStayIn);
humanPrefab.GiveItems(spawnedCharacter, submarine, Rand.RandSync.ServerAndClient, createNetworkEvents: false);
humanPrefab.GiveItems(spawnedCharacter, submarine, positionToStayIn as WayPoint, Rand.RandSync.ServerAndClient, createNetworkEvents: false);
characters.Add(spawnedCharacter);
characterItems.Add(spawnedCharacter, spawnedCharacter.Inventory.FindAllItems(recursive: true));
@@ -25,7 +25,8 @@ namespace Barotrauma
GoTo = 0x400,
ScanAlienRuins = 0x800,
ClearAlienRuins = 0x1000,
All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | Combat | AbandonedOutpost | Escort | Pirate | GoTo | ScanAlienRuins | ClearAlienRuins
End = 0x2000,
All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | Combat | AbandonedOutpost | Escort | Pirate | GoTo | ScanAlienRuins | ClearAlienRuins | End
}
partial class MissionPrefab : PrefabWithUintIdentifier
@@ -45,14 +46,15 @@ namespace Barotrauma
{ MissionType.Pirate, typeof(PirateMission) },
{ MissionType.GoTo, typeof(GoToMission) },
{ MissionType.ScanAlienRuins, typeof(ScanMission) },
{ MissionType.ClearAlienRuins, typeof(AlienRuinMission) }
{ MissionType.ClearAlienRuins, typeof(AlienRuinMission) },
{ MissionType.End, typeof(EndMission) }
};
public static readonly Dictionary<MissionType, Type> PvPMissionClasses = new Dictionary<MissionType, Type>()
{
{ MissionType.Combat, typeof(CombatMission) }
};
public static readonly HashSet<MissionType> HiddenMissionClasses = new HashSet<MissionType>() { MissionType.GoTo };
public static readonly HashSet<MissionType> HiddenMissionClasses = new HashSet<MissionType>() { MissionType.GoTo, MissionType.End };
private readonly ConstructorInfo constructor;
@@ -62,11 +64,7 @@ namespace Barotrauma
public readonly Identifier TextIdentifier;
private readonly string[] tags;
public IEnumerable<string> Tags
{
get { return tags; }
}
public readonly ImmutableHashSet<Identifier> Tags;
public readonly LocalizedString Name;
public readonly LocalizedString Description;
@@ -93,10 +91,24 @@ namespace Barotrauma
public readonly bool AllowRetry;
public readonly bool ShowInMenus, ShowStartMessage;
public readonly bool IsSideObjective;
public readonly bool AllowOtherMissionsInLevel;
public readonly bool RequireWreck, RequireRuin;
/// <summary>
/// If enabled, locations this mission takes place in cannot change their type
/// </summary>
public readonly bool BlockLocationTypeChanges;
public readonly bool ShowProgressBar;
public readonly bool ShowProgressInNumbers;
public readonly int MaxProgressState;
public readonly LocalizedString ProgressBarLabel;
/// <summary>
/// The mission can only be received when travelling from a location of the first type to a location of the second type
/// </summary>
@@ -144,7 +156,7 @@ namespace Barotrauma
TextIdentifier = element.GetAttributeIdentifier("textidentifier", Identifier);
tags = element.GetAttributeStringArray("tags", Array.Empty<string>(), convertToLowerInvariant: true);
Tags = element.GetAttributeIdentifierArray("tags", Array.Empty<Identifier>()).ToImmutableHashSet();
string nameTag = element.GetAttributeString("name", "");
Name = TextManager.Get($"MissionName.{TextIdentifier}");
@@ -167,16 +179,26 @@ namespace Barotrauma
Reward = element.GetAttributeInt("reward", 1);
AllowRetry = element.GetAttributeBool("allowretry", false);
ShowInMenus = element.GetAttributeBool("showinmenus", true);
ShowStartMessage = element.GetAttributeBool("showstartmessage", true);
IsSideObjective = element.GetAttributeBool("sideobjective", false);
RequireWreck = element.GetAttributeBool("requirewreck", false);
RequireRuin = element.GetAttributeBool("requireruin", false);
BlockLocationTypeChanges = element.GetAttributeBool(nameof(BlockLocationTypeChanges), false);
Commonness = element.GetAttributeInt("commonness", 1);
AllowOtherMissionsInLevel = element.GetAttributeBool("allowothermissionsinlevel", true);
if (element.GetAttribute("difficulty") != null)
{
int difficulty = element.GetAttributeInt("difficulty", MinDifficulty);
Difficulty = Math.Clamp(difficulty, MinDifficulty, MaxDifficulty);
}
ShowProgressBar = element.GetAttributeBool(nameof(ShowProgressBar), false);
ShowProgressInNumbers = element.GetAttributeBool(nameof(ShowProgressInNumbers), false);
MaxProgressState = element.GetAttributeInt(nameof(MaxProgressState), 1);
string progressBarLabel = element.GetAttributeString(nameof(ProgressBarLabel), "");
ProgressBarLabel = TextManager.Get(progressBarLabel).Fallback(progressBarLabel);
string successMessageTag = element.GetAttributeString("successmessage", "");
SuccessMessage = TextManager.Get($"MissionSuccess.{TextIdentifier}");
if (!string.IsNullOrEmpty(successMessageTag))
@@ -350,6 +372,7 @@ namespace Barotrauma
{
return
AllowedLocationTypes.Any(lt => lt == "any") ||
AllowedLocationTypes.Any(lt => lt == "anyoutpost" && from.HasOutpost()) ||
AllowedLocationTypes.Any(lt => lt == from.Type.Identifier);
}
@@ -357,11 +380,11 @@ namespace Barotrauma
{
if (fromType == "any" ||
fromType == from.Type.Identifier ||
(fromType == "anyoutpost" && from.HasOutpost()))
(fromType == "anyoutpost" && from.HasOutpost() && from.Type.Identifier != "abandoned"))
{
if (toType == "any" ||
toType == to.Type.Identifier ||
(toType == "anyoutpost" && to.HasOutpost()))
(toType == "anyoutpost" && to.HasOutpost() && to.Type.Identifier != "abandoned"))
{
return true;
}
@@ -16,17 +16,20 @@ namespace Barotrauma
private readonly Level.PositionType spawnPosType;
private Vector2? spawnPos = null;
public override IEnumerable<Vector2> SonarPositions
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
{
get
{
if (State > 0)
{
return Enumerable.Empty<Vector2>();
yield break;
}
else
{
return sonarPositions;
foreach (Vector2 sonarPos in sonarPositions)
{
yield return (Prefab.SonarLabel, sonarPos);
}
}
}
}
@@ -31,17 +31,17 @@ namespace Barotrauma
private Vector2 nestPosition;
public override IEnumerable<Vector2> SonarPositions
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
{
get
{
if (State > 0)
{
Enumerable.Empty<Vector2>();
yield break;
}
else
{
yield return nestPosition;
yield return (Prefab.SonarLabel, nestPosition);
}
}
}
@@ -260,9 +260,25 @@ namespace Barotrauma
int amount = Rand.Range(monster.Item2.X, monster.Item2.Y + 1);
for (int i = 0; i < amount; i++)
{
Character.Create(monster.Item1.Identifier, nestPosition + Rand.Vector(100.0f), ToolBox.RandomSeed(8), createNetworkEvent: true);
Vector2 offsetPosition;
int tries = 0;
do
{
offsetPosition = nestPosition + Rand.Vector(100.0f);
tries++;
if (tries > 10)
{
offsetPosition = nestPosition;
break;
}
} while (Level.Loaded.IsPositionInsideWall(offsetPosition));
Character.Create(monster.Item1.Identifier, offsetPosition, ToolBox.RandomSeed(8), createNetworkEvent: true);
}
}
if (Level.Loaded.IsPositionInsideWall(nestPosition))
{
DebugConsole.AddWarning($"Error in nest mission \"{Prefab.Identifier}\": nest position was inside a wall ({nestPosition}).");
}
monsterPrefabs.Clear();
break;
}
@@ -274,7 +290,7 @@ namespace Barotrauma
break;
case 1:
if (!Submarine.MainSub.AtEndExit && !Submarine.MainSub.AtStartExit) { return; }
if (!Submarine.MainSub.AtEitherExit) { return; }
State = 2;
break;
}
@@ -36,23 +36,32 @@ namespace Barotrauma
private readonly List<Vector2> patrolPositions = new List<Vector2>();
public override IEnumerable<Vector2> SonarPositions
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
{
get
{
var empty = Enumerable.Empty<Vector2>();
if (outsideOfSonarRange)
if (!outsideOfSonarRange || state > 1)
{
return State switch
{
0 => patrolPositions,
1 => lastSighting.HasValue ? lastSighting.Value.ToEnumerable() : empty,
_ => empty,
};
yield break;
}
else
else if (state == 0)
{
return empty;
foreach (Vector2 patrolPos in patrolPositions)
{
yield return (Prefab.SonarLabel, patrolPos);
}
}
else if (state == 1)
{
if (lastSighting.HasValue)
{
yield return (Prefab.SonarLabel, lastSighting.Value);
}
else
{
yield break;
}
}
}
}
@@ -85,6 +94,31 @@ namespace Barotrauma
characterTypeConfig = prefab.ConfigElement.GetChildElement("CharacterTypes");
addedMissionDifficultyPerPlayer = prefab.ConfigElement.GetAttributeFloat("addedmissiondifficultyperplayer", 0);
//make sure all referenced character types are defined
foreach (XElement characterElement in characterConfig.Elements())
{
var characterId = characterElement.GetAttributeString("typeidentifier", string.Empty);
var characterTypeElement = characterTypeConfig.Elements().FirstOrDefault(e => e.GetAttributeString("typeidentifier", string.Empty) == characterId);
if (characterTypeElement == null)
{
DebugConsole.ThrowError($"Error in mission \"{prefab.Identifier}\". Could not find a character type element for the character \"{characterId}\".");
}
}
//make sure all defined character types can be found from human prefabs
foreach (XElement characterTypeElement in characterTypeConfig.Elements())
{
foreach (XElement characterElement in characterTypeElement.Elements())
{
Identifier characterIdentifier = characterElement.GetAttributeIdentifier("identifier", Identifier.Empty);
Identifier characterFrom = characterElement.GetAttributeIdentifier("from", Identifier.Empty);
HumanPrefab humanPrefab = NPCSet.Get(characterFrom, characterIdentifier);
if (humanPrefab == null)
{
DebugConsole.ThrowError($"Error in mission \"{prefab.Identifier}\". Character prefab \"{characterIdentifier}\" not found in the NPC set \"{characterFrom}\".");
}
}
}
// for campaign missions, set level at construction
LevelData levelData = locations[0].Connections.Where(c => c.Locations.Contains(locations[1])).FirstOrDefault()?.LevelData ?? locations[0]?.LevelData;
if (levelData != null)
@@ -100,6 +134,7 @@ namespace Barotrauma
//level already set
return;
}
submarineInfo = null;
levelData = level;
missionDifficulty = level?.Difficulty ?? 0;
@@ -117,8 +152,15 @@ namespace Barotrauma
DebugConsole.ThrowError($"No path used for submarine for the pirate mission \"{Prefab.Identifier}\"!");
return;
}
// maybe a little redundant
var contentFile = ContentPackageManager.EnabledPackages.All.SelectMany(p => p.GetFiles<EnemySubmarineFile>()).FirstOrDefault(x => x.Path == submarinePath);
BaseSubFile contentFile =
GetSubFile<EnemySubmarineFile>(submarinePath) ??
GetSubFile<SubmarineFile>(submarinePath);
BaseSubFile GetSubFile<T>(ContentPath path) where T : BaseSubFile
{
return ContentPackageManager.EnabledPackages.All.SelectMany(p => p.GetFiles<T>()).FirstOrDefault(f => f.Path == submarinePath);
}
if (contentFile == null)
{
DebugConsole.ThrowError($"No submarine file found from the path {submarinePath}!");
@@ -241,9 +283,10 @@ namespace Barotrauma
// it is possible to get more than the "max" amount of characters if the modified difficulty is high enough; this is intentional
// if necessary, another "hard max" value could be used to clamp the value for performance/gameplay concerns
int amountCreated = GetDifficultyModifiedAmount(element.GetAttributeInt("minamount", 0), element.GetAttributeInt("maxamount", 0), enemyCreationDifficulty, rand);
var characterId = element.GetAttributeString("typeidentifier", string.Empty);
for (int i = 0; i < amountCreated; i++)
{
XElement characterType = characterTypeConfig.Elements().Where(e => e.GetAttributeString("typeidentifier", string.Empty) == element.GetAttributeString("typeidentifier", string.Empty)).FirstOrDefault();
XElement characterType = characterTypeConfig.Elements().Where(e => e.GetAttributeString("typeidentifier", string.Empty) == characterId).FirstOrDefault();
if (characterType == null)
{
@@ -253,7 +296,10 @@ namespace Barotrauma
XElement variantElement = GetRandomDifficultyModifiedElement(characterType, enemyCreationDifficulty, RandomnessModifier);
Character spawnedCharacter = CreateHuman(GetHumanPrefabFromElement(variantElement), characters, characterItems, enemySub, CharacterTeamType.None, null);
var humanPrefab = GetHumanPrefabFromElement(variantElement);
if (humanPrefab == null) { continue; }
Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, enemySub, CharacterTeamType.None, null);
if (!commanderAssigned)
{
bool isCommander = variantElement.GetAttributeBool("iscommander", false);
@@ -305,8 +351,9 @@ namespace Barotrauma
if (enemySub == null)
{
DebugConsole.ThrowError($"Enemy Submarine was not created. SubmarineInfo is likely not defined.");
// TODO: should we set the state to something here?
DebugConsole.ThrowError(submarineInfo == null ?
$"Error in PirateMission: enemy sub was not created (submarineInfo == null)." :
$"Error in PirateMission: enemy sub was not created.");
return;
}
@@ -345,10 +392,11 @@ namespace Barotrauma
protected override void UpdateMissionSpecific(float deltaTime)
{
if (state >= 2) { return; }
if (state >= 2 || enemySub == null) { return; }
float sqrSonarRange = MathUtils.Pow2(Sonar.DefaultSonarRange);
outsideOfSonarRange = Vector2.DistanceSquared(enemySub.WorldPosition, Submarine.MainSub.WorldPosition) > sqrSonarRange;
if (CheckWinState())
{
State = 2;
@@ -411,6 +459,7 @@ namespace Barotrauma
characters.Clear();
characterItems.Clear();
failed = !completed;
submarineInfo = null;
}
}
}
@@ -5,40 +5,182 @@ using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
partial class SalvageMission : Mission
{
private readonly ItemPrefab itemPrefab;
private Item item;
private readonly Level.PositionType spawnPositionType;
private readonly string containerTag;
private readonly string existingItemTag;
private readonly bool showMessageWhenPickedUp;
/// <summary>
/// Status effects executed on the target item when the mission starts. A random effect is chosen from each child list.
/// </summary>
private readonly List<List<StatusEffect>> statusEffects = new List<List<StatusEffect>>();
public override IEnumerable<Vector2> SonarPositions
private class Target
{
get
public Item Item;
/// <summary>
/// Note that the integer values matter here: the state of the target can't go back to a smaller value,
/// and a larger or equal value than the <see href="RequiredRetrievalState">RequiredRetrievalState</see> means the item counts as retrieved
/// (if the item needs to be picked up to be considered retrieved, it's also considered retrieved if it's in the sub)
/// </summary>
public enum RetrievalState
{
None = 0,
Interact = 1,
PickedUp = 2,
RetrievedToSub = 3
}
public readonly ItemPrefab ItemPrefab;
public readonly Level.PositionType SpawnPositionType;
public readonly string ContainerTag;
public readonly string ExistingItemTag;
public readonly bool RemoveItem;
public readonly LocalizedString SonarLabel;
public readonly bool AllowContinueBeforeRetrieved;
/// <summary>
/// Does the target need to be picked up or brought to the sub for mission to be considered successful.
/// If None, the target has no effect on the completion of the mission.
/// </summary>
public readonly RetrievalState RequiredRetrievalState;
public readonly bool HideLabelAfterRetrieved;
public bool Retrieved
{
if (item == null)
get
{
Enumerable.Empty<Vector2>();
return RequiredRetrievalState switch
{
RetrievalState.None => true,
RetrievalState.Interact or RetrievalState.PickedUp => State >= RequiredRetrievalState,
RetrievalState.RetrievedToSub => State == RetrievalState.RetrievedToSub,
_ => throw new NotImplementedException(),
};
}
}
private RetrievalState state;
public RetrievalState State
{
get { return state; }
set
{
if (value == state) { return; }
state = value;
#if SERVER
GameMain.Server?.UpdateMissionState(mission);
#endif
}
}
public bool Interacted;
private readonly SalvageMission mission;
/// <summary>
/// Status effects executed on the target item when the mission starts. A random effect is chosen from each child list.
/// </summary>
public readonly List<List<StatusEffect>> StatusEffects = new List<List<StatusEffect>>();
public Target(ContentXElement element, SalvageMission mission)
{
this.mission = mission;
ContainerTag = element.GetAttributeString("containertag", "");
RequiredRetrievalState = element.GetAttributeEnum("requireretrieval", RetrievalState.RetrievedToSub);
AllowContinueBeforeRetrieved = element.GetAttributeBool("allowcontinuebeforeretrieved", false);
HideLabelAfterRetrieved = element.GetAttributeBool("hidelabelafterretrieved", false);
string sonarLabelTag = element.GetAttributeString("sonarlabel", "");
if (!string.IsNullOrEmpty(sonarLabelTag))
{
SonarLabel =
TextManager.Get($"MissionSonarLabel.{sonarLabelTag}")
.Fallback(TextManager.Get(sonarLabelTag))
.Fallback(element.GetAttributeString("sonarlabel", ""));
}
ExistingItemTag = element.GetAttributeString("existingitemtag", "");
RemoveItem = element.GetAttributeBool("removeitem", true);
if (element.GetAttribute("itemname") != null)
{
DebugConsole.ThrowError("Error in SalvageMission - use item identifier instead of the name of the item.");
string itemName = element.GetAttributeString("itemname", "");
ItemPrefab = MapEntityPrefab.Find(itemName) as ItemPrefab;
if (ItemPrefab == null && ExistingItemTag.IsNullOrEmpty())
{
DebugConsole.ThrowError($"Error in SalvageMission: couldn't find an item prefab with the name \"{itemName}\"");
}
}
else
{
yield return item.GetRootInventoryOwner()?.WorldPosition ?? item.WorldPosition;
Identifier itemIdentifier = element.GetAttributeIdentifier("itemidentifier", Identifier.Empty);
if (!itemIdentifier.IsEmpty)
{
ItemPrefab = MapEntityPrefab.FindByIdentifier(itemIdentifier.ToIdentifier()) as ItemPrefab;
}
if (ItemPrefab == null)
{
string itemTag = element.GetAttributeString("itemtag", "");
ItemPrefab = MapEntityPrefab.GetRandom(p => p.Tags.Contains(itemTag), Rand.RandSync.Unsynced) as ItemPrefab;
}
if (ItemPrefab == null && ExistingItemTag.IsNullOrEmpty())
{
DebugConsole.ThrowError($"Error in SalvageMission - couldn't find an item prefab with the identifier \"{itemIdentifier}\"");
}
}
SpawnPositionType = element.GetAttributeEnum("spawntype", Level.PositionType.Cave | Level.PositionType.Ruin);
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "statuseffect":
{
var newEffect = StatusEffect.Load(subElement, parentDebugName: mission.Prefab.Name.Value);
if (newEffect == null) { continue; }
StatusEffects.Add(new List<StatusEffect> { newEffect });
break;
}
case "chooserandom":
StatusEffects.Add(new List<StatusEffect>());
foreach (var effectElement in subElement.Elements())
{
var newEffect = StatusEffect.Load(effectElement, parentDebugName: mission.Prefab.Name.Value);
if (newEffect == null) { continue; }
StatusEffects.Last().Add(newEffect);
}
break;
}
}
}
public void Reset()
{
state = RetrievalState.None;
Item = null;
}
}
private readonly List<Target> targets = new List<Target>();
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
{
get
{
foreach (var target in targets)
{
if (target.Retrieved && target.HideLabelAfterRetrieved) { continue; }
if (target.Item != null)
{
yield return (
target.SonarLabel ?? Prefab.SonarLabel,
target.Item.GetRootInventoryOwner()?.WorldPosition ?? target.Item.WorldPosition);
}
if (!target.AllowContinueBeforeRetrieved && !target.Retrieved) { break; }
}
}
}
@@ -46,225 +188,254 @@ namespace Barotrauma
public SalvageMission(MissionPrefab prefab, Location[] locations, Submarine sub)
: base(prefab, locations, sub)
{
containerTag = prefab.ConfigElement.GetAttributeString("containertag", "");
if (prefab.ConfigElement.GetAttribute("itemname") != null)
foreach (ContentXElement subElement in prefab.ConfigElement.Elements())
{
DebugConsole.ThrowError("Error in SalvageMission - use item identifier instead of the name of the item.");
string itemName = prefab.ConfigElement.GetAttributeString("itemname", "");
itemPrefab = MapEntityPrefab.Find(itemName) as ItemPrefab;
if (itemPrefab == null)
if (subElement.NameAsIdentifier() == "target")
{
DebugConsole.ThrowError("Error in SalvageMission: couldn't find an item prefab with the name " + itemName);
targets.Add(new Target(subElement, this));
}
}
else
if (!targets.Any())
{
string itemIdentifier = prefab.ConfigElement.GetAttributeString("itemidentifier", null);
if (itemIdentifier != null)
{
itemPrefab = MapEntityPrefab.FindByIdentifier(itemIdentifier.ToIdentifier()) as ItemPrefab;
}
if (itemPrefab == null)
{
string itemTag = prefab.ConfigElement.GetAttributeString("itemtag", "");
itemPrefab = MapEntityPrefab.GetRandom(p => p.Tags.Contains(itemTag), Rand.RandSync.Unsynced) as ItemPrefab;
}
if (itemPrefab == null)
{
DebugConsole.ThrowError("Error in SalvageMission - couldn't find an item prefab with the identifier " + itemIdentifier);
}
}
existingItemTag = prefab.ConfigElement.GetAttributeString("existingitemtag", "");
showMessageWhenPickedUp = prefab.ConfigElement.GetAttributeBool("showmessagewhenpickedup", false);
string spawnPositionTypeStr = prefab.ConfigElement.GetAttributeString("spawntype", "");
if (string.IsNullOrWhiteSpace(spawnPositionTypeStr) ||
!Enum.TryParse(spawnPositionTypeStr, true, out spawnPositionType))
{
spawnPositionType = Level.PositionType.Cave | Level.PositionType.Ruin;
}
foreach (var element in prefab.ConfigElement.Elements())
{
switch (element.Name.ToString().ToLowerInvariant())
{
case "statuseffect":
{
var newEffect = StatusEffect.Load(element, parentDebugName: prefab.Name.Value);
if (newEffect == null) { continue; }
statusEffects.Add(new List<StatusEffect> { newEffect });
break;
}
case "chooserandom":
statusEffects.Add(new List<StatusEffect>());
foreach (var subElement in element.Elements())
{
var newEffect = StatusEffect.Load(subElement, parentDebugName: prefab.Name.Value);
if (newEffect == null) { continue; }
statusEffects.Last().Add(newEffect);
}
break;
}
targets.Add(new Target(prefab.ConfigElement, this));
}
}
protected override void StartMissionSpecific(Level level)
{
#if SERVER
originalInventoryID = Entity.NullEntityID;
spawnInfo.Clear();
#endif
item = null;
if (!IsClient)
foreach (var target in targets)
{
//ruin/cave/wreck items are allowed to spawn close to the sub
float minDistance = spawnPositionType == Level.PositionType.Ruin || spawnPositionType == Level.PositionType.Cave || spawnPositionType == Level.PositionType.Wreck ?
0.0f : Level.Loaded.Size.X * 0.3f;
Vector2 position = Level.Loaded.GetRandomItemPos(spawnPositionType, 100.0f, minDistance, 30.0f);
if (!string.IsNullOrEmpty(existingItemTag))
bool usedExistingItem = false;
UInt16 originalInventoryID = 0;
byte originalItemContainerIndex = 0;
int originalSlotIndex = 0;
var executedEffectIndices = new List<(int listIndex, int effectIndex)>();
target.Reset();
if (!IsClient)
{
var suitableItems = Item.ItemList.Where(it => it.HasTag(existingItemTag));
switch (spawnPositionType)
//ruin/cave/wreck items are allowed to spawn close to the sub
float minDistance = target.SpawnPositionType switch
{
case Level.PositionType.Cave:
case Level.PositionType.MainPath:
case Level.PositionType.SidePath:
item = suitableItems.FirstOrDefault(it => Vector2.DistanceSquared(it.WorldPosition, position) < 1000.0f);
break;
case Level.PositionType.Ruin:
case Level.PositionType.Wreck:
foreach (Item it in suitableItems)
{
if (it.Submarine?.Info == null) { continue; }
if (spawnPositionType == Level.PositionType.Ruin && it.Submarine.Info.Type != SubmarineType.Ruin) { continue; }
if (spawnPositionType == Level.PositionType.Wreck && it.Submarine.Info.Type != SubmarineType.Wreck) { continue; }
Rectangle worldBorders = it.Submarine.Borders;
worldBorders.Location += it.Submarine.WorldPosition.ToPoint();
if (Submarine.RectContains(worldBorders, it.WorldPosition))
{
item = it;
#if SERVER
usedExistingItem = true;
#endif
break;
}
}
break;
}
}
Level.PositionType.Ruin or
Level.PositionType.Cave or
Level.PositionType.Wreck or
Level.PositionType.Outpost => 0.0f,
_ => Level.Loaded.Size.X * 0.3f,
};
Vector2 position =
target.SpawnPositionType == Level.PositionType.None ?
Vector2.Zero :
Level.Loaded.GetRandomItemPos(target.SpawnPositionType, 100.0f, minDistance, 30.0f);
if (item == null)
{
item = new Item(itemPrefab, position, null);
item.body.SetTransformIgnoreContacts(item.body.SimPosition, item.body.Rotation);
item.body.FarseerBody.BodyType = BodyType.Kinematic;
}
for (int i = 0; i < statusEffects.Count; i++)
{
List<StatusEffect> effectList = statusEffects[i];
if (effectList.Count == 0) { continue; }
int effectIndex = Rand.Int(effectList.Count);
var selectedEffect = effectList[effectIndex];
item.ApplyStatusEffect(selectedEffect, selectedEffect.type, deltaTime: 1.0f, worldPosition: item.Position);
#if SERVER
executedEffectIndices.Add(new Pair<int, int>(i, effectIndex));
#endif
}
//try to find a container and place the item inside it
if (!string.IsNullOrEmpty(containerTag) && item.ParentInventory == null)
{
List<ItemContainer> validContainers = new List<ItemContainer>();
foreach (Item it in Item.ItemList)
if (!string.IsNullOrEmpty(target.ExistingItemTag))
{
if (!it.HasTag(containerTag)) { continue; }
if (!it.IsPlayerTeamInteractable) { continue; }
switch (spawnPositionType)
var suitableItems = Item.ItemList.Where(it => it.HasTag(target.ExistingItemTag));
if (GameMain.GameSession?.Missions != null)
{
//don't choose an item that was already chosen as the target for another salvage mission
suitableItems = suitableItems.Where(it =>
GameMain.GameSession.Missions.None(m => m != this && m is SalvageMission salvageMission && salvageMission.targets.Any(t => t.Item == it)));
}
switch (target.SpawnPositionType)
{
case Level.PositionType.Cave:
case Level.PositionType.MainPath:
if (it.Submarine != null) { continue; }
case Level.PositionType.SidePath:
target.Item = suitableItems.FirstOrDefault(it => Vector2.DistanceSquared(it.WorldPosition, position) < 1000.0f);
#if SERVER
usedExistingItem = target.Item != null;
#endif
break;
case Level.PositionType.Ruin:
if (it.Submarine?.Info == null || !it.Submarine.Info.IsRuin) { continue; }
break;
case Level.PositionType.Wreck:
if (it.Submarine == null || it.Submarine.Info.Type != SubmarineType.Wreck) { continue; }
case Level.PositionType.Outpost:
foreach (Item it in suitableItems)
{
if (it.Submarine?.Info == null) { continue; }
if (target.SpawnPositionType == Level.PositionType.Ruin && it.Submarine.Info.Type != SubmarineType.Ruin) { continue; }
if (target.SpawnPositionType == Level.PositionType.Wreck && it.Submarine.Info.Type != SubmarineType.Wreck) { continue; }
if (target.SpawnPositionType == Level.PositionType.Outpost && it.Submarine.Info.Type != SubmarineType.Outpost) { continue; }
Rectangle worldBorders = it.Submarine.Borders;
worldBorders.Location += it.Submarine.WorldPosition.ToPoint();
if (Submarine.RectContains(worldBorders, it.WorldPosition))
{
target.Item = it;
#if SERVER
usedExistingItem = true;
#endif
break;
}
}
break;
default:
target.Item = suitableItems.FirstOrDefault();
#if SERVER
usedExistingItem = target.Item != null;
#endif
break;
}
var itemContainer = it.GetComponent<ItemContainer>();
if (itemContainer != null && itemContainer.Inventory.CanBePut(item)) { validContainers.Add(itemContainer); }
}
if (validContainers.Any())
if (target.Item == null)
{
var selectedContainer = validContainers.GetRandomUnsynced();
if (selectedContainer.Combine(item, user: null))
if (target.ItemPrefab == null && string.IsNullOrEmpty(target.ContainerTag))
{
DebugConsole.ThrowError($"Failed to find a target item for the mission \"{Prefab.Identifier}\". Item tag: {target.ExistingItemTag ?? "null"}");
continue;
}
target.Item = new Item(target.ItemPrefab, position, null);
target.Item.body.SetTransformIgnoreContacts(target.Item.body.SimPosition, target.Item.body.Rotation);
target.Item.body.FarseerBody.BodyType = BodyType.Kinematic;
}
else if (target.RequiredRetrievalState == Target.RetrievalState.Interact)
{
target.Item.OnInteract += () =>
{
target.Interacted = true;
};
}
for (int i = 0; i < target.StatusEffects.Count; i++)
{
List<StatusEffect> effectList = target.StatusEffects[i];
if (effectList.Count == 0) { continue; }
int effectIndex = Rand.Int(effectList.Count);
var selectedEffect = effectList[effectIndex];
target.Item.ApplyStatusEffect(selectedEffect, selectedEffect.type, deltaTime: 1.0f, worldPosition: target.Item.Position);
#if SERVER
originalInventoryID = selectedContainer.Item.ID;
originalItemContainerIndex = (byte)selectedContainer.Item.GetComponentIndex(selectedContainer);
originalSlotIndex = item.ParentInventory?.FindIndex(item) ?? -1;
executedEffectIndices.Add((i, effectIndex));
#endif
} // Placement successful
}
//try to find a container and place the item inside it
if (!string.IsNullOrEmpty(target.ContainerTag) && target.Item.ParentInventory == null)
{
List<ItemContainer> validContainers = new List<ItemContainer>();
foreach (Item it in Item.ItemList)
{
if (!it.HasTag(target.ContainerTag)) { continue; }
if (!it.IsPlayerTeamInteractable) { continue; }
switch (target.SpawnPositionType)
{
case Level.PositionType.Cave:
case Level.PositionType.MainPath:
if (it.Submarine != null) { continue; }
break;
case Level.PositionType.Ruin:
if (it.Submarine?.Info == null || !it.Submarine.Info.IsRuin) { continue; }
break;
case Level.PositionType.Wreck:
if (it.Submarine?.Info == null || it.Submarine.Info.Type != SubmarineType.Wreck) { continue; }
break;
}
var itemContainer = it.GetComponent<ItemContainer>();
if (itemContainer != null && itemContainer.Inventory.CanBePut(target.Item)) { validContainers.Add(itemContainer); }
}
if (validContainers.Any())
{
var selectedContainer = validContainers.GetRandomUnsynced();
if (selectedContainer.Combine(target.Item, user: null))
{
#if SERVER
originalInventoryID = selectedContainer.Item.ID;
originalItemContainerIndex = (byte)selectedContainer.Item.GetComponentIndex(selectedContainer);
originalSlotIndex = target.Item.ParentInventory?.FindIndex(target.Item) ?? -1;
#endif
} // Placement successful
}
}
}
#if SERVER
spawnInfo.Add(
target,
new SpawnInfo(usedExistingItem, originalInventoryID, originalItemContainerIndex, originalSlotIndex, executedEffectIndices));
#endif
}
}
protected override void UpdateMissionSpecific(float deltaTime)
{
if (item == null)
//make body dynamic when picked up
foreach (var target in targets)
{
#if DEBUG
DebugConsole.ThrowError("Error in salvage mission " + Prefab.Identifier + " (item was null)");
#endif
return;
var root = target.Item?.GetRootContainer() ?? target.Item;
if (root == null) { continue; }
if (target.Item.ParentInventory != null && target.Item.body != null) { target.Item.body.FarseerBody.BodyType = BodyType.Dynamic; }
}
if (IsClient)
if (IsClient) { return; }
for (int i = 0; i < targets.Count; i++)
{
if (item.ParentInventory != null && item.body != null) { item.body.FarseerBody.BodyType = BodyType.Dynamic; }
return;
}
switch (State)
{
case 0:
if (item.ParentInventory != null && item.body != null) { item.body.FarseerBody.BodyType = BodyType.Dynamic; }
if (showMessageWhenPickedUp)
{
if (!(item.GetRootInventoryOwner() is Character)) { return; }
}
else
{
Submarine parentSub = item.CurrentHull?.Submarine ?? item.GetRootInventoryOwner()?.Submarine;
if (parentSub == null || parentSub.Info.Type != SubmarineType.Player)
var target = targets[i];
if (i > 0 && !targets[i - 1].AllowContinueBeforeRetrieved && !targets[i - 1].Retrieved) { break; }
if (target.Item == null)
{
#if DEBUG
DebugConsole.ThrowError("Error in salvage mission " + Prefab.Identifier + " (item was null)");
#endif
return;
}
switch (target.State)
{
case Target.RetrievalState.None:
if (target.Interacted)
{
return;
TrySetRetrievalState(Target.RetrievalState.Interact);
}
}
State = 1;
break;
case 1:
if (!Submarine.MainSub.AtEndExit && !Submarine.MainSub.AtStartExit) { return; }
State = 2;
break;
var root = target.Item?.GetRootContainer() ?? target.Item;
if (root.ParentInventory?.Owner is Character character && character.TeamID == CharacterTeamType.Team1)
{
TrySetRetrievalState(Target.RetrievalState.PickedUp);
}
break;
case Target.RetrievalState.PickedUp:
Submarine parentSub = target.Item.CurrentHull?.Submarine ?? target.Item.GetRootInventoryOwner()?.Submarine;
if (parentSub != null)
{
if (parentSub.Info.Type == SubmarineType.Player || Level.IsLoadedFriendlyOutpost)
{
TrySetRetrievalState(Target.RetrievalState.RetrievedToSub);
}
}
break;
}
void TrySetRetrievalState(Target.RetrievalState retrievalState)
{
if (retrievalState < target.State) { return; }
bool wasRetrieved = false;
target.State = retrievalState;
//increment the mission state if the target became retrieved
if (!wasRetrieved && target.Retrieved) { State = i + 1; }
}
}
if (targets.All(t => t.Retrieved))
{
State = targets.Count + 1;
}
}
protected override bool DetermineCompleted()
{
var root = item?.GetRootContainer() ?? item;
return root?.CurrentHull?.Submarine != null && (root.CurrentHull.Submarine.AtEndExit || root.CurrentHull.Submarine.AtStartExit) && !item.Removed;
return targets.All(t => t.State >= t.RequiredRetrievalState);
}
protected override void EndMissionSpecific(bool completed)
{
item?.Remove();
item = null;
failed = !completed && state > 0;
//consider failed (can't attempt again) if we picked up any of the items but failed to bring them out of the level
failed = !completed && targets.Any(t => t.State >= Target.RetrievalState.PickedUp);
foreach (var target in targets)
{
if (target.RemoveItem)
{
target.Item?.Remove();
target.Reset();
}
}
}
}
}
@@ -32,25 +32,20 @@ namespace Barotrauma
}
}
public override IEnumerable<Vector2> SonarPositions
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
{
get
{
if (State > 0)
if (State > 0 || scanTargets.None())
{
return Enumerable.Empty<Vector2>();
}
else if (scanTargets.Any())
{
return scanTargets
.Where(kvp => !kvp.Value)
.Select(kvp => kvp.Key.WorldPosition);
return Enumerable.Empty<(LocalizedString Label, Vector2 Position)>();
}
else
{
return Enumerable.Empty<Vector2>();
}
return scanTargets
.Where(kvp => !kvp.Value)
.Select(kvp => (Prefab.SonarLabel, kvp.Key.WorldPosition));
}
}
}
@@ -244,7 +244,12 @@ namespace Barotrauma
float dist = Vector2.DistanceSquared(pos, refSub.WorldPosition);
foreach (Submarine sub in Submarine.Loaded)
{
if (sub.Info.Type != SubmarineType.Player && sub != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle) { continue; }
if (sub.Info.Type != SubmarineType.Player &&
sub.Info.Type != SubmarineType.EnemySubmarine &&
sub != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle)
{
continue;
}
float minDistToSub = GetMinDistanceToSub(sub);
if (dist < minDistToSub * minDistToSub) { continue; }

Some files were not shown because too many files have changed in this diff Show More