Faction Test 100.4.0.0

This commit is contained in:
Markus Isberg
2022-11-14 18:28:28 +02:00
parent 87426b68b2
commit c772b61fc1
412 changed files with 16984 additions and 5530 deletions
@@ -168,6 +168,7 @@ namespace Barotrauma
public void FaceTarget(ISpatialEntity target) => Character.AnimController.TargetDir = target.WorldPosition.X > Character.WorldPosition.X ? Direction.Right : Direction.Left;
public bool IsSteeringThroughGap { get; protected set; }
public bool IsTryingToSteerThroughGap { get; protected set; }
public virtual bool SteerThroughGap(Structure wall, WallSection section, Vector2 targetWorldPos, float deltaTime)
{
@@ -239,11 +239,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;
@@ -303,7 +298,11 @@ namespace Barotrauma
break;
}
}
//pets are friendly!
if (PetBehavior != null || Character.Params.Group == "human")
{
Character.TeamID = CharacterTeamType.FriendlyNPC;
}
ReevaluateAttacks();
outsideSteering = new SteeringManager(this);
insideSteering = new IndoorsSteeringManager(this, Character.Params.AI.CanOpenDoors, canAttackDoors);
@@ -442,6 +441,8 @@ 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));
if (steeringManager == insideSteering)
@@ -547,8 +548,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))
@@ -573,6 +575,7 @@ namespace Barotrauma
}
else
{
// Normally the monsters only use pathing inside submarines, not outside.
if (Character.Submarine != null && Character.Params.UsePathFinding)
{
if (steeringManager != insideSteering)
@@ -804,10 +807,6 @@ namespace Barotrauma
Reverse = true;
run = true;
}
else
{
Reverse = false;
}
SteeringManager.SteeringManual(deltaTime, dir * 0.2f);
}
else
@@ -841,7 +840,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);
}
@@ -1137,7 +1136,6 @@ namespace Barotrauma
return;
}
}
attackLimbSelectionTimer -= deltaTime;
if (AttackLimb == null || attackLimbSelectionTimer <= 0)
{
@@ -1147,7 +1145,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))
@@ -1372,7 +1371,6 @@ namespace Barotrauma
float distance = 0;
Limb attackTargetLimb = null;
Character targetCharacter = SelectedAiTarget.Entity as Character;
if (canAttack)
{
if (!Character.AnimController.SimplePhysicsEnabled)
@@ -1393,29 +1391,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;
}
}
@@ -1423,7 +1421,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;
@@ -1432,7 +1430,7 @@ namespace Barotrauma
}
// Check that we can reach the target
distance = toTarget.Length();
distance = toTargetOffset.Length();
canAttack = distance < AttackLimb.attack.Range;
if (canAttack)
{
@@ -1490,60 +1488,44 @@ namespace Barotrauma
canAttack = angle < MathHelper.ToRadians(AttackLimb.attack.RequiredAngle);
if (canAttack && AttackLimb.attack.AvoidFriendlyFire)
{
float minDistance = MathUtils.Pow(ConvertUnits.ToDisplayUnits(Character.AnimController.Collider.GetMaxExtent() * 3), 2);
bool IsFarEnough(Character other) => Vector2.DistanceSquared(Character.WorldPosition, other.WorldPosition) > minDistance;
if (SwarmBehavior != null)
canAttack = !IsBlocked(Character.GetRelativeSimPosition(SelectedAiTarget.Entity));
bool IsBlocked(Vector2 targetPosition)
{
canAttack = SwarmBehavior.Members.All(c => c == Character || IsFarEnough(c));
}
else
{
canAttack = Character.CharacterList.All(c => c == Character || !Character.IsFriendly(c) || IsFarEnough(c));
}
if (canAttack)
{
canAttack = !IsBlocked(attackSimPos) && !IsBlocked(AttackLimb.SimPosition + forward * ConvertUnits.ToSimUnits(AttackLimb.attack.Range));
bool IsBlocked(Vector2 targetPosition)
foreach (var body in Submarine.PickBodies(AttackLimb.SimPosition, targetPosition, myBodies, Physics.CollisionCharacter))
{
foreach (var body in Submarine.PickBodies(AttackLimb.SimPosition, targetPosition, myBodies, Physics.CollisionCharacter))
Character hitTarget = null;
if (body.UserData is Character c)
{
Character hitTarget = null;
if (body.UserData is Character c)
{
hitTarget = c;
}
else if (body.UserData is Limb limb)
{
hitTarget = limb.character;
}
if (hitTarget != null && !hitTarget.IsDead && Character.IsFriendly(hitTarget))
{
return true;
}
hitTarget = c;
}
else if (body.UserData is Limb limb)
{
hitTarget = limb.character;
}
if (hitTarget != null && !hitTarget.IsDead && Character.IsFriendly(hitTarget))
{
return true;
}
return false;
}
return false;
}
}
}
}
}
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);
@@ -1610,7 +1592,7 @@ namespace Barotrauma
}
}
}
else
else if (!IsTryingToSteerThroughGap)
{
if (AttackLimb.attack.Ranged)
{
@@ -1631,6 +1613,10 @@ namespace Barotrauma
SteeringManager.Reset();
}
}
else
{
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(SelectedAiTarget.Entity.WorldPosition - Character.WorldPosition));
}
}
else
{
@@ -1669,40 +1655,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;
}
@@ -1712,52 +1718,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;
@@ -1769,7 +1799,6 @@ namespace Barotrauma
break;
}
steerPos = MathUtils.RotatePointAroundTarget(SimPosition, targetPos, circleRotation);
requiredDistMultiplier = GetStrikeDistanceMultiplier(subSpeed);
if (IsBlocked(deltaTime, steerPos))
{
if (!inverseDir)
@@ -1781,7 +1810,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)
{
@@ -1791,16 +1820,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;
@@ -1822,18 +1859,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;
@@ -1850,35 +1888,79 @@ 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;
}
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);
float GetTargetMaxSpeed() => Character.ApplyTemporarySpeedLimits(Character.AnimController.SwimFastParams.MovementSpeed * (targetSub != null ? 0.3f : 0.5f));
}
}
else if (AttackLimb.attack.Ranged)
if (updateSteering)
{
// Too close
UpdateFallBack(attackWorldPos, deltaTime, followThrough: false);
}
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);
if (selectedTargetingParams.AttackPattern == AttackPattern.Straight && AttackLimb is Limb attackLimb && attackLimb.attack.Ranged)
{
bool advance = !canAttack && Character.InWater || 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.InWater)
{
SteeringManager.Reset();
FaceTarget(SelectedAiTarget.Entity);
}
}
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) ||
AttackLimb != null && AttackLimb.attack.Ranged))
{
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 30);
}
}
}
}
Entity targetEntity = wallTarget?.Structure ?? SelectedAiTarget?.Entity;
if (AttackLimb?.attack is Attack { Ranged: true } attack && targetEntity != null)
{
AimRangedAttack(attack, targetEntity);
}
if (canAttack)
{
if (!UpdateLimbAttack(deltaTime, AttackLimb, attackSimPos, distance, attackTargetLimb))
if (!UpdateLimbAttack(deltaTime, attackSimPos, damageTarget, distance, attackTargetLimb))
{
IgnoreTarget(SelectedAiTarget);
}
@@ -1887,6 +1969,31 @@ namespace Barotrauma
{
AttackLimb.attack.ResetAttackTimer();
}
void DisableAttacksIfLimbNotRanged()
{
if (AttackLimb?.attack is { Ranged: false })
{
canAttack = false;
}
}
}
public void AimRangedAttack(Attack attack, Entity targetEntity)
{
if (attack == null || attack.Ranged == false || targetEntity == null) { return; }
Character.SetInput(InputType.Aim, false, true);
if (attack.AimRotationTorque <= 0) { return; }
Limb limb = GetLimbToRotate(attack);
if (limb != null)
{
Vector2 toTarget = targetEntity.WorldPosition - limb.WorldPosition;
float offset = limb.Params.GetSpriteOrientation() - MathHelper.PiOver2;
limb.body.SuppressSmoothRotationCalls = false;
float angle = MathUtils.VectorToAngle(toTarget);
limb.body.SmoothRotate(angle + offset, attack.AimRotationTorque);
limb.body.SuppressSmoothRotationCalls = true;
}
}
private bool IsValidAttack(Limb attackingLimb, IEnumerable<AttackContext> currentContexts, Entity target)
@@ -1959,9 +2066,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;
}
}
@@ -2114,13 +2230,15 @@ namespace Barotrauma
}
// 10 dmg, 100 health -> 0.1
private float GetRelativeDamage(float dmg, float vitality) => dmg / Math.Max(vitality, 1.0f);
private static float GetRelativeDamage(float dmg, float vitality) => dmg / Math.Max(vitality, 1.0f);
private bool UpdateLimbAttack(float deltaTime, Limb attackingLimb, Vector2 attackSimPos, float distance = -1, Limb targetLimb = null)
private bool UpdateLimbAttack(float deltaTime, Vector2 attackSimPos, IDamageable damageTarget, float distance = -1, Limb targetLimb = null)
{
if (SelectedAiTarget?.Entity == null) { return false; }
if (attackingLimb?.attack == null) { return false; }
ActiveAttack = attackingLimb.attack;
if (AttackLimb?.attack == 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.
@@ -2131,92 +2249,106 @@ namespace Barotrauma
State = AIState.Attack;
}
}
IDamageable damageTarget = wallTarget != null ? wallTarget.Structure : SelectedAiTarget.Entity as IDamageable;
if (damageTarget != null)
if (ActiveAttack.Ranged && ActiveAttack.RequiredAngleToShoot > 0)
{
if (Character.Params.CanInteract && Character.Inventory != null)
Limb referenceLimb = GetLimbToRotate(ActiveAttack);
if (referenceLimb != null)
{
// Use equipped items (weapons)
Item item = GetEquippedItem(attackingLimb);
if (item != null)
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));
if (angle > ActiveAttack.RequiredAngleToShoot)
{
if (item.RequireAimToUse)
return true;
}
}
}
if (Character.Params.CanInteract && Character.Inventory != null)
{
// Use equipped items (weapons)
Item item = GetEquippedItem(AttackLimb);
if (item != null)
{
if (item.RequireAimToUse)
{
if (!Aim(deltaTime, spatialTarget, item))
{
if (!Aim(deltaTime, damageTarget as ISpatialEntity, item))
{
// Valid target, but can't shoot -> return true so that it will not be ignored.
return true;
}
// Valid target, but can't shoot -> return true so that it will not be ignored.
return true;
}
}
if (damageTarget != null)
{
Character.SetInput(item.IsShootable ? InputType.Shoot : InputType.Use, false, true);
item.Use(deltaTime, Character);
}
}
//simulate attack input to get the character to attack client-side
Character.SetInput(InputType.Attack, true, true);
if (!ActiveAttack.IsRunning)
{
}
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)
{
#if SERVER
GameMain.NetworkMember.CreateEntityEvent(Character, new Character.SetAttackTargetEventData(
attackingLimb,
damageTarget,
targetLimb,
SimPosition));
GameMain.NetworkMember.CreateEntityEvent(Character, new Character.SetAttackTargetEventData(
AttackLimb,
damageTarget,
targetLimb,
SimPosition));
#else
Character.PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3);
Character.PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3);
#endif
}
if (attackingLimb.UpdateAttack(deltaTime, attackSimPos, damageTarget, out AttackResult attackResult, distance, targetLimb))
}
if (AttackLimb.UpdateAttack(deltaTime, attackSimPos, damageTarget, out AttackResult attackResult, distance, targetLimb))
{
if (ActiveAttack.CoolDownTimer > 0)
{
if (attackingLimb.attack.CoolDownTimer > 0)
SetAimTimer(Math.Min(ActiveAttack.CoolDown, 1.5f));
}
if (LatchOntoAI != null && SelectedAiTarget.Entity is Character targetCharacter)
{
LatchOntoAI.SetAttachTarget(targetCharacter);
}
if (!ActiveAttack.Ranged)
{
if (damageTarget.Health > 0 && attackResult.Damage > 0)
{
SetAimTimer(Math.Min(attackingLimb.attack.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 Character))
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)
else
{
LatchOntoAI.SetAttachTarget(targetCharacter);
}
if (!attackingLimb.attack.Ranged)
{
if (damageTarget.Health > 0 && attackResult.Damage > 0)
{
// 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 Character))
{
// Halve the greed for attacking non-characters.
greed /= 2;
}
selectedTargetMemory.Priority += GetRelativeDamage(attackResult.Damage, damageTarget.Health) * greed;
}
else
{
selectedTargetMemory.Priority -= Math.Max(selectedTargetMemory.Priority / 2, 1);
return selectedTargetMemory.Priority > 1;
}
selectedTargetMemory.Priority -= Math.Max(selectedTargetMemory.Priority / 2, 1);
return selectedTargetMemory.Priority > 1;
}
}
return true;
}
return false;
return true;
}
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;
@@ -2238,11 +2370,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);
@@ -2299,7 +2431,6 @@ namespace Barotrauma
{
if (attackVector == null)
{
// TODO: test adding some random variance here?
attackVector = attackWorldPos - WorldPosition;
}
Vector2 dir = Vector2.Normalize(followThrough ? attackVector.Value : -attackVector.Value);
@@ -2319,6 +2450,16 @@ namespace Barotrauma
return true;
}
private Limb GetLimbToRotate(Attack attack)
{
Limb limb = AttackLimb;
if (attack.RotationLimbIndex > -1 && attack.RotationLimbIndex < Character.AnimController.Limbs.Length)
{
limb = Character.AnimController.Limbs[attack.RotationLimbIndex];
}
return limb;
}
#endregion
#region Eat
@@ -2526,13 +2667,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)
{
@@ -2633,7 +2780,7 @@ namespace Barotrauma
}
else if (CanPassThroughHole(s, i))
{
valueModifier *= isInnerWall ? 1 : 0;
valueModifier *= isInnerWall ? 0.5f : 0;
}
else if (!canAttackWalls)
{
@@ -3429,7 +3576,7 @@ namespace Barotrauma
private void ChangeParams(string tag, AIState state, float? priority = null, bool onlyExisting = false)
=> ChangeParams(tag.ToIdentifier(), state, priority, onlyExisting);
private void ChangeParams(Identifier tag, AIState state, float? priority = null, bool onlyExisting = false)
private void ChangeParams(Identifier tag, AIState state, float? priority = null, bool onlyExisting = false, bool ignoreAttacksIfNotInSameSub = false)
{
if (!AIParams.TryGetTarget(tag, out CharacterParams.TargetParams targetParams))
{
@@ -3437,6 +3584,11 @@ namespace Barotrauma
{
if (AIParams.TryAddNewTarget(tag, state, priority ?? minPriority, out targetParams))
{
if (state == AIState.Attack)
{
// Only applies to new temp target params. Shouldn't affect any existing definitions (handled below).
targetParams.IgnoreIfNotInSameSub = ignoreAttacksIfNotInSameSub;
}
tempParams.Add(tag, targetParams);
}
}
@@ -3470,7 +3622,7 @@ namespace Barotrauma
{
isStateChanged = true;
SetStateResetTimer();
ChangeParams(target.SpeciesName, state, priority);
ChangeParams(target.SpeciesName, state, priority, ignoreAttacksIfNotInSameSub: !target.IsHuman);
if (target.IsHuman)
{
priority = GetTargetParams("human")?.Priority;
@@ -3518,6 +3670,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)
@@ -3539,6 +3696,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);
@@ -3554,7 +3716,10 @@ namespace Barotrauma
{
// We only want to check the visibility when the target is in ruins/wreck/similiar place where sneaking should be possible.
// When the monsters attack the player sub, they wall hack so that they can be more aggressive.
checkVisibility = target.Entity.Submarine != null && target.Entity.Submarine == Character.Submarine && target.Entity.Submarine.TeamID == CharacterTeamType.None;
// Pets should always check the visibility, unless the pet and the target are both outside the submarine -> shouldn't target when they can't perceive (= no wall hack)
checkVisibility =
Character.IsPet && (Character.Submarine == null) != (target.Entity.Submarine == null) ||
target.Entity.Submarine != null && target.Entity.Submarine == Character.Submarine && target.Entity.Submarine.TeamID == CharacterTeamType.None;
}
if (dist > 0)
{
@@ -3639,6 +3804,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)
@@ -3698,6 +3864,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();
@@ -395,6 +395,10 @@ namespace Barotrauma
}
objectiveManager.UpdateObjectives(deltaTime);
if (reportProblemsTimer > 0)
{
reportProblemsTimer -= deltaTime;
}
if (reactTimer > 0.0f)
{
reactTimer -= deltaTime;
@@ -407,7 +411,6 @@ namespace Barotrauma
else
{
Character.UpdateTeam();
if (Character.CurrentHull != null)
{
if (Character.IsOnPlayerTeam)
@@ -425,19 +428,15 @@ namespace Barotrauma
}
}
}
if (Character.SpeechImpediment < 100.0f)
if (reportProblemsTimer <= 0.0f)
{
reportProblemsTimer -= deltaTime;
if (reportProblemsTimer <= 0.0f)
if (Character.Submarine != null && (Character.Submarine.TeamID == Character.TeamID || Character.Submarine.TeamID == Character.OriginalTeamID || Character.IsEscorted) && !Character.Submarine.Info.IsWreck)
{
if (Character.Submarine != null && (Character.Submarine.TeamID == Character.TeamID || Character.IsEscorted) && !Character.Submarine.Info.IsWreck)
{
ReportProblems();
}
reportProblemsTimer = reportProblemsInterval;
ReportProblems();
}
UpdateSpeaking();
reportProblemsTimer = reportProblemsInterval;
}
UpdateSpeaking();
UnequipUnnecessaryItems();
reactTimer = GetReactionTime();
}
@@ -912,7 +911,7 @@ namespace Barotrauma
{
Order newOrder = null;
Hull targetHull = null;
bool speak = true;
bool speak = Character.SpeechImpediment < 100;
if (Character.CurrentHull != null)
{
bool isFighting = ObjectiveManager.HasActiveObjective<AIObjectiveCombat>();
@@ -1063,17 +1062,15 @@ namespace Barotrauma
private void UpdateSpeaking()
{
if (!Character.IsOnPlayerTeam) { return; }
if (Character.SpeechImpediment >= 100) { return; }
if (Character.Oxygen < 20.0f)
{
Character.Speak(TextManager.Get("DialogLowOxygen").Value, null, Rand.Range(0.5f, 5.0f), "lowoxygen".ToIdentifier(), 30.0f);
}
if (Character.Bleeding > 2.0f)
{
Character.Speak(TextManager.Get("DialogBleeding").Value, null, Rand.Range(0.5f, 5.0f), "bleeding".ToIdentifier(), 30.0f);
}
if (Character.PressureTimer > 50.0f && Character.CurrentHull?.DisplayName != null)
{
Character.Speak(TextManager.GetWithVariable("DialogPressure", "[roomname]", Character.CurrentHull.DisplayName, FormatCapitals.Yes).Value, null, Rand.Range(0.5f, 5.0f), "pressure".ToIdentifier(), 30.0f);
@@ -1517,7 +1514,7 @@ namespace Barotrauma
startPos.X += MathHelper.Clamp(Character.AnimController.TargetMovement.X, -1.0f, 1.0f);
//do a raycast upwards to find any walls
float minCeilingDist = Character.AnimController.Collider.height / 2 + Character.AnimController.Collider.radius + 0.1f;
float minCeilingDist = Character.AnimController.Collider.Height / 2 + Character.AnimController.Collider.Radius + 0.1f;
shouldCrouch = Submarine.PickBody(startPos, startPos + Vector2.UnitY * minCeilingDist, null, Physics.CollisionWall, customPredicate: (fixture) => { return !(fixture.Body.UserData is Submarine); }) != null;
}
@@ -1615,7 +1612,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;
@@ -1628,7 +1625,7 @@ namespace Barotrauma
float accumulatedDamage = Math.Max(otherHumanAI.structureDamageAccumulator[character], maxAccumulatedDamage);
maxAccumulatedDamage = Math.Max(accumulatedDamage, maxAccumulatedDamage);
if (GameMain.GameSession?.Campaign?.Map?.CurrentLocation != null)
if (GameMain.GameSession?.Campaign?.Map?.CurrentLocation?.Reputation != null)
{
var reputationLoss = damageAmount * Reputation.ReputationLossPerWallDamage;
GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.AddReputation(-reputationLoss);
@@ -1724,7 +1721,7 @@ namespace Barotrauma
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);
@@ -1860,6 +1857,7 @@ namespace Barotrauma
bool targetAdded = false;
DoForEachCrewMember(caller, humanAI =>
{
if (caller != humanAI.Character && caller.SpeechImpediment >= 100) { return; }
var objective = humanAI.ObjectiveManager.GetObjective<T1>();
if (objective != null)
{
@@ -2021,7 +2019,7 @@ namespace Barotrauma
bool friendlyTeam = IsOnFriendlyTeam(me, other);
bool teamGood = sameTeam || friendlyTeam && !onlySameTeam;
if (!teamGood) { return false; }
bool speciesGood = other.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.Group);
bool speciesGood = other.IsPet || other.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.Group);
if (!speciesGood) { return false; }
if (me.TeamID == CharacterTeamType.FriendlyNPC && other.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign)
{
@@ -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;
}
@@ -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();
@@ -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
@@ -88,6 +88,10 @@ namespace Barotrauma
{
currentFlags.Add("EnterOutpost".ToIdentifier());
}
if (Level.Loaded.IsEndBiome)
{
currentFlags.Add("EndLevel".ToIdentifier());
}
}
if (GameMain.GameSession.EventManager.CurrentIntensity <= 0.2f)
{
@@ -126,6 +130,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);
}
}
@@ -748,6 +748,9 @@ namespace Barotrauma
}
if (!character.HasEquippedItem(Weapon, predicate: IsHandSlotType))
{
//clear aim and shoot inputs so the bot doesn't immediately fire the weapon if it was previously e.g. using a scooter
character.ClearInput(InputType.Aim);
character.ClearInput(InputType.Shoot);
Weapon.TryInteract(character, forceSelectKey: true);
var slots = Weapon.AllowedSlots.Where(s => IsHandSlotType(s));
if (character.Inventory.TryPutItem(Weapon, character, slots))
@@ -764,7 +767,7 @@ namespace Barotrauma
}
return true;
bool IsHandSlotType(InvSlotType s) => s == InvSlotType.LeftHand || s == InvSlotType.RightHand || s == (InvSlotType.LeftHand | InvSlotType.RightHand);
static bool IsHandSlotType(InvSlotType s) => s == InvSlotType.LeftHand || s == InvSlotType.RightHand || s == (InvSlotType.LeftHand | InvSlotType.RightHand);
}
private float findHullTimer;
@@ -23,8 +23,8 @@ namespace Barotrauma
protected override float TargetEvaluation()
{
if (!character.IsOnPlayerTeam) { return Targets.None() ? 0 : 100; }
int totalEnemies = Targets.Count();
if (!character.IsOnPlayerTeam && !character.IsOriginallyOnPlayerTeam) { return Targets.None() ? 0 : 100; }
int totalEnemies = Targets.Count;
if (totalEnemies == 0) { return 0; }
if (character.IsSecurity) { return 100; }
if (objectiveManager.IsOrder(this)) { return 100; }
@@ -67,7 +67,7 @@ namespace Barotrauma
if (target.CurrentHull == null) { return false; }
if (HumanAIController.IsFriendly(character, target)) { return false; }
if (!character.Submarine.IsConnectedTo(target.Submarine)) { return false; }
if (!targetCharactersInOtherSubs && character.Submarine.TeamID != target.Submarine.TeamID) { return false; }
if (!targetCharactersInOtherSubs && character.Submarine.TeamID != target.Submarine.TeamID && character.Submarine.TeamID != character.OriginalTeamID) { return false; }
if (target.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI)) { return false; }
if (target.IsArrested) { return false; }
if (EnemyAIController.IsLatchedToSomeoneElse(target, character)) { return false; }
@@ -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;
@@ -222,7 +222,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();
}
@@ -290,7 +290,7 @@ namespace Barotrauma
}
return;
}
if (component.AIOperate(deltaTime, character, this))
if (component.CrewAIOperate(deltaTime, character, this))
{
isDoneOperating = completionCondition == null || completionCondition();
}
@@ -502,10 +502,12 @@ namespace Barotrauma
public static IEnumerable<Affliction> GetTreatableAfflictions(Character character)
{
foreach (Affliction affliction in character.CharacterHealth.GetAllAfflictions())
var allAfflictions = character.CharacterHealth.GetAllAfflictions();
foreach (Affliction affliction in allAfflictions)
{
if (affliction.Prefab.IsBuff || affliction.Strength < affliction.Prefab.TreatmentThreshold) { continue; }
if (!affliction.Prefab.TreatmentSuitability.Any(kvp => kvp.Value > 0)) { continue; }
if (allAfflictions.Any(otherAffliction => affliction.Prefab.IgnoreTreatmentIfAfflictedBy.Contains(otherAffliction.Identifier))) { continue; }
yield return affliction;
}
}
@@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;
using static Barotrauma.CharacterParams;
namespace Barotrauma
{
@@ -44,7 +45,7 @@ namespace Barotrauma
public float PlayTimer { get; set; }
private float? unstunY { get; set; }
public EnemyAIController AiController { get; private set; } = null;
public EnemyAIController AIController { get; private set; } = null;
public Character Owner { get; set; }
@@ -134,8 +135,8 @@ namespace Barotrauma
aggregate += Items[i].Commonness;
if (aggregate >= r && Items[i].Prefab != null)
{
GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetProducedItem:" + pet.AiController.Character.SpeciesName + ":" + Items[i].Prefab.Identifier);
Entity.Spawner.AddItemToSpawnQueue(Items[i].Prefab, pet.AiController.Character.WorldPosition);
GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetProducedItem:" + pet.AIController.Character.SpeciesName + ":" + Items[i].Prefab.Identifier);
Entity.Spawner.AddItemToSpawnQueue(Items[i].Prefab, pet.AIController.Character.WorldPosition);
break;
}
}
@@ -160,8 +161,8 @@ namespace Barotrauma
public PetBehavior(XElement element, EnemyAIController aiController)
{
AiController = aiController;
AiController.Character.CanBeDragged = true;
AIController = aiController;
AIController.Character.CanBeDragged = true;
MaxHappiness = element.GetAttributeFloat("maxhappiness", 100.0f);
MaxHunger = element.GetAttributeFloat("maxhunger", 100.0f);
@@ -218,7 +219,7 @@ namespace Barotrauma
bool success = OnEat(item.GetTags());
if (success)
{
GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetEat:" + AiController.Character.SpeciesName + ":" + item.Prefab.Identifier);
GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetEat:" + AIController.Character.SpeciesName + ":" + item.Prefab.Identifier);
}
return success;
}
@@ -229,7 +230,7 @@ namespace Barotrauma
bool success = OnEat("dead".ToIdentifier());
if (success)
{
GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetEat:" + AiController.Character.SpeciesName + ":" + character.SpeciesName);
GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetEat:" + AIController.Character.SpeciesName + ":" + character.SpeciesName);
}
return success;
}
@@ -252,7 +253,7 @@ namespace Barotrauma
Hunger += foods[i].Hunger;
Happiness += foods[i].Happiness;
#if CLIENT
AiController.Character.PlaySound(CharacterSound.SoundType.Happy, 0.5f);
AIController.Character.PlaySound(CharacterSound.SoundType.Happy, 0.5f);
#endif
return true;
}
@@ -265,20 +266,20 @@ namespace Barotrauma
if (PlayTimer > 0.0f) { return; }
if (Owner == null) { Owner = player; }
PlayTimer = 5.0f;
AiController.Character.IsRagdolled = true;
AIController.Character.IsRagdolled = true;
Happiness += 10.0f;
AiController.Character.AnimController.MainLimb.body.LinearVelocity += new Vector2(0, PlayForce);
unstunY = AiController.Character.SimPosition.Y;
AIController.Character.AnimController.MainLimb.body.LinearVelocity += new Vector2(0, PlayForce);
unstunY = AIController.Character.SimPosition.Y;
#if CLIENT
AiController.Character.PlaySound(CharacterSound.SoundType.Happy, 0.9f);
AIController.Character.PlaySound(CharacterSound.SoundType.Happy, 0.9f);
#endif
}
public string GetTagName()
{
if (AiController.Character.Inventory != null)
if (AIController.Character.Inventory != null)
{
foreach (Item item in AiController.Character.Inventory.AllItems)
foreach (Item item in AIController.Character.Inventory.AllItems)
{
var tag = item.GetComponent<NameTag>();
if (tag != null && !string.IsNullOrWhiteSpace(tag.WrittenName))
@@ -293,7 +294,7 @@ namespace Barotrauma
public void Update(float deltaTime)
{
var character = AiController.Character;
var character = AIController.Character;
if (character?.Removed ?? true || character.IsDead) { return; }
if (unstunY.HasValue)
@@ -332,16 +333,27 @@ namespace Barotrauma
Food food = foods[i];
if (Hunger >= food.HungerRange.X && Hunger <= food.HungerRange.Y)
{
if (food.TargetParams == null &&
AiController.AIParams.TryAddNewTarget(food.Tag, AIState.Eat, food.Priority, out CharacterParams.TargetParams targetParams))
if (food.TargetParams == null)
{
targetParams.IgnoreContained = food.IgnoreContained;
food.TargetParams = targetParams;
if (AIController.AIParams.TryGetTarget(food.Tag, out TargetParams target))
{
food.TargetParams = target;
}
else if (AIController.AIParams.TryAddNewTarget(food.Tag, AIState.Eat, food.Priority, out TargetParams targetParams))
{
food.TargetParams = targetParams;
}
if (food.TargetParams != null)
{
food.TargetParams.State = AIState.Eat;
food.TargetParams.Priority = food.Priority;
food.TargetParams.IgnoreContained = food.IgnoreContained;
}
}
}
else if (food.TargetParams != null)
{
AiController.AIParams.RemoveTarget(food.TargetParams);
AIController.AIParams.RemoveTarget(food.TargetParams);
food.TargetParams = null;
}
}
@@ -116,10 +116,10 @@ namespace Barotrauma
}
// accept only the highest priority order
if (CurrentOrder != null && OrderedCharacter.GetCurrentOrderWithTopPriority() != CurrentOrder)
if (CurrentOrder == null || OrderedCharacter.GetCurrentOrderWithTopPriority() != CurrentOrder)
{
#if DEBUG
ShipCommandManager.ShipCommandLog($"Order {CurrentOrder.Name} did not match current order for character {OrderedCharacter} in {this}");
ShipCommandManager.ShipCommandLog($"{this} is no longer the top priority of {OrderedCharacter}, considering the issue unattended.");
#endif
return false;
}
@@ -356,7 +356,7 @@ namespace Barotrauma
ShipIssueWorkers.Add(new ShipIssueWorkerSteer(this, order));
}
foreach (Item item in CommandedSubmarine.GetItems(true).FindAll(i => i.HasTag("turret")))
foreach (Item item in CommandedSubmarine.GetItems(true).FindAll(i => i.HasTag("turret") && !i.HasTag("hardpoint")))
{
var order = new Order(OrderPrefab.Prefabs["operateweapons"], item, item.GetComponent<Turret>());
ShipIssueWorkers.Add(new ShipIssueWorkerOperateWeapons(this, order));
@@ -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,81 @@ 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;
}
}
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 +90,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 +109,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 +120,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 +181,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 +189,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 +198,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 +218,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;
@@ -228,7 +269,7 @@ namespace Barotrauma
foreach (Submarine submarine in Submarine.Loaded)
{
if (submarine.Info.Type != SubmarineType.Player) { continue; }
if (Vector2.DistanceSquared(submarine.WorldPosition, Wreck.WorldPosition) < minDist * minDist)
if (Vector2.DistanceSquared(submarine.WorldPosition, Submarine.WorldPosition) < minDist * minDist)
{
someoneNearby = true;
break;
@@ -237,14 +278,14 @@ namespace Barotrauma
foreach (Character c in Character.CharacterList)
{
if (c != Character.Controlled && !c.IsRemotePlayer) { continue; }
if (Vector2.DistanceSquared(c.WorldPosition, Wreck.WorldPosition) < minDist * minDist)
if (Vector2.DistanceSquared(c.WorldPosition, Submarine.WorldPosition) < minDist * minDist)
{
someoneNearby = true;
break;
}
}
if (!someoneNearby) { return; }
OperateTurrets(deltaTime);
OperateTurrets(deltaTime, Config.Entity);
if (!IsClient)
{
if (!initialCellsSpawned) { SpawnInitialCells(); }
@@ -287,7 +328,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 +355,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 +374,7 @@ namespace Barotrauma
public void Remove()
{
Kill();
RemoveThalamusItems(Wreck);
RemoveThalamusItems(Submarine);
thalamusItems?.Clear();
thalamusStructures?.Clear();
}
@@ -385,7 +426,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;
@@ -396,7 +437,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; }
@@ -422,19 +463,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)
{
@@ -87,7 +87,7 @@ namespace Barotrauma
}
public bool CanWalk => RagdollParams.CanWalk;
public bool IsMovingBackwards => !InWater && Math.Sign(targetMovement.X) == -Math.Sign(Dir);
public bool IsMovingBackwards => !InWater && Math.Sign(targetMovement.X) == -Math.Sign(Dir) && CurrentAnimationParams is not FishGroundedParams { Flip: false };
// TODO: define death anim duration in XML
protected float deathAnimTimer, deathAnimDuration = 5.0f;
@@ -135,8 +135,14 @@ namespace Barotrauma
public override void UpdateAnim(float deltaTime)
{
if (Frozen) return;
if (MainLimb == null) { return; }
//wait a bit for the ragdoll to "settle" (for joints to force the limbs to appropriate positions) before starting to animate
if (Timing.TotalTime - character.SpawnTime < 0.1f) { return; }
if (Frozen) { return; }
if (MainLimb == null)
{
ResetState();
return;
}
var mainLimb = MainLimb;
levitatingCollider = !IsHanging;
@@ -164,6 +170,7 @@ namespace Barotrauma
//cannot walk but on dry land -> wiggle around
UpdateDying(deltaTime);
}
ResetState();
return;
}
else
@@ -176,11 +183,17 @@ namespace Barotrauma
{
var lowestLimb = FindLowestLimb();
Collider.SetTransform(new Vector2(
Collider.SimPosition.X,
Math.Max(lowestLimb.SimPosition.Y + (Collider.radius + Collider.height / 2), Collider.SimPosition.Y)),
0.0f);
if (InWater)
{
Collider.SetTransform(new Vector2(Collider.SimPosition.X, MainLimb.SimPosition.Y), 0.0f);
}
else
{
Collider.SetTransform(new Vector2(
Collider.SimPosition.X,
Math.Max(lowestLimb.SimPosition.Y + (Collider.Radius + Collider.Height / 2), Collider.SimPosition.Y)),
0.0f);
}
Collider.Enabled = true;
}
@@ -223,6 +236,7 @@ namespace Barotrauma
if (character.SelectedCharacter != null)
{
DragCharacter(character.SelectedCharacter, deltaTime);
ResetState();
return;
}
if (character.AnimController.AnimationTestPose)
@@ -230,7 +244,11 @@ namespace Barotrauma
ApplyTestPose();
}
//don't flip when simply physics is enabled
if (SimplePhysicsEnabled) { return; }
if (SimplePhysicsEnabled)
{
ResetState();
return;
}
if (!character.IsRemotelyControlled && (character.AIController == null || character.AIController.CanFlip) && !Aiming)
{
@@ -264,43 +282,47 @@ namespace Barotrauma
}
}
if (!CurrentFishAnimation.Flip) { return; }
if (IsStuck) { return; }
if (character.AIController != null && !character.AIController.CanFlip) { return; }
flipCooldown -= deltaTime;
if (TargetDir != Direction.None && TargetDir != dir)
if (!IsStuck && CurrentFishAnimation.Flip && character.AIController is not { CanFlip: false })
{
flipTimer += deltaTime;
// Speed reductions are not taken into account here. It's intentional: an ai character cannot flip if it's heavily paralyzed (for example).
float requiredSpeed = CurrentAnimationParams.MovementSpeed / 2;
if (CurrentHull != null)
flipCooldown -= deltaTime;
if (TargetDir != Direction.None && TargetDir != dir)
{
// Enemy movement speeds are halved inside submarines
requiredSpeed /= 2;
}
bool isMovingFastEnough = Math.Abs(MainLimb.LinearVelocity.X) > requiredSpeed;
bool isTryingToMoveHorizontally = Math.Abs(TargetMovement.X) > Math.Abs(TargetMovement.Y);
if ((flipTimer > CurrentFishAnimation.FlipDelay && flipCooldown <= 0.0f && ((isMovingFastEnough && isTryingToMoveHorizontally) || IsMovingBackwards))
|| character.IsRemotePlayer)
{
Flip();
if (!inWater || (CurrentSwimParams != null && CurrentSwimParams.Mirror))
flipTimer += deltaTime;
// Speed reductions are not taken into account here. It's intentional: an ai character cannot flip if it's heavily paralyzed (for example).
float requiredSpeed = CurrentAnimationParams.MovementSpeed / 2;
if (CurrentHull != null)
{
Mirror(CurrentSwimParams != null ? CurrentSwimParams.MirrorLerp : true);
// Enemy movement speeds are halved inside submarines
requiredSpeed /= 2;
}
bool isMovingFastEnough = Math.Abs(MainLimb.LinearVelocity.X) > requiredSpeed;
bool isTryingToMoveHorizontally = Math.Abs(TargetMovement.X) > Math.Abs(TargetMovement.Y);
if ((flipTimer > CurrentFishAnimation.FlipDelay && flipCooldown <= 0.0f && ((isMovingFastEnough && isTryingToMoveHorizontally) || IsMovingBackwards))
|| character.IsRemotePlayer)
{
Flip();
if (!inWater || (CurrentSwimParams != null && CurrentSwimParams.Mirror))
{
Mirror(CurrentSwimParams != null ? CurrentSwimParams.MirrorLerp : true);
}
flipTimer = 0.0f;
flipCooldown = CurrentFishAnimation.FlipCooldown;
}
}
else
{
flipTimer = 0.0f;
flipCooldown = CurrentFishAnimation.FlipCooldown;
}
}
else
ResetState();
void ResetState()
{
flipTimer = 0.0f;
wasAiming = aiming;
aiming = false;
wasAimingMelee = aimingMelee;
aimingMelee = false;
}
wasAiming = aiming;
aiming = false;
wasAimingMelee = aimingMelee;
aimingMelee = false;
}
private bool CanDrag(Character target)
@@ -463,19 +485,26 @@ namespace Barotrauma
if (SimplePhysicsEnabled) { return; }
mainLimb.PullJointEnabled = true;
if (aiming && movement.Length() <= 0.1f)
{
Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition);
Vector2 diff = (mousePos - (GetLimb(LimbType.Torso) ?? MainLimb).SimPosition) * Dir;
TargetMovement = new Vector2(0.0f, -0.1f);
float newRotation = MathUtils.VectorToAngle(diff);
Collider.SmoothRotate(newRotation, CurrentSwimParams.SteerTorque * character.SpeedMultiplier);
}
if (!isMoving)
if (!isMoving && !CurrentSwimParams.UpdateAnimationWhenNotMoving)
{
WalkPos = MathHelper.SmoothStep(WalkPos, MathHelper.PiOver2, deltaTime * 5);
mainLimb.PullJointWorldAnchorB = Collider.SimPosition;
if (aiming)
{
Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition);
Vector2 diff = (mousePos - (GetLimb(LimbType.Torso) ?? MainLimb).SimPosition) * Dir;
TargetMovement = new Vector2(0.0f, -0.1f);
float newRotation = MathHelper.WrapAngle(MathUtils.VectorToAngle(diff) - MathHelper.PiOver2 * Dir);
Collider.SmoothRotate(newRotation, CurrentSwimParams.SteerTorque * character.SpeedMultiplier * 2);
if (TorsoAngle.HasValue)
{
Limb torso = GetLimb(LimbType.Torso);
if (torso != null)
{
SmoothRotateWithoutWrapping(torso, newRotation + TorsoAngle.Value * Dir, mainLimb, TorsoTorque * 2);
}
}
}
}
else
{
@@ -168,7 +168,7 @@ namespace Barotrauma
{
get
{
float shoulderHeight = Collider.height / 2.0f;
float shoulderHeight = Collider.Height / 2.0f;
if (inWater)
{
shoulderHeight += 0.4f;
@@ -299,7 +299,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();
@@ -610,15 +610,18 @@ namespace Barotrauma
torsoAngle -= herpesStrength / 150.0f;
torso.body.SmoothRotate(torsoAngle * Dir, CurrentGroundedParams.TorsoTorque);
}
if (!Aiming && CurrentGroundedParams.FixedHeadAngle && HeadAngle.HasValue)
if (!head.Disabled)
{
float headAngle = HeadAngle.Value;
if (Crouching && !movingHorizontally) { headAngle -= HumanCrouchParams.ExtraHeadAngleWhenStationary; }
head.body.SmoothRotate(headAngle * Dir, CurrentGroundedParams.HeadTorque);
}
else
{
RotateHead(head);
if (!Aiming && CurrentGroundedParams.FixedHeadAngle && HeadAngle.HasValue)
{
float headAngle = HeadAngle.Value;
if (Crouching && !movingHorizontally) { headAngle -= HumanCrouchParams.ExtraHeadAngleWhenStationary; }
head.body.SmoothRotate(headAngle * Dir, CurrentGroundedParams.HeadTorque);
}
else
{
RotateHead(head);
}
}
if (!onGround)
@@ -1117,7 +1120,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;
@@ -1212,7 +1215,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) &&
@@ -1389,7 +1392,7 @@ namespace Barotrauma
target.Oxygen += deltaTime * 0.5f; //Stabilize them
}
bool powerfulCPR = character.HasAbilityFlag(AbilityFlags.PowerfulCPR);
float cprBoost = character.GetStatValue(StatTypes.CPRBoost);
int skill = (int)character.GetSkillLevel("medical");
//pump for 15 seconds (cprAnimTimer 0-15), then do mouth-to-mouth for 2 seconds (cprAnimTimer 15-17)
@@ -1406,7 +1409,7 @@ namespace Barotrauma
{
if (target.Oxygen < -10.0f)
{
if (powerfulCPR)
if (cprBoost >= 1f)
{
//prevent the patient from suffocating no matter how fast their oxygen level is dropping
target.Oxygen = Math.Max(target.Oxygen, -10.0f);
@@ -1453,7 +1456,7 @@ namespace Barotrauma
reviveChance = (float)Math.Pow(reviveChance, CPRSettings.Active.ReviveChanceExponent);
reviveChance = MathHelper.Clamp(reviveChance, CPRSettings.Active.ReviveChanceMin, CPRSettings.Active.ReviveChanceMax);
if (powerfulCPR) { reviveChance *= 2.0f; }
reviveChance *= 1f + cprBoost;
if (Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) <= reviveChance)
{
@@ -1833,8 +1836,6 @@ namespace Barotrauma
{
heldItem.FlipX(relativeToSub: false);
}
// TODO: was this added by a mistake?
//heldItem.FlipX(relativeToSub: false);
}
foreach (Limb limb in Limbs)
@@ -169,18 +169,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;
@@ -571,6 +571,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;
@@ -676,6 +680,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)
@@ -873,7 +881,7 @@ namespace Barotrauma
foreach (Limb limb in Limbs)
{
if (limb == null || limb.IsSevered) { continue; }
if (limb == null || limb.IsSevered || !limb.DoesFlip) { continue; }
limb.Dir = Dir;
limb.MouthPos = new Vector2(-limb.MouthPos.X, limb.MouthPos.Y);
limb.MirrorPullJoint();
@@ -1279,7 +1287,7 @@ namespace Barotrauma
if (!inWater && character.AllowInput && levitatingCollider && Collider.LinearVelocity.Y > -ImpactTolerance && onGround)
{
float targetY = standOnFloorY + ((float)Math.Abs(Math.Cos(Collider.Rotation)) * Collider.height * 0.5f) + Collider.radius + ColliderHeightFromFloor;
float targetY = standOnFloorY + ((float)Math.Abs(Math.Cos(Collider.Rotation)) * Collider.Height * 0.5f) + Collider.Radius + ColliderHeightFromFloor;
if (Math.Abs(Collider.SimPosition.Y - targetY) > 0.01f && onGround)
{
if (Stairs != null)
@@ -1597,7 +1605,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)
@@ -1787,7 +1795,7 @@ namespace Barotrauma
protected void CheckDistFromCollider()
{
float allowedDist = Math.Max(Math.Max(Collider.radius, Collider.width), Collider.height) * 2.0f;
float allowedDist = Math.Max(Math.Max(Collider.Radius, Collider.Width), Collider.Height) * 2.0f;
allowedDist = Math.Max(allowedDist, 1.0f);
float resetDist = allowedDist * 5.0f;
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Items.Components;
namespace Barotrauma
{
@@ -180,15 +181,30 @@ 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]
public float RequiredAngleToShoot { get; set; }
[Serialize(0f, IsPropertySaveable.Yes, description: "How much the attack limb is rotated towards the target. Default 0 = no rotation. Only affects ranged attacks."), Editable]
public float AimRotationTorque { get; set; }
[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>
@@ -521,7 +537,7 @@ namespace Barotrauma
effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
targets.Clear();
targets.AddRange(effect.GetNearbyTargets(worldPosition, targets));
effect.AddNearbyTargets(worldPosition, targets);
effect.Apply(effectType, deltaTime, targetEntity, targets);
}
if (effect.HasTargetType(StatusEffect.TargetType.UseTarget))
@@ -529,6 +545,12 @@ namespace Barotrauma
effect.Apply(effectType, deltaTime, targetEntity, attacker, worldPosition);
}
}
if (effect.HasTargetType(StatusEffect.TargetType.Contained))
{
targets.Clear();
targets.AddRange(attacker.Inventory.AllItems);
effect.Apply(effectType, deltaTime, attacker, targets);
}
}
return attackResult;
@@ -554,7 +576,15 @@ namespace Barotrauma
DamageParticles(deltaTime, worldPosition);
var attackResult = targetLimb.character.ApplyAttack(attacker, worldPosition, this, deltaTime, playSound, targetLimb, penetration: Penetration);
float penetration = Penetration;
float? penetrationValue = SourceItem?.GetComponent<RangedWeapon>()?.Penetration;
if (penetrationValue.HasValue)
{
penetration += penetrationValue.Value;
}
var attackResult = targetLimb.character.ApplyAttack(attacker, worldPosition, this, deltaTime, playSound, targetLimb, penetration);
var effectType = attackResult.Damage > 0.0f ? ActionType.OnUse : ActionType.OnFailure;
foreach (StatusEffect effect in statusEffects)
@@ -584,13 +614,19 @@ namespace Barotrauma
effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
targets.Clear();
targets.AddRange(effect.GetNearbyTargets(worldPosition, targets));
effect.AddNearbyTargets(worldPosition, targets);
effect.Apply(effectType, deltaTime, targetLimb.character, targets);
}
if (effect.HasTargetType(StatusEffect.TargetType.UseTarget))
{
effect.Apply(effectType, deltaTime, targetLimb.character, attacker, worldPosition);
}
if (effect.HasTargetType(StatusEffect.TargetType.Contained))
{
targets.Clear();
targets.AddRange(attacker.Inventory.AllItems);
effect.Apply(effectType, deltaTime, attacker, targets);
}
}
return attackResult;
@@ -129,6 +129,13 @@ namespace Barotrauma
public bool IsCommanding => IsPlayer || (AIController is HumanAIController humanAI && humanAI.ShipCommandManager != null && humanAI.ShipCommandManager.Active);
public bool IsBot => !IsPlayer && AIController is HumanAIController humanAI && humanAI.Enabled;
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
@@ -178,6 +185,13 @@ namespace Barotrauma
}
}
private CharacterTeamType? originalTeamID;
public CharacterTeamType OriginalTeamID
{
get { return originalTeamID ?? teamID; }
}
private Wallet wallet;
public Wallet Wallet
@@ -199,7 +213,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()
{
@@ -214,20 +228,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;
@@ -271,7 +281,7 @@ namespace Barotrauma
{
if (currentTeamChange == removedTeamChange)
{
currentTeamChange = activeTeamChanges[OriginalTeamIdentifier];
currentTeamChange = activeTeamChanges[OriginalChangeTeamIdentifier];
}
}
return activeTeamChanges.Remove(identifier);
@@ -305,7 +315,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;
@@ -611,7 +623,9 @@ namespace Barotrauma
CharacterHealth.SetHealthBarVisibility(value == null);
#endif
bool isServerOrSingleplayer = GameMain.IsSingleplayer || GameMain.NetworkMember is { IsServer: true };
if (IsPlayer && isServerOrSingleplayer && value is { IsDead: true, Wallet: { Balance: var balance } grabbedWallet } && balance > 0)
CheckTalents(AbilityEffectType.OnLootCharacter, new AbilityCharacterLoot(value));
if (IsPlayer && isServerOrSingleplayer && value is { IsDead: true, Wallet: { Balance: var balance and > 0 } grabbedWallet })
{
#if SERVER
if (GameMain.GameSession.Campaign is MultiPlayerCampaign mpCampaign && GameMain.Server is { ServerSettings: { } settings })
@@ -999,7 +1013,7 @@ namespace Barotrauma
}
}
public bool InWater => AnimController?.InWater ?? false;
public bool InWater => AnimController is AnimController { InWater: true };
public bool GodMode = false;
@@ -1053,6 +1067,8 @@ namespace Barotrauma
}
}
public HashSet<Identifier> MarkedAsLooted = new();
public bool IsInFriendlySub => Submarine != null && Submarine.TeamID == TeamID;
public delegate void OnDeathHandler(Character character, CauseOfDeath causeOfDeath);
@@ -1574,19 +1590,37 @@ namespace Barotrauma
}
if (createNetworkEvent && GameMain.NetworkMember is { IsServer: true })
{
GameMain.NetworkMember.CreateEntityEvent(item, new Item.ChangePropertyEventData(item.SerializableProperties[nameof(item.Tags).ToIdentifier()]));
GameMain.NetworkMember.CreateEntityEvent(item, new Item.ChangePropertyEventData(item.SerializableProperties[nameof(item.Tags).ToIdentifier()], item));
}
}
}
public float GetSkillLevel(string skillIdentifier) =>
GetSkillLevel(skillIdentifier.ToIdentifier());
private static readonly ImmutableDictionary<Identifier, StatTypes> overrideStatTypes = new Dictionary<Identifier, StatTypes>
{
{ new("helm"), StatTypes.HelmSkillOverride },
{ new("medical"), StatTypes.MedicalSkillOverride },
{ new("weapons"), StatTypes.WeaponsSkillOverride },
{ new("electrical"), StatTypes.ElectricalSkillOverride },
{ new("mechanical"), StatTypes.MechanicalSkillOverride }
}.ToImmutableDictionary();
public float GetSkillLevel(Identifier skillIdentifier)
{
if (Info?.Job == null) { return 0.0f; }
float skillLevel = Info.Job.GetSkillLevel(skillIdentifier);
if (overrideStatTypes.TryGetValue(skillIdentifier, out StatTypes statType))
{
float skillOverride = GetStatValue(statType);
if (skillOverride > skillLevel)
{
skillLevel = skillOverride;
}
}
// apply multipliers first so that multipliers only affect base skill value
foreach (Affliction affliction in CharacterHealth.GetAllAfflictions())
{
@@ -1617,6 +1651,7 @@ namespace Barotrauma
skillLevel += GetStatValue(GetSkillStatType(skillIdentifier));
return skillLevel;
}
@@ -1948,6 +1983,15 @@ namespace Barotrauma
}
}
#endif
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && Controlled != this && IsKeyDown(InputType.Aim))
{
if (currentAttackTarget.AttackLimb?.attack is Attack { Ranged: true } attack && AIController is EnemyAIController enemyAi)
{
enemyAi.AimRangedAttack(attack, currentAttackTarget.DamageTarget as Entity);
}
}
if (attackCoolDown > 0.0f)
{
attackCoolDown -= deltaTime;
@@ -1958,7 +2002,7 @@ namespace Barotrauma
{
if ((currentAttackTarget.DamageTarget as Entity)?.Removed ?? false)
{
currentAttackTarget = default(AttackTargetData);
currentAttackTarget = default;
}
currentAttackTarget.AttackLimb?.UpdateAttack(deltaTime, currentAttackTarget.AttackPos, currentAttackTarget.DamageTarget, out _);
}
@@ -2053,58 +2097,56 @@ namespace Barotrauma
}
}
bool CanUseItemsWhenSelected(Item item) => item == null || !item.Prefab.DisableItemUsageWhenSelected;
if (CanUseItemsWhenSelected(SelectedItem) && CanUseItemsWhenSelected(SelectedSecondaryItem))
if (Inventory != null)
{
foreach (Item item in HeldItems)
bool CanUseItemsWhenSelected(Item item) => item == null || !item.Prefab.DisableItemUsageWhenSelected;
if (CanUseItemsWhenSelected(SelectedItem) && CanUseItemsWhenSelected(SelectedSecondaryItem))
{
if (IsKeyDown(InputType.Aim) || !item.RequireAimToSecondaryUse)
foreach (Item item in HeldItems)
{
item.SecondaryUse(deltaTime, this);
tryUseItem(item, deltaTime);
}
if (IsKeyDown(InputType.Use) && !item.IsShootable)
foreach (Item item in Inventory.AllItems)
{
if (!item.RequireAimToUse || IsKeyDown(InputType.Aim))
if (item.GetComponent<Wearable>() is { AllowUseWhenWorn: true } && HasEquippedItem(item))
{
item.Use(deltaTime, this);
tryUseItem(item, deltaTime);
}
}
if (IsKeyDown(InputType.Shoot) && item.IsShootable)
}
}
void tryUseItem(Item item, float deltaTime)
{
if (IsKeyDown(InputType.Aim) || !item.RequireAimToSecondaryUse)
{
item.SecondaryUse(deltaTime, this);
}
if (IsKeyDown(InputType.Use) && !item.IsShootable)
{
if (!item.RequireAimToUse || IsKeyDown(InputType.Aim))
{
if (!item.RequireAimToUse || IsKeyDown(InputType.Aim))
{
item.Use(deltaTime, this);
}
item.Use(deltaTime, this);
}
}
if (IsKeyDown(InputType.Shoot) && item.IsShootable)
{
if (!item.RequireAimToUse || IsKeyDown(InputType.Aim))
{
item.Use(deltaTime, this);
}
#if CLIENT
else if (item.RequireAimToUse && !IsKeyDown(InputType.Aim))
{
HintManager.OnShootWithoutAiming(this, item);
}
#endif
else if (item.RequireAimToUse && !IsKeyDown(InputType.Aim))
{
HintManager.OnShootWithoutAiming(this, item);
}
#endif
}
}
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)
@@ -2878,7 +2920,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);
}
}
@@ -3788,7 +3832,7 @@ namespace Barotrauma
return attackResult;
}
public void TrySeverLimbJoints(Limb targetLimb, float severLimbsProbability, float damage, bool allowBeheading, Character attacker = null)
public void TrySeverLimbJoints(Limb targetLimb, float severLimbsProbability, float damage, bool allowBeheading, bool ignoreSeveranceProbabilityModifier = false, Character attacker = null)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
#if DEBUG
@@ -3798,7 +3842,7 @@ namespace Barotrauma
return;
}
#endif
if (damage < targetLimb.Params.MinSeveranceDamage) { return; }
if (damage > 0 && damage < targetLimb.Params.MinSeveranceDamage) { return; }
if (!IsDead)
{
if (!allowBeheading && targetLimb.type == LimbType.Head) { return; }
@@ -3816,7 +3860,7 @@ namespace Barotrauma
var referenceLimb = targetLimb.type == LimbType.Head && targetLimb.Params.ID == 0 ? joint.LimbA : joint.LimbB;
if (referenceLimb != targetLimb) { continue; }
float probability = severLimbsProbability;
if (!IsDead)
if (!IsDead && !ignoreSeveranceProbabilityModifier)
{
probability *= joint.Params.SeveranceProbabilityModifier;
}
@@ -3938,13 +3982,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)
{
@@ -3992,7 +4029,12 @@ namespace Barotrauma
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;
}
@@ -4075,7 +4117,7 @@ namespace Barotrauma
statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
targets.Clear();
targets.AddRange(statusEffect.GetNearbyTargets(WorldPosition, targets));
statusEffect.AddNearbyTargets(WorldPosition, targets);
statusEffect.Apply(actionType, deltaTime, this, targets);
}
else if (statusEffect.targetLimbs != null)
@@ -4709,6 +4751,8 @@ namespace Barotrauma
public bool HasJob(string identifier) => Info?.Job?.Prefab.Identifier == identifier;
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);
@@ -4789,6 +4833,32 @@ namespace Barotrauma
return info.UnlockedTalents.Contains(identifier);
}
private readonly HashSet<Hull> sameRoomHulls = new();
/// <summary>
/// Check if the character is in the same room
/// Room and hull differ in that a room can consist of multiple linked hulls
/// </summary>
public bool IsInSameRoomAs(Character character)
{
if (character == this) { return true; }
if (character.CurrentHull is null || CurrentHull is null)
{
// Outside doesn't count as a room
return false;
}
if (character.Submarine != Submarine) { return false; }
if (character.CurrentHull == CurrentHull) { return true; }
sameRoomHulls.Clear();
CurrentHull.GetLinkedEntities(sameRoomHulls);
sameRoomHulls.Add(CurrentHull);
return sameRoomHulls.Contains(character.CurrentHull);
}
public bool HasUnlockedAllTalents()
{
if (TalentTree.JobTalentTrees.TryGet(Info.Job.Prefab.Identifier, out TalentTree talentTree))
@@ -4797,7 +4867,7 @@ namespace Barotrauma
{
foreach (TalentOption talentOption in talentSubTree.TalentOptionStages)
{
if (talentOption.TalentIdentifiers.None(t => HasTalent(t)))
if (talentOption.TalentIdentifiers.None(HasTalent))
{
return false;
}
@@ -4842,6 +4912,19 @@ namespace Barotrauma
return characterTalents.Any(t => t.UnlockedRecipes.Contains(recipeIdentifier));
}
public bool HasStoreAccessForItem(ItemPrefab prefab)
{
foreach (CharacterTalent talent in characterTalents)
{
foreach (Identifier unlockedItem in talent.UnlockedStoreItems)
{
if (prefab.Tags.Contains(unlockedItem)) { return true; }
}
}
return false;
}
/// <summary>
/// Shows visual notification of money gained by the specific player. Useful for mid-mission monetary gains.
/// </summary>
@@ -4900,7 +4983,7 @@ namespace Barotrauma
/// </summary>
private readonly Dictionary<StatTypes, float> wearableStatValues = new Dictionary<StatTypes, float>();
public float GetStatValue(StatTypes statType)
public float GetStatValue(StatTypes statType, bool includeSaved = true)
{
if (!IsHuman) { return 0f; }
@@ -4913,7 +4996,7 @@ namespace Barotrauma
{
statValue += CharacterHealth.GetStatValue(statType);
}
if (Info != null)
if (Info != null && includeSaved)
{
// could be optimized by instead updating the Character.cs statvalues dictionary whenever the CharacterInfo.cs values change
statValue += Info.GetSavedStatValue(statType);
@@ -5054,6 +5137,16 @@ namespace Barotrauma
}
}
internal sealed class AbilityCharacterLoot : AbilityObject, IAbilityCharacter
{
public Character Character { get; set; }
public AbilityCharacterLoot(Character character)
{
Character = character;
}
}
class AbilityCharacterKill : AbilityObject, IAbilityCharacter
{
public AbilityCharacterKill(Character character, Character killer)
@@ -305,6 +305,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>
@@ -657,7 +659,6 @@ namespace Barotrauma
{
Name = GetRandomName(randSync);
}
TryLoadNameAndTitle(npcIdentifier);
SetPersonalityTrait();
@@ -735,9 +736,7 @@ namespace Barotrauma
Name = infoElement.GetAttributeString("name", "");
OriginalName = infoElement.GetAttributeString("originalname", null);
Salary = infoElement.GetAttributeInt("salary", 1000);
ExperiencePoints = infoElement.GetAttributeInt("experiencepoints", 0);
UnlockedTalents = new HashSet<Identifier>(infoElement.GetAttributeIdentifierArray("unlockedtalents", Array.Empty<Identifier>()));
AdditionalTalentPoints = infoElement.GetAttributeInt("additionaltalentpoints", 0);
HashSet<Identifier> tags = infoElement.GetAttributeIdentifierArray("tags", Array.Empty<Identifier>()).ToHashSet();
LoadTagsBackwardsCompatibility(infoElement, tags);
@@ -813,18 +812,24 @@ namespace Barotrauma
infoElement.GetAttributeIdentifier("npcid", Identifier.Empty));
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;
if (subElement.Name.ToString().Equals("job", StringComparison.OrdinalIgnoreCase) && !jobCreated)
Identifier elementName = subElement.Name.ToIdentifier();
if (elementName == "job" && !jobCreated)
{
Job = new Job(subElement);
jobCreated = true;
// there used to be a break here, but it had to be removed to make room for statvalues
// using the jobCreated boolean to make sure that only the first job found is created
}
else if (subElement.Name.ToString().Equals("savedstatvalues", StringComparison.OrdinalIgnoreCase))
else if (elementName == "savedstatvalues")
{
foreach (XElement savedStat in subElement.Elements())
{
@@ -838,8 +843,8 @@ namespace Barotrauma
float value = savedStat.GetAttributeFloat("statvalue", 0f);
if (value == 0f) { continue; }
string statIdentifier = savedStat.GetAttributeString("statidentifier", "").ToLowerInvariant();
if (string.IsNullOrEmpty(statIdentifier))
Identifier statIdentifier = savedStat.GetAttributeIdentifier("statidentifier", Identifier.Empty);
if (statIdentifier.IsEmpty)
{
DebugConsole.ThrowError("Stat identifier not specified for Stat Value when loading character data in CharacterInfo!");
return;
@@ -849,6 +854,20 @@ namespace Barotrauma
ChangeSavedStatValue(statType, value, statIdentifier, removeOnDeath);
}
}
else if (elementName == "talents")
{
Version version = subElement.GetAttributeVersion("version", GameMain.Version); // for future maybe
foreach (XElement talentElement in subElement.Elements())
{
if (talentElement.Name.ToIdentifier() != "talent") { continue; }
Identifier talentIdentifier = talentElement.GetAttributeIdentifier("identifier", Identifier.Empty);
if (talentIdentifier == Identifier.Empty) { continue; }
UnlockedTalents.Add(talentIdentifier);
}
}
}
LoadHeadAttachments();
}
@@ -1125,7 +1144,7 @@ namespace Barotrauma
partial void LoadAttachmentSprites();
private int CalculateSalary()
public int CalculateSalary()
{
if (Name == null || Job == null) { return 0; }
@@ -1149,13 +1168,17 @@ namespace Barotrauma
increase *= 1f + Character.GetStatValue(StatTypes.SkillGainSpeed);
increase = GetSkillSpecificGain(increase, skillIdentifier);
float prevLevel = Job.GetSkillLevel(skillIdentifier);
Job.IncreaseSkillLevel(skillIdentifier, increase, Character.HasAbilityFlag(AbilityFlags.GainSkillPastMaximum));
float newLevel = Job.GetSkillLevel(skillIdentifier);
if ((int)newLevel > (int)prevLevel)
{
{
float extraLevel = Character.GetStatValue(StatTypes.ExtraLevelGain);
Job.IncreaseSkillLevel(skillIdentifier, extraLevel, Character.HasAbilityFlag(AbilityFlags.GainSkillPastMaximum));
// assume we are getting at least 1 point in skill, since this logic only runs in such cases
float increaseSinceLastSkillPoint = MathHelper.Max(increase, 1f);
var abilitySkillGain = new AbilitySkillGain(increaseSinceLastSkillPoint, skillIdentifier, Character, gainedFromAbility);
@@ -1169,6 +1192,25 @@ namespace Barotrauma
OnSkillChanged(skillIdentifier, prevLevel, newLevel);
}
private static readonly ImmutableDictionary<Identifier, StatTypes> skillGainStatValues = new Dictionary<Identifier, StatTypes>
{
{ new("helm"), StatTypes.HelmSkillGainSpeed },
{ new("medical"), StatTypes.WeaponsSkillGainSpeed },
{ new("weapons"), StatTypes.MedicalSkillGainSpeed },
{ new("electrical"), StatTypes.ElectricalSkillGainSpeed },
{ new("mechanical"), StatTypes.MechanicalSkillGainSpeed }
}.ToImmutableDictionary();
private float GetSkillSpecificGain(float increase, Identifier skillIdentifier)
{
if (skillGainStatValues.TryGetValue(skillIdentifier, out StatTypes statType))
{
increase *= 1f + Character.GetStatValue(statType);
}
return increase;
}
public void SetSkillLevel(Identifier skillIdentifier, float level)
{
if (Job == null) { return; }
@@ -1194,10 +1236,6 @@ namespace Barotrauma
int prevAmount = ExperiencePoints;
var experienceGainMultiplier = new AbilityExperienceGainMultiplier(1f);
if (isMissionExperience)
{
Character?.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplier);
}
experienceGainMultiplier.Value += Character?.GetStatValue(StatTypes.ExperienceGainMultiplier) ?? 0;
amount = (int)(amount * experienceGainMultiplier.Value);
@@ -1314,7 +1352,6 @@ namespace Barotrauma
new XAttribute("tags", string.Join(",", Head.Preset.TagSet)),
new XAttribute("salary", Salary),
new XAttribute("experiencepoints", ExperiencePoints),
new XAttribute("unlockedtalents", string.Join(",", UnlockedTalents)),
new XAttribute("additionaltalentpoints", AdditionalTalentPoints),
new XAttribute("hairindex", Head.HairIndex),
new XAttribute("beardindex", Head.BeardIndex),
@@ -1337,6 +1374,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)
@@ -1363,7 +1407,16 @@ namespace Barotrauma
}
}
XElement talentElement = new XElement("Talents");
talentElement.Add(new XAttribute("version", GameMain.Version.ToString()));
foreach (Identifier talentIdentifier in UnlockedTalents)
{
talentElement.Add(new XElement("Talent", new XAttribute("identifier", talentIdentifier)));
}
charElement.Add(savedStatElement);
charElement.Add(talentElement);
parentElement?.Add(charElement);
return charElement;
}
@@ -1717,20 +1770,33 @@ namespace Barotrauma
}
}
public void ResetSavedStatValue(string statIdentifier)
public void ResetSavedStatValue(Identifier statIdentifier)
{
foreach (StatTypes statType in SavedStatValues.Keys)
{
bool changed = false;
foreach (SavedStatValue savedStatValue in SavedStatValues[statType])
{
if (savedStatValue.StatIdentifier != statIdentifier) { continue; }
if (!MatchesIdentifier(savedStatValue.StatIdentifier, statIdentifier)) { continue; }
if (MathUtils.NearlyEqual(savedStatValue.StatValue, 0.0f)) { continue; }
savedStatValue.StatValue = 0.0f;
changed = true;
}
if (changed) { OnPermanentStatChanged(statType); }
}
static bool MatchesIdentifier(Identifier statIdentifier, Identifier identifier)
{
if (statIdentifier == identifier) { return true; }
if (identifier.IndexOf('*') is var index and > -1)
{
return statIdentifier.StartsWith(identifier[0..index]);
}
return false;
}
}
public float GetSavedStatValue(StatTypes statType)
@@ -1748,7 +1814,7 @@ namespace Barotrauma
{
if (SavedStatValues.TryGetValue(statType, out var statValues))
{
return statValues.Where(s => s.StatIdentifier == statIdentifier).Sum(v => v.StatValue);
return statValues.Where(value => ToolBox.StatIdentifierMatches(value.StatIdentifier, statIdentifier)).Sum(static v => v.StatValue);
}
else
{
@@ -1756,7 +1822,7 @@ namespace Barotrauma
}
}
public void ChangeSavedStatValue(StatTypes statType, float value, string statIdentifier, bool removeOnDeath, float maxValue = float.MaxValue, bool setValue = false)
public void ChangeSavedStatValue(StatTypes statType, float value, Identifier statIdentifier, bool removeOnDeath, float maxValue = float.MaxValue, bool setValue = false)
{
if (!SavedStatValues.ContainsKey(statType))
{
@@ -1779,13 +1845,13 @@ namespace Barotrauma
}
}
public class SavedStatValue
internal sealed class SavedStatValue
{
public string StatIdentifier { get; set; }
public Identifier StatIdentifier { get; set; }
public float StatValue { get; set; }
public bool RemoveOnDeath { get; set; }
public SavedStatValue(string statIdentifier, float value, bool removeOnDeath)
public SavedStatValue(Identifier statIdentifier, float value, bool removeOnDeath)
{
StatValue = value;
RemoveOnDeath = removeOnDeath;
@@ -1793,7 +1859,7 @@ namespace Barotrauma
}
}
class AbilitySkillGain : AbilityObject, IAbilityValue, IAbilitySkillIdentifier, IAbilityCharacter
internal sealed class AbilitySkillGain : AbilityObject, IAbilityValue, IAbilitySkillIdentifier, IAbilityCharacter
{
public AbilitySkillGain(float skillAmount, Identifier skillIdentifier, Character character, bool gainedFromAbility)
{
@@ -71,6 +71,13 @@ namespace Barotrauma
/// </summary>
public Character Source;
private readonly static LocalizedString[] strengthTexts = new LocalizedString[]
{
TextManager.Get("AfflictionStrengthLow"),
TextManager.Get("AfflictionStrengthMedium"),
TextManager.Get("AfflictionStrengthHigh")
};
public Affliction(AfflictionPrefab prefab, float strength)
{
#if CLIENT
@@ -89,6 +96,7 @@ namespace Barotrauma
}
}
public void Serialize(XElement element)
{
SerializableProperty.SerializeProperties(this, element);
@@ -108,6 +116,17 @@ namespace Barotrauma
public override string ToString() => Prefab == null ? "Affliction (Invalid)" : $"Affliction ({Prefab.Name})";
public LocalizedString GetStrengthText()
{
return GetStrengthText(Strength, Prefab.MaxStrength);
}
public static LocalizedString GetStrengthText(float strength, float maxStrength)
{
return strengthTexts[
MathHelper.Clamp((int)Math.Floor(strength / maxStrength * strengthTexts.Length), 0, strengthTexts.Length - 1)];
}
public AfflictionPrefab.Effect GetActiveEffect() => Prefab.GetActiveEffect(Strength);
public float GetVitalityDecrease(CharacterHealth characterHealth)
@@ -429,7 +448,7 @@ namespace Barotrauma
statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
targets.Clear();
targets.AddRange(statusEffect.GetNearbyTargets(characterHealth.Character.WorldPosition, targets));
statusEffect.AddNearbyTargets(characterHealth.Character.WorldPosition, targets);
statusEffect.Apply(type, deltaTime, characterHealth.Character, targets);
}
}
@@ -10,7 +10,8 @@
public override void Update(CharacterHealth characterHealth, Limb targetLimb, float deltaTime)
{
base.Update(characterHealth, targetLimb, deltaTime);
characterHealth.BloodlossAmount += Strength * (1.0f / 60.0f) * deltaTime;
float bloodlossResistance = GetResistance(characterHealth.BloodlossAffliction.Identifier);
characterHealth.BloodlossAmount += Strength * (1.0f - bloodlossResistance) / 60.0f * deltaTime;
if (Source != null)
{
characterHealth.BloodlossAffliction.Source = Source;
@@ -22,7 +22,7 @@ namespace Barotrauma
private Character character;
private bool stun = true;
private bool stun = false;
private readonly List<Affliction> huskInfection = new List<Affliction>();
@@ -216,7 +216,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);
}
@@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Reflection;
using System.Xml.Linq;
using Barotrauma.Extensions;
using System.Collections.Immutable;
namespace Barotrauma
{
@@ -67,7 +68,6 @@ namespace Barotrauma
}
// Remove "[speciesname]" for backward support (we don't use it anymore)
HuskedSpeciesName = HuskedSpeciesName.Remove("[speciesname]").ToIdentifier();
TargetSpecies = element.GetAttributeIdentifierArray("targets", Array.Empty<Identifier>(), trim: true);
if (TargetSpecies.Length == 0)
{
DebugConsole.NewMessage($"No 'targets' defined for the husk affliction ({Identifier}) in {element}", Color.Orange);
@@ -109,7 +109,6 @@ namespace Barotrauma
public float TransformThresholdOnDeath;
public readonly Identifier HuskedSpeciesName;
public readonly Identifier[] TargetSpecies;
public readonly bool TransferBuffs;
public readonly bool SendMessages;
@@ -214,7 +213,6 @@ namespace Barotrauma
[Serialize("", IsPropertySaveable.No)]
public Identifier DialogFlag { get; private set; }
[Serialize("", IsPropertySaveable.No)]
public Identifier Tag { get; private set; }
@@ -276,6 +274,47 @@ namespace Barotrauma
}
}
public class Description
{
public enum TargetType
{
Any,
Self,
OtherCharacter
}
public readonly LocalizedString Text;
public readonly Identifier TextTag;
public readonly float MinStrength, MaxStrength;
public readonly TargetType Target;
public Description(ContentXElement element, AfflictionPrefab affliction)
{
TextTag = element.GetAttributeIdentifier("textidentifier", Identifier.Empty);
if (!TextTag.IsEmpty)
{
Text = TextManager.Get(TextTag);
}
string text = element.GetAttributeString("text", string.Empty);
if (!text.IsNullOrEmpty())
{
Text = Text?.Fallback(text) ?? text;
}
else if (TextTag.IsEmpty)
{
DebugConsole.ThrowError($"Error in affliction \"{affliction.Identifier}\" - no text defined for one of the descriptions.");
}
MinStrength = element.GetAttributeFloat(nameof(MinStrength), 0.0f);
MaxStrength = element.GetAttributeFloat(nameof(MaxStrength), 100.0f);
if (MinStrength >= MaxStrength)
{
DebugConsole.ThrowError($"Error in affliction \"{affliction.Identifier}\" - max strength is not larger than min.");
}
Target = element.GetAttributeEnum(nameof(Target), TargetType.Any);
}
}
public class PeriodicEffect
{
public readonly List<StatusEffect> StatusEffects = new List<StatusEffect>();
@@ -313,7 +352,6 @@ namespace Barotrauma
public static readonly PrefabCollection<AfflictionPrefab> Prefabs = new PrefabCollection<AfflictionPrefab>();
private bool disposed = false;
public override void Dispose() { }
public static IEnumerable<AfflictionPrefab> List => Prefabs;
@@ -330,15 +368,21 @@ namespace Barotrauma
//(e.g. mental health problems on head, lack of oxygen on torso...)
public readonly LimbType IndicatorLimb;
public readonly LocalizedString Name, Description;
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 LocalizedString CauseOfDeathDescription, SelfCauseOfDeathDescription;
private readonly LocalizedString defaultDescription;
public readonly ImmutableList<Description> Descriptions;
public readonly bool HideIconAfterDelay;
//how high the strength has to be for the affliction to take affect
public readonly float ActivationThreshold = 0.0f;
//how high the strength has to be for the affliction icon to be shown in the UI
@@ -355,6 +399,11 @@ namespace Barotrauma
//how strong the affliction needs to be before bots attempt to treat it
public readonly float TreatmentThreshold = 5.0f;
/// <summary>
/// Bots will not try to treat the affliction if the character has any of these afflictions
/// </summary>
public ImmutableHashSet<Identifier> IgnoreTreatmentIfAfflictedBy;
/// <summary>
/// The affliction is automatically removed after this time. 0 = unlimited
/// </summary>
@@ -384,6 +433,10 @@ namespace Barotrauma
private readonly ConstructorInfo constructor;
public Identifier[] TargetSpecies { get; protected set; }
public readonly bool ResetBetweenRounds;
public IEnumerable<KeyValuePair<Identifier, float>> TreatmentSuitability
{
get
@@ -411,13 +464,14 @@ namespace Barotrauma
{
Name = Name.Fallback(fallbackName);
}
Description = TextManager.Get($"AfflictionDescription.{TranslationIdentifier}");
defaultDescription = TextManager.Get($"AfflictionDescription.{TranslationIdentifier}");
string fallbackDescription = element.GetAttributeString("description", "");
if (!string.IsNullOrEmpty(fallbackDescription))
{
Description = Description.Fallback(fallbackDescription);
defaultDescription = defaultDescription.Fallback(fallbackDescription);
}
IsBuff = element.GetAttributeBool("isbuff", false);
IsBuff = element.GetAttributeBool(nameof(IsBuff), false);
AffectMachines = element.GetAttributeBool(nameof(AffectMachines), true);
HealableInMedicalClinic = element.GetAttributeBool("healableinmedicalclinic",
!IsBuff &&
@@ -426,6 +480,8 @@ namespace Barotrauma
HealCostMultiplier = element.GetAttributeFloat(nameof(HealCostMultiplier), 1f);
BaseHealCost = element.GetAttributeInt(nameof(BaseHealCost), 0);
IgnoreTreatmentIfAfflictedBy = element.GetAttributeIdentifierArray(nameof(IgnoreTreatmentIfAfflictedBy), Array.Empty<Identifier>()).ToImmutableHashSet();
Duration = element.GetAttributeFloat(nameof(Duration), 0.0f);
if (element.GetAttribute("nameidentifier") != null)
@@ -443,28 +499,35 @@ namespace Barotrauma
}
}
ActivationThreshold = element.GetAttributeFloat("activationthreshold", 0.0f);
ShowIconThreshold = element.GetAttributeFloat("showiconthreshold", Math.Max(ActivationThreshold, 0.05f));
ShowIconToOthersThreshold = element.GetAttributeFloat("showicontoothersthreshold", ShowIconThreshold);
MaxStrength = element.GetAttributeFloat("maxstrength", 100.0f);
GrainBurst = element.GetAttributeFloat(nameof(GrainBurst).ToLowerInvariant(), 0.0f);
HideIconAfterDelay = element.GetAttributeBool(nameof(HideIconAfterDelay), false);
ShowInHealthScannerThreshold = element.GetAttributeFloat("showinhealthscannerthreshold",
ActivationThreshold = element.GetAttributeFloat(nameof(ActivationThreshold), 0.0f);
ShowIconThreshold = element.GetAttributeFloat(nameof(ShowIconThreshold), Math.Max(ActivationThreshold, 0.05f));
ShowIconToOthersThreshold = element.GetAttributeFloat(nameof(ShowIconToOthersThreshold), ShowIconThreshold);
MaxStrength = element.GetAttributeFloat(nameof(MaxStrength), 100.0f);
GrainBurst = element.GetAttributeFloat(nameof(GrainBurst), 0.0f);
ShowInHealthScannerThreshold = element.GetAttributeFloat(nameof(ShowInHealthScannerThreshold),
Math.Max(ActivationThreshold, AfflictionType == "talentbuff" ? float.MaxValue : ShowIconToOthersThreshold));
TreatmentThreshold = element.GetAttributeFloat("treatmentthreshold", Math.Max(ActivationThreshold, 5.0f));
TreatmentThreshold = element.GetAttributeFloat(nameof(TreatmentThreshold), Math.Max(ActivationThreshold, 5.0f));
DamageOverlayAlpha = element.GetAttributeFloat("damageoverlayalpha", 0.0f);
BurnOverlayAlpha = element.GetAttributeFloat("burnoverlayalpha", 0.0f);
DamageOverlayAlpha = element.GetAttributeFloat(nameof(DamageOverlayAlpha), 0.0f);
BurnOverlayAlpha = element.GetAttributeFloat(nameof(BurnOverlayAlpha), 0.0f);
KarmaChangeOnApplied = element.GetAttributeFloat("karmachangeonapplied", 0.0f);
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", ""));
IconColors = element.GetAttributeColorArray("iconcolors", null);
AfflictionOverlayAlphaIsLinear = element.GetAttributeBool("afflictionoverlayalphaislinear", false);
AchievementOnRemoved = element.GetAttributeIdentifier("achievementonremoved", "");
IconColors = element.GetAttributeColorArray(nameof(IconColors), null);
AfflictionOverlayAlphaIsLinear = element.GetAttributeBool(nameof(AfflictionOverlayAlphaIsLinear), false);
AchievementOnRemoved = element.GetAttributeIdentifier(nameof(AchievementOnRemoved), "");
TargetSpecies = element.GetAttributeIdentifierArray("targets", Array.Empty<Identifier>(), trim: true);
ResetBetweenRounds = element.GetAttributeBool("resetbetweenrounds", false);
List<Description> descriptions = new List<Description>();
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
@@ -481,15 +544,38 @@ namespace Barotrauma
case "effect":
case "periodiceffect":
break;
case "description":
descriptions.Add(new Description(subElement, this));
break;
default:
DebugConsole.AddWarning($"Unrecognized element in affliction \"{Identifier}\" ({subElement.Name})");
break;
}
}
Descriptions = descriptions.ToImmutableList();
constructor = type.GetConstructor(new[] { typeof(AfflictionPrefab), typeof(float) });
}
public LocalizedString GetDescription(float strength, Description.TargetType targetType)
{
foreach (var description in Descriptions)
{
if (strength < description.MinStrength || strength > description.MaxStrength) { continue; }
switch (targetType)
{
case Description.TargetType.Self:
if (description.Target == Description.TargetType.OtherCharacter) { continue; }
break;
case Description.TargetType.OtherCharacter:
if (description.Target == Description.TargetType.Self) { continue; }
break;
}
return description.Text;
}
return defaultDescription;
}
public static void LoadAllEffects()
{
Prefabs.ForEach(p => p.LoadEffects());
@@ -104,7 +104,7 @@ namespace Barotrauma
public bool DoesBleed
{
get => Character.Params.Health.DoesBleed;
get => Character.Params.Health.DoesBleed && !Character.Params.IsMachine;
private set => Character.Params.Health.DoesBleed = value;
}
@@ -140,9 +140,20 @@ namespace Barotrauma
private float vitality;
public float Vitality
{
get
{
return Character.IsDead ? minVitality : vitality;
get
{
if (Character.IsDead)
{
return minVitality;
}
if (Character.HasAbilityFlag(AbilityFlags.CanNotDieToAfflictions))
{
return Math.Max(vitality, MinVitality + 1);
}
return vitality;
}
private set
{
@@ -539,7 +550,7 @@ namespace Barotrauma
amount -= reduceAmount;
if (treatmentAction != null)
{
if (treatmentAction.Value == ActionType.OnUse)
if (treatmentAction.Value == ActionType.OnUse || treatmentAction.Value == ActionType.OnSuccess)
{
matchingAffliction.AppliedAsSuccessfulTreatmentTime = Timing.TotalTime;
}
@@ -679,17 +690,12 @@ namespace Barotrauma
private void AddLimbAffliction(LimbHealth limbHealth, Affliction newAffliction, bool allowStacking = true)
{
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") { return; }
if (Character.Params.Health.PoisonImmunity && newAffliction.Prefab.AfflictionType == "poison") { return; }
if (newAffliction.Prefab is AfflictionPrefabHusk huskPrefab)
{
if (huskPrefab.TargetSpecies.None(s => s == Character.SpeciesName))
{
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)
@@ -868,19 +874,24 @@ namespace Barotrauma
{
if (!Character.NeedsOxygen) { return; }
float oxygenlowResistance = GetResistance(oxygenLowAffliction.Prefab);
float prevOxygen = OxygenAmount;
if (IsUnconscious)
{
//clamp above 0.1 (no amount of oxygen low resistance should keep the character alive indefinitely)
float decreaseSpeed = Math.Max(0.1f, 1f - oxygenlowResistance);
//the character dies of oxygen deprivation in 100 seconds after losing consciousness
OxygenAmount = MathHelper.Clamp(OxygenAmount - 1.0f * deltaTime, -100.0f, 100.0f);
OxygenAmount = MathHelper.Clamp(OxygenAmount - decreaseSpeed * deltaTime, -100.0f, 100.0f);
}
else
{
float decreaseSpeed = -5.0f;
float increaseSpeed = 10.0f;
float oxygenlowResistance = GetResistance(oxygenLowAffliction.Prefab);
decreaseSpeed *= (1f - oxygenlowResistance);
increaseSpeed *= (1f + oxygenlowResistance);
float holdBreathMultiplier = 1f + GetStatValue(StatTypes.HoldBreathMultiplier);
decreaseSpeed *= holdBreathMultiplier;
OxygenAmount = MathHelper.Clamp(OxygenAmount + deltaTime * (Character.OxygenAvailable < InsufficientOxygenThreshold ? decreaseSpeed : increaseSpeed), -100.0f, 100.0f);
}
@@ -1062,6 +1073,7 @@ namespace Barotrauma
}
if (strength <= affliction.Prefab.TreatmentThreshold) { continue; }
if (afflictions.Any(otherAffliction => affliction.Prefab.IgnoreTreatmentIfAfflictedBy.Contains(otherAffliction.Key.Identifier))) { continue; }
if (ignoreHiddenAfflictions)
{
@@ -1217,6 +1229,7 @@ namespace Barotrauma
var affliction = kvp.Key;
var limbHealth = kvp.Value;
if (affliction.Strength <= 0.0f || limbHealth != null) { continue; }
if (kvp.Key.Prefab.ResetBetweenRounds) { continue; }
healthElement.Add(new XElement("Affliction",
new XAttribute("identifier", affliction.Identifier),
new XAttribute("strength", affliction.Strength.ToString("G", CultureInfo.InvariantCulture))));
@@ -79,27 +79,35 @@ namespace Barotrauma
public ref readonly ImmutableArray<Identifier> ParsedAfflictionTypes => ref parsedAfflictionTypes;
public DamageModifier(XElement element, string parentDebugName)
public DamageModifier(XElement element, string parentDebugName, bool checkErrors = true)
{
Deserialize(element);
if (element.Attribute("afflictionnames") != null)
{
DebugConsole.ThrowError("Error in DamageModifier config (" + parentDebugName + ") - define afflictions using identifiers or types instead of names.");
}
foreach (var afflictionType in parsedAfflictionTypes)
if (checkErrors)
{
if (!AfflictionPrefab.Prefabs.Any(p => p.AfflictionType == afflictionType))
foreach (var afflictionType in parsedAfflictionTypes)
{
createWarningOrError($"Potentially invalid damage modifier in \"{parentDebugName}\". Could not find any afflictions of the type \"{afflictionType}\". Did you mean to use an affliction identifier instead?");
}
}
foreach (var afflictionIdentifier in parsedAfflictionIdentifiers)
{
if (!AfflictionPrefab.Prefabs.ContainsKey(afflictionIdentifier))
{
createWarningOrError($"Potentially invalid damage modifier in \"{parentDebugName}\". Could not find any afflictions with the identifier \"{afflictionIdentifier}\". Did you mean to use an affliction type instead?");
if (!AfflictionPrefab.Prefabs.Any(p => p.AfflictionType == afflictionType))
{
createWarningOrError($"Potentially invalid damage modifier in \"{parentDebugName}\". Could not find any afflictions of the type \"{afflictionType}\". Did you mean to use an affliction identifier instead?");
}
}
foreach (var afflictionIdentifier in parsedAfflictionIdentifiers)
{
if (!AfflictionPrefab.Prefabs.ContainsKey(afflictionIdentifier))
{
createWarningOrError($"Potentially invalid damage modifier in \"{parentDebugName}\". Could not find any afflictions with the identifier \"{afflictionIdentifier}\". Did you mean to use an affliction type instead?");
}
}
if (!parsedAfflictionTypes.Any() && !parsedAfflictionIdentifiers.Any())
{
createWarningOrError($"Potentially invalid damage modifier in \"{parentDebugName}\". Neither affliction types of identifiers defined.");
}
}
static void createWarningOrError(string msg)
{
#if DEBUG
@@ -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,9 @@ namespace Barotrauma
public Identifier[] PreferredOutpostModuleTypes { get; protected set; }
[Serialize("", IsPropertySaveable.No)]
public Identifier Faction { get; set; }
public XElement Element { get; protected set; }
@@ -97,6 +126,11 @@ namespace Barotrauma
this.NpcSetIdentifier = npcSetIdentifier;
}
public IEnumerable<Identifier> GetTags()
{
return tags;
}
public IEnumerable<Identifier> GetModuleFlags()
{
return moduleFlags;
@@ -177,13 +211,23 @@ 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);
}
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;
}
@@ -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
@@ -637,13 +637,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);
@@ -778,6 +778,7 @@ namespace Barotrauma
{
var abilityAfflictionCharacter = new AbilityAfflictionCharacter(newAffliction, character);
attacker.CheckTalents(AbilityEffectType.OnAddDamageAffliction, abilityAfflictionCharacter);
newAffliction = abilityAfflictionCharacter.Affliction;
}
if (applyAffliction)
{
@@ -896,6 +897,12 @@ namespace Barotrauma
{
reEnableTimer = duration;
}
#if CLIENT
if (Hidden && LightSource != null)
{
LightSource.Enabled = false;
}
#endif
}
public void ReEnable()
@@ -1060,7 +1067,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)
@@ -1189,12 +1196,30 @@ namespace Barotrauma
statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
targets.Clear();
targets.AddRange(statusEffect.GetNearbyTargets(WorldPosition, targets));
statusEffect.AddNearbyTargets(WorldPosition, targets);
statusEffect.Apply(actionType, deltaTime, character, targets);
}
else
{
if (statusEffect.HasTargetType(StatusEffect.TargetType.Character))
if (statusEffect.HasTargetType(StatusEffect.TargetType.Contained) && character.Inventory is { } inventory)
{
foreach (Item item in inventory.AllItems)
{
if (statusEffect.TargetIdentifiers != null &&
!statusEffect.TargetIdentifiers.Contains(item.Prefab.Identifier) &&
statusEffect.TargetIdentifiers.None(id => item.HasTag(id)))
{
continue;
}
if (statusEffect.TargetSlot > -1)
{
if (inventory.FindIndex(item) != statusEffect.TargetSlot) { continue; }
}
targets.Add(item);
}
}
else if (statusEffect.HasTargetType(StatusEffect.TargetType.Character))
{
statusEffect.Apply(actionType, deltaTime, character, character, WorldPosition);
}
@@ -1237,7 +1262,8 @@ namespace Barotrauma
}
private float blinkTimer;
private float blinkPhase;
public float BlinkPhase;
public bool FreezeBlinkState;
private float TotalBlinkDurationOut => Params.BlinkDurationOut + Params.BlinkHoldTime;
@@ -1250,16 +1276,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);
@@ -1267,15 +1302,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>
@@ -50,9 +50,15 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.Yes, description: "Can the creature live without water or does it die on dry land?"), Editable]
public bool NeedsWater { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Is this creature an artificial creature, like robot or machine that shouldn't be affected by afflictions that affect only organic creatures? Overrides DoesBleed."), Editable]
public bool IsMachine { get; set; }
[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; }
@@ -606,6 +612,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; }
@@ -649,7 +658,7 @@ namespace Barotrauma
if (HasTag(tag))
{
target = null;
DebugConsole.ThrowError($"Multiple targets with the same tag ('{tag}') defined! Only the first will be used!");
DebugConsole.AddWarning($"Trying to add multiple targets with the same tag ('{tag}') defined! Only the first will be used!");
return false;
}
else
@@ -814,10 +823,19 @@ namespace Barotrauma
[Serialize(5000f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 20000f)]
public float CircleStartDistance { get; private set; }
[Serialize(1f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.5f, MaxValueFloat = 2f)]
[Serialize(false, IsPropertySaveable.Yes, description:"Normally the target size is taken into account when calculating the distance to the target. Set this true to skip that.")]
public bool IgnoreTargetSize { get; private set; }
[Serialize(1f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 100f)]
public float CircleRotationSpeed { get; private set; }
[Serialize(5f, IsPropertySaveable.Yes), Editable(MinValueFloat = 1f, MaxValueFloat = 10f)]
[Serialize(false, IsPropertySaveable.Yes, description:"When enabled, the circle rotation speed can change when the target is far. When this setting is disabled (default), the character will head directly towards the target when it's too far."), Editable]
public bool DynamicCircleRotationSpeed { get; private set; }
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 1f)]
public float CircleRandomRotationFactor { get; private set; }
[Serialize(5f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 10f)]
public float CircleStrikeDistanceMultiplier { get; private set; }
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 50f)]
@@ -6,6 +6,7 @@ using System.Linq;
using Barotrauma.IO;
using System.Xml;
using Barotrauma.Extensions;
using FarseerPhysics;
#if CLIENT
using Barotrauma.SpriteDeformations;
#endif
@@ -621,13 +622,13 @@ namespace Barotrauma
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
public float SteerForce { get; set; }
[Serialize(0f, IsPropertySaveable.Yes, description: "Radius of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
[Serialize(0f, IsPropertySaveable.Yes, description: "Radius of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 2048)]
public float Radius { get; set; }
[Serialize(0f, IsPropertySaveable.Yes, description: "Height of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
[Serialize(0f, IsPropertySaveable.Yes, description: "Height of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 2048)]
public float Height { get; set; }
[Serialize(0f, IsPropertySaveable.Yes, description: "Width of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
[Serialize(0f, IsPropertySaveable.Yes, description: "Width of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 2048)]
public float Width { get; set; }
[Serialize(10f, IsPropertySaveable.Yes, description: "The more the density the heavier the limb is."), Editable(MinValueFloat = 0.01f, MaxValueFloat = 100, DecimalCount = 2)]
@@ -706,6 +707,15 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool OnlyBlinkInWater { get; set; }
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool UseTextureOffsetForBlinking { get; set; }
[Serialize("0.5, 0.5", IsPropertySaveable.Yes), Editable(DecimalCount = 2, MinValueFloat = 0f, MaxValueFloat = 1f)]
public Vector2 BlinkTextureOffsetIn { get; set; }
[Serialize("0.5, 0.5", IsPropertySaveable.Yes), Editable(DecimalCount = 2, MinValueFloat = 0f, MaxValueFloat = 1f)]
public Vector2 BlinkTextureOffsetOut { get; set; }
[Serialize(TransitionMode.Linear, IsPropertySaveable.Yes), Editable]
public TransitionMode BlinkTransitionIn { get; private set; }
@@ -1026,15 +1036,18 @@ namespace Barotrauma
}
}
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 2048)]
public float Radius { get; set; }
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 2048)]
public float Height { get; set; }
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 2048)]
public float Width { get; set; }
[Serialize(BodyType.Dynamic, IsPropertySaveable.Yes), Editable]
public BodyType BodyType { get; set; }
public ColliderParams(ContentXElement element, RagdollParams ragdoll, string name = null) : base(element, ragdoll)
{
Name = name;
@@ -1,8 +1,5 @@
using Microsoft.Xna.Framework;
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
@@ -34,6 +31,7 @@ namespace Barotrauma.Abilities
Alive = 4,
Monster = 5,
InFriendlySubmarine = 6,
Large = 7,
};
protected List<TargetType> ParseTargetTypes(string[] targetTypeStrings)
@@ -41,8 +39,7 @@ namespace Barotrauma.Abilities
List<TargetType> targetTypes = new List<TargetType>();
foreach (string targetTypeString in targetTypeStrings)
{
TargetType targetType = TargetType.Any;
if (!Enum.TryParse(targetTypeString, true, out targetType))
if (!Enum.TryParse(targetTypeString, true, out TargetType targetType))
{
DebugConsole.ThrowError("Invalid target type type \"" + targetTypeString + "\" in CharacterTalent (" + characterTalent.DebugIdentifier + ")");
}
@@ -83,6 +80,9 @@ namespace Barotrauma.Abilities
return !targetCharacter.IsHuman;
case TargetType.InFriendlySubmarine:
return targetCharacter.Submarine != null && targetCharacter.Submarine.TeamID == character.TeamID;
case TargetType.Large:
// mass of mudraptor is ~48
return targetCharacter.AnimController is { Mass: > 50.0f };
default:
return true;
}
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
@@ -8,11 +9,13 @@ namespace Barotrauma.Abilities
{
private readonly List<TargetType> targetTypes;
private List<PropertyConditional> conditionals = new List<PropertyConditional>();
private readonly List<PropertyConditional> conditionals = new List<PropertyConditional>();
public AbilityConditionCharacter(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
targetTypes = ParseTargetTypes(conditionElement.GetAttributeStringArray("targettypes", Array.Empty<string>(), convertToLowerInvariant: true));
targetTypes = ParseTargetTypes(
conditionElement.GetAttributeStringArray("targettypes",
conditionElement.GetAttributeStringArray("targettype", Array.Empty<string>())));
foreach (XElement subElement in conditionElement.Elements())
{
@@ -28,13 +31,18 @@ namespace Barotrauma.Abilities
break;
}
}
if (!targetTypes.Any() && !conditionals.Any())
{
DebugConsole.ThrowError($"Error in talent \"{characterTalent}\". No target types or conditionals defined - the condition will match any character.");
}
}
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
if (abilityObject is IAbilityCharacter abilityCharacter)
{
if (!(abilityCharacter.Character is Character character)) { return false; }
if (abilityCharacter.Character is not Character character) { return false; }
if (!IsViableTarget(targetTypes, character)) { return false; }
foreach (var conditional in conditionals)
{
@@ -0,0 +1,19 @@
namespace Barotrauma.Abilities
{
internal sealed class AbilityConditionCharacterNotLooted : AbilityConditionData
{
private readonly Identifier identifier;
public AbilityConditionCharacterNotLooted(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
identifier = conditionElement.GetAttributeIdentifier("identifier", Identifier.Empty);
}
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
if (abilityObject is not IAbilityCharacter ability) { return false; }
return !ability.Character.MarkedAsLooted.Contains(identifier);
}
}
}
@@ -0,0 +1,16 @@
#nullable enable
namespace Barotrauma.Abilities
{
internal sealed class AbilityConditionCharacterUnconcious : AbilityConditionData
{
public AbilityConditionCharacterUnconcious(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { }
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
if (abilityObject is not IAbilityCharacter targetCharacter) { return false; }
return targetCharacter.Character.IsUnconscious;
}
}
}
@@ -1,6 +1,5 @@
using System;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
@@ -13,6 +12,11 @@ namespace Barotrauma.Abilities
{
identifiers = conditionElement.GetAttributeStringArray("identifiers", Array.Empty<string>(), convertToLowerInvariant: true);
tags = conditionElement.GetAttributeStringArray("tags", Array.Empty<string>(), convertToLowerInvariant: true);
if (!identifiers.Any() && !tags.Any())
{
DebugConsole.ThrowError($"Error in talent \"{characterTalent}\". No identifiers or tags defined.");
}
}
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
@@ -8,6 +8,7 @@ namespace Barotrauma.Abilities
{
private readonly bool? hasOutpost;
private readonly Identifier[] locationIdentifiers;
private readonly bool isPositiveReputation;
public AbilityConditionLocation(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
@@ -16,12 +17,20 @@ namespace Barotrauma.Abilities
hasOutpost = conditionElement.GetAttributeBool("hasoutpost", false);
}
locationIdentifiers = conditionElement.GetAttributeIdentifierArray("locationtype", Array.Empty<Identifier>());
isPositiveReputation = conditionElement.GetAttributeBool("ispositivereputation", false);
}
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
if (abilityObject is IAbilityLocation abilityLocation)
{
if (isPositiveReputation)
{
if (abilityLocation.Location?.Reputation is not { } reputation) { return false; }
if (reputation.Value <= 0) { return false; }
}
if (locationIdentifiers.Any())
{
if (!locationIdentifiers.Contains(abilityLocation.Location.Type.Identifier)) { return false; }
@@ -1,38 +1,53 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionMission : AbilityConditionData
{
private readonly MissionType missionType;
private readonly ImmutableHashSet<MissionType> missionType;
private readonly ImmutableHashSet<Identifier> factions;
public AbilityConditionMission(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
string missionTypeString = conditionElement.GetAttributeString("missiontype", "None");
if (!Enum.TryParse(missionTypeString, out missionType))
string[] missionTypeStrings = conditionElement.GetAttributeStringArray("missiontype", new []{ "None" })!;
HashSet<MissionType> missionTypes = new HashSet<MissionType>();
factions = conditionElement.GetAttributeIdentifierImmutableHashSet("faction", ImmutableHashSet<Identifier>.Empty);
foreach (string missionTypeString in missionTypeStrings)
{
DebugConsole.ThrowError("Error in AbilityConditionMission \"" + characterTalent.DebugIdentifier + "\" - \"" + missionTypeString + "\" is not a valid mission type.");
return;
}
if (missionType == MissionType.None)
{
DebugConsole.ThrowError("Error in AbilityConditionMission \"" + characterTalent.DebugIdentifier + "\" - mission type cannot be none.");
return;
if (!Enum.TryParse(missionTypeString, out MissionType parsedMission) || parsedMission is MissionType.None)
{
if (factions.IsEmpty)
{
DebugConsole.ThrowError($"Error in AbilityConditionMission \"{characterTalent.DebugIdentifier}\" - \"{missionTypeString}\" is not a valid mission type.");
}
continue;
}
missionTypes.Add(parsedMission);
}
missionType = missionTypes.ToImmutableHashSet();
}
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
if ((abilityObject as IAbilityMission)?.Mission is Mission mission)
if (abilityObject is IAbilityMission { Mission: { } mission })
{
return mission.Prefab.Type == missionType;
}
else
{
LogAbilityConditionError(abilityObject, typeof(IAbilityMission));
return false;
if (factions.Any())
{
// FIXME there's probably a better way to check the faction affiliated with the mission later
return mission.ReputationRewards.Keys.Any(factionIdentifier => factions.Contains(factionIdentifier));
}
return missionType.Contains(mission.Prefab.Type);
}
LogAbilityConditionError(abilityObject, typeof(IAbilityMission));
return false;
}
}
}
@@ -1,5 +1,4 @@
using System;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
@@ -0,0 +1,47 @@
using System;
using Microsoft.Xna.Framework;
namespace Barotrauma.Abilities
{
internal sealed class AbilityConditionAllyNearby : AbilityConditionDataless
{
private enum NearbyCharacterTruthy
{
OneCharacterMatches,
NoCharacterMatches
}
private readonly NearbyCharacterTruthy truthyWhen;
private readonly float distance;
public AbilityConditionAllyNearby(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
truthyWhen = conditionElement.GetAttributeEnum("truthywhen", NearbyCharacterTruthy.OneCharacterMatches);
distance = conditionElement.GetAttributeFloat("distance", 10f);
}
protected override bool MatchesConditionSpecific()
{
bool trueCondition = truthyWhen switch
{
NearbyCharacterTruthy.OneCharacterMatches => true,
NearbyCharacterTruthy.NoCharacterMatches => false,
_ => throw new ArgumentOutOfRangeException(nameof(truthyWhen))
};
foreach (Character ally in Character.GetFriendlyCrew(character))
{
if (ally == character) { continue; }
float distanceToCharacter = Vector2.DistanceSquared(ally.WorldPosition, character.WorldPosition);
if (distanceToCharacter < distance * distance)
{
return trueCondition;
}
}
return !trueCondition;
}
}
}
@@ -0,0 +1,22 @@
#nullable enable
namespace Barotrauma.Abilities
{
internal sealed class AbilityConditionCrewMemberUnconscious : AbilityConditionDataless
{
public AbilityConditionCrewMemberUnconscious(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { }
protected override bool MatchesConditionSpecific()
{
foreach (Character c in GameSession.GetSessionCrewCharacters(CharacterType.Both))
{
if (c.IsUnconscious)
{
return true;
}
}
return false;
}
}
}
@@ -1,9 +1,5 @@
using System;
using Barotrauma.Items.Components;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Extensions;
using Barotrauma.Extensions;
using System;
namespace Barotrauma.Abilities
{
@@ -22,7 +18,7 @@ namespace Barotrauma.Abilities
{
if (tags.None())
{
return character.GetEquippedItem(null) is Item;
return character.GetEquippedItem(null) != null;
}
if (requireAll)
@@ -0,0 +1,43 @@
#nullable enable
using System;
namespace Barotrauma.Abilities
{
internal sealed class AbilityConditionHasLevel : AbilityConditionDataless
{
private readonly Option<int> matchedLevel;
private readonly Option<int> minLevel;
public AbilityConditionHasLevel(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
matchedLevel = conditionElement.GetAttributeInt("levelequals", 0) is var match and not 0
? Option<int>.Some(match)
: Option<int>.None();
minLevel = conditionElement.GetAttributeInt("minlevel", 0) is var min and not 0
? Option<int>.Some(min)
: Option<int>.None();
if (matchedLevel.IsNone() && minLevel.IsNone())
{
throw new Exception($"{nameof(AbilityConditionHasLevel)} must have either \"levelequals\" or \"minlevel\" attribute.");
}
}
protected override bool MatchesConditionSpecific()
{
if (matchedLevel.TryUnwrap(out int match))
{
return character.Info.GetCurrentLevel() == match;
}
if (minLevel.TryUnwrap(out int min))
{
return character.Info.GetCurrentLevel() >= min;
}
return false;
}
}
}
@@ -1,13 +1,11 @@
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class AbilityConditionHasPermanentStat : AbilityConditionDataless
{
private readonly Identifier statIdentifier;
private readonly StatTypes statType;
private readonly float min;
private readonly PermanentStatPlaceholder placeholder;
public AbilityConditionHasPermanentStat(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
@@ -19,11 +17,14 @@ namespace Barotrauma.Abilities
string statTypeName = conditionElement.GetAttributeString("stattype", string.Empty);
statType = string.IsNullOrEmpty(statTypeName) ? StatTypes.None : CharacterAbilityGroup.ParseStatType(statTypeName, characterTalent.DebugIdentifier);
min = conditionElement.GetAttributeFloat("min", 0f);
placeholder = conditionElement.GetAttributeEnum("placeholder", PermanentStatPlaceholder.None);
}
protected override bool MatchesConditionSpecific()
{
return character.Info.GetSavedStatValue(statType, statIdentifier) >= min;
Identifier identifier = CharacterAbilityGivePermanentStat.HandlePlaceholders(placeholder, statIdentifier);
return character.Info.GetSavedStatValue(statType, identifier) >= min;
}
}
}
@@ -0,0 +1,19 @@
namespace Barotrauma.Abilities
{
class AbilityConditionHasTalent : AbilityConditionDataless
{
private readonly Identifier talentIdentifier;
public AbilityConditionHasTalent(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
talentIdentifier = conditionElement.GetAttributeIdentifier("identifier", Identifier.Empty);
}
protected override bool MatchesConditionSpecific()
{
bool result = character.HasTalent(talentIdentifier);
return result;
}
}
}
@@ -0,0 +1,34 @@
#nullable enable
using System.Collections.Immutable;
namespace Barotrauma.Abilities;
internal sealed class AbilityConditionHoldingItem : AbilityConditionDataless
{
private readonly ImmutableHashSet<Identifier> tags;
public AbilityConditionHoldingItem(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
tags = conditionElement.GetAttributeIdentifierImmutableHashSet("tags", ImmutableHashSet<Identifier>.Empty);
}
protected override bool MatchesConditionSpecific()
{
if (tags.Count is 0)
{
return HasItemInHand(character, null);
}
foreach (Identifier tag in tags)
{
if (HasItemInHand(character, tag)) { return true; }
}
return false;
static bool HasItemInHand(Character character, Identifier? tagOrIdentifier) =>
character.GetEquippedItem(tagOrIdentifier?.Value, InvSlotType.RightHand) is not null ||
character.GetEquippedItem(tagOrIdentifier?.Value, InvSlotType.LeftHand) is not null;
}
}
@@ -0,0 +1,23 @@
#nullable enable
namespace Barotrauma.Abilities
{
internal sealed class AbilityConditionLowestLevel : AbilityConditionDataless
{
public AbilityConditionLowestLevel(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { }
protected override bool MatchesConditionSpecific()
{
int ownLevel = character.Info.GetCurrentLevel();
foreach (Character crew in GameSession.GetSessionCrewCharacters(CharacterType.Both))
{
if (crew == character) { continue; }
if (crew.Info.GetCurrentLevel() < ownLevel) { return false; }
}
return true;
}
}
}
@@ -0,0 +1,39 @@
#nullable enable
using System;
using System.Collections.Immutable;
using Microsoft.Xna.Framework;
namespace Barotrauma.Abilities;
internal sealed class AbilityConditionNearbyCharacterCount : AbilityConditionDataless
{
private readonly float distance;
private readonly int count;
private readonly ImmutableHashSet<TargetType> targetTypes;
public AbilityConditionNearbyCharacterCount(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
distance = conditionElement.GetAttributeFloat("distance", 10f);
count = conditionElement.GetAttributeInt("count", 1);
targetTypes = ParseTargetTypes(conditionElement.GetAttributeStringArray("targettypes", Array.Empty<string>(), convertToLowerInvariant: true)).ToImmutableHashSet();
}
protected override bool MatchesConditionSpecific()
{
int amountNeeded = count;
foreach (Character otherCharacter in Character.CharacterList)
{
if (character.Submarine != otherCharacter.Submarine) { continue; }
if (!IsViableTarget(targetTypes, otherCharacter)) { continue; }
if (Vector2.DistanceSquared(character.WorldPosition, otherCharacter.WorldPosition) < distance * distance)
{
amountNeeded--;
if (amountNeeded <= 0) { return true; }
}
}
return false;
}
}
@@ -15,5 +15,4 @@ namespace Barotrauma.Abilities
}
public Character Character { get; set; }
}
}
@@ -67,7 +67,7 @@ namespace Barotrauma.Abilities
if (abilityObject is null)
{
ApplyEffect();
}
}
else
{
ApplyEffect(abilityObject);
@@ -0,0 +1,35 @@
#nullable enable
using Microsoft.Xna.Framework;
namespace Barotrauma.Abilities
{
internal sealed class CharacterAbilityApplyStatusEffectToNonHumans : CharacterAbilityApplyStatusEffects
{
private readonly float maxDistance;
public CharacterAbilityApplyStatusEffectToNonHumans(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
maxDistance = abilityElement.GetAttributeFloat("maxdistance", float.MaxValue);
}
protected override void ApplyEffect()
{
foreach (Character character in Character.CharacterList)
{
if (character.IsHuman) { continue; }
if (maxDistance < float.MaxValue)
{
if (Vector2.DistanceSquared(character.WorldPosition, Character.WorldPosition) > maxDistance * maxDistance) { continue; }
}
ApplyEffectSpecific(character);
}
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
ApplyEffect();
}
}
}
@@ -17,6 +17,8 @@ namespace Barotrauma.Abilities
readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
private bool effectBeingApplied;
public CharacterAbilityApplyStatusEffects(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
statusEffects = CharacterAbilityGroup.ParseStatusEffects(CharacterTalent, abilityElement.GetChildElement("statuseffects"));
@@ -29,44 +31,57 @@ namespace Barotrauma.Abilities
protected void ApplyEffectSpecific(Character targetCharacter)
{
foreach (var statusEffect in statusEffects)
//prevent an infinite loop if an effect triggers itself
//(e.g. a talent that triggers when an affliction is applied, and applies that same affliction)
if (effectBeingApplied) { return; }
effectBeingApplied = true;
try
{
if (statusEffect.HasTargetType(StatusEffect.TargetType.UseTarget))
foreach (var statusEffect in statusEffects)
{
// currently used to spawn items on the targeted character
statusEffect.SetUser(targetCharacter);
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, targetCharacter, targetCharacter);
}
else if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
targets.Clear();
targets.AddRange(statusEffect.GetNearbyTargets(targetCharacter.WorldPosition, targets));
if (!nearbyCharactersAppliesToSelf)
if (statusEffect.HasTargetType(StatusEffect.TargetType.UseTarget))
{
targets.RemoveAll(c => c == Character);
// currently used to spawn items on the targeted character
statusEffect.SetUser(targetCharacter);
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, targetCharacter, targetCharacter);
}
if (!nearbyCharactersAppliesToAllies)
else if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
targets.RemoveAll(c => c is Character otherCharacter && HumanAIController.IsFriendly(otherCharacter, Character));
targets.Clear();
statusEffect.AddNearbyTargets(targetCharacter.WorldPosition, targets);
if (!nearbyCharactersAppliesToSelf)
{
targets.RemoveAll(c => c == Character);
}
if (!nearbyCharactersAppliesToAllies)
{
targets.RemoveAll(c => c is Character otherCharacter && HumanAIController.IsFriendly(otherCharacter, Character));
}
if (!nearbyCharactersAppliesToEnemies)
{
targets.RemoveAll(c => c is Character otherCharacter && !HumanAIController.IsFriendly(otherCharacter, Character));
}
statusEffect.SetUser(Character);
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, targetCharacter, targets);
}
if (!nearbyCharactersAppliesToEnemies)
else if (statusEffect.HasTargetType(StatusEffect.TargetType.Character))
{
targets.RemoveAll(c => c is Character otherCharacter && !HumanAIController.IsFriendly(otherCharacter, Character));
statusEffect.SetUser(Character);
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, targetCharacter);
}
else
{
statusEffect.SetUser(Character);
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, Character);
}
statusEffect.SetUser(Character);
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, targetCharacter, targets);
}
else if (statusEffect.HasTargetType(StatusEffect.TargetType.Character))
{
statusEffect.SetUser(Character);
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, targetCharacter);
}
else
{
statusEffect.SetUser(Character);
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, Character);
}
}
finally
{
effectBeingApplied = false;
}
}
protected override void ApplyEffect()
{
@@ -1,4 +1,5 @@
using Microsoft.Xna.Framework;
using System.Collections.Immutable;
using Microsoft.Xna.Framework;
namespace Barotrauma.Abilities
{
@@ -6,11 +7,15 @@ namespace Barotrauma.Abilities
{
private readonly bool allowSelf;
private readonly float maxDistance = float.MaxValue;
private readonly bool inSameRoom;
private readonly ImmutableHashSet<Identifier> jobIdentifiers;
public CharacterAbilityApplyStatusEffectsToAllies(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
allowSelf = abilityElement.GetAttributeBool("allowself", true);
maxDistance = abilityElement.GetAttributeFloat("maxdistance", float.MaxValue);
inSameRoom = abilityElement.GetAttributeBool("insameroom", false);
jobIdentifiers = abilityElement.GetAttributeIdentifierImmutableHashSet("jobs", ImmutableHashSet<Identifier>.Empty);
}
@@ -19,6 +24,27 @@ namespace Barotrauma.Abilities
foreach (Character character in Character.GetFriendlyCrew(Character))
{
if (!allowSelf && character == Character) { continue; }
if (!jobIdentifiers.IsEmpty)
{
bool hadJob = false;
foreach (Identifier job in jobIdentifiers)
{
if (character.HasJob(job.Value))
{
hadJob = true;
break;
}
}
if (!hadJob) { continue; }
}
if (inSameRoom && !character.IsInSameRoomAs(Character))
{
continue;
}
if (maxDistance < float.MaxValue)
{
if (Vector2.DistanceSquared(character.WorldPosition, Character.WorldPosition) > maxDistance * maxDistance) { continue; }
@@ -0,0 +1,63 @@
#nullable enable
using System.Collections.Generic;
using System.Collections.Immutable;
namespace Barotrauma.Abilities
{
internal sealed class CharacterAbilityApplyStatusEffectsToApprenticeship : CharacterAbilityApplyStatusEffects
{
private readonly bool invert;
private readonly ImmutableHashSet<JobPrefab> jobPrefabList = JobPrefab.Prefabs.ToImmutableHashSet();
public CharacterAbilityApplyStatusEffectsToApprenticeship(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
invert = abilityElement.GetAttributeBool("invert", false);
}
protected override void ApplyEffect()
{
ApplyEffectSpecific(Character);
JobPrefab? apprenticeJob = GetApprenticeJob(Character, jobPrefabList);
if (apprenticeJob is null)
{
DebugConsole.ThrowError($"{nameof(CharacterAbilityUnlockApprenticeshipTalentTree)}: Could not find apprentice job for character {Character.Name}");
return;
}
foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both))
{
JobPrefab? characterJob = character.Info?.Job?.Prefab;
if (characterJob is null) { continue; }
switch (characterJob.Identifier == apprenticeJob.Identifier)
{
case true when invert:
continue;
case false when !invert:
continue;
}
ApplyEffectSpecific(character);
}
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
ApplyEffect();
}
public static JobPrefab? GetApprenticeJob(Character character, IReadOnlyCollection<JobPrefab> jobList)
{
foreach (JobPrefab prefab in jobList)
{
if (character.Info.GetSavedStatValue(StatTypes.Apprenticeship, prefab.Identifier) > 0)
{
return prefab;
}
}
return null;
}
}
}
@@ -6,12 +6,15 @@ namespace Barotrauma.Abilities
class CharacterAbilityGainSimultaneousSkill : CharacterAbility
{
private readonly Identifier skillIdentifier;
private readonly bool ignoreAbilitySkillGain;
private readonly bool ignoreAbilitySkillGain,
targetAllies;
public CharacterAbilityGainSimultaneousSkill(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
skillIdentifier = abilityElement.GetAttributeIdentifier("skillidentifier", "");
ignoreAbilitySkillGain = abilityElement.GetAttributeBool("ignoreabilityskillgain", true);
targetAllies = abilityElement.GetAttributeBool("targetallies", false);
}
protected override void ApplyEffect(AbilityObject abilityObject)
@@ -19,7 +22,20 @@ namespace Barotrauma.Abilities
if (abilityObject is AbilitySkillGain abilitySkillGain)
{
if (ignoreAbilitySkillGain && abilitySkillGain.GainedFromAbility) { return; }
Character.Info?.IncreaseSkillLevel(skillIdentifier, abilitySkillGain.Value, gainedFromAbility: true);
Identifier identifier = skillIdentifier == "inherit" ? abilitySkillGain.SkillIdentifier : skillIdentifier;
if (targetAllies)
{
foreach (Character character in Character.GetFriendlyCrew(Character))
{
if (character == Character) { continue; }
Character.Info?.IncreaseSkillLevel(identifier, abilitySkillGain.Value, gainedFromAbility: true);
}
}
else
{
Character.Info?.IncreaseSkillLevel(identifier, abilitySkillGain.Value, gainedFromAbility: true);
}
}
else
{
@@ -0,0 +1,35 @@
namespace Barotrauma.Abilities;
internal sealed class CharacterAbilityGiveExperience : CharacterAbility
{
public override bool AppliesEffectOnIntervalUpdate => true;
private readonly int amount;
public CharacterAbilityGiveExperience(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
amount = abilityElement.GetAttributeInt("amount", 0);
}
private void ApplyEffectSpecific(Character targetCharacter)
{
targetCharacter.Info?.GiveExperience(amount);
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
if ((abilityObject as IAbilityCharacter)?.Character is { } targetCharacter)
{
ApplyEffectSpecific(targetCharacter);
}
else
{
ApplyEffectSpecific(Character);
}
}
protected override void ApplyEffect()
{
ApplyEffectSpecific(Character);
}
}
@@ -0,0 +1,31 @@
#nullable enable
namespace Barotrauma.Abilities
{
internal sealed class CharacterAbilityGiveItemStat : CharacterAbility
{
private readonly ItemTalentStats stat;
private readonly float value;
public CharacterAbilityGiveItemStat(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
stat = abilityElement.GetAttributeEnum("stattype", ItemTalentStats.None);
value = abilityElement.GetAttributeFloat("value", 0f);
}
protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate)
{
if (conditionsMatched)
{
ApplyEffect();
}
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
if (abilityObject is not IAbilityItem ability) { return; }
ability.Item.StatManager.ApplyStat(stat, value, CharacterTalent);
}
}
}
@@ -0,0 +1,41 @@
#nullable enable
using System.Collections.Immutable;
namespace Barotrauma.Abilities
{
internal sealed class CharacterAbilityGiveItemStatToTags: CharacterAbility
{
private readonly ItemTalentStats stat;
private readonly float value;
private readonly ImmutableHashSet<Identifier> tags;
public CharacterAbilityGiveItemStatToTags(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
stat = abilityElement.GetAttributeEnum("stattype", ItemTalentStats.None);
value = abilityElement.GetAttributeFloat("value", 0f);
tags = abilityElement.GetAttributeIdentifierImmutableHashSet("tags", ImmutableHashSet<Identifier>.Empty);
}
protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate)
{
if (conditionsMatched)
{
ApplyEffect();
}
}
protected override void ApplyEffect()
{
if (Character?.Submarine is null) { return; }
foreach (Item item in Character.Submarine.GetItems(true))
{
if (item.HasTag(tags) || tags.Contains(item.Prefab.Identifier))
{
item.StatManager.ApplyStat(stat, value, CharacterTalent);
}
}
}
}
}
@@ -1,11 +1,17 @@
using Barotrauma.Extensions;
using System.Xml.Linq;
using System;
namespace Barotrauma.Abilities
{
public enum PermanentStatPlaceholder
{
None,
LocationName,
LocationIndex
}
class CharacterAbilityGivePermanentStat : CharacterAbility
{
private readonly string statIdentifier;
private readonly Identifier statIdentifier;
private readonly StatTypes statType;
private readonly float value;
private readonly float maxValue;
@@ -13,6 +19,7 @@ namespace Barotrauma.Abilities
private readonly bool removeOnDeath;
private readonly bool giveOnAddingFirstTime;
private readonly bool setValue;
private readonly PermanentStatPlaceholder placeholder;
//private readonly float maximumValue;
public override bool AllowClientSimulation => true;
@@ -20,7 +27,7 @@ namespace Barotrauma.Abilities
public CharacterAbilityGivePermanentStat(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
statIdentifier = abilityElement.GetAttributeString("statidentifier", "").ToLowerInvariant();
statIdentifier = abilityElement.GetAttributeIdentifier("statidentifier", Identifier.Empty);
string statTypeName = abilityElement.GetAttributeString("stattype", string.Empty);
statType = string.IsNullOrEmpty(statTypeName) ? StatTypes.None : CharacterAbilityGroup.ParseStatType(statTypeName, CharacterTalent.DebugIdentifier);
value = abilityElement.GetAttributeFloat("value", 0f);
@@ -29,6 +36,7 @@ namespace Barotrauma.Abilities
removeOnDeath = abilityElement.GetAttributeBool("removeondeath", false);
giveOnAddingFirstTime = abilityElement.GetAttributeBool("giveonaddingfirsttime", characterAbilityGroup.AbilityEffectType == AbilityEffectType.None);
setValue = abilityElement.GetAttributeBool("setvalue", false);
placeholder = abilityElement.GetAttributeEnum("placeholder", PermanentStatPlaceholder.None);
}
public override void InitializeAbility(bool addingFirstTime)
@@ -51,14 +59,33 @@ namespace Barotrauma.Abilities
private void ApplyEffectSpecific()
{
Identifier identifier = HandlePlaceholders(placeholder, statIdentifier);
if (targetAllies)
{
Character.GetFriendlyCrew(Character).ForEach(c => c?.Info.ChangeSavedStatValue(statType, value, statIdentifier, removeOnDeath, maxValue: maxValue, setValue: setValue));
foreach (Character c in Character.GetFriendlyCrew(Character))
{
c?.Info.ChangeSavedStatValue(statType, value, identifier, removeOnDeath, maxValue: maxValue, setValue: setValue);
}
}
else
{
Character?.Info.ChangeSavedStatValue(statType, value, statIdentifier, removeOnDeath, maxValue: maxValue, setValue: setValue);
Character?.Info.ChangeSavedStatValue(statType, value, identifier, removeOnDeath, maxValue: maxValue, setValue: setValue);
}
}
public static Identifier HandlePlaceholders(PermanentStatPlaceholder placeholder, Identifier original)
{
if (GameMain.GameSession?.Campaign?.Map is not { } map) { return original; }
switch (placeholder)
{
case PermanentStatPlaceholder.LocationName when map.CurrentLocation is { } location:
return original.Replace("[placeholder]", location.Name);
case PermanentStatPlaceholder.LocationIndex:
return original.Replace("[placeholder]", map.CurrentLocationIndex.ToString());
}
return original;
}
}
}
@@ -0,0 +1,31 @@
#nullable enable
namespace Barotrauma.Abilities
{
internal sealed class CharacterAbilityGiveReputation : CharacterAbility
{
private readonly Identifier factionIdentifier;
private readonly float amount;
public CharacterAbilityGiveReputation(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
factionIdentifier = abilityElement.GetAttributeIdentifier("identifier", Identifier.Empty);
amount = abilityElement.GetAttributeFloat("amount", 0f);
}
protected override void ApplyEffect()
{
if (GameMain.GameSession?.Campaign is not { } campaign) { return; }
foreach (Faction faction in campaign.Factions)
{
if (faction.Prefab.Identifier != factionIdentifier) { continue; }
faction.Reputation.AddReputation(amount);
break;
}
}
protected override void ApplyEffect(AbilityObject abilityObject) => ApplyEffect();
}
}
@@ -0,0 +1,25 @@
#nullable enable
namespace Barotrauma.Abilities
{
internal sealed class CharacterAbilityGiveTalentPointsToAllies : CharacterAbility
{
private readonly int amount;
public CharacterAbilityGiveTalentPointsToAllies(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
amount = abilityElement.GetAttributeInt("amount", 0);
}
public override void InitializeAbility(bool addingFirstTime)
{
if (!addingFirstTime) { return; }
foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both))
{
if (character.Info is null) { return; }
character.Info.AdditionalTalentPoints += amount;
}
}
}
}
@@ -0,0 +1,18 @@
namespace Barotrauma.Abilities
{
internal sealed class CharacterAbilityMarkAsLooted: CharacterAbility
{
private readonly Identifier identifier;
public CharacterAbilityMarkAsLooted(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
identifier = abilityElement.GetAttributeIdentifier("identifier", Identifier.Empty);
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
if (abilityObject is not IAbilityCharacter { Character: { } character }) { return; }
character.MarkedAsLooted.Add(identifier);
}
}
}
@@ -1,30 +1,36 @@
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class CharacterAbilityModifyAffliction : CharacterAbility
{
private readonly string[] afflictionIdentifiers;
private readonly Identifier[] afflictionIdentifiers;
private readonly Identifier replaceWith;
private readonly float addedMultiplier;
public CharacterAbilityModifyAffliction(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
afflictionIdentifiers = abilityElement.GetAttributeStringArray("afflictionidentifiers", new string[0], convertToLowerInvariant: true);
afflictionIdentifiers = abilityElement.GetAttributeIdentifierArray("afflictionidentifiers", System.Array.Empty<Identifier>());
replaceWith = abilityElement.GetAttributeIdentifier("replacewith", Identifier.Empty);
addedMultiplier = abilityElement.GetAttributeFloat("addedmultiplier", 0f);
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
if ((abilityObject as IAbilityAffliction)?.Affliction is Affliction affliction)
var abilityAffliction = abilityObject as IAbilityAffliction;
if (abilityAffliction?.Affliction is Affliction affliction)
{
foreach (string afflictionIdentifier in afflictionIdentifiers)
foreach (Identifier afflictionIdentifier in afflictionIdentifiers)
{
if (affliction.Identifier == afflictionIdentifier)
if (affliction.Identifier != afflictionIdentifier) { continue; }
affliction.Strength *= 1 + addedMultiplier;
if (!replaceWith.IsEmpty)
{
affliction.Strength *= 1 + addedMultiplier;
}
if (AfflictionPrefab.Prefabs.TryGet(replaceWith, out AfflictionPrefab afflictionPrefab))
{
abilityAffliction.Affliction = new Affliction(afflictionPrefab, abilityAffliction.Affliction.Strength);
}
}
}
}
else
@@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
@@ -0,0 +1,27 @@
#nullable enable
namespace Barotrauma.Abilities
{
internal sealed class CharacterAbilityReduceAffliction : CharacterAbility
{
private readonly Identifier afflictionId;
private readonly float amount;
public CharacterAbilityReduceAffliction(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
afflictionId = abilityElement.GetAttributeIdentifier("afflictionid", abilityElement.GetAttributeIdentifier("affliction", Identifier.Empty));
amount = abilityElement.GetAttributeFloat("amount", 0);
if (afflictionId.IsEmpty)
{
DebugConsole.ThrowError($"Error in {nameof(CharacterAbilityReduceAffliction)} - affliction identifier not set.");
}
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
if (abilityObject is not IAbilityCharacter character) { return; }
character.Character.CharacterHealth.ReduceAfflictionOnAllLimbs(afflictionId, amount);
}
}
}
@@ -0,0 +1,19 @@
#nullable enable
using Barotrauma.Items.Components;
namespace Barotrauma.Abilities
{
internal sealed class CharacterAbilityRemoveRandomIngredient : CharacterAbility
{
public CharacterAbilityRemoveRandomIngredient(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { }
protected override void ApplyEffect(AbilityObject abilityObject)
{
if (abilityObject is not Fabricator.AbilityFabricationItemIngredients { Items.Count: > 0 } ingredients) { return; }
int randomIndex = Rand.Int(ingredients.Items.Count, Rand.RandSync.Unsynced);
ingredients.Items.RemoveAt(randomIndex);
}
}
}
@@ -1,16 +1,15 @@
using System.Xml.Linq;

namespace Barotrauma.Abilities
{
class CharacterAbilityResetPermanentStat : CharacterAbility
{
private readonly string statIdentifier;
private readonly Identifier statIdentifier;
public override bool AppliesEffectOnIntervalUpdate => true;
public override bool AllowClientSimulation => true;
public CharacterAbilityResetPermanentStat(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
statIdentifier = abilityElement.GetAttributeString("statidentifier", "").ToLowerInvariant();
statIdentifier = abilityElement.GetAttributeIdentifier("statidentifier", Identifier.Empty);
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
@@ -0,0 +1,34 @@
#nullable enable
namespace Barotrauma.Abilities
{
internal sealed class CharacterAbilitySetMetadataInt : CharacterAbility
{
private readonly Identifier identifier;
private readonly int value;
public CharacterAbilitySetMetadataInt(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
identifier = abilityElement.GetAttributeIdentifier("identifier", Identifier.Empty);
value = abilityElement.GetAttributeInt("value", 0);
}
public override void InitializeAbility(bool addingFirstTime)
{
ApplyEffect();
}
protected override void ApplyEffect()
{
if (identifier == Identifier.Empty) { return; }
if (GameMain.GameSession?.Campaign?.CampaignMetadata is not { } metadata) { return; }
metadata.SetValue(identifier, value);
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
ApplyEffect();
}
}
}
@@ -1,29 +0,0 @@
namespace Barotrauma.Abilities
{
class CharacterAbilityUnlockTree : CharacterAbility
{
public CharacterAbilityUnlockTree(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
}
public override void InitializeAbility(bool addingFirstTime)
{
if (!TalentTree.JobTalentTrees.TryGet(Character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return; }
var subTree = talentTree.TalentSubTrees.Find(t => t.AllTalentIdentifiers.Contains(CharacterTalent.Prefab.Identifier));
if (subTree == null) { return; }
subTree.ForceUnlock = true;
if (!addingFirstTime) { return; }
foreach (var talentId in subTree.AllTalentIdentifiers)
{
if (talentId == CharacterTalent.Prefab.Identifier) { continue; }
if (Character.GiveTalent(talentId))
{
Character.Info.AdditionalTalentPoints++;
}
}
}
}
}
@@ -0,0 +1,50 @@
#nullable enable
using System.Collections.Generic;
using System.Collections.Immutable;
using Barotrauma.Extensions;
namespace Barotrauma.Abilities
{
internal sealed class CharacterAbilityUnlockApprenticeshipTalentTree : CharacterAbility
{
public CharacterAbilityUnlockApprenticeshipTalentTree(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { }
public override void InitializeAbility(bool addingFirstTime)
{
if (!addingFirstTime) { return; }
JobPrefab? apprentice = CharacterAbilityApplyStatusEffectsToApprenticeship.GetApprenticeJob(Character, JobPrefab.Prefabs.ToImmutableHashSet());
if (apprentice is null)
{
DebugConsole.ThrowError($"{nameof(CharacterAbilityUnlockApprenticeshipTalentTree)}: Could not find apprentice job for character {Character.Name}");
return;
}
if (!TalentTree.JobTalentTrees.TryGet(apprentice.Identifier, out TalentTree? talentTree)) { return; }
HashSet<ImmutableHashSet<Identifier>> talentsTrees = new HashSet<ImmutableHashSet<Identifier>>();
foreach (TalentSubTree subTree in talentTree.TalentSubTrees)
{
if (subTree.Type != TalentTreeType.Specialization) { continue; }
talentsTrees.Add(subTree.AllTalentIdentifiers);
}
ImmutableHashSet<Identifier> selectedTalentTree = talentsTrees.GetRandomUnsynced();
foreach (Identifier identifier in selectedTalentTree)
{
if (Character.HasTalent(identifier)) { continue; }
if (Character.GiveTalent(identifier))
{
Character.Info.AdditionalTalentPoints++;
}
}
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
ApplyEffect();
}
}
}
@@ -18,12 +18,15 @@ namespace Barotrauma.Abilities
protected readonly int maxTriggerCount;
protected int timesTriggered = 0;
// add support for OR conditions?
// add support for OR conditions?
protected readonly List<AbilityCondition> abilityConditions = new List<AbilityCondition>();
// separate dictionaries for each type of characterability?
protected readonly List<CharacterAbility> characterAbilities = new List<CharacterAbility>();
/// <summary>
/// List of abilities that are triggered by this group.
/// Fallback abilities are triggered if the conditional fails
/// </summary>
protected readonly List<CharacterAbility> characterAbilities = new List<CharacterAbility>(),
fallbackAbilities = new List<CharacterAbility>();
public CharacterAbilityGroup(AbilityEffectType abilityEffectType, CharacterTalent characterTalent, ContentXElement abilityElementGroup)
{
@@ -38,6 +41,9 @@ namespace Barotrauma.Abilities
case "abilities":
LoadAbilities(subElement);
break;
case "fallbackabilities":
LoadFallbackAbilities(subElement);
break;
case "conditions":
LoadConditions(subElement);
break;
@@ -47,10 +53,23 @@ namespace Barotrauma.Abilities
public void ActivateAbilityGroup(bool addingFirstTime)
{
if (!CheckActivatingCondition()) { return; }
foreach (var characterAbility in characterAbilities)
{
characterAbility.InitializeAbility(addingFirstTime);
}
foreach (var characterAbility in fallbackAbilities)
{
characterAbility.InitializeAbility(addingFirstTime);
}
}
private bool CheckActivatingCondition()
{
if (AbilityEffectType is not AbilityEffectType.None) { return true; }
return !abilityConditions.Any(static abilityCondition => !abilityCondition.MatchesCondition());
}
public void LoadConditions(ContentXElement conditionElements)
@@ -85,6 +104,17 @@ namespace Barotrauma.Abilities
characterAbilities.Add(characterAbility);
}
public void AddFallbackAbility(CharacterAbility characterAbility)
{
if (characterAbility == null)
{
DebugConsole.ThrowError($"Trying to add null ability for talent {CharacterTalent.DebugIdentifier}!");
return;
}
fallbackAbilities.Add(characterAbility);
}
// XML
private AbilityCondition ConstructCondition(CharacterTalent characterTalent, ContentXElement conditionElement, bool errorMessages = true)
{
@@ -135,6 +165,14 @@ namespace Barotrauma.Abilities
}
}
private void LoadFallbackAbilities(ContentXElement abilityElements)
{
foreach (var abilityElementGroup in abilityElements.Elements())
{
AddFallbackAbility(ConstructAbility(abilityElementGroup, CharacterTalent));
}
}
private CharacterAbility ConstructAbility(ContentXElement abilityElement, CharacterTalent characterTalent)
{
CharacterAbility newAbility = CharacterAbility.Load(abilityElement, this);
@@ -1,29 +1,38 @@
namespace Barotrauma.Abilities
using System.Collections.Generic;
namespace Barotrauma.Abilities
{
class CharacterAbilityGroupEffect : CharacterAbilityGroup
{
public CharacterAbilityGroupEffect(AbilityEffectType abilityEffectType, CharacterTalent characterTalent, ContentXElement abilityElementGroup) :
public CharacterAbilityGroupEffect(AbilityEffectType abilityEffectType, CharacterTalent characterTalent, ContentXElement abilityElementGroup) :
base(abilityEffectType, characterTalent, abilityElementGroup) { }
public void CheckAbilityGroup(AbilityObject abilityObject)
{
if (!IsActive) { return; }
if (IsApplicable(abilityObject))
if (IsOverTriggerCount) { return; }
List<CharacterAbility> abilities = IsApplicable(abilityObject) ? characterAbilities : fallbackAbilities;
foreach (CharacterAbility characterAbility in abilities)
{
foreach (var characterAbility in characterAbilities)
if (characterAbility.IsViable())
{
if (characterAbility.IsViable())
{
characterAbility.ApplyAbilityEffect(abilityObject);
}
characterAbility.ApplyAbilityEffect(abilityObject);
}
}
if (abilities.Count > 0)
{
timesTriggered++;
}
}
private bool IsOverTriggerCount => timesTriggered >= maxTriggerCount;
private bool IsApplicable(AbilityObject abilityObject)
{
if (timesTriggered >= maxTriggerCount) { return false; }
foreach (var abilityCondition in abilityConditions)
{
if (!abilityCondition.MatchesCondition(abilityObject))
@@ -31,7 +40,8 @@
return false;
}
}
return true;
}
}
}
}
@@ -1,4 +1,7 @@
namespace Barotrauma.Abilities
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityGroupInterval : CharacterAbilityGroup
{
@@ -9,48 +12,72 @@
private float effectDelayTimer;
public CharacterAbilityGroupInterval(AbilityEffectType abilityEffectType, CharacterTalent characterTalent, ContentXElement abilityElementGroup) :
public CharacterAbilityGroupInterval(AbilityEffectType abilityEffectType, CharacterTalent characterTalent, ContentXElement abilityElementGroup) :
base(abilityEffectType, characterTalent, abilityElementGroup)
{
{
// too many overlapping intervals could cause hitching? maybe randomize a little
interval = abilityElementGroup.GetAttributeFloat("interval", 0f);
effectDelay = abilityElementGroup.GetAttributeFloat("effectdelay", 0f);
}
public void UpdateAbilityGroup(float deltaTime)
{
if (!IsActive) { return; }
TimeSinceLastUpdate += deltaTime;
if (TimeSinceLastUpdate >= interval)
{
bool conditionsMatched = IsApplicable();
effectDelayTimer = conditionsMatched ? effectDelayTimer + TimeSinceLastUpdate : 0f;
conditionsMatched &= effectDelayTimer >= effectDelay;
foreach (var characterAbility in characterAbilities)
{
if (characterAbility.IsViable())
{
characterAbility.UpdateCharacterAbility(conditionsMatched, TimeSinceLastUpdate);
}
}
if (conditionsMatched)
{
timesTriggered++;
}
TimeSinceLastUpdate = 0;
TimeSinceLastUpdate += deltaTime;
if (TimeSinceLastUpdate < interval) { return; }
bool conditionsMatched;
if (AllConditionsMatched())
{
effectDelayTimer += TimeSinceLastUpdate;
bool shouldApplyDelayedEffect = effectDelayTimer >= effectDelay;
conditionsMatched = shouldApplyDelayedEffect;
}
else
{
effectDelayTimer = 0f;
conditionsMatched = false;
}
bool hasFallbacks = fallbackAbilities.Count > 0;
List<CharacterAbility> abilitiesToRun =
!conditionsMatched && hasFallbacks
? fallbackAbilities
: characterAbilities;
if (hasFallbacks)
{
conditionsMatched = true;
}
foreach (var characterAbility in abilitiesToRun)
{
if (!characterAbility.IsViable()) { continue; }
characterAbility.UpdateCharacterAbility(conditionsMatched, TimeSinceLastUpdate);
}
if (conditionsMatched)
{
timesTriggered++;
}
TimeSinceLastUpdate = 0;
}
private bool IsApplicable()
private bool AllConditionsMatched()
{
if (timesTriggered >= maxTriggerCount) { return false; }
foreach (var abilityCondition in abilityConditions)
{
if (!abilityCondition.MatchesCondition())
{
return false;
}
if (!abilityCondition.MatchesCondition()) { return false; }
}
return true;
}
}
}
}
@@ -19,6 +19,7 @@ namespace Barotrauma
// works functionally but a missing recipe is not represented on GUI side. this might be better placed in the character class itself, though it might be fine here as well
public List<Identifier> UnlockedRecipes { get; } = new List<Identifier>();
public List<Identifier> UnlockedStoreItems { get; } = new List<Identifier>();
public CharacterTalent(TalentPrefab talentPrefab, Character character)
{
@@ -45,7 +46,17 @@ namespace Barotrauma
}
else
{
DebugConsole.ThrowError("No recipe identifier defined for talent " + DebugIdentifier);
DebugConsole.ThrowError($"No recipe identifier defined for talent {DebugIdentifier}");
}
break;
case "addedstoreitem":
if (subElement.GetAttributeIdentifier("itemtag", Identifier.Empty) is { IsEmpty: false } storeItemTag)
{
UnlockedStoreItems.Add(storeItemTag);
}
else
{
DebugConsole.ThrowError($"No store item identifier defined for talent {DebugIdentifier}");
}
break;
}
@@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
using System.Xml.Linq;
#if CLIENT
using Microsoft.Xna.Framework;
#endif
namespace Barotrauma
{
@@ -14,6 +14,10 @@ namespace Barotrauma
public readonly Sprite Icon;
#if CLIENT
public readonly Option<Color> ColorOverride;
#endif
public static readonly PrefabCollection<TalentPrefab> TalentPrefabs = new PrefabCollection<TalentPrefab>();
public ContentXElement ConfigElement
@@ -28,8 +32,22 @@ namespace Barotrauma
DisplayName = TextManager.Get($"talentname.{Identifier}").Fallback(Identifier.Value);
Description = "";
Identifier nameIdentifier = element.GetAttributeIdentifier("nameidentifier", Identifier.Empty);
if (!nameIdentifier.IsEmpty)
{
DisplayName = TextManager.Get(nameIdentifier).Fallback(Identifier.Value);
}
Description = string.Empty;
#if CLIENT
Color colorOverride = element.GetAttributeColor("coloroverride", Color.TransparentBlack);
ColorOverride = colorOverride != Color.TransparentBlack
? Option<Color>.Some(colorOverride)
: Option<Color>.None();
#endif
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
@@ -5,9 +5,9 @@ using System.Linq;
namespace Barotrauma
{
class TalentTree : Prefab
internal sealed class TalentTree : Prefab
{
public enum TalentTreeStageState
public enum TalentStages
{
Invalid,
Locked,
@@ -40,16 +40,17 @@ namespace Barotrauma
DebugConsole.ThrowError($"No job defined for talent tree in \"{file.Path}\"!");
return;
}
List<TalentSubTree> subTrees = new List<TalentSubTree>();
foreach (var subTreeElement in element.GetChildElements("subtree"))
{
subTrees.Add(new TalentSubTree(subTreeElement));
}
TalentSubTrees = subTrees.ToImmutableArray();
AllTalentIdentifiers = TalentSubTrees.SelectMany(t => t.AllTalentIdentifiers).ToImmutableHashSet();
}
public bool TalentIsInTree(Identifier talentIdentifier)
{
return AllTalentIdentifiers.Contains(talentIdentifier);
@@ -57,31 +58,44 @@ namespace Barotrauma
public static bool IsViableTalentForCharacter(Character character, Identifier talentIdentifier)
{
return IsViableTalentForCharacter(character, talentIdentifier, character?.Info?.UnlockedTalents ?? (ICollection<Identifier>)Array.Empty<Identifier>());
return IsViableTalentForCharacter(character, talentIdentifier, character?.Info?.UnlockedTalents ?? (IReadOnlyCollection<Identifier>)Array.Empty<Identifier>());
}
public static bool TalentTreeMeetsRequirements(TalentTree tree, TalentSubTree targetTree, IReadOnlyCollection<Identifier> selectedTalents)
{
IEnumerable<TalentSubTree> blockingSubTrees = tree.TalentSubTrees.Where(tst => tst.BlockedTrees.Contains(targetTree.Identifier)),
requiredSubTrees = tree.TalentSubTrees.Where(tst => targetTree.RequiredTrees.Contains(tst.Identifier));
return requiredSubTrees.All(tst => tst.IsCompleted(selectedTalents)) && // check if we meet requirements
!blockingSubTrees.Any(tst => tst.HasAnyTalent(selectedTalents)); // check if any other talent trees are blocking this one
}
// i hate this function - markus
// me too - joonas
public static TalentTreeStageState GetTalentOptionStageState(Character character, Identifier subTreeIdentifier, int index, List<Identifier> selectedTalents)
public static TalentStages GetTalentOptionStageState(Character character, Identifier subTreeIdentifier, int index, IReadOnlyCollection<Identifier> selectedTalents)
{
if (character?.Info?.Job.Prefab is null) { return TalentTreeStageState.Invalid; }
if (character?.Info?.Job.Prefab is null) { return TalentStages.Invalid; }
if (!JobTalentTrees.TryGet(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return TalentTreeStageState.Invalid; }
if (!JobTalentTrees.TryGet(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return TalentStages.Invalid; }
TalentSubTree subTree = talentTree.TalentSubTrees.FirstOrDefault(tst => tst.Identifier == subTreeIdentifier);
TalentSubTree subTree = talentTree!.TalentSubTrees.FirstOrDefault(tst => tst.Identifier == subTreeIdentifier);
if (subTree is null) { return TalentStages.Invalid; }
if (subTree == null) { return TalentTreeStageState.Invalid; }
if (!TalentTreeMeetsRequirements(talentTree, subTree, selectedTalents))
{
return TalentStages.Locked;
}
TalentOption targetTalentOption = subTree.TalentOptionStages[index];
if (targetTalentOption.TalentIdentifiers.Any(t => character.HasTalent(t)))
if (targetTalentOption.HasEnoughTalents(character.Info))
{
return TalentTreeStageState.Unlocked;
return TalentStages.Unlocked;
}
if (targetTalentOption.TalentIdentifiers.Any(t => selectedTalents.Contains(t)))
if (targetTalentOption.HasSelectedTalent(selectedTalents))
{
return TalentTreeStageState.Highlighted;
return TalentStages.Highlighted;
}
bool hasTalentInLastTier = true;
@@ -91,55 +105,51 @@ namespace Barotrauma
if (lastindex >= 0)
{
TalentOption lastLatentOption = subTree.TalentOptionStages[lastindex];
hasTalentInLastTier = lastLatentOption.TalentIdentifiers.Any(HasTalent);
isLastTalentPurchased = lastLatentOption.TalentIdentifiers.Any(t => character.HasTalent(t));
hasTalentInLastTier = lastLatentOption.HasEnoughTalents(selectedTalents);
isLastTalentPurchased = lastLatentOption.HasEnoughTalents(character.Info);
}
if (!hasTalentInLastTier)
{
return TalentTreeStageState.Locked;
return TalentStages.Locked;
}
bool hasPointsForNewTalent = character.Info.GetTotalTalentPoints() - selectedTalents.Count > 0;
if (hasPointsForNewTalent)
{
return isLastTalentPurchased ? TalentTreeStageState.Highlighted : TalentTreeStageState.Available;
return isLastTalentPurchased ? TalentStages.Highlighted : TalentStages.Available;
}
return TalentTreeStageState.Locked;
bool HasTalent(Identifier talentId)
{
return selectedTalents.Contains(talentId);
}
return TalentStages.Locked;
}
public static bool IsViableTalentForCharacter(Character character, Identifier talentIdentifier, ICollection<Identifier> selectedTalents)
public static bool IsViableTalentForCharacter(Character character, Identifier talentIdentifier, IReadOnlyCollection<Identifier> selectedTalents)
{
if (character?.Info?.Job.Prefab == null) { return false; }
if (character.Info.GetTotalTalentPoints() - selectedTalents.Count() <= 0) { return false; }
if (character.Info.GetTotalTalentPoints() - selectedTalents.Count <= 0) { return false; }
if (!JobTalentTrees.TryGet(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return false; }
foreach (var subTree in talentTree.TalentSubTrees)
foreach (Character c in GameSession.GetSessionCrewCharacters(CharacterType.Both))
{
if (subTree.ForceUnlock && subTree.TalentOptionStages.Any(option => option.TalentIdentifiers.Contains(talentIdentifier))) { return true; }
if (c.Info.GetSavedStatValue(StatTypes.LockedTalents, talentIdentifier) >= 1) { return false; }
}
foreach (var subTree in talentTree!.TalentSubTrees)
{
foreach (var talentOptionStage in subTree.TalentOptionStages)
{
bool hasTalentInThisTier = talentOptionStage.TalentIdentifiers.Any(t => selectedTalents.Contains(t));
bool hasTalentInThisTier = talentOptionStage.HasEnoughTalents(selectedTalents);
if (!hasTalentInThisTier)
{
if (talentOptionStage.TalentIdentifiers.Contains(talentIdentifier))
{
return true;
}
else
{
break;
return TalentTreeMeetsRequirements(talentTree, subTree, selectedTalents);
}
break;
}
}
}
@@ -164,60 +174,130 @@ namespace Barotrauma
}
}
}
return viableTalents;
}
public override void Dispose() { }
}
class TalentSubTree
internal enum TalentTreeType
{
Specialization,
Primary
}
internal sealed class TalentSubTree
{
public Identifier Identifier { get; }
public LocalizedString DisplayName { get; }
public bool ForceUnlock;
public readonly ImmutableArray<TalentOption> TalentOptionStages;
public readonly ImmutableHashSet<Identifier> AllTalentIdentifiers;
public readonly TalentTreeType Type;
public readonly ImmutableHashSet<Identifier> RequiredTrees;
public readonly ImmutableHashSet<Identifier> BlockedTrees;
public bool IsCompleted(IReadOnlyCollection<Identifier> talents) => TalentOptionStages.All(option => option.HasEnoughTalents(talents));
public bool HasAnyTalent(IReadOnlyCollection<Identifier> talents) => TalentOptionStages.Any(option => option.HasSelectedTalent(talents));
public TalentSubTree(ContentXElement subTreeElement)
{
Identifier = subTreeElement.GetAttributeIdentifier("identifier", "");
DisplayName = TextManager.Get("talenttree." + Identifier).Fallback(Identifier.Value);
string nameIdentifier = subTreeElement.GetAttributeString("nameidentifier", string.Empty);
if (string.IsNullOrWhiteSpace(nameIdentifier))
{
nameIdentifier = $"talenttree.{Identifier}";
}
DisplayName = TextManager.Get(nameIdentifier).Fallback(Identifier.Value);
Type = subTreeElement.GetAttributeEnum("type", TalentTreeType.Specialization);
RequiredTrees = subTreeElement.GetAttributeIdentifierImmutableHashSet("requires", ImmutableHashSet<Identifier>.Empty);
BlockedTrees = subTreeElement.GetAttributeIdentifierImmutableHashSet("blocks", ImmutableHashSet<Identifier>.Empty);
List<TalentOption> talentOptionStages = new List<TalentOption>();
foreach (var talentOptionsElement in subTreeElement.GetChildElements("talentoptions"))
{
talentOptionStages.Add(new TalentOption(talentOptionsElement, Identifier));
}
TalentOptionStages = talentOptionStages.ToImmutableArray();
AllTalentIdentifiers = TalentOptionStages.SelectMany(t => t.TalentIdentifiers).ToImmutableHashSet();
}
}
class TalentOption
internal readonly struct TalentOption
{
private readonly ImmutableHashSet<Identifier> talentIdentifiers;
public IEnumerable<Identifier> TalentIdentifiers => talentIdentifiers;
public bool HasTalent(Identifier talentIdentifier)
public readonly int MaxChosenTalents;
/// <summary>
/// When specified the talent option will show talent with this identifier
/// and clicking on it will expand the talent option to show the talents
/// </summary>
public readonly Dictionary<Identifier, ImmutableHashSet<Identifier>> ShowCaseTalents = new Dictionary<Identifier, ImmutableHashSet<Identifier>>();
public bool HasEnoughTalents(CharacterInfo character) => CountMatchingTalents(character.UnlockedTalents) >= MaxChosenTalents;
public bool HasEnoughTalents(IReadOnlyCollection<Identifier> selectedTalents) => CountMatchingTalents(selectedTalents) >= MaxChosenTalents;
// No LINQ
public bool HasSelectedTalent(IReadOnlyCollection<Identifier> selectedTalents)
{
return talentIdentifiers.Contains(talentIdentifier);
foreach (Identifier talent in selectedTalents)
{
if (talentIdentifiers.Contains(talent))
{
return true;
}
}
return false;
}
public int CountMatchingTalents(IReadOnlyCollection<Identifier> talents)
{
int i = 0;
foreach (Identifier talent in talents)
{
if (talentIdentifiers.Contains(talent))
{
i++;
}
}
return i;
}
public TalentOption(ContentXElement talentOptionsElement, Identifier debugIdentifier)
{
var talentIdentifiers = new HashSet<Identifier>();
foreach (var talentOptionElement in talentOptionsElement.GetChildElements("talentoption"))
MaxChosenTalents = talentOptionsElement.GetAttributeInt("maxchosentalents", 1);
HashSet<Identifier> identifiers = new HashSet<Identifier>();
foreach (ContentXElement talentOptionElement in talentOptionsElement.Elements())
{
Identifier identifier = talentOptionElement.GetAttributeIdentifier("identifier", Identifier.Empty);
talentIdentifiers.Add(identifier);
Identifier elementName = talentOptionElement.Name.ToIdentifier();
if (elementName == "talentoption")
{
identifiers.Add(talentOptionElement.GetAttributeIdentifier("identifier", Identifier.Empty));
}
else if (elementName == "showcasetalent")
{
Identifier showCaseIdentifier = talentOptionElement.GetAttributeIdentifier("identifier", Identifier.Empty);
HashSet<Identifier> showCaseTalentIdentifiers = new HashSet<Identifier>();
foreach (ContentXElement subElement in talentOptionElement.Elements())
{
Identifier identifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty);
showCaseTalentIdentifiers.Add(identifier);
identifiers.Add(identifier);
}
ShowCaseTalents.Add(showCaseIdentifier, showCaseTalentIdentifiers.ToImmutableHashSet());
}
}
this.talentIdentifiers = talentIdentifiers.ToImmutableHashSet();
talentIdentifiers = identifiers.ToImmutableHashSet();
}
}
}
}
@@ -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);
}
}
@@ -18,7 +18,7 @@ namespace Barotrauma
public const string LocalModsDir = "LocalMods";
public static readonly string WorkshopModsDir = Barotrauma.IO.Path.Combine(
SaveUtil.SaveFolder,
SaveUtil.DefaultSaveFolder,
"WorkshopMods",
"Installed");
@@ -1,6 +1,8 @@
#nullable enable
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;
@@ -63,6 +65,8 @@ 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);
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);
@@ -121,6 +121,11 @@ namespace Barotrauma
public static bool operator !=(string str, in Identifier? identifier) =>
!(identifier == str);
internal int IndexOf(char c) => Value.IndexOf(c);
internal Identifier this[Range range] => Value[range].ToIdentifier();
internal Char this[int i] => Value[i];
}
public static class IdentifierExtensions
@@ -844,7 +844,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) =>
@@ -1046,11 +1054,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))
{
@@ -1222,7 +1225,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
{
@@ -1750,6 +1753,17 @@ namespace Barotrauma
NewMessage("Set minimum loading time to " + time + " seconds.", Color.White);
}));
commands.Add(new Command("resetcharacternetstate", "resetcharacternetstate [character name]: A debug-only command that resets a character's network state, intended for diagnosing character syncing issues.", null,
() =>
{
if (GameMain.NetworkMember == null) { return null; }
return new string[][]
{
Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray()
};
}));
commands.Add(new Command("storeinfo", "", (string[] args) =>
{
if (GameMain.GameSession?.Map?.CurrentLocation is Location location)
@@ -1803,6 +1817,7 @@ namespace Barotrauma
commands.Add(new Command("lighting|lights", "Toggle lighting on/off (client-only).", null, isCheat: true));
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("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));
@@ -2187,7 +2202,7 @@ namespace Barotrauma
//Dont do a thing, random is basically Human points anyways - its in the help description.
break;
default:
var matchingCharacter = FindMatchingCharacter(args.Skip(1).ToArray());
var matchingCharacter = FindMatchingCharacter(args.Skip(1).Take(1).ToArray());
if (matchingCharacter != null){ spawnInventory = matchingCharacter.Inventory; }
break;
}
@@ -43,6 +43,7 @@ namespace Barotrauma
OnRepairComplete,
OnItemFabricationSkillGain,
OnItemFabricatedAmount,
OnItemFabricatedIngredients,
OnAllyItemFabricatedAmount,
OnOpenItemContainer,
OnUseRangedWeapon,
@@ -51,6 +52,7 @@ namespace Barotrauma
OnSelfRagdoll,
OnRagdoll,
OnRoundEnd,
OnLootCharacter,
OnAnyMissionCompleted,
OnAllMissionsCompleted,
OnGiveOrder,
@@ -80,6 +82,11 @@ namespace Barotrauma
// Skills
ElectricalSkillBonus,
HelmSkillBonus,
HelmSkillOverride,
MedicalSkillOverride,
WeaponsSkillOverride,
ElectricalSkillOverride,
MechanicalSkillOverride,
MechanicalSkillBonus,
MedicalSkillBonus,
WeaponsSkillBonus,
@@ -105,6 +112,7 @@ namespace Barotrauma
RangedSpreadReduction,
// Utility
RepairSpeed,
MechanicalRepairSpeed,
DeconstructorSpeedMultiplier,
RepairToolStructureRepairMultiplier,
RepairToolStructureDamageMultiplier,
@@ -115,20 +123,57 @@ namespace Barotrauma
GeneticMaterialRefineBonus,
GeneticMaterialTaintedProbabilityReductionOnCombine,
SkillGainSpeed,
ExtraLevelGain,
HelmSkillGainSpeed,
WeaponsSkillGainSpeed,
MedicalSkillGainSpeed,
ElectricalSkillGainSpeed,
MechanicalSkillGainSpeed,
MedicalItemApplyingMultiplier,
MedicalItemDurationMultiplier,
PoisonMultiplier,
// Tinker
TinkeringDuration,
TinkeringStrength,
TinkeringDamage,
// Misc
ReputationGainMultiplier,
ReputationLossMultiplier,
MissionMoneyGainMultiplier,
ExperienceGainMultiplier,
MissionExperienceGainMultiplier,
ExtraMissionCount,
ExtraSpecialSalesCount,
ApplyTreatmentsOnSelfFraction,
StoreSellMultiplier,
StoreBuyMultiplierAffiliated,
StoreBuyMultiplier,
ShipyardBuyMultiplierAffiliated,
ShipyardBuyMultiplier,
MaxAttachableCount,
ExplosionRadiusMultiplier,
ExplosionDamageMultiplier,
FabricateMedicineSpeedMultiplier,
BallastFloraDamageMultiplier,
HoldBreathMultiplier,
Apprenticeship,
Affiliation,
CPRBoost,
LockedTalents
}
internal enum ItemTalentStats
{
None,
DetoriationSpeed,
BatteryCapacity,
EngineSpeed,
EngineMaxSpeed,
PumpSpeed,
PumpMaxFlow,
ReactorMaxOutput,
ReactorFuelEfficiency,
DeconstructorSpeed,
FabricationSpeed
}
[Flags]
@@ -145,8 +190,8 @@ namespace Barotrauma
GainSkillPastMaximum = 0x80,
RetainExperienceForNewCharacter = 0x100,
AllowSecondOrderedTarget = 0x200,
PowerfulCPR = 0x400,
AlwaysStayConscious = 0x800,
AlwaysStayConscious = 0x400,
CanNotDieToAfflictions = 0x800,
}
[Flags]
@@ -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;
@@ -25,11 +25,14 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.Yes)]
public bool RequireEquipped { get; set; }
[Serialize(true, IsPropertySaveable.Yes)]
public bool Recursive { get; set; }
[Serialize(-1, IsPropertySaveable.Yes)]
public int ItemContainerIndex { get; set; }
private readonly IReadOnlyList<PropertyConditional> conditionals;
private readonly Identifier[] itemIdentifierSplit;
private readonly Identifier[] itemTags;
@@ -97,7 +100,7 @@ namespace Barotrauma
{
if (inventory == null) { return false; }
int count = 0;
foreach (Item item in inventory.FindAllItems(it => itemTags.Any(it.HasTag) || itemIdentifierSplit.Contains(it.Prefab.Identifier)))
foreach (Item item in inventory.FindAllItems(it => itemTags.Any(it.HasTag) || itemIdentifierSplit.Contains(it.Prefab.Identifier), recursive: Recursive))
{
if (!ConditionalsMatch(item, character)) { continue; }
count++;
@@ -0,0 +1,58 @@
using System;
using System.Linq;
namespace Barotrauma;
class CheckMissionAction : BinaryOptionAction
{
public enum MissionType
{
Current,
Selected,
Available
}
[Serialize(MissionType.Current, IsPropertySaveable.Yes)]
public MissionType Type { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier MissionIdentifier { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier MissionTag { get; set; }
[Serialize(1, IsPropertySaveable.Yes)]
public int MissionCount { get; set; }
public CheckMissionAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
MissionCount = Math.Max(MissionCount, 0);
}
protected override bool? DetermineSuccess()
{
var missions = Type switch
{
MissionType.Current => GameMain.GameSession?.Missions,
MissionType.Selected => GameMain.GameSession?.Campaign?.Missions,
MissionType.Available => GameMain.GameSession?.Map?.CurrentLocation?.AvailableMissions,
_ => null
};
if (missions is not null)
{
if (!MissionIdentifier.IsEmpty)
{
return missions.Any(m => m.Prefab.Identifier == MissionIdentifier);
}
else if (!MissionTag.IsEmpty)
{
return missions.Count(m => m.Prefab.Tags.Contains(MissionTag.Value)) >= MissionCount;
}
else
{
return missions.Count() >= MissionCount;
}
}
return MissionIdentifier.IsEmpty && MissionTag.IsEmpty && MissionCount == 0;
}
}
@@ -0,0 +1,15 @@
namespace Barotrauma;
partial class CheckObjectiveAction : BinaryOptionAction
{
public CheckObjectiveAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
protected override bool? DetermineSuccess()
{
bool success = false;
DetermineSuccessProjSpecific(ref success);
return success;
}
partial void DetermineSuccessProjSpecific(ref bool success);
}
@@ -1,7 +1,15 @@
using Barotrauma.Extensions;
namespace Barotrauma
{
class CheckOrderAction : BinaryOptionAction
{
public enum OrderPriority
{
Top,
Any
}
[Serialize("", IsPropertySaveable.Yes)]
public Identifier TargetTag { get; set; }
@@ -14,35 +22,58 @@ namespace Barotrauma
[Serialize("", IsPropertySaveable.Yes)]
public Identifier OrderTargetTag { get; set; }
[Serialize(OrderPriority.Top, IsPropertySaveable.Yes)]
public OrderPriority Priority { get; set; }
public CheckOrderAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
protected override bool? DetermineSuccess()
{
Character targetCharacter = null;
if (!TargetTag.IsEmpty)
var targetCharacters = ParentEvent.GetTargets(TargetTag);
if (targetCharacters.None())
{
foreach (var t in ParentEvent.GetTargets(TargetTag))
{
if (t is Character c)
{
targetCharacter = c;
break;
}
}
}
if (targetCharacter == null)
{
DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckOrderAction but no valid target character was found for tag \"{TargetTag}\"! This will cause the check to automatically fail.");
DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckOrderAction but no valid target characters were found for tag \"{TargetTag}\"! This will cause the check to automatically fail.");
return false;
}
var currentOrderInfo = targetCharacter.GetCurrentOrderWithTopPriority();
if (currentOrderInfo?.Identifier == OrderIdentifier)
foreach (var t in targetCharacters)
{
if (!OrderTargetTag.IsEmpty)
if (t is not Character c)
{
if (currentOrderInfo.TargetEntity is not Item targetItem || !targetItem.HasTag(OrderTargetTag)) { return false; }
continue;
}
if (Priority == OrderPriority.Top)
{
if (c.GetCurrentOrderWithTopPriority() is Order topPrioOrder && IsMatch(topPrioOrder))
{
return true;
}
}
else if (Priority == OrderPriority.Any)
{
foreach (var order in c.CurrentOrders)
{
if (IsMatch(order))
{
return true;
}
}
}
bool IsMatch(Order order)
{
if (order?.Identifier == OrderIdentifier)
{
if (!OrderTargetTag.IsEmpty && (order.TargetEntity is not Item targetItem || !targetItem.HasTag(OrderTargetTag)))
{
return false;
}
if (OrderOption.IsEmpty || order?.Option == OrderOption)
{
return true;
}
}
return false;
}
return OrderOption.IsEmpty || currentOrderInfo?.Option == OrderOption;
}
return false;
}
@@ -0,0 +1,87 @@
using System;
using System.Linq;
namespace Barotrauma;
class CheckPurchasedItemsAction : BinaryOptionAction
{
public enum TransactionType
{
Purchased,
Sold
}
[Serialize(TransactionType.Purchased, IsPropertySaveable.Yes)]
public TransactionType Type { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier ItemIdentifier { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier ItemTag { get; set; }
[Serialize(1, IsPropertySaveable.Yes)]
public int MinCount { get; set; }
public CheckPurchasedItemsAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
MinCount = Math.Max(MinCount, 1);
}
protected override bool? DetermineSuccess()
{
if (ItemIdentifier.IsEmpty && ItemTag.IsEmpty)
{
return false;
}
if (GameMain.GameSession?.Campaign?.CargoManager is not CargoManager cargoManager)
{
return false;
}
if (Type == TransactionType.Purchased)
{
int totalPurchased = 0;
foreach ((Identifier id, var items) in cargoManager.PurchasedItems)
{
if (!ItemIdentifier.IsEmpty)
{
totalPurchased += items.Find(i => i.ItemPrefabIdentifier == ItemIdentifier)?.Quantity ?? 0;
}
else if (!ItemTag.IsEmpty)
{
foreach (var item in items)
{
if (item.ItemPrefab.Tags.Contains(ItemTag))
{
totalPurchased += item.Quantity;
}
}
}
if (totalPurchased >= MinCount)
{
return true;
}
}
}
else
{
int totalSold = 0;
foreach ((Identifier id, var items) in cargoManager.SoldItems)
{
if (!ItemIdentifier.IsEmpty)
{
totalSold += items.Count(i => i.ItemPrefab.Identifier == ItemIdentifier);
}
else if (!ItemTag.IsEmpty)
{
totalSold += items.Count(i => i.ItemPrefab.Tags.Contains(ItemTag));
}
if (totalSold >= MinCount)
{
return true;
}
}
}
return false;
}
}
@@ -59,7 +59,10 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.Yes)]
public bool ContinueConversation { get; set; }
public Character speaker
[Serialize(false, IsPropertySaveable.Yes)]
public bool IgnoreInterruptDistance { get; set; }
public Character Speaker
{
get;
private set;
@@ -124,7 +127,7 @@ namespace Barotrauma
#else
foreach (Client c in GameMain.Server.ConnectedClients)
{
if (c.InGame && c.Character != null) { ServerWrite(speaker, c, interrupt); }
if (c.InGame && c.Character != null) { ServerWrite(Speaker, c, interrupt); }
}
#endif
ResetSpeaker();
@@ -160,7 +163,7 @@ namespace Barotrauma
selectedOption = -1;
interrupt = false;
dialogOpened = false;
speaker = null;
Speaker = null;
}
public override bool SetGoToTarget(string goTo)
@@ -181,15 +184,14 @@ namespace Barotrauma
private void ResetSpeaker()
{
if (speaker == null) { return; }
speaker.CampaignInteractionType = CampaignMode.InteractionType.None;
speaker.ActiveConversation = null;
speaker.SetCustomInteract(null, null);
if (Speaker == null) { return; }
Speaker.CampaignInteractionType = CampaignMode.InteractionType.None;
Speaker.ActiveConversation = null;
Speaker.SetCustomInteract(null, null);
#if SERVER
GameMain.NetworkMember.CreateEntityEvent(speaker, new Character.AssignCampaignInteractionEventData());
GameMain.NetworkMember.CreateEntityEvent(Speaker, new Character.AssignCampaignInteractionEventData());
#endif
var humanAI = speaker.AIController as HumanAIController;
if (humanAI != null && !speaker.IsDead && !speaker.Removed)
if (Speaker.AIController is HumanAIController humanAI && !Speaker.IsDead && !Speaker.Removed)
{
humanAI.ClearForcedOrder();
if (prevIdleObjective != null) { humanAI.ObjectiveManager.AddObjective(prevIdleObjective); }
@@ -207,7 +209,6 @@ namespace Barotrauma
public override void Update(float deltaTime)
{
lastActiveTime = Timing.TotalTime;
if (interrupt)
{
Interrupted?.Update(deltaTime);
@@ -216,6 +217,7 @@ namespace Barotrauma
{
if (dialogOpened)
{
lastActiveTime = Timing.TotalTime;
#if CLIENT
if (GUIMessageBox.MessageBoxes.Any(mb => mb.UserData as string == "ConversationAction"))
{
@@ -226,7 +228,7 @@ namespace Barotrauma
Reset();
}
#endif
if (ShouldInterrupt())
if (ShouldInterrupt(requireTarget: true))
{
ResetSpeaker();
interrupt = true;
@@ -236,34 +238,34 @@ namespace Barotrauma
if (!SpeakerTag.IsEmpty)
{
if (speaker != null && !speaker.Removed && speaker.CampaignInteractionType == CampaignMode.InteractionType.Talk && speaker.ActiveConversation?.ParentEvent != this.ParentEvent) { return; }
speaker = ParentEvent.GetTargets(SpeakerTag).FirstOrDefault(e => e is Character) as Character;
if (speaker == null || speaker.Removed)
if (Speaker != null && !Speaker.Removed && Speaker.CampaignInteractionType == CampaignMode.InteractionType.Talk && Speaker.ActiveConversation?.ParentEvent != this.ParentEvent) { return; }
Speaker = ParentEvent.GetTargets(SpeakerTag).FirstOrDefault(e => e is Character) as Character;
if (Speaker == null || Speaker.Removed)
{
return;
}
//some conversation already assigned to the speaker, wait for it to be removed
if (speaker.CampaignInteractionType == CampaignMode.InteractionType.Talk && speaker.ActiveConversation?.ParentEvent != this.ParentEvent)
if (Speaker.CampaignInteractionType == CampaignMode.InteractionType.Talk && Speaker.ActiveConversation?.ParentEvent != this.ParentEvent)
{
return;
}
else if (!WaitForInteraction)
{
TryStartConversation(speaker);
TryStartConversation(Speaker);
}
else if (speaker.ActiveConversation != this)
else if (Speaker.ActiveConversation != this)
{
speaker.CampaignInteractionType = CampaignMode.InteractionType.Talk;
speaker.ActiveConversation = this;
Speaker.CampaignInteractionType = CampaignMode.InteractionType.Talk;
Speaker.ActiveConversation = this;
#if CLIENT
speaker.SetCustomInteract(
Speaker.SetCustomInteract(
TryStartConversation,
TextManager.GetWithVariable("CampaignInteraction.Talk", "[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Use)));
#else
speaker.SetCustomInteract(
Speaker.SetCustomInteract(
TryStartConversation,
TextManager.Get("CampaignInteraction.Talk"));
GameMain.NetworkMember.CreateEntityEvent(speaker, new Character.AssignCampaignInteractionEventData());
GameMain.NetworkMember.CreateEntityEvent(Speaker, new Character.AssignCampaignInteractionEventData());
#endif
}
return;
@@ -275,7 +277,9 @@ namespace Barotrauma
}
else
{
if (ShouldInterrupt())
//after the conversation has been finished and the target character assigned,
//we no longer care if we still have a target
if (ShouldInterrupt(requireTarget: false))
{
ResetSpeaker();
interrupt = true;
@@ -287,35 +291,36 @@ namespace Barotrauma
}
}
private bool ShouldInterrupt()
private bool ShouldInterrupt(bool requireTarget)
{
IEnumerable<Entity> targets = Enumerable.Empty<Entity>();
if (!TargetTag.IsEmpty)
if (!TargetTag.IsEmpty && requireTarget)
{
targets = ParentEvent.GetTargets(TargetTag).Where(e => IsValidTarget(e));
targets = ParentEvent.GetTargets(TargetTag).Where(e => IsValidTarget(e, requireTarget));
if (!targets.Any()) { return true; }
}
if (speaker != null)
if (Speaker != null)
{
if (!TargetTag.IsEmpty)
if (!TargetTag.IsEmpty && requireTarget && !IgnoreInterruptDistance)
{
if (targets.All(t => Vector2.DistanceSquared(t.WorldPosition, speaker.WorldPosition) > InterruptDistance * InterruptDistance)) { return true; }
if (targets.All(t => Vector2.DistanceSquared(t.WorldPosition, Speaker.WorldPosition) > InterruptDistance * InterruptDistance)) { return true; }
}
if (speaker.AIController is HumanAIController humanAI && !humanAI.AllowCampaignInteraction())
if (Speaker.AIController is HumanAIController humanAI && !humanAI.AllowCampaignInteraction())
{
return true;
}
return speaker.Removed || speaker.IsDead || speaker.IsIncapacitated;
return Speaker.Removed || Speaker.IsDead || Speaker.IsIncapacitated;
}
return false;
}
private bool IsValidTarget(Entity e)
private bool IsValidTarget(Entity e, bool requirePlayerControlled = true)
{
bool isValid = e is Character character && !character.Removed && !character.IsDead && !character.IsIncapacitated &&
(e == Character.Controlled || character.IsRemotePlayer);
bool isValid =
e is Character character && !character.Removed && !character.IsDead && !character.IsIncapacitated &&
(character == Character.Controlled || character.IsRemotePlayer || !requirePlayerControlled);
#if SERVER
if (!dialogOpened)
{
@@ -40,7 +40,8 @@ namespace Barotrauma
DebugConsole.ThrowError($"Error in event prefab \"{scriptedEvent.Prefab.Identifier}\". Status effect configured as a sub action (text: \"{Text}\"). Please configure status effects as child elements of a StatusEffectAction.");
continue;
}
Actions.Add(Instantiate(scriptedEvent, e));
var action = Instantiate(scriptedEvent, e);
if (action != null) { Actions.Add(action); }
}
}
@@ -149,6 +150,10 @@ namespace Barotrauma
ConstructorInfo constructor = actionType.GetConstructor(new[] { typeof(ScriptedEvent), typeof(ContentXElement) });
try
{
if (constructor == null)
{
throw new Exception($"Error in scripted event \"{scriptedEvent.Prefab.Identifier}\" - could not find a constructor for the EventAction \"{actionType}\".");
}
return constructor.Invoke(new object[] { scriptedEvent, element }) as EventAction;
}
catch (Exception ex)
@@ -49,6 +49,12 @@ namespace Barotrauma
[Serialize("", IsPropertySaveable.Yes)]
public Identifier ObjectiveTag { get; set; }
[Serialize(true, IsPropertySaveable.Yes)]
public bool ObjectiveCanBeCompleted { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier ParentObjectiveId { get; set; }
private bool isFinished = false;
public MessageBoxAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
@@ -1,8 +1,10 @@
using System;
using System.Collections.Generic;
using System.Xml.Linq;
using Barotrauma.Extensions;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
namespace Barotrauma
{
@@ -14,8 +16,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; }
@@ -38,6 +42,7 @@ 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();
}
public override bool IsFinished(ref string goTo)
@@ -56,14 +61,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, Barotrauma.LocationType.Prefabs[LocationTypes[0]]);
unlockLocation = emptyLocation;
}
}
@@ -84,6 +89,7 @@ namespace Barotrauma
}
if (unlockedMission != null)
{
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,46 +105,80 @@ namespace Barotrauma
IconColor = unlockedMission.Prefab.IconColor
};
#else
NotifyMissionUnlock(unlockedMission);
#endif
NotifyMissionUnlock(unlockedMission, unlockLocation);
#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()
@@ -147,7 +187,7 @@ namespace Barotrauma
}
#if SERVER
private void NotifyMissionUnlock(Mission mission)
private static void NotifyMissionUnlock(Mission mission, Location unlockLocation)
{
foreach (Client client in GameMain.Server.ConnectedClients)
{
@@ -155,6 +195,7 @@ namespace Barotrauma
outmsg.WriteByte((byte)ServerPacketHeader.EVENTACTION);
outmsg.WriteByte((byte)EventManager.NetworkEventType.MISSION);
outmsg.WriteIdentifier(mission.Prefab.Identifier);
outmsg.WriteInt32(GameMain.GameSession?.Map?.Locations.IndexOf(unlockLocation) ?? -1);
outmsg.WriteString(mission.Name.Value);
GameMain.Server.ServerPeer.Send(outmsg, client.Connection, DeliveryMethod.Reliable);
}
@@ -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
{
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)}";
}
}
}

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