Build 1.1.4.0
This commit is contained in:
@@ -168,6 +168,7 @@ namespace Barotrauma
|
||||
public void FaceTarget(ISpatialEntity target) => Character.AnimController.TargetDir = target.WorldPosition.X > Character.WorldPosition.X ? Direction.Right : Direction.Left;
|
||||
|
||||
public bool IsSteeringThroughGap { get; protected set; }
|
||||
public bool IsTryingToSteerThroughGap { get; protected set; }
|
||||
|
||||
public virtual bool SteerThroughGap(Structure wall, WallSection section, Vector2 targetWorldPos, float deltaTime)
|
||||
{
|
||||
@@ -444,7 +445,7 @@ namespace Barotrauma
|
||||
if (EscapeTarget != null)
|
||||
{
|
||||
var door = EscapeTarget.ConnectedDoor;
|
||||
bool isClosedDoor = door != null && !door.IsOpen;
|
||||
bool isClosedDoor = door != null && door.IsClosed;
|
||||
Vector2 diff = EscapeTarget.WorldPosition - Character.WorldPosition;
|
||||
float sqrDist = diff.LengthSquared();
|
||||
bool isClose = sqrDist < MathUtils.Pow2(100);
|
||||
|
||||
@@ -245,11 +245,6 @@ namespace Barotrauma
|
||||
{
|
||||
throw new Exception($"Tried to create an enemy ai controller for human!");
|
||||
}
|
||||
if (Character.Params.Group == "human")
|
||||
{
|
||||
// Pet
|
||||
Character.TeamID = CharacterTeamType.FriendlyNPC;
|
||||
}
|
||||
var mainElement = c.Params.OriginalElement.IsOverride() ? c.Params.OriginalElement.FirstElement() : c.Params.OriginalElement;
|
||||
targetMemories = new Dictionary<AITarget, AITargetMemory>();
|
||||
steeringManager = outsideSteering;
|
||||
@@ -309,17 +304,20 @@ namespace Barotrauma
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//pets are friendly!
|
||||
if (PetBehavior != null || Character.Group == "human")
|
||||
{
|
||||
Character.TeamID = CharacterTeamType.FriendlyNPC;
|
||||
}
|
||||
ReevaluateAttacks();
|
||||
outsideSteering = new SteeringManager(this);
|
||||
insideSteering = new IndoorsSteeringManager(this, Character.Params.AI.CanOpenDoors, canAttackDoors);
|
||||
steeringManager = outsideSteering;
|
||||
State = AIState.Idle;
|
||||
|
||||
requiredHoleCount = (int)Math.Ceiling(ConvertUnits.ToDisplayUnits(colliderWidth) / Structure.WallSectionSize);
|
||||
|
||||
myBodies = Character.AnimController.Limbs.Select(l => l.body.FarseerBody).ToList();
|
||||
myBodies.Add(Character.AnimController.Collider.FarseerBody);
|
||||
CreatureMetrics.UnlockInEditor(Character.SpeciesName);
|
||||
}
|
||||
|
||||
private CharacterParams.AIParams _aiParams;
|
||||
@@ -452,6 +450,7 @@ namespace Barotrauma
|
||||
base.Update(deltaTime);
|
||||
UpdateTriggers(deltaTime);
|
||||
Character.ClearInputs();
|
||||
IsTryingToSteerThroughGap = false;
|
||||
Reverse = false;
|
||||
|
||||
bool ignorePlatforms = Character.AnimController.TargetMovement.Y < -0.5f && (-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X));
|
||||
@@ -558,8 +557,9 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
if (AIParams.CanOpenDoors)
|
||||
if (Character.Params.UsePathFinding && Character.Params.AI.UsePathFindingToGetInside && AIParams.CanOpenDoors)
|
||||
{
|
||||
// Meant for monsters outside the player sub that target something inside the sub and can use the doors to access the sub (Husk).
|
||||
bool IsCloseEnoughToTargetSub(float threshold) => SelectedAiTarget?.Entity?.Submarine is Submarine sub && sub != null && Vector2.DistanceSquared(Character.WorldPosition, sub.WorldPosition) < MathUtils.Pow(Math.Max(sub.Borders.Size.X, sub.Borders.Size.Y) / 2 + threshold, 2);
|
||||
|
||||
if (Character.Submarine != null || HasValidPath() && IsCloseEnoughToTargetSub(maxSteeringBuffer) || IsCloseEnoughToTargetSub(steeringBuffer))
|
||||
@@ -584,6 +584,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
// Normally the monsters only use pathing inside submarines, not outside.
|
||||
if (Character.Submarine != null && Character.Params.UsePathFinding)
|
||||
{
|
||||
if (steeringManager != insideSteering)
|
||||
@@ -848,7 +849,7 @@ namespace Barotrauma
|
||||
IsSteeringThroughGap = false;
|
||||
if (SwarmBehavior != null)
|
||||
{
|
||||
SwarmBehavior.IsActive = State == AIState.Idle && Character.CurrentHull == null;
|
||||
SwarmBehavior.IsActive = SwarmBehavior.ForceActive || State == AIState.Idle && Character.CurrentHull == null;
|
||||
SwarmBehavior.Refresh();
|
||||
SwarmBehavior.UpdateSteering(deltaTime);
|
||||
}
|
||||
@@ -876,7 +877,7 @@ namespace Barotrauma
|
||||
var pathSteering = SteeringManager as IndoorsSteeringManager;
|
||||
if (pathSteering == null)
|
||||
{
|
||||
if (SimPosition.Y < ConvertUnits.ToSimUnits(Character.CharacterHealth.CrushDepth * 0.75f))
|
||||
if (Level.Loaded != null && Level.Loaded.GetRealWorldDepth(WorldPosition.Y) > Character.CharacterHealth.CrushDepth * 0.75f)
|
||||
{
|
||||
// Steer straight up if very deep
|
||||
SteeringManager.SteeringManual(deltaTime, Vector2.UnitY);
|
||||
@@ -1144,7 +1145,6 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
attackLimbSelectionTimer -= deltaTime;
|
||||
if (AttackLimb == null || attackLimbSelectionTimer <= 0)
|
||||
{
|
||||
@@ -1154,7 +1154,8 @@ namespace Barotrauma
|
||||
AttackLimb = GetAttackLimb(attackWorldPos);
|
||||
}
|
||||
}
|
||||
|
||||
Character targetCharacter = SelectedAiTarget.Entity as Character;
|
||||
IDamageable damageTarget = wallTarget != null ? wallTarget.Structure : SelectedAiTarget.Entity as IDamageable;
|
||||
bool canAttack = true;
|
||||
bool pursue = false;
|
||||
if (IsCoolDownRunning && (_previousAttackLimb == null || AttackLimb == null || AttackLimb.attack.CoolDownTimer > 0))
|
||||
@@ -1379,7 +1380,6 @@ namespace Barotrauma
|
||||
|
||||
float distance = 0;
|
||||
Limb attackTargetLimb = null;
|
||||
Character targetCharacter = SelectedAiTarget.Entity as Character;
|
||||
if (canAttack)
|
||||
{
|
||||
if (!Character.AnimController.SimplePhysicsEnabled)
|
||||
@@ -1400,29 +1400,29 @@ namespace Barotrauma
|
||||
attackSimPos = Character.GetRelativeSimPosition(attackTargetLimb);
|
||||
}
|
||||
}
|
||||
|
||||
Vector2 attackLimbPos = Character.AnimController.SimplePhysicsEnabled ? Character.WorldPosition : AttackLimb.WorldPosition;
|
||||
Vector2 toTarget = attackWorldPos - attackLimbPos;
|
||||
Vector2 toTargetOffset = toTarget;
|
||||
// Add a margin when the target is moving away, because otherwise it might be difficult to reach it if the attack takes some time to execute
|
||||
if (wallTarget != null && Character.Submarine == null)
|
||||
{
|
||||
if (wallTarget.Structure.Submarine != null)
|
||||
{
|
||||
Vector2 margin = CalculateMargin(wallTarget.Structure.Submarine.Velocity);
|
||||
toTarget += margin;
|
||||
toTargetOffset += margin;
|
||||
}
|
||||
}
|
||||
else if (targetCharacter != null)
|
||||
{
|
||||
Vector2 margin = CalculateMargin(targetCharacter.AnimController.Collider.LinearVelocity);
|
||||
toTarget += margin;
|
||||
toTargetOffset += margin;
|
||||
}
|
||||
else if (SelectedAiTarget.Entity is MapEntity e)
|
||||
{
|
||||
if (e.Submarine != null)
|
||||
{
|
||||
Vector2 margin = CalculateMargin(e.Submarine.Velocity);
|
||||
toTarget += margin;
|
||||
toTargetOffset += margin;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1430,7 +1430,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (targetVelocity == Vector2.Zero) { return Vector2.Zero; }
|
||||
float diff = AttackLimb.attack.Range - AttackLimb.attack.DamageRange;
|
||||
if (diff <= 0 || toTarget.LengthSquared() <= MathUtils.Pow2(AttackLimb.attack.DamageRange)) { return Vector2.Zero; }
|
||||
if (diff <= 0 || toTargetOffset.LengthSquared() <= MathUtils.Pow2(AttackLimb.attack.DamageRange)) { return Vector2.Zero; }
|
||||
float dot = Vector2.Dot(Vector2.Normalize(targetVelocity), Vector2.Normalize(Character.AnimController.Collider.LinearVelocity));
|
||||
if (dot <= 0 || !MathUtils.IsValid(dot)) { return Vector2.Zero; }
|
||||
float distanceOffset = diff * AttackLimb.attack.Duration;
|
||||
@@ -1439,7 +1439,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
// Check that we can reach the target
|
||||
distance = toTarget.Length();
|
||||
distance = toTargetOffset.Length();
|
||||
canAttack = distance < AttackLimb.attack.Range;
|
||||
if (canAttack)
|
||||
{
|
||||
@@ -1523,20 +1523,18 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
Limb steeringLimb = canAttack && !AttackLimb.attack.Ranged ? AttackLimb : null;
|
||||
bool updateSteering = true;
|
||||
if (steeringLimb == null)
|
||||
{
|
||||
// If the attacking limb is a hand or claw, for example, using it as the steering limb can end in the result where the character circles around the target.
|
||||
steeringLimb = Character.AnimController.GetLimb(LimbType.Head) ?? Character.AnimController.GetLimb(LimbType.Torso);
|
||||
}
|
||||
|
||||
if (steeringLimb == null)
|
||||
{
|
||||
State = AIState.Idle;
|
||||
return;
|
||||
}
|
||||
|
||||
var pathSteering = SteeringManager as IndoorsSteeringManager;
|
||||
|
||||
if (AttackLimb != null && AttackLimb.attack.Retreat)
|
||||
{
|
||||
UpdateFallBack(attackWorldPos, deltaTime, followThrough: false);
|
||||
@@ -1603,7 +1601,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
else if (!IsTryingToSteerThroughGap)
|
||||
{
|
||||
if (AttackLimb.attack.Ranged)
|
||||
{
|
||||
@@ -1624,6 +1622,10 @@ namespace Barotrauma
|
||||
SteeringManager.Reset();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(SelectedAiTarget.Entity.WorldPosition - Character.WorldPosition));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1662,40 +1664,60 @@ namespace Barotrauma
|
||||
if (IsAttackRunning && CirclePhase != CirclePhase.Strike) { break; }
|
||||
if (selectedTargetingParams == null) { break; }
|
||||
var targetSub = SelectedAiTarget.Entity?.Submarine;
|
||||
if (targetSub == null) { break; }
|
||||
float subSize = Math.Max(targetSub.Borders.Width, targetSub.Borders.Height) / 2;
|
||||
float sqrDistToSub = Vector2.DistanceSquared(WorldPosition, targetSub.WorldPosition);
|
||||
ISpatialEntity spatialTarget = targetSub ?? SelectedAiTarget.Entity;
|
||||
float targetSize = 0;
|
||||
if (!selectedTargetingParams.IgnoreTargetSize)
|
||||
{
|
||||
targetSize =
|
||||
targetSub != null ? Math.Max(targetSub.Borders.Width, targetSub.Borders.Height) / 2 :
|
||||
targetCharacter != null ? ConvertUnits.ToDisplayUnits(targetCharacter.AnimController.Collider.GetSize().X) : 100;
|
||||
}
|
||||
float sqrDistToTarget = Vector2.DistanceSquared(WorldPosition, spatialTarget.WorldPosition);
|
||||
bool isProgressive = AIParams.MaxAggression - AIParams.StartAggression > 0;
|
||||
switch (CirclePhase)
|
||||
{
|
||||
case CirclePhase.Start:
|
||||
currentAttackIntensity = MathUtils.InverseLerp(AIParams.StartAggression, AIParams.MaxAggression, aggressionIntensity * Rand.Range(0.9f, 1.1f));
|
||||
currentAttackIntensity = MathUtils.InverseLerp(AIParams.StartAggression, AIParams.MaxAggression, ClampIntensity(aggressionIntensity));
|
||||
inverseDir = false;
|
||||
circleDir = GetDirFromHeadingInRadius();
|
||||
circleRotation = 0;
|
||||
strikeTimer = 0;
|
||||
blockCheckTimer = 0;
|
||||
breakCircling = false;
|
||||
float minRotationSpeed = 0.01f * selectedTargetingParams.CircleRotationSpeed;
|
||||
float maxRotationSpeed = 0.5f * selectedTargetingParams.CircleRotationSpeed;
|
||||
float minFallBackDistance = selectedTargetingParams.CircleStartDistance * 0.5f;
|
||||
float maxFallBackDistance = selectedTargetingParams.CircleStartDistance;
|
||||
float maxRandomOffset = selectedTargetingParams.CircleMaxRandomOffset;
|
||||
// The lower the rotation speed, the slower the progression. Also the distance to the target stays longer.
|
||||
// So basically if the value is higher, the creature will strike the sub more quickly and with more precision.
|
||||
circleRotationSpeed = MathHelper.Lerp(minRotationSpeed, maxRotationSpeed, currentAttackIntensity * Rand.Range(0.9f, 1.1f));
|
||||
circleFallbackDistance = MathHelper.Lerp(maxFallBackDistance, minFallBackDistance, currentAttackIntensity * Rand.Range(0.9f, 1.1f));
|
||||
circleOffset = Rand.Vector(MathHelper.Lerp(selectedTargetingParams.CircleMaxRandomOffset, 0, currentAttackIntensity * Rand.Range(0.9f, 1.1f)));
|
||||
canAttack = false;
|
||||
float ClampIntensity(float intensity) => MathHelper.Clamp(intensity * Rand.Range(0.9f, 1.1f), AIParams.StartAggression, AIParams.MaxAggression);
|
||||
if (isProgressive)
|
||||
{
|
||||
float intensity = ClampIntensity(currentAttackIntensity);
|
||||
float minRotationSpeed = 0.01f * selectedTargetingParams.CircleRotationSpeed;
|
||||
float maxRotationSpeed = 0.5f * selectedTargetingParams.CircleRotationSpeed;
|
||||
circleRotationSpeed = MathHelper.Lerp(minRotationSpeed, maxRotationSpeed, intensity);
|
||||
circleFallbackDistance = MathHelper.Lerp(maxFallBackDistance, minFallBackDistance, intensity);
|
||||
circleOffset = Rand.Vector(MathHelper.Lerp(maxRandomOffset, 0, intensity));
|
||||
}
|
||||
else
|
||||
{
|
||||
circleRotationSpeed = selectedTargetingParams.CircleRotationSpeed;
|
||||
circleFallbackDistance = maxFallBackDistance;
|
||||
circleOffset = Rand.Vector(maxRandomOffset);
|
||||
}
|
||||
circleRotationSpeed *= Rand.Range(1 - selectedTargetingParams.CircleRandomRotationFactor, 1 + selectedTargetingParams.CircleRandomRotationFactor);
|
||||
aggressionIntensity = Math.Clamp(aggressionIntensity, AIParams.StartAggression, AIParams.MaxAggression);
|
||||
if (targetSub.Borders.Width < 1000)
|
||||
DisableAttacksIfLimbNotRanged();
|
||||
if (targetSub != null && targetSub.Borders.Width < 1000 && AttackLimb?.attack is { Ranged: false })
|
||||
{
|
||||
breakCircling = true;
|
||||
CirclePhase = CirclePhase.CloseIn;
|
||||
}
|
||||
else if (sqrDistToSub > MathUtils.Pow2(subSize + selectedTargetingParams.CircleStartDistance))
|
||||
else if (sqrDistToTarget > MathUtils.Pow2(targetSize + selectedTargetingParams.CircleStartDistance))
|
||||
{
|
||||
CirclePhase = CirclePhase.CloseIn;
|
||||
}
|
||||
else if (sqrDistToSub < MathUtils.Pow2(subSize + circleFallbackDistance))
|
||||
else if (sqrDistToTarget < MathUtils.Pow2(targetSize + circleFallbackDistance))
|
||||
{
|
||||
CirclePhase = CirclePhase.FallBack;
|
||||
}
|
||||
@@ -1705,52 +1727,76 @@ namespace Barotrauma
|
||||
}
|
||||
break;
|
||||
case CirclePhase.CloseIn:
|
||||
if (AttackLimb != null && distance > 0 && distance < AttackLimb.attack.Range * GetStrikeDistanceMultiplier(targetSub.Velocity))
|
||||
Vector2 targetVelocity = GetTargetVelocity();
|
||||
float targetDistance = selectedTargetingParams.IgnoreTargetSize ? selectedTargetingParams.CircleStartDistance * 0.9f:
|
||||
targetSize + selectedTargetingParams.CircleStartDistance / 2;
|
||||
if (AttackLimb != null && distance > 0 && distance < AttackLimb.attack.Range * GetStrikeDistanceMultiplier(targetVelocity))
|
||||
{
|
||||
strikeTimer = AttackLimb.attack.CoolDown;
|
||||
CirclePhase = CirclePhase.Strike;
|
||||
}
|
||||
else if (!breakCircling && sqrDistToSub <= MathUtils.Pow2(subSize + selectedTargetingParams.CircleStartDistance / 2) && targetSub.Velocity.LengthSquared() <= MathUtils.Pow2(GetTargetMaxSpeed()))
|
||||
else if (!breakCircling && sqrDistToTarget <= MathUtils.Pow2(targetDistance) && targetVelocity.LengthSquared() <= MathUtils.Pow2(GetTargetMaxSpeed()))
|
||||
{
|
||||
CirclePhase = CirclePhase.Advance;
|
||||
}
|
||||
canAttack = false;
|
||||
DisableAttacksIfLimbNotRanged();
|
||||
break;
|
||||
case CirclePhase.FallBack:
|
||||
updateSteering = false;
|
||||
bool isBlocked = !UpdateFallBack(attackWorldPos, deltaTime, followThrough: false, checkBlocking: true);
|
||||
if (isBlocked || sqrDistToSub > MathUtils.Pow2(subSize + circleFallbackDistance))
|
||||
if (isBlocked || sqrDistToTarget > MathUtils.Pow2(targetSize + circleFallbackDistance))
|
||||
{
|
||||
CirclePhase = CirclePhase.Advance;
|
||||
break;
|
||||
}
|
||||
return;
|
||||
DisableAttacksIfLimbNotRanged();
|
||||
break;
|
||||
case CirclePhase.Advance:
|
||||
Vector2 subSpeed = targetSub.Velocity;
|
||||
float requiredDistMultiplier = 1;
|
||||
// If the target sub is moving fast, just steer towards the target until close enough to strike
|
||||
if (breakCircling || subSpeed.LengthSquared() > MathUtils.Pow2(GetTargetMaxSpeed()) || sqrDistToSub > MathUtils.Pow2(subSize + selectedTargetingParams.CircleStartDistance * 1.2f))
|
||||
Vector2 targetVel = GetTargetVelocity();
|
||||
// If the target is moving fast, just steer towards the target
|
||||
if (breakCircling || targetVel.LengthSquared() > MathUtils.Pow2(GetTargetMaxSpeed()))
|
||||
{
|
||||
CirclePhase = CirclePhase.CloseIn;
|
||||
}
|
||||
else if (sqrDistToTarget > MathUtils.Pow2(targetSize + selectedTargetingParams.CircleStartDistance * 1.2f))
|
||||
{
|
||||
if (selectedTargetingParams.DynamicCircleRotationSpeed && circleRotationSpeed < 100)
|
||||
{
|
||||
circleRotationSpeed *= 1 + deltaTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
CirclePhase = CirclePhase.CloseIn;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
circleRotation += deltaTime * circleRotationSpeed * circleDir;
|
||||
if (circleRotation < -360)
|
||||
float rotationStep = circleRotationSpeed * deltaTime * circleDir;
|
||||
if (isProgressive)
|
||||
{
|
||||
circleRotation += 360;
|
||||
circleRotation += rotationStep;
|
||||
}
|
||||
else if (circleRotation > 360)
|
||||
else
|
||||
{
|
||||
circleRotation -= 360;
|
||||
circleRotation = rotationStep;
|
||||
}
|
||||
Vector2 targetPos = attackSimPos + circleOffset;
|
||||
if (Vector2.DistanceSquared(SimPosition, targetPos) < 100)
|
||||
float targetDist = targetSize;
|
||||
if (targetDist <= 0)
|
||||
{
|
||||
targetDist = circleFallbackDistance;
|
||||
}
|
||||
if (targetSub != null && AttackLimb?.attack is { Ranged: true })
|
||||
{
|
||||
targetDist += circleFallbackDistance / 2;
|
||||
}
|
||||
if (Vector2.DistanceSquared(SimPosition, targetPos) < ConvertUnits.ToSimUnits(targetDist))
|
||||
{
|
||||
// Too close to the target point
|
||||
// When the offset position is outside of the sub it happens that the creature sometimes reaches the target point,
|
||||
// which makes it continue circling around the point (as supposed)
|
||||
// But when there is some offset and the offset is too near, this is not what we want.
|
||||
if (AttackLimb != null && sqrDistToSub < MathUtils.Pow2(subSize + circleFallbackDistance))
|
||||
if (canAttack && AttackLimb?.attack is { Ranged: false } && sqrDistToTarget < MathUtils.Pow2(targetSize + circleFallbackDistance))
|
||||
{
|
||||
CirclePhase = CirclePhase.Strike;
|
||||
strikeTimer = AttackLimb.attack.CoolDown;
|
||||
@@ -1762,7 +1808,6 @@ namespace Barotrauma
|
||||
break;
|
||||
}
|
||||
steerPos = MathUtils.RotatePointAroundTarget(SimPosition, targetPos, circleRotation);
|
||||
requiredDistMultiplier = GetStrikeDistanceMultiplier(subSpeed);
|
||||
if (IsBlocked(deltaTime, steerPos))
|
||||
{
|
||||
if (!inverseDir)
|
||||
@@ -1774,7 +1819,7 @@ namespace Barotrauma
|
||||
else if (circleRotationSpeed < 1)
|
||||
{
|
||||
// Then try increasing the rotation speed to change the movement curve
|
||||
circleRotationSpeed *= 1.1f;
|
||||
circleRotationSpeed *= 1 + deltaTime;
|
||||
}
|
||||
else if (circleOffset.LengthSquared() > 0.1f)
|
||||
{
|
||||
@@ -1784,16 +1829,24 @@ namespace Barotrauma
|
||||
else
|
||||
{
|
||||
// If we still fail, just steer towards the target
|
||||
breakCircling = true;
|
||||
breakCircling = AttackLimb?.attack is { Ranged: false };
|
||||
if (!breakCircling)
|
||||
{
|
||||
CirclePhase = CirclePhase.FallBack;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (AttackLimb != null && distance > 0 && distance < AttackLimb.attack.Range * requiredDistMultiplier && IsFacing(margin: MathHelper.Lerp(0.5f, 0.9f, currentAttackIntensity)))
|
||||
if (AttackLimb?.attack is { Ranged: false })
|
||||
{
|
||||
strikeTimer = AttackLimb.attack.CoolDown;
|
||||
CirclePhase = CirclePhase.Strike;
|
||||
canAttack = false;
|
||||
float requiredDistMultiplier = GetStrikeDistanceMultiplier(targetVel);
|
||||
if (distance > 0 && distance < AttackLimb.attack.Range * requiredDistMultiplier && IsFacing(margin: MathHelper.Lerp(0.5f, 0.9f, currentAttackIntensity)))
|
||||
{
|
||||
strikeTimer = AttackLimb.attack.CoolDown;
|
||||
CirclePhase = CirclePhase.Strike;
|
||||
}
|
||||
}
|
||||
canAttack = false;
|
||||
break;
|
||||
case CirclePhase.Strike:
|
||||
strikeTimer -= deltaTime;
|
||||
@@ -1815,18 +1868,19 @@ namespace Barotrauma
|
||||
return Vector2.Dot(Vector2.Normalize(attackWorldPos - WorldPosition), forward) > margin;
|
||||
}
|
||||
|
||||
float GetStrikeDistanceMultiplier(Vector2 subSpeed)
|
||||
float GetStrikeDistanceMultiplier(Vector2 targetVelocity)
|
||||
{
|
||||
if (selectedTargetingParams.CircleStrikeDistanceMultiplier < 1) { return 0; }
|
||||
float requiredDistMultiplier = 2;
|
||||
bool isHeading = Steering != null && Vector2.Dot(Vector2.Normalize(attackWorldPos - WorldPosition), Vector2.Normalize(Steering)) > 0.9f;
|
||||
bool isHeading = Vector2.Dot(Vector2.Normalize(attackWorldPos - WorldPosition), Vector2.Normalize(Steering)) > 0.9f;
|
||||
if (isHeading)
|
||||
{
|
||||
requiredDistMultiplier = selectedTargetingParams.CircleStrikeDistanceMultiplier;
|
||||
float subSpeedHorizontal = Math.Abs(subSpeed.X);
|
||||
if (subSpeedHorizontal > 1)
|
||||
float targetVelocityHorizontal = Math.Abs(targetVelocity.X);
|
||||
if (targetVelocityHorizontal > 1)
|
||||
{
|
||||
// Reduce the required distance if the target is moving.
|
||||
requiredDistMultiplier -= MathHelper.Lerp(0, Math.Max(selectedTargetingParams.CircleStrikeDistanceMultiplier - 1, 1), Math.Clamp(subSpeedHorizontal / 10, 0, 1));
|
||||
requiredDistMultiplier -= MathHelper.Lerp(0, Math.Max(selectedTargetingParams.CircleStrikeDistanceMultiplier - 1, 1), Math.Clamp(targetVelocityHorizontal / 10, 0, 1));
|
||||
if (requiredDistMultiplier < 2)
|
||||
{
|
||||
requiredDistMultiplier = 2;
|
||||
@@ -1843,19 +1897,59 @@ namespace Barotrauma
|
||||
return angle > MathHelper.Pi || angle < -MathHelper.Pi ? -1 : 1;
|
||||
}
|
||||
|
||||
float GetTargetMaxSpeed() => Character.ApplyTemporarySpeedLimits(Character.AnimController.CurrentSwimParams.MovementSpeed * 0.3f);
|
||||
Vector2 GetTargetVelocity()
|
||||
{
|
||||
if (targetSub != null)
|
||||
{
|
||||
return targetSub.Velocity;
|
||||
}
|
||||
else if (targetCharacter != null)
|
||||
{
|
||||
return targetCharacter.AnimController.Collider.LinearVelocity;
|
||||
}
|
||||
return Vector2.Zero;
|
||||
}
|
||||
|
||||
float GetTargetMaxSpeed() => Character.ApplyTemporarySpeedLimits(Character.AnimController.SwimFastParams.MovementSpeed * (targetSub != null ? 0.3f : 0.5f));
|
||||
}
|
||||
}
|
||||
if (selectedTargetingParams.AttackPattern == AttackPattern.Straight && AttackLimb is Limb attackLimb && attackLimb.attack.Ranged)
|
||||
if (updateSteering)
|
||||
{
|
||||
bool advance = !canAttack && Character.CurrentHull == null || distance > attackLimb.attack.Range * 0.9f;
|
||||
bool fallBack = canAttack && distance < Math.Min(250, attackLimb.attack.Range * 0.25f);
|
||||
if (fallBack)
|
||||
if (selectedTargetingParams.AttackPattern == AttackPattern.Straight && AttackLimb is Limb attackLimb && attackLimb.attack.Ranged)
|
||||
{
|
||||
Reverse = true;
|
||||
UpdateFallBack(attackWorldPos, deltaTime, followThrough: false);
|
||||
bool advance = !canAttack && Character.CurrentHull == null || distance > attackLimb.attack.Range * 0.9f;
|
||||
bool fallBack = canAttack && distance < Math.Min(250, attackLimb.attack.Range * 0.25f);
|
||||
if (fallBack)
|
||||
{
|
||||
Reverse = true;
|
||||
UpdateFallBack(attackWorldPos, deltaTime, followThrough: false);
|
||||
}
|
||||
else if (advance)
|
||||
{
|
||||
if (pathSteering != null)
|
||||
{
|
||||
pathSteering.SteeringSeek(steerPos, weight: 10, minGapWidth: minGapSize);
|
||||
}
|
||||
else
|
||||
{
|
||||
SteeringManager.SteeringSeek(steerPos, 10);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Character.CurrentHull == null && !canAttack)
|
||||
{
|
||||
SteeringManager.SteeringWander(avoidWanderingOutsideLevel: true);
|
||||
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 5);
|
||||
}
|
||||
else
|
||||
{
|
||||
SteeringManager.Reset();
|
||||
FaceTarget(SelectedAiTarget.Entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (advance)
|
||||
else if (!canAttack || distance > Math.Min(AttackLimb.attack.Range * 0.9f, 100))
|
||||
{
|
||||
if (pathSteering != null)
|
||||
{
|
||||
@@ -1866,41 +1960,18 @@ namespace Barotrauma
|
||||
SteeringManager.SteeringSeek(steerPos, 10);
|
||||
}
|
||||
}
|
||||
else
|
||||
if (Character.CurrentHull == null && (SelectedAiTarget?.Entity is Character c && c.Submarine == null ||
|
||||
distance == 0 ||
|
||||
distance > ConvertUnits.ToDisplayUnits(avoidLookAheadDistance * 2) ||
|
||||
AttackLimb != null && AttackLimb.attack.Ranged))
|
||||
{
|
||||
if (Character.CurrentHull == null && !canAttack)
|
||||
{
|
||||
SteeringManager.SteeringWander(avoidWanderingOutsideLevel: true);
|
||||
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 5);
|
||||
}
|
||||
else
|
||||
{
|
||||
SteeringManager.Reset();
|
||||
FaceTarget(SelectedAiTarget.Entity);
|
||||
}
|
||||
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 30);
|
||||
}
|
||||
}
|
||||
else if (!canAttack || distance > Math.Min(AttackLimb.attack.Range * 0.9f, 100))
|
||||
{
|
||||
if (pathSteering != null)
|
||||
{
|
||||
pathSteering.SteeringSeek(steerPos, weight: 10, minGapWidth: minGapSize);
|
||||
}
|
||||
else
|
||||
{
|
||||
SteeringManager.SteeringSeek(steerPos, 10);
|
||||
}
|
||||
}
|
||||
|
||||
if (Character.CurrentHull == null && (SelectedAiTarget?.Entity is Character c && c.Submarine == null || distance == 0 || distance > ConvertUnits.ToDisplayUnits(avoidLookAheadDistance * 2)))
|
||||
{
|
||||
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 30);
|
||||
}
|
||||
}
|
||||
}
|
||||
Entity targetEntity = wallTarget?.Structure ?? SelectedAiTarget?.Entity;
|
||||
IDamageable damageTarget = targetEntity as IDamageable;
|
||||
if (AttackLimb?.attack is Attack { Ranged: true} attack)
|
||||
if (AttackLimb?.attack is Attack { Ranged: true } attack)
|
||||
{
|
||||
AimRangedAttack(attack, targetEntity);
|
||||
}
|
||||
@@ -1915,12 +1986,21 @@ namespace Barotrauma
|
||||
{
|
||||
AttackLimb.attack.ResetAttackTimer();
|
||||
}
|
||||
|
||||
void DisableAttacksIfLimbNotRanged()
|
||||
{
|
||||
if (AttackLimb?.attack is { Ranged: false })
|
||||
{
|
||||
canAttack = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void AimRangedAttack(Attack attack, Entity targetEntity)
|
||||
{
|
||||
if (attack is not { Ranged: true } || targetEntity is not { Removed: false }) { return; }
|
||||
Character.SetInput(InputType.Aim, false, true);
|
||||
if (attack.AimRotationTorque <= 0) { return; }
|
||||
Limb limb = GetLimbToRotate(attack);
|
||||
if (limb != null)
|
||||
{
|
||||
@@ -2003,9 +2083,18 @@ namespace Barotrauma
|
||||
float prio = 1 + limb.attack.Priority;
|
||||
if (Character.AnimController.SimplePhysicsEnabled) { return prio; }
|
||||
float dist = Vector2.Distance(limb.WorldPosition, attackPos);
|
||||
// The limb is ignored if the target is not close. Prevents character going in reverse if very far away from it.
|
||||
// We also need a max value that is more than the actual range.
|
||||
float distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, limb.attack.Range * 3, dist));
|
||||
float distanceFactor = 1;
|
||||
if (limb.attack.Ranged)
|
||||
{
|
||||
float min = 100;
|
||||
distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(min, Math.Max(limb.attack.Range / 2, min), dist));
|
||||
}
|
||||
else
|
||||
{
|
||||
// The limb is ignored if the target is not close. Prevents character going in reverse if very far away from it.
|
||||
// We also need a max value that is more than the actual range.
|
||||
distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, limb.attack.Range * 3, dist));
|
||||
}
|
||||
return prio * distanceFactor;
|
||||
}
|
||||
}
|
||||
@@ -2164,7 +2253,9 @@ namespace Barotrauma
|
||||
{
|
||||
if (SelectedAiTarget?.Entity == null) { return false; }
|
||||
if (AttackLimb?.attack == null) { return false; }
|
||||
if (damageTarget == null) { return false; }
|
||||
ISpatialEntity spatialTarget = wallTarget != null ? wallTarget.Structure : SelectedAiTarget.Entity as ISpatialEntity;
|
||||
if (spatialTarget == null) { return false; }
|
||||
ActiveAttack = AttackLimb.attack;
|
||||
if (wallTarget != null)
|
||||
{
|
||||
// If the selected target is not the wall target, make the wall target the selected target.
|
||||
@@ -2176,13 +2267,14 @@ namespace Barotrauma
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (damageTarget == null) { return false; }
|
||||
ActiveAttack = AttackLimb.attack;
|
||||
if (ActiveAttack.Ranged && ActiveAttack.RequiredAngleToShoot > 0)
|
||||
{
|
||||
Limb referenceLimb = GetLimbToRotate(ActiveAttack);
|
||||
if (referenceLimb != null)
|
||||
{
|
||||
Vector2 toTarget = damageTarget.WorldPosition - referenceLimb.WorldPosition;
|
||||
Vector2 toTarget = spatialTarget.WorldPosition - referenceLimb.WorldPosition;
|
||||
float offset = referenceLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2;
|
||||
Vector2 forward = VectorExtensions.Forward(referenceLimb.body.TransformedRotation - offset * referenceLimb.Dir);
|
||||
float angle = MathHelper.ToDegrees(VectorExtensions.Angle(forward, toTarget));
|
||||
@@ -2200,16 +2292,20 @@ namespace Barotrauma
|
||||
{
|
||||
if (item.RequireAimToUse)
|
||||
{
|
||||
if (!Aim(deltaTime, damageTarget as ISpatialEntity, item))
|
||||
if (!Aim(deltaTime, spatialTarget, item))
|
||||
{
|
||||
// Valid target, but can't shoot -> return true so that it will not be ignored.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Character.SetInput(item.IsShootable ? InputType.Shoot : InputType.Use, false, true);
|
||||
item.Use(deltaTime, Character);
|
||||
if (damageTarget != null)
|
||||
{
|
||||
Character.SetInput(item.IsShootable ? InputType.Shoot : InputType.Use, false, true);
|
||||
item.Use(deltaTime, Character);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (damageTarget == null) { return true; }
|
||||
//simulate attack input to get the character to attack client-side
|
||||
Character.SetInput(InputType.Attack, true, true);
|
||||
if (!ActiveAttack.IsRunning)
|
||||
@@ -2224,20 +2320,11 @@ namespace Barotrauma
|
||||
Character.PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3);
|
||||
#endif
|
||||
}
|
||||
|
||||
if (AttackLimb.UpdateAttack(deltaTime, attackSimPos, damageTarget, out AttackResult attackResult, distance, targetLimb))
|
||||
{
|
||||
if (ActiveAttack.CoolDownTimer > 0)
|
||||
{
|
||||
SetAimTimer(Math.Min(ActiveAttack.CoolDown, 1.5f));
|
||||
// Managed to hit a living/non-destroyed target. Increase the priority more if the target is low in health -> dies easily/soon
|
||||
float greed = AIParams.AggressionGreed;
|
||||
if (damageTarget is not Barotrauma.Character)
|
||||
{
|
||||
// Halve the greed for attacking non-characters.
|
||||
greed /= 2;
|
||||
}
|
||||
selectedTargetMemory.Priority += GetRelativeDamage(attackResult.Damage, damageTarget.Health) * greed;
|
||||
}
|
||||
if (LatchOntoAI != null && SelectedAiTarget.Entity is Character targetCharacter)
|
||||
{
|
||||
@@ -2269,10 +2356,19 @@ namespace Barotrauma
|
||||
private float aimTimer;
|
||||
private float visibilityCheckTimer;
|
||||
private bool canSeeTarget;
|
||||
private float sinTime;
|
||||
private bool Aim(float deltaTime, ISpatialEntity target, Item weapon)
|
||||
{
|
||||
if (target == null || weapon == null) { return false; }
|
||||
if (AttackLimb == null) { return false; }
|
||||
Vector2 toTarget = target.WorldPosition - weapon.WorldPosition;
|
||||
float dist = toTarget.Length();
|
||||
Character.CursorPosition = target.WorldPosition;
|
||||
if (AttackLimb.attack.SwayAmount > 0)
|
||||
{
|
||||
sinTime += deltaTime * AttackLimb.attack.SwayFrequency;
|
||||
Character.CursorPosition += VectorExtensions.Forward(weapon.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, dist / 2 * AttackLimb.attack.SwayAmount);
|
||||
}
|
||||
if (Character.Submarine != null)
|
||||
{
|
||||
Character.CursorPosition -= Character.Submarine.Position;
|
||||
@@ -2294,11 +2390,11 @@ namespace Barotrauma
|
||||
aimTimer -= deltaTime;
|
||||
return false;
|
||||
}
|
||||
Vector2 toTarget = target.WorldPosition - weapon.WorldPosition;
|
||||
float angle = VectorExtensions.Angle(VectorExtensions.Forward(weapon.body.TransformedRotation), toTarget);
|
||||
float distanceFactor = MathHelper.Lerp(1.0f, 0.1f, MathUtils.InverseLerp(100, 1000, toTarget.Length()));
|
||||
float minDistance = 300;
|
||||
float distanceFactor = MathHelper.Lerp(1.0f, 0.1f, MathUtils.InverseLerp(minDistance, 1000, dist));
|
||||
float margin = MathHelper.PiOver4 * distanceFactor;
|
||||
if (angle < margin)
|
||||
if (angle < margin || dist < minDistance)
|
||||
{
|
||||
var collisionCategories = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel;
|
||||
var pickedBody = Submarine.PickBody(weapon.SimPosition, Character.GetRelativeSimPosition(target), myBodies, collisionCategories, allowInsideFixture: true);
|
||||
@@ -2591,13 +2687,19 @@ namespace Barotrauma
|
||||
{
|
||||
// Ignore all structures, items, and hulls inside these subs.
|
||||
if (aiTarget.Entity.Submarine != null)
|
||||
{
|
||||
if (aiTarget.Entity.Submarine.Info.IsWreck ||
|
||||
aiTarget.Entity.Submarine.Info.IsBeacon ||
|
||||
{
|
||||
if (aiTarget.Entity.Submarine.Info.IsWreck ||
|
||||
aiTarget.Entity.Submarine.Info.IsBeacon ||
|
||||
UnattackableSubmarines.Contains(aiTarget.Entity.Submarine))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
//ignore the megaruin in end levels
|
||||
if (aiTarget.Entity.Submarine.Info.OutpostGenerationParams != null &&
|
||||
aiTarget.Entity.Submarine.Info.OutpostGenerationParams.ForceToEndLocationIndex > -1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (aiTarget.Entity is Hull hull)
|
||||
{
|
||||
@@ -2698,7 +2800,7 @@ namespace Barotrauma
|
||||
}
|
||||
else if (CanPassThroughHole(s, i))
|
||||
{
|
||||
valueModifier *= isInnerWall ? 1 : 0;
|
||||
valueModifier *= isInnerWall ? 0.5f : 0;
|
||||
}
|
||||
else if (!canAttackWalls)
|
||||
{
|
||||
@@ -2968,7 +3070,8 @@ namespace Barotrauma
|
||||
// In the attack state allow going into non-allowed zone only when chasing a target.
|
||||
if (State == targetParams.State && SelectedAiTarget == aiTarget) { break; }
|
||||
}
|
||||
if (!IsPositionInsideAllowedZone(aiTarget.WorldPosition, out _))
|
||||
bool insideSameSub = aiTarget?.Entity?.Submarine != null && aiTarget.Entity.Submarine == Character.Submarine;
|
||||
if (!insideSameSub && !IsPositionInsideAllowedZone(aiTarget.WorldPosition, out _))
|
||||
{
|
||||
// If we have recently been damaged by the target (or another player/bot in the same team) allow targeting it even when we are in the idle state.
|
||||
bool isTargetInPlayerTeam = IsTargetInPlayerTeam(aiTarget);
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+11
-10
@@ -648,11 +648,11 @@ namespace Barotrauma
|
||||
{
|
||||
statusEffects = statusEffects.Concat(hitEffects);
|
||||
}
|
||||
float afflictionsStun = attack.Afflictions.Keys.Sum(a => a.Identifier == "stun" ? a.Strength : 0);
|
||||
float afflictionsStun = attack.Afflictions.Keys.Sum(a => a.Identifier == AfflictionPrefab.StunType ? a.Strength : 0);
|
||||
float effectsStun = statusEffects.None() ? 0 : statusEffects.Max(se =>
|
||||
{
|
||||
float stunAmount = 0;
|
||||
var stunAffliction = se.Afflictions.Find(a => a.Identifier == "stun");
|
||||
var stunAffliction = se.Afflictions.Find(a => a.Identifier == AfflictionPrefab.StunType);
|
||||
if (stunAffliction != null)
|
||||
{
|
||||
stunAmount = stunAffliction.Strength;
|
||||
@@ -1176,30 +1176,31 @@ namespace Barotrauma
|
||||
if (sqrDistance > repairTool.Range * repairTool.Range) { return; }
|
||||
}
|
||||
float aimFactor = MathHelper.PiOver2 * (1 - AimAccuracy);
|
||||
if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.Position - Weapon.Position) < MathHelper.PiOver4 + aimFactor)
|
||||
if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.WorldPosition - Weapon.WorldPosition) < MathHelper.PiOver4 + aimFactor)
|
||||
{
|
||||
if (myBodies == null)
|
||||
{
|
||||
myBodies = character.AnimController.Limbs.Select(l => l.body.FarseerBody);
|
||||
}
|
||||
var collisionCategories = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel;
|
||||
var pickedBody = Submarine.PickBody(Weapon.SimPosition, Enemy.SimPosition, myBodies, collisionCategories, allowInsideFixture: true);
|
||||
if (pickedBody != null)
|
||||
// Check that we don't hit friendlies. No need to check the walls, because there's a separate check for that at 1096 (which intentionally has a small delay)
|
||||
var pickedBodies = Submarine.PickBodies(Weapon.SimPosition, Character.GetRelativeSimPosition(from: Weapon, to: Enemy), myBodies, Physics.CollisionCharacter);
|
||||
foreach (var body in pickedBodies)
|
||||
{
|
||||
Character target = null;
|
||||
if (pickedBody.UserData is Character c)
|
||||
if (body.UserData is Character c)
|
||||
{
|
||||
target = c;
|
||||
}
|
||||
else if (pickedBody.UserData is Limb limb)
|
||||
else if (body.UserData is Limb limb)
|
||||
{
|
||||
target = limb.character;
|
||||
}
|
||||
if (target != null && (target == Enemy || !HumanAIController.IsFriendly(target)))
|
||||
if (target != null && target != Enemy && HumanAIController.IsFriendly(target))
|
||||
{
|
||||
UseWeapon(deltaTime);
|
||||
return;
|
||||
}
|
||||
}
|
||||
UseWeapon(deltaTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -4,6 +4,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -200,7 +201,8 @@ namespace Barotrauma
|
||||
(container.Item.GetRootContainer()?.OwnInventory?.Locked ?? false) ||
|
||||
ItemToContain == null || ItemToContain.Removed ||
|
||||
!ItemToContain.IsOwnedBy(character) || container.Item.GetRootInventoryOwner() is Character c && c != character,
|
||||
SpeakIfFails = !objectiveManager.IsCurrentOrder<AIObjectiveCleanupItems>()
|
||||
SpeakIfFails = !objectiveManager.IsCurrentOrder<AIObjectiveCleanupItems>(),
|
||||
endNodeFilter = n => Vector2.DistanceSquared(n.Waypoint.WorldPosition, container.Item.WorldPosition) <= MathUtils.Pow2(AIObjectiveGetItem.DefaultReach)
|
||||
},
|
||||
onAbandon: () => Abandon = true,
|
||||
onCompleted: () => RemoveSubObjective(ref goToObjective));
|
||||
|
||||
+39
-29
@@ -19,7 +19,6 @@ namespace Barotrauma
|
||||
|
||||
private AIObjectiveGetItem getExtinguisherObjective;
|
||||
private AIObjectiveGoTo gotoObjective;
|
||||
private float useExtinquisherTimer;
|
||||
|
||||
public AIObjectiveExtinguishFire(Character character, Hull targetHull, AIObjectiveManager objectiveManager, float priorityModifier = 1)
|
||||
: base(character, objectiveManager, priorityModifier)
|
||||
@@ -44,7 +43,8 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
float yDist = Math.Abs(character.WorldPosition.Y - targetHull.WorldPosition.Y);
|
||||
float characterY = character.CurrentHull?.WorldPosition.Y ?? character.WorldPosition.Y;
|
||||
float yDist = Math.Abs(characterY - targetHull.WorldPosition.Y);
|
||||
yDist = yDist > 100 ? yDist * 3 : 0;
|
||||
float dist = Math.Abs(character.WorldPosition.X - targetHull.WorldPosition.X) + yDist;
|
||||
float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, dist));
|
||||
@@ -119,24 +119,18 @@ namespace Barotrauma
|
||||
Abandon = true;
|
||||
break;
|
||||
}
|
||||
float xDist = Math.Abs(character.WorldPosition.X - fs.WorldPosition.X) - fs.DamageRange;
|
||||
float yDist = Math.Abs(character.WorldPosition.Y - fs.WorldPosition.Y);
|
||||
bool inRange = xDist + yDist < extinguisher.Range;
|
||||
// Use the hull position, because the fire x pos is sometimes inside a wall -> the bot can't ever see it and continues running towards the wall.
|
||||
ISpatialEntity lookTarget = character.CurrentHull == targetHull || character.CurrentHull.linkedTo.Contains(targetHull) ? targetHull : fs as ISpatialEntity;
|
||||
bool move = !inRange || !character.CanSeeTarget(lookTarget);
|
||||
if ((inRange && character.CanSeeTarget(lookTarget)) || useExtinquisherTimer > 0)
|
||||
float xDist = Math.Abs(character.WorldPosition.X - fs.WorldPosition.X);
|
||||
float yDist = Math.Abs(character.CurrentHull.WorldPosition.Y - targetHull.WorldPosition.Y);
|
||||
float dist = xDist + yDist;
|
||||
bool inRange = dist < extinguisher.Range;
|
||||
bool isInDamageRange = fs.IsInDamageRange(character, fs.DamageRange) && character.CanSeeTarget(targetHull);
|
||||
bool moveCloser = !isInDamageRange && (!inRange || !character.CanSeeTarget(targetHull));
|
||||
bool operateExtinguisher = !moveCloser || (dist < extinguisher.Range * 1.2f && character.CanSeeTarget(targetHull));
|
||||
if (operateExtinguisher)
|
||||
{
|
||||
useExtinquisherTimer += deltaTime;
|
||||
if (useExtinquisherTimer > 2.0f)
|
||||
{
|
||||
useExtinquisherTimer = 0.0f;
|
||||
}
|
||||
// Aim
|
||||
character.CursorPosition = fs.Position;
|
||||
Vector2 fromCharacterToFireSource = fs.WorldPosition - character.WorldPosition;
|
||||
float dist = fromCharacterToFireSource.Length();
|
||||
character.CursorPosition += VectorExtensions.Forward(extinguisherItem.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, dist / 2);
|
||||
character.CursorPosition += VectorExtensions.Forward(extinguisherItem.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, fromCharacterToFireSource.Length() / 2);
|
||||
if (extinguisherItem.RequireAimToUse)
|
||||
{
|
||||
character.SetInput(InputType.Aim, false, true);
|
||||
@@ -148,25 +142,29 @@ namespace Barotrauma
|
||||
{
|
||||
character.Speak(TextManager.GetWithVariable("DialogPutOutFire", "[roomname]", targetHull.DisplayName, FormatCapitals.Yes).Value, null, 0, "putoutfire".ToIdentifier(), 10.0f);
|
||||
}
|
||||
// Prevents running into the flames.
|
||||
objectiveManager.CurrentObjective.ForceWalk = true;
|
||||
}
|
||||
if (move)
|
||||
if (moveCloser)
|
||||
{
|
||||
//go to the first firesource
|
||||
if (TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(fs, character, objectiveManager, closeEnough: Math.Max(fs.DamageRange, extinguisher.Range * 0.7f))
|
||||
{
|
||||
DialogueIdentifier = "dialogcannotreachfire".ToIdentifier(),
|
||||
TargetName = fs.Hull.DisplayName
|
||||
},
|
||||
onAbandon: () => Abandon = true,
|
||||
onCompleted: () => RemoveSubObjective(ref gotoObjective)))
|
||||
if (TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(fs, character, objectiveManager, closeEnough: extinguisher.Range * 0.8f)
|
||||
{
|
||||
DialogueIdentifier = "dialogcannotreachfire".ToIdentifier(),
|
||||
TargetName = fs.Hull.DisplayName,
|
||||
},
|
||||
onAbandon: () => Abandon = true,
|
||||
onCompleted: () => RemoveSubObjective(ref gotoObjective)))
|
||||
{
|
||||
gotoObjective.requiredCondition = () => character.CanSeeTarget(targetHull);
|
||||
}
|
||||
}
|
||||
else
|
||||
else if (!operateExtinguisher || isInDamageRange)
|
||||
{
|
||||
character.AIController.SteeringManager.Reset();
|
||||
// Don't walk into the flames.
|
||||
RemoveSubObjective(ref gotoObjective);
|
||||
SteeringManager.Reset();
|
||||
}
|
||||
// Only target one fire source at the time.
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -177,8 +175,20 @@ namespace Barotrauma
|
||||
base.Reset();
|
||||
getExtinguisherObjective = null;
|
||||
gotoObjective = null;
|
||||
useExtinquisherTimer = 0;
|
||||
sinTime = 0;
|
||||
SteeringManager.Reset();
|
||||
}
|
||||
|
||||
protected override void OnCompleted()
|
||||
{
|
||||
base.OnCompleted();
|
||||
SteeringManager.Reset();
|
||||
}
|
||||
|
||||
protected override void OnAbandon()
|
||||
{
|
||||
base.OnAbandon();
|
||||
SteeringManager.Reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
-2
@@ -24,7 +24,7 @@ namespace Barotrauma
|
||||
protected override float TargetEvaluation()
|
||||
{
|
||||
if (Targets.None()) { return 0; }
|
||||
if (!character.IsOnPlayerTeam) { return 100; }
|
||||
if (!character.IsOnPlayerTeam && !character.IsOriginallyOnPlayerTeam) { return 100; }
|
||||
if (character.IsSecurity) { return 100; }
|
||||
if (objectiveManager.IsOrder(this)) { return 100; }
|
||||
// If there's any security officers onboard, leave fighting for them.
|
||||
@@ -66,7 +66,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; }
|
||||
|
||||
+24
-19
@@ -47,19 +47,12 @@ namespace Barotrauma
|
||||
}
|
||||
if (character.CurrentHull == null)
|
||||
{
|
||||
if (!character.NeedsAir)
|
||||
{
|
||||
Priority = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
Priority = (
|
||||
objectiveManager.HasOrder<AIObjectiveGoTo>(o => o.Priority > 0) ||
|
||||
objectiveManager.HasOrder<AIObjectiveReturn>(o => o.Priority > 0) ||
|
||||
objectiveManager.HasActiveObjective<AIObjectiveRescue>() ||
|
||||
objectiveManager.Objectives.Any(o => o is AIObjectiveCombat && o.Priority > 0))
|
||||
&& HumanAIController.HasDivingSuit(character) ? 0 : 100;
|
||||
}
|
||||
Priority = (
|
||||
objectiveManager.HasOrder<AIObjectiveGoTo>(o => o.Priority > 0) ||
|
||||
objectiveManager.HasOrder<AIObjectiveReturn>(o => o.Priority > 0) ||
|
||||
objectiveManager.HasActiveObjective<AIObjectiveRescue>() ||
|
||||
objectiveManager.Objectives.Any(o => o is AIObjectiveCombat && o.Priority > 0))
|
||||
&& ((character.IsImmuneToPressure && !character.IsLowInOxygen)|| HumanAIController.HasDivingSuit(character)) ? 0 : 100;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -118,6 +111,11 @@ namespace Barotrauma
|
||||
if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD)
|
||||
{
|
||||
Priority -= priorityDecrease * deltaTime;
|
||||
if (currenthullSafety >= 100)
|
||||
{
|
||||
// Reduce the priority to zero so that the bot can get switch to other objectives immediately, e.g. when entering the airlock.
|
||||
Priority = 0;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -140,8 +138,8 @@ namespace Barotrauma
|
||||
{
|
||||
if (resetPriority) { return; }
|
||||
var currentHull = character.CurrentHull;
|
||||
bool dangerousPressure = !character.IsProtectedFromPressure && (currentHull == null || currentHull.LethalPressure > 0);
|
||||
bool shouldActOnSuffocation = character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false);
|
||||
bool dangerousPressure = currentHull == null || currentHull.LethalPressure > 0 && character.PressureProtection <= 0;
|
||||
if (!character.LockHands && (!dangerousPressure || shouldActOnSuffocation || cannotFindSafeHull))
|
||||
{
|
||||
bool needsDivingGear = HumanAIController.NeedsDivingGear(currentHull, out bool needsDivingSuit);
|
||||
@@ -221,7 +219,11 @@ namespace Barotrauma
|
||||
TryAddSubObjective(ref goToObjective,
|
||||
constructor: () => new AIObjectiveGoTo(currentSafeHull, character, objectiveManager, getDivingGearIfNeeded: true)
|
||||
{
|
||||
AllowGoingOutside = HumanAIController.HasDivingSuit(character, conditionPercentage: 50)
|
||||
AllowGoingOutside =
|
||||
character.IsProtectedFromPressure ||
|
||||
character.CurrentHull == null ||
|
||||
character.CurrentHull.IsTaggedAirlock() ||
|
||||
character.CurrentHull.LeadsOutside(character)
|
||||
},
|
||||
onCompleted: () =>
|
||||
{
|
||||
@@ -352,8 +354,8 @@ namespace Barotrauma
|
||||
//tends to make the method much faster, because we find a potential hull earlier and can discard further-away hulls more easily
|
||||
//(for instance, an NPC in an outpost might otherwise go through all the hulls in the main sub first and do tons of expensive
|
||||
//path calculations, only to discard all of them when going through the hulls in the outpost)
|
||||
float hullSuitability = EstimateHullSuitability(character, hull);
|
||||
if (!hulls.Any())
|
||||
float hullSuitability = EstimateHullSuitability(character, hull);
|
||||
if (hulls.None())
|
||||
{
|
||||
hulls.Add(hull);
|
||||
}
|
||||
@@ -448,9 +450,12 @@ namespace Barotrauma
|
||||
{
|
||||
hullSafety = 100;
|
||||
}
|
||||
float characterY = character.CurrentHull?.WorldPosition.Y ?? character.WorldPosition.Y;
|
||||
float yDist = Math.Abs(characterY - potentialHull.WorldPosition.Y);
|
||||
yDist = yDist > 100 ? yDist * 3 : 0;
|
||||
float distance = Math.Abs(character.WorldPosition.X - potentialHull.WorldPosition.X) + yDist;
|
||||
// Huge preference for closer targets
|
||||
float distance = Vector2.DistanceSquared(character.WorldPosition, potentialHull.WorldPosition);
|
||||
float distanceFactor = MathHelper.Lerp(1, 0.2f, MathUtils.InverseLerp(0, MathUtils.Pow(100000, 2), distance));
|
||||
float distanceFactor = MathHelper.Lerp(1, 0.2f, MathUtils.InverseLerp(0, 10000, distance));
|
||||
hullSafety *= distanceFactor;
|
||||
// If the target is not inside a friendly submarine, considerably reduce the hull safety.
|
||||
// Intentionally exclude wrecks from this check
|
||||
|
||||
+14
-10
@@ -155,17 +155,21 @@ namespace Barotrauma
|
||||
bool canOperate = toLeak.LengthSquared() < reach * reach;
|
||||
if (canOperate)
|
||||
{
|
||||
TryAddSubObjective(ref operateObjective, () => new AIObjectiveOperateItem(repairTool, character, objectiveManager, option: Identifier.Empty, requireEquip: true, operateTarget: Leak),
|
||||
onAbandon: () => Abandon = true,
|
||||
onCompleted: () =>
|
||||
TryAddSubObjective(ref operateObjective, () => new AIObjectiveOperateItem(repairTool, character, objectiveManager, option: Identifier.Empty, requireEquip: true, operateTarget: Leak)
|
||||
{
|
||||
// Use an empty filter to override the default
|
||||
EndNodeFilter = n => true
|
||||
},
|
||||
onAbandon: () => Abandon = true,
|
||||
onCompleted: () =>
|
||||
{
|
||||
if (CheckObjectiveSpecific()) { IsCompleted = true; }
|
||||
else
|
||||
{
|
||||
if (CheckObjectiveSpecific()) { IsCompleted = true; }
|
||||
else
|
||||
{
|
||||
// Failed to operate. Probably too far.
|
||||
Abandon = true;
|
||||
}
|
||||
});
|
||||
// Failed to operate. Probably too far.
|
||||
Abandon = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
+18
-7
@@ -123,6 +123,11 @@ namespace Barotrauma
|
||||
return ignoredTags;
|
||||
}
|
||||
|
||||
public static Func<PathNode, bool> CreateEndNodeFilter(ISpatialEntity targetEntity)
|
||||
{
|
||||
return n => (n.Waypoint.Ladders == null || n.Waypoint.IsInWater) && Vector2.DistanceSquared(n.Waypoint.WorldPosition, targetEntity.WorldPosition) <= MathUtils.Pow2(DefaultReach);
|
||||
}
|
||||
|
||||
private bool CheckInventory()
|
||||
{
|
||||
if (IdentifiersOrTags == null) { return false; }
|
||||
@@ -155,11 +160,6 @@ namespace Barotrauma
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
if (character.Submarine == null)
|
||||
{
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
if (IdentifiersOrTags != null && !isDoneSeeking)
|
||||
{
|
||||
if (checkInventory)
|
||||
@@ -171,9 +171,14 @@ namespace Barotrauma
|
||||
}
|
||||
if (!isDoneSeeking)
|
||||
{
|
||||
if (character.Submarine == null)
|
||||
{
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
if (!AllowDangerousPressure)
|
||||
{
|
||||
bool dangerousPressure = character.CurrentHull == null || character.CurrentHull.LethalPressure > 0 && character.PressureProtection <= 0;
|
||||
bool dangerousPressure = !character.IsProtectedFromPressure && (character.CurrentHull == null || character.CurrentHull.LethalPressure > 0);
|
||||
if (dangerousPressure)
|
||||
{
|
||||
#if DEBUG
|
||||
@@ -192,6 +197,11 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (character.Submarine == null)
|
||||
{
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
if (targetItem == null || targetItem.Removed)
|
||||
{
|
||||
#if DEBUG
|
||||
@@ -307,7 +317,8 @@ namespace Barotrauma
|
||||
{
|
||||
// If the root container changes, the item is no longer where it was (taken by someone -> need to find another item)
|
||||
AbortCondition = obj => targetItem == null || targetItem.GetRootInventoryOwner() != moveToTarget,
|
||||
SpeakIfFails = false
|
||||
SpeakIfFails = false,
|
||||
endNodeFilter = CreateEndNodeFilter(moveToTarget)
|
||||
};
|
||||
},
|
||||
onAbandon: () =>
|
||||
|
||||
+56
-25
@@ -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)
|
||||
{
|
||||
|
||||
+2
-4
@@ -170,7 +170,8 @@ namespace Barotrauma
|
||||
TargetHull = character.CurrentHull;
|
||||
}
|
||||
|
||||
if (behavior == BehaviorType.StayInHull)
|
||||
bool currentTargetIsInvalid = currentTarget == null || IsForbidden(currentTarget) || (PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull)));
|
||||
if (behavior == BehaviorType.StayInHull && !currentTargetIsInvalid)
|
||||
{
|
||||
currentTarget = TargetHull;
|
||||
bool stayInHull = character.CurrentHull == currentTarget && IsSteeringFinished() && !character.IsClimbing;
|
||||
@@ -190,9 +191,6 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
bool currentTargetIsInvalid = currentTarget == null || IsForbidden(currentTarget) ||
|
||||
(PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull)));
|
||||
|
||||
if (currentTarget != null && !currentTargetIsInvalid)
|
||||
{
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted)
|
||||
|
||||
+1
-1
@@ -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;
|
||||
}
|
||||
|
||||
+8
-3
@@ -23,6 +23,11 @@ namespace Barotrauma
|
||||
private AIObjectiveGoTo goToObjective;
|
||||
private AIObjectiveGetItem getItemObjective;
|
||||
|
||||
/// <summary>
|
||||
/// If undefined, a default filter will be used.
|
||||
/// </summary>
|
||||
public Func<PathNode, bool> EndNodeFilter;
|
||||
|
||||
public bool Override { get; set; } = true;
|
||||
|
||||
public override bool CanBeCompleted => base.CanBeCompleted && (!useController || controller != null);
|
||||
@@ -222,7 +227,7 @@ namespace Barotrauma
|
||||
{
|
||||
target.Item.TryInteract(character, forceSelectKey: true);
|
||||
}
|
||||
if (component.AIOperate(deltaTime, character, this))
|
||||
if (component.CrewAIOperate(deltaTime, character, this))
|
||||
{
|
||||
isDoneOperating = completionCondition == null || completionCondition();
|
||||
}
|
||||
@@ -232,7 +237,7 @@ namespace Barotrauma
|
||||
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(target.Item, character, objectiveManager, closeEnough: 50)
|
||||
{
|
||||
TargetName = target.Item.Name,
|
||||
endNodeFilter = node => node.Waypoint.Ladders == null
|
||||
endNodeFilter = EndNodeFilter ?? AIObjectiveGetItem.CreateEndNodeFilter(target.Item)
|
||||
},
|
||||
onAbandon: () => Abandon = true,
|
||||
onCompleted: () => RemoveSubObjective(ref goToObjective));
|
||||
@@ -290,7 +295,7 @@ namespace Barotrauma
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (component.AIOperate(deltaTime, character, this))
|
||||
if (component.CrewAIOperate(deltaTime, character, this))
|
||||
{
|
||||
isDoneOperating = completionCondition == null || completionCondition();
|
||||
}
|
||||
|
||||
+22
-12
@@ -17,7 +17,6 @@ namespace Barotrauma
|
||||
|
||||
private AIObjectiveGoTo goToObjective;
|
||||
private AIObjectiveContainItem refuelObjective;
|
||||
private float previousCondition = -1;
|
||||
private RepairTool repairTool;
|
||||
|
||||
private const float WaitTimeBeforeRepair = 0.5f;
|
||||
@@ -196,15 +195,7 @@ namespace Barotrauma
|
||||
Abandon = true;
|
||||
}
|
||||
}
|
||||
if (previousCondition == -1)
|
||||
{
|
||||
previousCondition = Item.Condition;
|
||||
}
|
||||
else if (Item.Condition < previousCondition)
|
||||
{
|
||||
// If the current condition is less than the previous condition, we can't complete the task, so let's abandon it. The item is probably deteriorating at a greater speed than we can repair it.
|
||||
Abandon = true;
|
||||
}
|
||||
CheckPreviousCondition(deltaTime);
|
||||
}
|
||||
if (Abandon)
|
||||
{
|
||||
@@ -229,7 +220,6 @@ namespace Barotrauma
|
||||
TryAddSubObjective(ref goToObjective,
|
||||
constructor: () =>
|
||||
{
|
||||
previousCondition = -1;
|
||||
var objective = new AIObjectiveGoTo(Item, character, objectiveManager)
|
||||
{
|
||||
TargetName = Item.Name
|
||||
@@ -251,6 +241,27 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private const float conditionCheckDelay = 1;
|
||||
private float conditionCheckTimer;
|
||||
private float previousCondition;
|
||||
private void CheckPreviousCondition(float deltaTime)
|
||||
{
|
||||
if (Item == null || Item.Removed) { return; }
|
||||
conditionCheckTimer -= deltaTime;
|
||||
if (conditionCheckTimer > 0) { return; }
|
||||
conditionCheckTimer = conditionCheckDelay;
|
||||
if (previousCondition > -1 && Item.Condition < previousCondition)
|
||||
{
|
||||
// If the current condition is less than the previous condition, we can't complete the task, so let's abandon it. The item is probably deteriorating at a greater speed than we can repair it.
|
||||
Abandon = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the previous condition is not yet stored or if it's valid (greater or equal to current condition), save the condition for the next check here.
|
||||
previousCondition = Item.Condition;
|
||||
}
|
||||
}
|
||||
|
||||
private void FindRepairTool()
|
||||
{
|
||||
foreach (Repairable repairable in Item.Repairables)
|
||||
@@ -303,7 +314,6 @@ namespace Barotrauma
|
||||
base.Reset();
|
||||
goToObjective = null;
|
||||
refuelObjective = null;
|
||||
previousCondition = -1;
|
||||
repairTool = null;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -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),
|
||||
|
||||
+2
-2
@@ -71,11 +71,11 @@ namespace Barotrauma
|
||||
{
|
||||
float strength = character.CharacterHealth.GetPredictedStrength(affliction, predictFutureDuration: 10.0f);
|
||||
vitality -= affliction.GetVitalityDecrease(character.CharacterHealth, strength) / character.MaxVitality * 100;
|
||||
if (affliction.Prefab.AfflictionType == "paralysis")
|
||||
if (affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType)
|
||||
{
|
||||
vitality -= affliction.Strength;
|
||||
}
|
||||
else if (affliction.Prefab.AfflictionType == "poison")
|
||||
else if (affliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType)
|
||||
{
|
||||
vitality -= affliction.Strength;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@ namespace Barotrauma
|
||||
private AIObjectiveGoTo moveInsideObjective, moveOutsideObjective;
|
||||
private bool usingEscapeBehavior, isSteeringThroughGap;
|
||||
|
||||
public override bool AllowOutsideSubmarine => true;
|
||||
public override bool AllowInAnySub => true;
|
||||
|
||||
public AIObjectiveReturn(Character character, Character orderGiver, AIObjectiveManager objectiveManager, float priorityModifier = 1.0f) : base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
ReturnTarget = GetReturnTarget(Submarine.MainSubs) ?? GetReturnTarget(Submarine.Loaded);
|
||||
|
||||
@@ -465,6 +465,10 @@ namespace Barotrauma
|
||||
public readonly OrderTarget TargetPosition;
|
||||
|
||||
private ISpatialEntity targetSpatialEntity;
|
||||
|
||||
/// <summary>
|
||||
/// Note this property doesn't return the follow target of the Follow objective, as expected!
|
||||
/// </summary>
|
||||
public ISpatialEntity TargetSpatialEntity
|
||||
{
|
||||
get
|
||||
|
||||
@@ -348,6 +348,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (body.UserData is Submarine) { return false; }
|
||||
if (body.UserData is Structure s && !s.IsPlatform) { return false; }
|
||||
if (body.UserData is Voronoi2.VoronoiCell) { return false; }
|
||||
if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { return false; }
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -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)
|
||||
{
|
||||
|
||||
+14
-1
@@ -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);
|
||||
|
||||
+85
-50
@@ -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; }
|
||||
|
||||
+48
-44
@@ -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; }
|
||||
|
||||
+60
-43
@@ -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)
|
||||
|
||||
+3
@@ -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) :
|
||||
|
||||
+9
-2
@@ -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);
|
||||
}
|
||||
|
||||
+506
-114
@@ -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="""">
|
||||
/// 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);
|
||||
|
||||
+3
@@ -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
|
||||
{
|
||||
|
||||
|
||||
+4
-3
@@ -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;
|
||||
|
||||
+11
-7
@@ -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)]
|
||||
|
||||
+19
-6
@@ -6,6 +6,7 @@ using System.Linq;
|
||||
using Barotrauma.IO;
|
||||
using System.Xml;
|
||||
using Barotrauma.Extensions;
|
||||
using FarseerPhysics;
|
||||
#if CLIENT
|
||||
using Barotrauma.SpriteDeformations;
|
||||
#endif
|
||||
@@ -621,13 +622,13 @@ namespace Barotrauma
|
||||
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
|
||||
public float SteerForce { get; set; }
|
||||
|
||||
[Serialize(0f, IsPropertySaveable.Yes, description: "Radius of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
|
||||
[Serialize(0f, IsPropertySaveable.Yes, description: "Radius of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 2048)]
|
||||
public float Radius { get; set; }
|
||||
|
||||
[Serialize(0f, IsPropertySaveable.Yes, description: "Height of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
|
||||
[Serialize(0f, IsPropertySaveable.Yes, description: "Height of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 2048)]
|
||||
public float Height { get; set; }
|
||||
|
||||
[Serialize(0f, IsPropertySaveable.Yes, description: "Width of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
|
||||
[Serialize(0f, IsPropertySaveable.Yes, description: "Width of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 2048)]
|
||||
public float Width { get; set; }
|
||||
|
||||
[Serialize(10f, IsPropertySaveable.Yes, description: "The more the density the heavier the limb is."), Editable(MinValueFloat = 0.01f, MaxValueFloat = 100, DecimalCount = 2)]
|
||||
@@ -706,6 +707,15 @@ namespace Barotrauma
|
||||
[Serialize(false, IsPropertySaveable.Yes), Editable]
|
||||
public bool OnlyBlinkInWater { get; set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes), Editable]
|
||||
public bool UseTextureOffsetForBlinking { get; set; }
|
||||
|
||||
[Serialize("0.5, 0.5", IsPropertySaveable.Yes), Editable(DecimalCount = 2, MinValueFloat = 0f, MaxValueFloat = 1f)]
|
||||
public Vector2 BlinkTextureOffsetIn { get; set; }
|
||||
|
||||
[Serialize("0.5, 0.5", IsPropertySaveable.Yes), Editable(DecimalCount = 2, MinValueFloat = 0f, MaxValueFloat = 1f)]
|
||||
public Vector2 BlinkTextureOffsetOut { get; set; }
|
||||
|
||||
[Serialize(TransitionMode.Linear, IsPropertySaveable.Yes), Editable]
|
||||
public TransitionMode BlinkTransitionIn { get; private set; }
|
||||
|
||||
@@ -1026,15 +1036,18 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
|
||||
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 2048)]
|
||||
public float Radius { get; set; }
|
||||
|
||||
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
|
||||
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 2048)]
|
||||
public float Height { get; set; }
|
||||
|
||||
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
|
||||
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 2048)]
|
||||
public float Width { get; set; }
|
||||
|
||||
[Serialize(BodyType.Dynamic, IsPropertySaveable.Yes), Editable]
|
||||
public BodyType BodyType { get; set; }
|
||||
|
||||
public ColliderParams(ContentXElement element, RagdollParams ragdoll, string name = null) : base(element, ragdoll)
|
||||
{
|
||||
Name = name;
|
||||
|
||||
+2
-8
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+24
-18
@@ -14,45 +14,51 @@ namespace Barotrauma.Abilities
|
||||
{
|
||||
string[] missionTypeStrings = conditionElement.GetAttributeStringArray("missiontype", new []{ "None" })!;
|
||||
HashSet<MissionType> missionTypes = new HashSet<MissionType>();
|
||||
isAffiliated = conditionElement.GetAttributeBool("isaffiliated", false);
|
||||
|
||||
foreach (string missionTypeString in missionTypeStrings)
|
||||
{
|
||||
if (!Enum.TryParse(missionTypeString, out MissionType parsedMission) || parsedMission is MissionType.None)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in AbilityConditionMission \"{characterTalent.DebugIdentifier}\" - \"{missionTypeString}\" is not a valid mission type.");
|
||||
return;
|
||||
if (!isAffiliated)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in AbilityConditionMission \"{characterTalent.DebugIdentifier}\" - \"{missionTypeString}\" is not a valid mission type.");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
missionTypes.Add(parsedMission);
|
||||
}
|
||||
|
||||
missionType = missionTypes.ToImmutableHashSet();
|
||||
isAffiliated = conditionElement.GetAttributeBool("isaffiliated", false);
|
||||
}
|
||||
|
||||
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
|
||||
{
|
||||
if (abilityObject is IAbilityMission { Mission: { } mission })
|
||||
{
|
||||
if (isAffiliated)
|
||||
if (!isAffiliated) { return CheckMissionType(); }
|
||||
|
||||
if (GameMain.GameSession?.Campaign?.Factions is not { } factions) { return false; }
|
||||
|
||||
foreach (var (factionIdentifier, amount) in mission.ReputationRewards)
|
||||
{
|
||||
if (GameMain.GameSession?.Campaign?.Factions is not { } factions) { return false; }
|
||||
|
||||
foreach (var (factionIdentifier, amount) in mission.ReputationRewards)
|
||||
if (amount <= 0) { continue; }
|
||||
if (GetMatchingFaction(factionIdentifier) is { } faction &&
|
||||
Faction.GetPlayerAffiliationStatus(faction) is FactionAffiliation.Positive)
|
||||
{
|
||||
if (amount <= 0) { continue; }
|
||||
|
||||
Faction faction = factions.FirstOrDefault(faction => factionIdentifier == faction.Prefab.Identifier);
|
||||
|
||||
if (faction?.GetPlayerAffiliationStatus() is FactionAffiliation.Affiliated)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return CheckMissionType();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return missionType.Contains(mission.Prefab.Type);
|
||||
return false;
|
||||
|
||||
Faction GetMatchingFaction(Identifier factionIdentifier) =>
|
||||
factionIdentifier == "location"
|
||||
? mission.OriginLocation?.Faction
|
||||
: factions.FirstOrDefault(f => factionIdentifier == f.Prefab.Identifier);
|
||||
|
||||
bool CheckMissionType() => missionType.IsEmpty || missionType.Contains(mission.Prefab.Type);
|
||||
}
|
||||
|
||||
LogAbilityConditionError(abilityObject, typeof(IAbilityMission));
|
||||
|
||||
-1
@@ -23,7 +23,6 @@
|
||||
protected override bool MatchesConditionSpecific()
|
||||
{
|
||||
Identifier identifier = CharacterAbilityGivePermanentStat.HandlePlaceholders(placeholder, statIdentifier);
|
||||
|
||||
return character.Info.GetSavedStatValue(statType, identifier) >= min;
|
||||
}
|
||||
}
|
||||
|
||||
+34
-1
@@ -4,6 +4,7 @@ using System;
|
||||
using Barotrauma.Extensions;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma.Abilities
|
||||
{
|
||||
@@ -29,11 +30,33 @@ namespace Barotrauma.Abilities
|
||||
|
||||
if (!TalentTree.JobTalentTrees.TryGet(apprentice.Identifier, out TalentTree? talentTree)) { return; }
|
||||
|
||||
ImmutableHashSet<Character> characters = GameSession.GetSessionCrewCharacters(CharacterType.Both);
|
||||
|
||||
HashSet<ImmutableHashSet<Identifier>> talentsTrees = new HashSet<ImmutableHashSet<Identifier>>();
|
||||
foreach (TalentSubTree subTree in talentTree.TalentSubTrees)
|
||||
{
|
||||
if (subTree.Type != TalentTreeType.Specialization) { continue; }
|
||||
talentsTrees.Add(subTree.AllTalentIdentifiers);
|
||||
|
||||
HashSet<Identifier> identifiers = new HashSet<Identifier>();
|
||||
foreach (TalentOption option in subTree.TalentOptionStages)
|
||||
{
|
||||
foreach (Identifier identifier in option.TalentIdentifiers)
|
||||
{
|
||||
if (IsShowCaseTalent(identifier, option) || TalentTree.IsTalentLocked(identifier, characters)) { continue; }
|
||||
|
||||
identifiers.Add(identifier);
|
||||
}
|
||||
|
||||
foreach (var (_, value) in option.ShowCaseTalents)
|
||||
{
|
||||
var ids = value.Where(i => !TalentTree.IsTalentLocked(i, characters)).ToImmutableHashSet();
|
||||
if (ids.Count is 0) { continue; }
|
||||
|
||||
identifiers.Add(value.GetRandomUnsynced());
|
||||
}
|
||||
}
|
||||
|
||||
talentsTrees.Add(identifiers.ToImmutableHashSet());
|
||||
}
|
||||
|
||||
ImmutableHashSet<Identifier> selectedTalentTree = talentsTrees.GetRandomUnsynced();
|
||||
@@ -44,6 +67,16 @@ namespace Barotrauma.Abilities
|
||||
|
||||
Character.GiveTalent(identifier);
|
||||
}
|
||||
|
||||
static bool IsShowCaseTalent(Identifier identifier, TalentOption option)
|
||||
{
|
||||
foreach (var (_, value) in option.ShowCaseTalents)
|
||||
{
|
||||
if (value.Contains(identifier)) { return true; }
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ApplyEffect(AbilityObject abilityObject)
|
||||
|
||||
@@ -131,6 +131,8 @@ namespace Barotrauma
|
||||
if (character.Info.GetTotalTalentPoints() - selectedTalents.Count <= 0) { return false; }
|
||||
if (!JobTalentTrees.TryGet(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return false; }
|
||||
|
||||
if (IsTalentLocked(talentIdentifier)) { return false; }
|
||||
|
||||
foreach (var subTree in talentTree!.TalentSubTrees)
|
||||
{
|
||||
if (subTree.AllTalentIdentifiers.Contains(talentIdentifier) && subTree.HasMaxTalents(selectedTalents)) { return false; }
|
||||
@@ -152,6 +154,18 @@ namespace Barotrauma
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool IsTalentLocked(Identifier talentIdentifier, ImmutableHashSet<Character> characterList = null)
|
||||
{
|
||||
characterList ??= GameSession.GetSessionCrewCharacters(CharacterType.Both);
|
||||
|
||||
foreach (Character c in characterList)
|
||||
{
|
||||
if (c.Info.GetSavedStatValue(StatTypes.LockedTalents, talentIdentifier) >= 1) { return true; }
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static List<Identifier> CheckTalentSelection(Character controlledCharacter, IEnumerable<Identifier> selectedTalents)
|
||||
{
|
||||
List<Identifier> viableTalents = new List<Identifier>();
|
||||
|
||||
+15
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
+3
-9
@@ -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)}";
|
||||
}
|
||||
}
|
||||
}
|
||||
+20
-13
@@ -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())
|
||||
|
||||
+25
-11
@@ -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)
|
||||
|
||||
+41
-23
@@ -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
Reference in New Issue
Block a user