v1.4.4.1 (Blood in the Water Update)

This commit is contained in:
Regalis11
2024-04-24 18:09:05 +03:00
parent 89b91d1c3e
commit ff1b8951a7
397 changed files with 15250 additions and 6479 deletions
@@ -76,9 +76,9 @@ namespace Barotrauma
get { return Character.AnimController.Collider.LinearVelocity; }
}
public virtual bool CanEnterSubmarine
public virtual CanEnterSubmarine CanEnterSubmarine
{
get { return true; }
get { return Character.AnimController.CanEnterSubmarine; }
}
public virtual bool CanFlip
@@ -327,8 +327,17 @@ namespace Barotrauma
{
if (otherItem.Prefab.Identifier == item.Prefab.Identifier || otherItem.HasIdentifierOrTags(targetTags))
{
// Shouldn't try dropping identical items, because that causes infinite looping when trying to get multiple items of the same type and if can't fit them all in the inventory.
return false;
bool switchingToBetterSuit =
targetTags != null &&
targetTags.FirstOrDefault() == Tags.HeavyDivingGear &&
AIObjectiveFindDivingGear.IsSuitablePressureProtection(item, Tags.HeavyDivingGear, Character) &&
!AIObjectiveFindDivingGear.IsSuitablePressureProtection(otherItem, Tags.HeavyDivingGear, Character);
// Shouldn't try dropping identical items, because that causes infinite looping when trying to get multiple items
// of the same type and if can't fit them all in the inventory.
if (!switchingToBetterSuit)
{
return false;
}
}
//if everything else fails, simply drop the existing item
otherItem.Drop(Character);
@@ -176,12 +176,13 @@ namespace Barotrauma
}
}
public override bool CanEnterSubmarine
public override CanEnterSubmarine CanEnterSubmarine
{
get
{
//can't enter a submarine when attached to something
return Character.AnimController.CanEnterSubmarine && (LatchOntoAI == null || !LatchOntoAI.IsAttachedToSub);
//can't enter a submarine when attached to one
if (LatchOntoAI is { IsAttachedToSub: true }) { return CanEnterSubmarine.False; }
return Character.AnimController.CanEnterSubmarine;
}
}
@@ -535,12 +536,27 @@ namespace Barotrauma
FadeMemories(updateMemoriesInverval);
updateMemoriesTimer = updateMemoriesInverval;
}
if (Math.Max(Character.HealthPercentage, 0) < FleeHealthThreshold && SelectedAiTarget != null &&
SelectedAiTarget.Entity is Character target && (target.IsHuman && CanPerceive(SelectedAiTarget) || IsBeingChasedBy(target)))
if (Math.Max(Character.HealthPercentage, 0) < FleeHealthThreshold && SelectedAiTarget != null)
{
// Keep fleeing if being chased
State = AIState.Flee;
Character target = SelectedAiTarget.Entity as Character;
if (target == null && SelectedAiTarget.Entity is Item targetItem)
{
target = GetOwner(targetItem);
}
bool shouldFlee = false;
if (target != null)
{
// Keep fleeing if being chased or if we see a human target (that don't have enemy ai).
shouldFlee = target.IsHuman && CanPerceive(SelectedAiTarget) || IsBeingChasedBy(target);
}
// If we should not flee, just idle. Don't allow any other AI state when below the health threshold.
State = shouldFlee ? AIState.Flee : AIState.Idle;
wallTarget = null;
if (State != AIState.Flee)
{
SelectedAiTarget = null;
_lastAiTarget = null;
}
}
else
{
@@ -614,7 +630,7 @@ namespace Barotrauma
steeringManager = outsideSteering;
}
}
bool useSteeringLengthAsMovementSpeed = State == AIState.Idle && Character.AnimController.InWater;
bool run = false;
switch (State)
@@ -870,11 +886,11 @@ namespace Barotrauma
}
// Ensure that the creature keeps inside the level
SteerInsideLevel(deltaTime);
float defaultSpeed = Character.AnimController.GetCurrentSpeed(run && Character.CanRun);
//calculate a normalized Steering value at this point: we multiply it with the actual, desired speed in ApplyMovementLimits
steeringManager.Update(1.0f);
float speed = useSteeringLengthAsMovementSpeed ? Steering.Length() : defaultSpeed;
Character.AnimController.TargetMovement = Character.ApplyMovementLimits(Steering, speed);
float speed = Character.AnimController.GetCurrentSpeed(run && Character.CanRun);
// Doesn't work if less than 1, when we use steering length as movement speed.
steeringManager.Update(Math.Max(speed, 1.0f));
float movementSpeed = useSteeringLengthAsMovementSpeed ? Steering.Length() : speed;
Character.AnimController.TargetMovement = Character.ApplyMovementLimits(Steering, movementSpeed);
if (Character.CurrentHull != null && Character.AnimController.InWater)
{
// Limit the swimming speed inside the sub.
@@ -1087,6 +1103,22 @@ namespace Barotrauma
// How long the monster tries to reach out for the target when it's close to it before ignoring it.
private const float reachTimeOut = 10;
private bool IsSameTarget(AITarget target, AITarget otherTarget)
{
if (target?.Entity == otherTarget?.Entity) { return true; }
if (IsItemInCharacterInventory(target, otherTarget) || IsItemInCharacterInventory(otherTarget, target)) { return true; }
return false;
bool IsItemInCharacterInventory(AITarget potentialItem, AITarget potentialCharacter)
{
if (potentialItem?.Entity is Item item && potentialCharacter?.Entity is Character character)
{
return item.ParentInventory?.Owner == character;
}
return false;
}
}
private void UpdateAttack(float deltaTime)
{
if (SelectedAiTarget == null || SelectedAiTarget.Entity == null || SelectedAiTarget.Entity.Removed)
@@ -1132,7 +1164,7 @@ namespace Barotrauma
attackSimPos = Character.GetRelativeSimPosition(SelectedAiTarget.Entity);
}
if (Character.AnimController.CanEnterSubmarine)
if (Character.AnimController.CanEnterSubmarine == CanEnterSubmarine.True)
{
if (TrySteerThroughGaps(deltaTime))
{
@@ -1177,13 +1209,21 @@ namespace Barotrauma
if (IsCoolDownRunning && (_previousAttackLimb == null || AttackLimb == null || AttackLimb.attack.CoolDownTimer > 0))
{
var currentAttackLimb = AttackLimb ?? _previousAttackLimb;
if (currentAttackLimb.attack.CoolDownTimer >= currentAttackLimb.attack.CoolDown + currentAttackLimb.attack.CurrentRandomCoolDown - currentAttackLimb.attack.AfterAttackDelay)
if (currentAttackLimb.attack.CoolDownTimer >=
currentAttackLimb.attack.CoolDown + currentAttackLimb.attack.CurrentRandomCoolDown - currentAttackLimb.attack.AfterAttackDelay)
{
return;
}
AIBehaviorAfterAttack activeBehavior = currentAttackLimb.attack.AfterAttack;
currentAttackLimb.attack.AfterAttackTimer += deltaTime;
AIBehaviorAfterAttack activeBehavior =
currentAttackLimb.attack.AfterAttackSecondaryDelay > 0 && currentAttackLimb.attack.AfterAttackTimer > currentAttackLimb.attack.AfterAttackSecondaryDelay ?
currentAttackLimb.attack.AfterAttackSecondary :
currentAttackLimb.attack.AfterAttack;
switch (activeBehavior)
{
case AIBehaviorAfterAttack.Eat:
UpdateEating(deltaTime);
return;
case AIBehaviorAfterAttack.Pursue:
case AIBehaviorAfterAttack.PursueIfCanAttack:
if (currentAttackLimb.attack.SecondaryCoolDown <= 0)
@@ -1205,7 +1245,7 @@ namespace Barotrauma
if (currentAttackLimb.attack.SecondaryCoolDownTimer <= 0)
{
// Don't allow attacking when the attack target has just changed.
if (_previousAiTarget != null && SelectedAiTarget != _previousAiTarget)
if (_previousAiTarget != null && !IsSameTarget(SelectedAiTarget, _previousAiTarget))
{
canAttack = false;
if (activeBehavior == AIBehaviorAfterAttack.PursueIfCanAttack)
@@ -1266,27 +1306,24 @@ namespace Barotrauma
if (currentAttackLimb.attack.SecondaryCoolDownTimer <= 0)
{
// Don't allow attacking when the attack target has just changed.
if (_previousAiTarget != null && SelectedAiTarget != _previousAiTarget)
if (_previousAiTarget != null && !IsSameTarget(SelectedAiTarget, _previousAiTarget))
{
UpdateFallBack(attackWorldPos, deltaTime, activeBehavior == AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
return;
}
// If the secondary cooldown is defined and expired, check if we can switch the attack
var newLimb = GetAttackLimb(attackWorldPos, currentAttackLimb);
if (newLimb != null)
{
// Attack with the new limb
AttackLimb = newLimb;
}
else
{
// If the secondary cooldown is defined and expired, check if we can switch the attack
var newLimb = GetAttackLimb(attackWorldPos, currentAttackLimb);
if (newLimb != null)
{
// Attack with the new limb
AttackLimb = newLimb;
}
else
{
// No new limb was found.
UpdateFallBack(attackWorldPos, deltaTime, activeBehavior == AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
return;
}
}
// No new limb was found.
UpdateFallBack(attackWorldPos, deltaTime, activeBehavior == AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
return;
}
}
else
{
@@ -1308,27 +1345,24 @@ namespace Barotrauma
if (currentAttackLimb.attack.SecondaryCoolDownTimer <= 0)
{
// Don't allow attacking when the attack target has just changed.
if (_previousAiTarget != null && SelectedAiTarget != _previousAiTarget)
if (_previousAiTarget != null && !IsSameTarget(SelectedAiTarget, _previousAiTarget))
{
UpdateIdle(deltaTime, followLastTarget: false);
return;
}
// If the secondary cooldown is defined and expired, check if we can switch the attack
var newLimb = GetAttackLimb(attackWorldPos, currentAttackLimb);
if (newLimb != null)
{
// Attack with the new limb
AttackLimb = newLimb;
}
else
{
// If the secondary cooldown is defined and expired, check if we can switch the attack
var newLimb = GetAttackLimb(attackWorldPos, currentAttackLimb);
if (newLimb != null)
{
// Attack with the new limb
AttackLimb = newLimb;
}
else
{
// No new limb was found.
UpdateIdle(deltaTime, followLastTarget: false);
return;
}
}
// No new limb was found.
UpdateIdle(deltaTime, followLastTarget: false);
return;
}
}
else
{
@@ -1341,6 +1375,9 @@ namespace Barotrauma
case AIBehaviorAfterAttack.FollowThrough:
UpdateFallBack(attackWorldPos, deltaTime, followThrough: true);
return;
case AIBehaviorAfterAttack.FollowThroughWithoutObstacleAvoidance:
UpdateFallBack(attackWorldPos, deltaTime, followThrough: true, avoidObstacles: false);
return;
case AIBehaviorAfterAttack.FallBack:
case AIBehaviorAfterAttack.Reverse:
default:
@@ -1724,7 +1761,7 @@ namespace Barotrauma
circleRotationSpeed *= Rand.Range(1 - selectedTargetingParams.CircleRandomRotationFactor, 1 + selectedTargetingParams.CircleRandomRotationFactor);
aggressionIntensity = Math.Clamp(aggressionIntensity, AIParams.StartAggression, AIParams.MaxAggression);
DisableAttacksIfLimbNotRanged();
if (targetSub != null && targetSub.Borders.Width < 1000 && AttackLimb?.attack is { Ranged: false })
if (targetSub is { Borders.Width: < 1000 } && AttackLimb?.attack is { Ranged: false })
{
breakCircling = true;
CirclePhase = CirclePhase.CloseIn;
@@ -1982,7 +2019,7 @@ namespace Barotrauma
Entity targetEntity = wallTarget?.Structure ?? SelectedAiTarget?.Entity;
if (AttackLimb?.attack is Attack { Ranged: true } attack)
{
AimRangedAttack(attack, targetEntity);
AimRangedAttack(attack, attackTargetLimb as ISpatialEntity ?? targetEntity);
}
if (canAttack)
{
@@ -2005,9 +2042,10 @@ namespace Barotrauma
}
}
public void AimRangedAttack(Attack attack, Entity targetEntity)
public void AimRangedAttack(Attack attack, ISpatialEntity targetEntity)
{
if (attack is not { Ranged: true } || targetEntity is not { Removed: false }) { return; }
if (attack is not { Ranged: true }) { return; }
if (targetEntity is Entity { Removed: true }) { return; }
Character.SetInput(InputType.Aim, false, true);
if (attack.AimRotationTorque <= 0) { return; }
Limb limb = GetLimbToRotate(attack);
@@ -2115,7 +2153,10 @@ namespace Barotrauma
bool wasLatched = IsLatchedOnSub;
Character.AnimController.ReleaseStuckLimbs();
LatchOntoAI?.DeattachFromBody(reset: true, cooldown: 1);
if (attackResult.Damage > 0)
{
LatchOntoAI?.DeattachFromBody(reset: true, cooldown: 1);
}
if (attacker == null || attacker.AiTarget == null || attacker.Removed || attacker.IsDead) { return; }
if (attackResult.Damage >= AIParams.DamageThreshold)
{
@@ -2283,7 +2324,7 @@ namespace Barotrauma
Limb referenceLimb = GetLimbToRotate(ActiveAttack);
if (referenceLimb != null)
{
Vector2 toTarget = spatialTarget.WorldPosition - referenceLimb.WorldPosition;
Vector2 toTarget = attackWorldPos - 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));
@@ -2456,7 +2497,7 @@ namespace Barotrauma
}
private Vector2? attackVector = null;
private bool UpdateFallBack(Vector2 attackWorldPos, float deltaTime, bool followThrough, bool checkBlocking = false)
private bool UpdateFallBack(Vector2 attackWorldPos, float deltaTime, bool followThrough, bool checkBlocking = false, bool avoidObstacles = true)
{
if (attackVector == null)
{
@@ -2468,7 +2509,7 @@ namespace Barotrauma
dir = Vector2.UnitY;
}
steeringManager.SteeringManual(deltaTime, dir);
if (Character.AnimController.InWater && !Reverse)
if (Character.AnimController.InWater && !Reverse && avoidObstacles)
{
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15);
}
@@ -2779,10 +2820,16 @@ namespace Barotrauma
// Ignore inner walls when outside (walltargets still work)
continue;
}
if (!Character.AnimController.CanEnterSubmarine && IsWallDisabled(s))
bool attemptToGetInside =
Character.AnimController.CanEnterSubmarine == CanEnterSubmarine.True ||
//characters that are aggressive boarders can partially enter the sub can attempt to push through holes
(Character.AnimController.CanEnterSubmarine == CanEnterSubmarine.Partial && IsAggressiveBoarder);
if (!attemptToGetInside && IsWallDisabled(s))
{
continue;
}
// Prefer weaker walls (200 is the default for normal hull walls)
valueModifier = 200f / s.MaxHealth;
for (int i = 0; i < s.Sections.Length; i++)
@@ -2790,7 +2837,7 @@ namespace Barotrauma
var section = s.Sections[i];
if (section.gap == null) { continue; }
bool leadsInside = !section.gap.IsRoomToRoom && section.gap.FlowTargetHull != null;
if (Character.AnimController.CanEnterSubmarine)
if (attemptToGetInside)
{
if (!isCharacterInside)
{
@@ -2873,9 +2920,11 @@ namespace Barotrauma
{
if (!canAttackDoors) { continue; }
}
else if (!Character.AnimController.CanEnterSubmarine)
else if (Character.AnimController.CanEnterSubmarine != CanEnterSubmarine.True)
{
// Ignore broken and open doors, if cannot enter submarine
// Also ignore them if the monster can only partially enter the sub:
// these monsters tend to be too large to get through doors anyway.
continue;
}
if (IsAggressiveBoarder)
@@ -2911,6 +2960,13 @@ namespace Barotrauma
if (targetParams.IgnoreInside && Character.CurrentHull != null) { continue; }
if (targetParams.IgnoreOutside && Character.CurrentHull == null) { continue; }
if (targetParams.IgnoreIncapacitated && targetCharacter != null && targetCharacter.IsIncapacitated) { continue; }
if (targetParams.IgnoreTargetInside && aiTarget.Entity.Submarine != null) { continue; }
if (targetParams.IgnoreTargetOutside && aiTarget.Entity.Submarine == null) { continue; }
if (aiTarget.Entity is ISerializableEntity se)
{
if (targetParams.Conditionals.Any(c => !c.TargetSelf && !c.Matches(se))) { continue; }
}
if (targetParams.Conditionals.Any(c => c.TargetSelf && !c.Matches(Character))) { continue; }
if (targetParams.IgnoreIfNotInSameSub)
{
if (aiTarget.Entity.Submarine != Character.Submarine) { continue; }
@@ -2981,6 +3037,16 @@ namespace Barotrauma
{
dist *= 0.9f;
}
if (targetParams.PerceptionDistanceMultiplier > 0.0f)
{
dist /= targetParams.PerceptionDistanceMultiplier;
}
if (targetParams.MaxPerceptionDistance > 0.0f &&
dist * dist > targetParams.MaxPerceptionDistance * targetParams.MaxPerceptionDistance)
{
continue;
}
if (!CanPerceive(aiTarget, dist, checkVisibility: SelectedAiTarget != aiTarget))
{
@@ -3196,7 +3262,7 @@ namespace Barotrauma
{
if ((SelectedAiTarget != null || wallTarget != null) && IsLatchedOnSub)
{
if (!(SelectedAiTarget?.Entity is Structure wall))
if (SelectedAiTarget?.Entity is not Structure wall)
{
wall = wallTarget?.Structure;
}
@@ -3251,9 +3317,10 @@ namespace Barotrauma
if (HasValidPath(requireNonDirty: true)) { return; }
wallHits.Clear();
Structure wall = null;
Vector2 rayStart = AttackLimb != null ? AttackLimb.SimPosition : SimPosition;
Vector2 refPos = AttackLimb != null ? AttackLimb.SimPosition : SimPosition;
if (AIParams.WallTargetingMethod.HasFlag(WallTargetingMethod.Target))
{
Vector2 rayStart = refPos;
Vector2 rayEnd = SelectedAiTarget.SimPosition;
if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null)
{
@@ -3267,6 +3334,7 @@ namespace Barotrauma
}
if (AIParams.WallTargetingMethod.HasFlag(WallTargetingMethod.Heading))
{
Vector2 rayStart = refPos;
Vector2 rayEnd = rayStart + VectorExtensions.Forward(Character.AnimController.Collider.Rotation + MathHelper.PiOver2, avoidLookAheadDistance * 5);
if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null)
{
@@ -3282,6 +3350,7 @@ namespace Barotrauma
}
if (AIParams.WallTargetingMethod.HasFlag(WallTargetingMethod.Steering))
{
Vector2 rayStart = refPos;
Vector2 rayEnd = rayStart + Steering * 5;
if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null)
{
@@ -3297,18 +3366,25 @@ namespace Barotrauma
}
if (wallHits.Any())
{
Vector2 targetdiff = ConvertUnits.ToSimUnits(SelectedAiTarget.WorldPosition - (AttackLimb != null ? AttackLimb.WorldPosition : WorldPosition));
float targetDistance = targetdiff.LengthSquared();
Body closestBody = null;
float closestDistance = 0;
int sectionIndex = -1;
Vector2 sectionPos = Vector2.Zero;
foreach ((Body body, int index, Vector2 sectionPosition) in wallHits)
{
float distance = Vector2.DistanceSquared(SimPosition, sectionPosition);
Structure structure = body.UserData as Structure;
float distance = Vector2.DistanceSquared(
refPos,
Submarine.GetRelativeSimPosition(ConvertUnits.ToSimUnits(sectionPosition), Character.Submarine, structure.Submarine));
//if the wall is further than the target (e.g. at the other side of the sub?), we shouldn't be targeting it
if (distance > targetDistance) { continue; }
if (closestBody == null || closestDistance == 0 || distance < closestDistance)
{
closestBody = body;
closestDistance = distance;
wall = closestBody.UserData as Structure;
wall = structure;
sectionPos = sectionPosition;
sectionIndex = index;
}
@@ -3326,14 +3402,18 @@ namespace Barotrauma
sectionPos.X += (wall.BodyWidth <= 0.0f ? wall.Rect.Width : wall.BodyWidth) / 2 * attachTargetNormal.X;
}
LatchOntoAI?.SetAttachTarget(wall, ConvertUnits.ToSimUnits(sectionPos), attachTargetNormal);
if (Character.AnimController.CanEnterSubmarine || !wall.SectionBodyDisabled(sectionIndex) && !IsWallDisabled(wall))
if (Character.AnimController.CanEnterSubmarine == CanEnterSubmarine.True ||
!wall.SectionBodyDisabled(sectionIndex) && !IsWallDisabled(wall))
{
if (wall.NoAITarget && Character.AnimController.CanEnterSubmarine)
if (wall.NoAITarget && Character.AnimController.CanEnterSubmarine == CanEnterSubmarine.True)
{
bool isTargetingDoor = SelectedAiTarget.Entity is Item i && i.GetComponent<Door>() != null;
// Blocked by a wall that shouldn't be targeted. The main intention here is to prevent monsters from entering the the tail and the nose pieces.
if (!isTargetingDoor)
{
//TODO: this might cause problems: many wall pieces (like smaller shuttle pieces
//and small decorative wall structures are currently marked as having no AI target,
//which can mean a monster very frequently ignores targets inside because they're blocked by those structures
IgnoreTarget(SelectedAiTarget);
ResetAITarget();
}
@@ -3353,7 +3433,9 @@ namespace Barotrauma
void DoRayCast(Vector2 rayStart, Vector2 rayEnd)
{
Body hitTarget = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true, ignoreSensors: CanEnterSubmarine, ignoreDisabledWalls: CanEnterSubmarine);
Body hitTarget = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true,
ignoreSensors: CanEnterSubmarine != CanEnterSubmarine.False,
ignoreDisabledWalls: CanEnterSubmarine != CanEnterSubmarine.False);
if (hitTarget != null && IsValid(hitTarget, out wall))
{
int sectionIndex = wall.FindSectionIndex(ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition));
@@ -3371,7 +3453,8 @@ namespace Barotrauma
{
if (wall.SectionBodyDisabled(i))
{
if (Character.AnimController.CanEnterSubmarine && CanPassThroughHole(wall, i, requiredHoleCount))
if (Character.AnimController.CanEnterSubmarine != CanEnterSubmarine.False &&
CanPassThroughHole(wall, i, requiredHoleCount))
{
sectionIndex = i;
break;
@@ -3394,14 +3477,14 @@ namespace Barotrauma
{
wall = null;
if (Submarine.LastPickedFraction == 1.0f) { return false; }
if (!(hit.UserData is Structure w)) { return false; }
if (hit.UserData is not Structure w) { return false; }
if (w.Submarine == null) { return false; }
if (w.Submarine != SelectedAiTarget.Entity.Submarine) { return false; }
if (Character.Submarine == null)
{
if (w.Prefab.Tags.Contains("inner"))
{
if (!Character.AnimController.CanEnterSubmarine) { return false; }
if (Character.AnimController.CanEnterSubmarine == CanEnterSubmarine.False) { return false; }
}
else if (!AIParams.TargetOuterWalls)
{
@@ -3731,7 +3814,7 @@ namespace Barotrauma
}
reachTimer = 0;
sinTime = 0;
if (breakCircling && strikeTimer <= 0)
if (breakCircling && strikeTimer <= 0 && CirclePhase != CirclePhase.CloseIn)
{
CirclePhase = CirclePhase.Start;
}
@@ -3757,7 +3840,7 @@ namespace Barotrauma
blockCheckTimer = 0;
reachTimer = 0;
sinTime = 0;
if (breakCircling && strikeTimer <= 0)
if (breakCircling && strikeTimer <= 0 && CirclePhase != CirclePhase.CloseIn)
{
CirclePhase = CirclePhase.Start;
}
@@ -3765,7 +3848,12 @@ namespace Barotrauma
private void SetStateResetTimer() => stateResetTimer = stateResetCooldown * Rand.Range(0.75f, 1.25f);
private float GetPerceivingRange(AITarget target) => Math.Max(target.SightRange * Sight, target.SoundRange * Hearing);
private float GetPerceivingRange(AITarget target)
{
float maxSightOrSoundRange = Math.Max(target.SightRange * Sight, target.SoundRange * Hearing);
if (AIParams.MaxPerceptionDistance >= 0 && maxSightOrSoundRange > AIParams.MaxPerceptionDistance) { return AIParams.MaxPerceptionDistance; }
return maxSightOrSoundRange;
}
private bool CanPerceive(AITarget target, float dist = -1, float distSquared = -1, bool checkVisibility = false)
{
@@ -3783,6 +3871,7 @@ namespace Barotrauma
}
if (dist > 0)
{
if (AIParams.MaxPerceptionDistance >= 0 && dist > AIParams.MaxPerceptionDistance) { return false; }
insideSightRange = IsInRange(dist, target.SightRange, Sight);
if (!checkVisibility && insideSightRange) { return true; }
insideSoundRange = IsInRange(dist, target.SoundRange, Hearing);
@@ -3793,6 +3882,7 @@ namespace Barotrauma
{
distSquared = Vector2.DistanceSquared(Character.WorldPosition, target.WorldPosition);
}
if (AIParams.MaxPerceptionDistance >= 0 && distSquared > AIParams.MaxPerceptionDistance * AIParams.MaxPerceptionDistance) { return false; }
insideSightRange = IsInRangeSqr(distSquared, target.SightRange, Sight);
if (!checkVisibility && insideSightRange) { return true; }
insideSoundRange = IsInRangeSqr(distSquared, target.SoundRange, Hearing);
@@ -72,6 +72,11 @@ namespace Barotrauma
/// How far other characters can hear reports done by this character (e.g. reports for fires, intruders). Defaults to infinity.
/// </summary>
public float ReportRange { get; set; } = float.PositiveInfinity;
/// <summary>
/// How far the character can seek new weapons from.
/// </summary>
public float FindWeaponsRange { get; set; } = float.PositiveInfinity;
private float _aimSpeed = 1;
public float AimSpeed
@@ -150,9 +155,13 @@ namespace Barotrauma
}
public override bool IsMentallyUnstable =>
MentalStateManager == null ? false :
MentalStateManager.CurrentMentalType != MentalStateManager.MentalType.Normal &&
MentalStateManager.CurrentMentalType != MentalStateManager.MentalType.Confused;
MentalStateManager is
{
CurrentMentalType:
MentalStateManager.MentalType.Afraid or
MentalStateManager.MentalType.Desperate or
MentalStateManager.MentalType.Berserk
};
public ShipCommandManager ShipCommandManager { get; private set; }
@@ -817,7 +826,7 @@ namespace Barotrauma
private readonly HashSet<Item> itemsToRelocate = new HashSet<Item>();
private void HandleRelocation(Item item)
public void HandleRelocation(Item item)
{
if (item.SpawnedInCurrentOutpost) { return; }
if (item.Submarine == null) { return; }
@@ -837,7 +846,10 @@ namespace Barotrauma
// In the campaign mode, undocking happens after leaving the outpost, so we can't use that.
campaign.BeforeLevelLoading += Relocate;
}
campaign.ItemsRelocatedToMainSub = true;
#if CLIENT
HintManager.OnItemMarkedForRelocation();
#endif
void Relocate()
{
if (item == null || item.Removed) { return; }
@@ -1566,7 +1578,7 @@ namespace Barotrauma
{
HoldPosition = Character.Info?.Job?.Prefab.Identifier == "watchman",
AbortCondition = abortCondition,
allowHoldFire = allowHoldFire,
AllowHoldFire = allowHoldFire,
};
if (onAbort != null)
{
@@ -1583,12 +1595,16 @@ namespace Barotrauma
public void SetOrder(Order order, bool speak = true)
{
objectiveManager.SetOrder(order, speak);
#if CLIENT
HintManager.OnSetOrder(Character, order);
#endif
}
public void SetForcedOrder(Order order)
public AIObjective SetForcedOrder(Order order)
{
var objective = ObjectiveManager.CreateObjective(order);
ObjectiveManager.SetForcedOrder(objective);
return objective;
}
public void ClearForcedOrder()
@@ -1677,14 +1693,17 @@ namespace Barotrauma
return false;
}
public static bool HasDivingGear(Character character, float conditionPercentage = 0, bool requireOxygenTank = true) => HasDivingSuit(character, conditionPercentage, requireOxygenTank) || HasDivingMask(character, conditionPercentage, requireOxygenTank);
public static bool HasDivingGear(Character character, float conditionPercentage = 0, bool requireOxygenTank = true) =>
HasDivingSuit(character, conditionPercentage, requireOxygenTank) || HasDivingMask(character, conditionPercentage, requireOxygenTank);
/// <summary>
/// Check whether the character has a diving suit in usable condition plus some oxygen.
/// Check whether the character has a diving suit in usable condition, suitable pressure protection for the depth, plus some oxygen.
/// </summary>
public static bool HasDivingSuit(Character character, float conditionPercentage = 0, bool requireOxygenTank = true)
public static bool HasDivingSuit(Character character, float conditionPercentage = 0, bool requireOxygenTank = true, bool requireSuitablePressureProtection = true)
=> HasItem(character, Tags.HeavyDivingGear, out _, requireOxygenTank ? Tags.OxygenSource : Identifier.Empty, conditionPercentage, requireEquipped: true,
predicate: (Item item) => character.HasEquippedItem(item, InvSlotType.OuterClothes | InvSlotType.InnerClothes));
predicate: (Item item) =>
character.HasEquippedItem(item, InvSlotType.OuterClothes | InvSlotType.InnerClothes) &&
(!requireSuitablePressureProtection || AIObjectiveFindDivingGear.IsSuitablePressureProtection(item, Tags.HeavyDivingGear, character)));
/// <summary>
/// Check whether the character has a diving mask in usable condition plus some oxygen.
@@ -1838,7 +1857,7 @@ namespace Barotrauma
foreach (Character otherCharacter in Character.CharacterList)
{
if (otherCharacter == thief || otherCharacter.TeamID == thief.TeamID || otherCharacter.IsIncapacitated || otherCharacter.Stun > 0.0f ||
otherCharacter.Info?.Job == null || otherCharacter.AIController is not HumanAIController otherHumanAI ||
otherCharacter.Info?.Job == null || otherCharacter.AIController is not HumanAIController otherHumanAI || otherCharacter.IsEscorted ||
Vector2.DistanceSquared(otherCharacter.WorldPosition, thief.WorldPosition) > 1000.0f * 1000.0f)
{
continue;
@@ -2064,7 +2083,7 @@ namespace Barotrauma
visibleHulls = VisibleHulls;
}
bool ignoreFire = objectiveManager.CurrentOrder is AIObjectiveExtinguishFires extinguishOrder && extinguishOrder.Priority > 0 || objectiveManager.HasActiveObjective<AIObjectiveExtinguishFire>();
bool ignoreOxygen = HasDivingGear(character);
bool ignoreOxygen = HasDivingGear(character);
bool ignoreEnemies = ObjectiveManager.IsCurrentOrder<AIObjectiveFightIntruders>() || ObjectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>();
float safety = CalculateHullSafety(hull, visibleHulls, character, ignoreWater: false, ignoreOxygen, ignoreFire, ignoreEnemies);
if (isCurrentHull)
@@ -2197,11 +2216,29 @@ namespace Barotrauma
public static bool IsFriendly(Character me, Character other, bool onlySameTeam = false)
{
if (other.IsHusk)
{
// Disguised as husk
return me.IsDisguisedAsHusk;
}
else
{
if (other.IsPrisoner && me.IsPrisoner)
{
// Both prisoners
return true;
}
if (other.IsHostileEscortee && me.IsHostileEscortee)
{
// Both hostile escortees
return true;
}
}
bool sameTeam = me.TeamID == other.TeamID;
bool teamGood = sameTeam || !onlySameTeam && me.IsOnFriendlyTeam(other);
if (!teamGood)
{
return other.IsHusk && me.IsDisguisedAsHusk;
return false;
}
if (other.IsPet)
{
@@ -6,6 +6,7 @@ using System;
using System.Collections.Generic;
using System.Xml.Linq;
using System.Linq;
using Voronoi2;
namespace Barotrauma
{
@@ -32,14 +33,21 @@ namespace Barotrauma
private Vector2 _attachPos;
/// <summary>
/// The character won't latch onto anything when the cooldown is active (activates after the character deattaches for whatever reason).
/// </summary>
private float attachCooldown;
private Limb attachLimb;
private readonly Limb attachLimb;
private Vector2 localAttachPos;
private float attachLimbRotation;
private readonly float attachLimbRotation;
private float jointDir;
private float latchedDuration;
private readonly bool freezeWhenLatched;
public List<Joint> AttachJoints { get; } = new List<Joint>();
public Vector2? AttachPos
@@ -54,18 +62,19 @@ namespace Barotrauma
public LatchOntoAI(XElement element, EnemyAIController enemyAI)
{
AttachToWalls = element.GetAttributeBool("attachtowalls", false);
AttachToSub = element.GetAttributeBool("attachtosub", false);
AttachToCharacters = element.GetAttributeBool("attachtocharacters", false);
minDeattachSpeed = element.GetAttributeFloat("mindeattachspeed", 5.0f);
maxDeattachSpeed = Math.Max(minDeattachSpeed, element.GetAttributeFloat("maxdeattachspeed", 8.0f));
maxAttachDuration = element.GetAttributeFloat("maxattachduration", -1.0f);
coolDown = element.GetAttributeFloat("cooldown", 2f);
damageOnDetach = element.GetAttributeFloat("damageondetach", 0.0f);
detachStun = element.GetAttributeFloat("detachstun", 0.0f);
localAttachPos = ConvertUnits.ToSimUnits(element.GetAttributeVector2("localattachpos", Vector2.Zero));
attachLimbRotation = MathHelper.ToRadians(element.GetAttributeFloat("attachlimbrotation", 0.0f));
weld = element.GetAttributeBool("weld", true);
AttachToWalls = element.GetAttributeBool(nameof(AttachToWalls), false);
AttachToSub = element.GetAttributeBool(nameof(AttachToSub), false);
AttachToCharacters = element.GetAttributeBool(nameof(AttachToCharacters), false);
minDeattachSpeed = element.GetAttributeFloat(nameof(minDeattachSpeed), 5.0f);
maxDeattachSpeed = Math.Max(minDeattachSpeed, element.GetAttributeFloat(nameof(maxDeattachSpeed), 8.0f));
maxAttachDuration = element.GetAttributeFloat(nameof(maxAttachDuration), -1.0f);
coolDown = element.GetAttributeFloat(nameof(coolDown), 2f);
damageOnDetach = element.GetAttributeFloat(nameof(damageOnDetach), 0.0f);
detachStun = element.GetAttributeFloat(nameof(detachStun), 0.0f);
localAttachPos = ConvertUnits.ToSimUnits(element.GetAttributeVector2(nameof(localAttachPos), Vector2.Zero));
attachLimbRotation = MathHelper.ToRadians(element.GetAttributeFloat(nameof(attachLimbRotation), 0.0f));
weld = element.GetAttributeBool(nameof(weld), true);
freezeWhenLatched = element.GetAttributeBool(nameof(freezeWhenLatched), false);
string limbString = element.GetAttributeString("attachlimb", null);
attachLimb = enemyAI.Character.AnimController.Limbs.FirstOrDefault(l => string.Equals(l.Name, limbString, StringComparison.OrdinalIgnoreCase));
@@ -108,7 +117,23 @@ namespace Barotrauma
targetBody = target.AnimController.Collider.FarseerBody;
attachSurfaceNormal = Vector2.Normalize(character.WorldPosition - target.WorldPosition);
}
public void SetAttachTarget(VoronoiCell levelWall)
{
if (!AttachToWalls) { return; }
Reset();
foreach (Voronoi2.GraphEdge edge in levelWall.Edges)
{
if (MathUtils.GetLineSegmentIntersection(edge.Point1, edge.Point2, character.WorldPosition, levelWall.Center, out Vector2 intersection))
{
attachSurfaceNormal = edge.GetNormal(levelWall);
targetBody = levelWall.Body;
_attachPos = ConvertUnits.ToSimUnits(intersection);
return;
}
}
}
public void Update(EnemyAIController enemyAI, float deltaTime)
{
if (TargetCharacter != null && character.Submarine != TargetCharacter.Submarine ||
@@ -119,6 +144,17 @@ namespace Barotrauma
}
if (IsAttached)
{
latchedDuration += deltaTime;
if (freezeWhenLatched && targetBody is { BodyType: BodyType.Static } &&
/*brief delay to let the ragdoll "settle"*/
latchedDuration > 5.0f)
{
foreach (var limb in character.AnimController.Limbs)
{
limb.body.LinearVelocity = Vector2.Zero;
limb.body.AngularVelocity = 0.0f;
}
}
if (Math.Sign(attachLimb.Dir) != Math.Sign(jointDir))
{
var attachJoint = AttachJoints[0];
@@ -241,7 +277,7 @@ namespace Barotrauma
{
DeattachFromBody(reset: false);
}
else
else if (attachCooldown <= 0.0f)
{
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;
@@ -259,6 +295,10 @@ namespace Barotrauma
enemyAI.SteeringManager.SteeringSeek(_attachPos);
}
}
else if (IsAttached)
{
enemyAI.SteeringManager.Reset();
}
break;
case AIState.Attack:
case AIState.Aggressive:
@@ -281,11 +321,11 @@ namespace Barotrauma
if (IsAttached && targetBody != null && deattachCheckTimer <= 0.0f)
{
attachCooldown = coolDown;
bool deattach = false;
if (maxAttachDuration > 0)
{
deattach = true;
attachCooldown = coolDown;
}
if (!deattach && TargetWall != null && TargetSubmarine != null)
{
@@ -294,7 +334,6 @@ namespace Barotrauma
if (enemyAI.CanPassThroughHole(TargetWall, targetSection))
{
deattach = true;
attachCooldown = coolDown;
}
if (!deattach)
{
@@ -327,7 +366,7 @@ namespace Barotrauma
}
}
private void AttachToBody(Vector2 attachPos)
public void AttachToBody(Vector2 attachPos, Vector2? forceAttachSurfaceNormal = null, Vector2? forceColliderSimPosition = null)
{
if (attachLimb == null) { return; }
if (targetBody == null) { return; }
@@ -343,6 +382,12 @@ namespace Barotrauma
jointDir = attachLimb.Dir;
if (forceAttachSurfaceNormal.HasValue) { attachSurfaceNormal = forceAttachSurfaceNormal.Value; }
if (forceColliderSimPosition.HasValue)
{
character.TeleportTo(ConvertUnits.ToDisplayUnits(forceColliderSimPosition.Value));
}
Vector2 transformedLocalAttachPos = localAttachPos * attachLimb.Scale * attachLimb.Params.Ragdoll.LimbScale;
if (jointDir < 0.0f)
{
@@ -350,6 +395,9 @@ namespace Barotrauma
}
float angle = MathUtils.VectorToAngle(-attachSurfaceNormal) - MathHelper.PiOver2 + attachLimbRotation * attachLimb.Dir;
//make sure the angle "has the same number of revolutions" as the reference limb
//(e.g. we don't want to rotate the legs to 0 if the torso is at 360, because that'd blow up the hip joints)
angle = attachLimb.body.WrapAngleToSameNumberOfRevolutions(angle);
attachLimb.body.SetTransform(attachPos + attachSurfaceNormal * transformedLocalAttachPos.Length(), angle);
var limbJoint = new WeldJoint(attachLimb.body.FarseerBody, targetBody,
@@ -392,10 +440,26 @@ namespace Barotrauma
{
deattachCheckTimer = maxAttachDuration;
}
#if SERVER
if (TargetCharacter != null)
{
GameMain.Server.CreateEntityEvent(character, new Character.LatchedOntoTargetEventData(character, TargetCharacter, attachSurfaceNormal, attachPos));
}
else if (TargetWall != null)
{
GameMain.Server.CreateEntityEvent(character, new Character.LatchedOntoTargetEventData(character, TargetWall, attachSurfaceNormal, attachPos));
}
else if (targetBody.UserData is Voronoi2.VoronoiCell cell)
{
GameMain.Server.CreateEntityEvent(character, new Character.LatchedOntoTargetEventData(character, cell, attachSurfaceNormal, attachPos));
}
#endif
}
public void DeattachFromBody(bool reset, float cooldown = 0)
{
bool wasAttached = IsAttached;
foreach (Joint joint in AttachJoints)
{
GameMain.World.Remove(joint);
@@ -410,6 +474,12 @@ namespace Barotrauma
{
Reset();
}
#if SERVER
if (wasAttached)
{
GameMain.Server.CreateEntityEvent(character, new Character.LatchedOntoTargetEventData());
}
#endif
}
private void Reset()
@@ -294,6 +294,41 @@ namespace Barotrauma
return Priority;
}
/// <summary>
/// Get a normalized value representing how close the target position is.
/// The value is a rough estimation, where vertical movement is assumed to be more costly than horizontal.
/// </summary>
/// <param name="targetWorldPos">Position of the target</param>
/// <param name="verticalDistanceMultiplier">How much more costly vertical movement is than horizontal</param>
/// <param name="maxDistance">Maximum distance, after which the factor will reach it's minimum value (= anything beyond this point is "as far as it can be").</param>
/// <param name="factorAtMaxDistance">The factor at the maximum distance and beyond (= how "viable" very far-away targets should be considered).</param>
/// <param name="factorAtMinDistance">The factor at the minimum distance (= how viable a target that's 0 units a way is considered).</param>
public static float GetDistanceFactor(Vector2 selfPos, Vector2 targetWorldPos, float factorAtMaxDistance, float verticalDistanceMultiplier = 3, float maxDistance = 10000.0f, float factorAtMinDistance = 1.0f)
{
float yDist = Math.Abs(selfPos.Y - targetWorldPos.Y);
yDist = yDist > 100 ? yDist * verticalDistanceMultiplier : 0;
float distance = Math.Abs(selfPos.X - targetWorldPos.X) + yDist;
float distanceFactor = MathHelper.Lerp(factorAtMinDistance, factorAtMaxDistance, MathUtils.InverseLerp(0, maxDistance, distance));
return
factorAtMinDistance > factorAtMaxDistance ?
MathHelper.Clamp(distanceFactor, factorAtMaxDistance, factorAtMinDistance) :
MathHelper.Clamp(distanceFactor, factorAtMinDistance, factorAtMaxDistance);
}
/// <summary>
/// Get a normalized value representing how close the target position is.
/// The value is a rough estimation, where vertical movement is assumed to be more costly than horizontal.
/// </summary>
/// <param name="targetWorldPos">Position of the target</param>
/// <param name="verticalDistanceMultiplier">How much more costly vertical movement is than horizontal</param>
/// <param name="maxDistance">Maximum distance, after which the factor will reach it's minimum value (= anything beyond this point is "as far as it can be").</param>
/// <param name="factorAtMaxDistance">The factor at the maximum distance and beyond (= how "viable" very far-away targets should be considered).</param>
/// <param name="factorAtMinDistance">The factor at the minimum distance (= how viable a target that's 0 units a way is considered).</param>
protected float GetDistanceFactor(Vector2 targetWorldPos, float factorAtMaxDistance, float verticalDistanceMultiplier = 3, float maxDistance = 10000.0f, float factorAtMinDistance = 1.0f)
{
return GetDistanceFactor(character.WorldPosition, targetWorldPos, factorAtMaxDistance, verticalDistanceMultiplier, maxDistance, factorAtMinDistance);
}
private void UpdateDevotion(float deltaTime)
{
var currentObjective = objectiveManager.CurrentObjective;
@@ -463,7 +498,7 @@ namespace Barotrauma
{
hasBeenChecked = true;
CheckSubObjectives();
if (subObjectives.None() || ConcurrentObjectives && subObjectives.All(so => so is AIObjectiveGoTo))
if (subObjectives.None() || ConcurrentObjectives)
{
if (Check())
{
@@ -509,7 +544,7 @@ namespace Barotrauma
public virtual void SpeakAfterOrderReceived() { }
protected static bool CanEquip(Character character, Item item, bool allowWearing)
protected static bool CanPutInInventory(Character character, Item item, bool allowWearing)
{
if (item == null) { return false; }
bool canEquip = false;
@@ -550,6 +585,6 @@ namespace Barotrauma
return canEquip && character.Inventory.CanBePut(item);
}
protected bool CanEquip(Item item, bool allowWearing) => CanEquip(character, item, allowWearing);
protected bool CanEquip(Item item, bool allowWearing) => CanPutInInventory(character, item, allowWearing);
}
}
@@ -21,6 +21,11 @@ namespace Barotrauma
private AIObjectiveDecontainItem decontainObjective;
private int itemIndex = 0;
/// <summary>
/// Allows decontainObjective to be interrupted if this objective gets abandoned (e.g. due to the item no longer being eligible for cleanup)
/// </summary>
public override bool ConcurrentObjectives => true;
public AIObjectiveCleanupItem(Item item, Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
@@ -39,10 +44,8 @@ namespace Barotrauma
float distanceFactor = 0.9f;
if (!IsPriority && item.CurrentHull != character.CurrentHull)
{
float yDist = Math.Abs(character.WorldPosition.Y - item.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 5 : 0;
float dist = Math.Abs(character.WorldPosition.X - item.WorldPosition.X) + yDist;
distanceFactor = MathHelper.Lerp(0.9f, 0, MathUtils.InverseLerp(0, 5000, dist));
distanceFactor = GetDistanceFactor(item.WorldPosition, verticalDistanceMultiplier: 5, maxDistance: 5000,
factorAtMinDistance: 0.9f, factorAtMaxDistance: 0);
}
bool isSelected = character.HasItem(item);
float selectedBonus = isSelected ? 100 - MaxDevotion : 0;
@@ -116,7 +119,7 @@ namespace Barotrauma
protected override bool CheckObjectiveSpecific()
{
if (item.IgnoreByAI(character))
if (item.IgnoreByAI(character) || Item.DeconstructItems.Contains(item))
{
Abandon = true;
}
@@ -56,8 +56,15 @@ namespace Barotrauma
// The validity changes when a character picks the item up.
if (!IsValidTarget(target, character, checkInventory: true)) { return Objectives.ContainsKey(target) && IsItemInsideValidSubmarine(target, character); }
if (target.CurrentHull.FireSources.Count > 0) { return false; }
// Don't clean up items in rooms that have enemies inside.
if (Character.CharacterList.Any(c => c.CurrentHull == target.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; }
foreach (Character c in Character.CharacterList)
{
if (c == character || !HumanAIController.IsActive(c)) { continue; }
if (c.CurrentHull == target.CurrentHull && !HumanAIController.IsFriendly(c))
{
// Don't clean up items in rooms that have enemies inside.
return false;
}
}
return true;
}
@@ -89,9 +96,10 @@ namespace Barotrauma
IsItemInsideValidSubmarine(container, character) &&
!container.IsClaimedByBallastFlora;
public static bool IsValidTarget(Item item, Character character, bool checkInventory, bool allowUnloading = true)
public static bool IsValidTarget(Item item, Character character, bool checkInventory, bool allowUnloading = true, bool requireValidContainer = true, bool ignoreItemsMarkedForDeconstruction = true)
{
if (item == null) { return false; }
if (item.DontCleanUp) { return false; }
if ((item.SpawnedInCurrentOutpost && !item.AllowStealing) == character.IsOnPlayerTeam) { return false; }
if (item.ParentInventory != null)
{
@@ -101,8 +109,9 @@ namespace Barotrauma
return false;
}
if (!allowUnloading) { return false; }
if (!IsValidContainer(item.Container, character)) { return false; }
if (requireValidContainer && !IsValidContainer(item.Container, character)) { return false; }
}
if (ignoreItemsMarkedForDeconstruction && Item.DeconstructItems.Contains(item)) { return false; }
if (!item.HasAccess(character)) { return false; }
if (character != null && !IsItemInsideValidSubmarine(item, character)) { return false; }
if (item.HasBallastFloraInHull) { return false; }
@@ -121,11 +130,16 @@ namespace Barotrauma
return false;
}
}
if (item.GetComponent<Rope>() is { IsActive: true, Snapped: false })
{
// Don't clean up spears with an active rope component.
return false;
}
if (!checkInventory)
{
return true;
}
return CanEquip(character, item, allowWearing: false);
return CanPutInInventory(character, item, allowWearing: false);
}
public override void OnDeselected()
@@ -22,13 +22,13 @@ namespace Barotrauma
private readonly CombatMode initialMode;
private float checkWeaponsTimer;
private readonly float checkWeaponsInterval = 1;
private const float checkWeaponsInterval = 1;
private float ignoreWeaponTimer;
private readonly float ignoredWeaponsClearTime = 10;
private const float ignoredWeaponsClearTime = 10;
private readonly float goodWeaponPriority = 30;
private const float goodWeaponPriority = 30;
private readonly float arrestHoldFireTime = 8;
private const float arrestHoldFireTime = 8;
private float holdFireTimer;
private bool hasAimed;
private bool isLethalWeapon;
@@ -79,14 +79,17 @@ namespace Barotrauma
private bool canSeeTarget;
private float visibilityCheckTimer;
private readonly float visibilityCheckInterval = 0.2f;
private const float visibilityCheckInterval = 0.2f;
private float sqrDistance;
private readonly float maxDistance = 2000;
private readonly float distanceCheckInterval = 0.2f;
private const float maxDistance = 2000;
private const float distanceCheckInterval = 0.2f;
private float distanceTimer;
private const float closeDistanceThreshold = 300;
private const float floorHeightApproximate = 100;
public bool allowHoldFire;
public bool AllowHoldFire;
/// <summary>
/// Don't start using a weapon if this condition is true
@@ -95,26 +98,63 @@ namespace Barotrauma
public enum CombatMode
{
Defensive, // Use weapons against the enemy, but try to retreat to a safe place
Offensive, // Engage the enemy and keep attacking it
Arrest, // Try to arrest the enemy without using lethal weapons (stunning + handcuffs)
Retreat, // Run to a safe place without attacking the target
None // Don't use
/// <summary>
/// Use weapons against the enemy, but try to retreat to a safe place.
/// </summary>
Defensive,
/// <summary>
/// Engage the enemy and keep attacking it.
/// </summary>
Offensive,
/// <summary>
/// Try to arrest the enemy without using lethal weapons (stunning + handcuffs).
/// </summary>
Arrest,
/// <summary>
/// Attempt to retreat to a safe place. Unlike in the Defensive mode, the character won't try to attack the enemy.
/// </summary>
Retreat,
/// <summary>
/// Does nothing.
/// </summary>
None
}
public CombatMode Mode { get; private set; }
private bool IsOffensiveOrArrest => initialMode == CombatMode.Offensive || initialMode == CombatMode.Arrest;
private bool IsOffensiveOrArrest => initialMode is CombatMode.Offensive or CombatMode.Arrest;
private bool TargetEliminated => IsEnemyDisabled || Enemy.IsUnconscious && Enemy.Params.Health.ConstantHealthRegeneration <= 0.0f || Enemy.IsArrested && !character.IsInstigator;
private bool IsEnemyDisabled => Enemy == null || Enemy.Removed || Enemy.IsDead;
private float AimSpeed => HumanAIController.AimSpeed;
private float AimAccuracy => HumanAIController.AimAccuracy;
private bool IsEnemyCloserThan(float margin) =>
Enemy != null && Enemy.CurrentHull != null &&
character.InWater && Vector2.DistanceSquared(character.WorldPosition, Enemy.WorldPosition) < margin * margin ||
HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull) && Math.Abs(character.WorldPosition.X - Enemy.WorldPosition.X) < margin;
/// <summary>
/// This is just an approximation that attempts to take different rooms and floors into account.
/// It can be equal to a simple distance check, but when the target is nearby, we only use the horizontal axis.
/// It's used for checking whether the enemy is close in certain situations, not for checking the distance to the enemy in general.
/// </summary>
private bool IsEnemyClose(float margin)
{
if (Enemy == null) { return false; }
Vector2 toEnemy = Enemy.WorldPosition - character.WorldPosition;
if (character.CurrentHull != null && Enemy.CurrentHull != null && character.CurrentHull != Enemy.CurrentHull)
{
// Inside, not in the same hull with the enemy
if (Math.Abs(toEnemy.Y) > floorHeightApproximate)
{
// Different floor
return false;
}
if (HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull))
{
// Potentially visible and on the same floor -> use only the horizontal distance.
return Math.Abs(toEnemy.X) < margin;
}
}
// Outside or inside in the same hull -> use the normal distance check.
return Vector2.DistanceSquared(character.WorldPosition, Enemy.WorldPosition) < margin * margin;
}
public AIObjectiveCombat(Character character, Character enemy, CombatMode mode, AIObjectiveManager objectiveManager, float priorityModifier = 1, float coolDown = 10.0f)
: base(character, objectiveManager, priorityModifier)
@@ -147,7 +187,7 @@ namespace Barotrauma
protected override float GetPriority()
{
if (Enemy == null)
if (Enemy == null || Enemy.Removed)
{
Priority = 0;
Abandon = true;
@@ -169,9 +209,9 @@ namespace Barotrauma
else
{
// 91-100
float minPriority = AIObjectiveManager.EmergencyObjectivePriority + 1;
float maxPriority = AIObjectiveManager.MaxObjectivePriority;
float priorityScale = maxPriority - minPriority;
const float minPriority = AIObjectiveManager.EmergencyObjectivePriority + 1;
const float maxPriority = AIObjectiveManager.MaxObjectivePriority;
const float priorityScale = maxPriority - minPriority;
float xDist = Math.Abs(character.WorldPosition.X - Enemy.WorldPosition.X);
float yDist = Math.Abs(character.WorldPosition.Y - Enemy.WorldPosition.Y);
if (HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull))
@@ -208,12 +248,12 @@ namespace Barotrauma
ignoredWeapons.Clear();
ignoreWeaponTimer = ignoredWeaponsClearTime;
}
bool isCurrentObjective = objectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>();
if (findSafety != null && isCurrentObjective)
bool isFightingIntruders = objectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>();
if (findSafety != null && isFightingIntruders)
{
findSafety.Priority = 0;
}
if (!AllowCoolDown && !character.IsOnPlayerTeam && !isCurrentObjective)
if (!AllowCoolDown && !character.IsOnPlayerTeam && !isFightingIntruders)
{
distanceTimer -= deltaTime;
if (distanceTimer < 0)
@@ -226,7 +266,7 @@ namespace Barotrauma
protected override bool CheckObjectiveSpecific()
{
if (character.Submarine == null || character.Submarine.TeamID != CharacterTeamType.FriendlyNPC)
if (character.Submarine is not { TeamID: CharacterTeamType.FriendlyNPC })
{
// Can't lose the target in friendly outposts.
if (sqrDistance > maxDistance * maxDistance)
@@ -343,12 +383,15 @@ namespace Barotrauma
RemoveSubObjective(ref seekAmmunitionObjective);
return false;
}
bool isAllowedToSeekWeapons = character.CurrentHull != null && !IsEnemyCloserThan(300) && character.IsOnPlayerTeam && IsOffensiveOrArrest;
bool isAllowedToSeekWeapons = character.IsHostileEscortee || character.IsPrisoner || // Prisoners and terrorists etc are always allowed to seek new weapons.
(character.IsInFriendlySub // Other characters need to be on a friendly sub in order to "know" where the weapons are. This also prevents NPCs "stealing" player items.
&& IsOffensiveOrArrest // = Defensive or retreating AI shouldn't seek new weapons.
&& !character.IsInstigator); // Instigators (= aggressive NPCs spawned with events) shouldn't seek new weapons, because we don't want them to grab e.g. an smg, if they spawn with a wrench or something.
if (checkWeaponsTimer < 0)
{
checkWeaponsTimer = checkWeaponsInterval;
// First go through all weapons and try to reload without seeking ammunition
var allWeapons = FindWeaponsFromInventory();
HashSet<ItemComponent> allWeapons = FindWeaponsFromInventory();
while (allWeapons.Any())
{
Weapon = GetWeapon(allWeapons, out _weaponComponent);
@@ -369,14 +412,20 @@ namespace Barotrauma
// All good, the weapon is loaded
break;
}
if (Reload(seekAmmo: isAllowedToSeekWeapons))
bool seekAmmo = isAllowedToSeekWeapons && seekAmmunitionObjective == null && !IsEnemyClose(closeDistanceThreshold);
if (Reload(seekAmmo: seekAmmo))
{
// All good, we can use the weapon.
break;
}
else if (seekAmmunitionObjective != null)
{
// Seeking ammo.
break;
}
else
{
// No ammo.
// No ammo and should not try to seek ammo.
allWeapons.Remove(WeaponComponent);
Weapon = null;
}
@@ -409,16 +458,16 @@ namespace Barotrauma
Mode = CombatMode.Retreat;
}
}
else if (seekAmmunitionObjective == null && (WeaponComponent == null || (WeaponComponent.CombatPriority < goodWeaponPriority)))
else if (seekAmmunitionObjective == null && (WeaponComponent == null || (WeaponComponent.CombatPriority < goodWeaponPriority && !IsEnemyClose(closeDistanceThreshold))))
{
// Poor weapon equipped -> try to find better.
RemoveSubObjective(ref seekAmmunitionObjective);
// No weapon or only a poor weapon equipped -> try to find better.
RemoveSubObjective(ref retreatObjective);
RemoveSubObjective(ref followTargetObjective);
TryAddSubObjective(ref seekWeaponObjective,
constructor: () => new AIObjectiveGetItem(character, "weapon".ToIdentifier(), objectiveManager, equip: true, checkInventory: false)
{
AllowStealing = HumanAIController.IsMentallyUnstable,
AbortCondition = obj => IsEnemyClose(200),
EvaluateCombatPriority = false, // Use a custom formula instead
GetItemPriority = i =>
{
@@ -427,7 +476,39 @@ namespace Barotrauma
float priority = 0;
if (GetWeaponComponent(i) is ItemComponent ic)
{
priority = GetWeaponPriority(ic, prioritizeMelee: false, isCloseToEnemy: false, out _) / 100;
priority = GetWeaponPriority(ic, prioritizeMelee: false, canSeekAmmo: true, out _) / 100;
}
if (priority <= 0) { return 0; }
// Check that we are not running directly towards the enemy.
Vector2 toItem = i.WorldPosition - character.WorldPosition;
float range = HumanAIController.FindWeaponsRange;
if (range is > 0 and < float.PositiveInfinity)
{
// Y distance is irrelevant when we are on the same floor. If we are on a different floor, let's double it.
float yDiff = Math.Abs(toItem.Y) > floorHeightApproximate ? toItem.Y * 2 : 0;
Vector2 adjustedDiff = new Vector2(toItem.X, yDiff);
if (adjustedDiff.LengthSquared() > MathUtils.Pow2(range))
{
// Too far -> not allowed to seek.
return 0;
}
}
Vector2 toEnemy = Enemy.WorldPosition - character.WorldPosition;
if (Math.Sign(toItem.X) == Math.Sign(toEnemy.X))
{
// Going towards the enemy -> reduce the priority.
priority *= 0.5f;
}
if (i.CurrentHull != null && !HumanAIController.VisibleHulls.Contains(i.CurrentHull))
{
if (Math.Abs(toItem.Y) > floorHeightApproximate && Math.Abs(toEnemy.Y) > floorHeightApproximate)
{
if (Math.Sign(toItem.Y) == Math.Sign(toEnemy.Y))
{
// Different floor, at the direction of the enemy -> reduce the priority.
priority *= 0.75f;
}
}
}
return priority;
}
@@ -441,19 +522,19 @@ namespace Barotrauma
SpeakNoWeapons();
Mode = CombatMode.Retreat;
}
else
else if (!objectiveManager.HasActiveObjective<AIObjectiveFightIntruders>())
{
// Poor weapon equipped
Mode = CombatMode.Defensive;
}
});
}
}
else
else if (seekAmmunitionObjective == null && seekWeaponObjective == null)
{
if (!CheckWeapon(seekAmmo: false))
{
Weapon = null;
RemoveSubObjective(ref seekAmmunitionObjective);
}
}
return Weapon != null;
@@ -504,10 +585,14 @@ namespace Barotrauma
item.GetComponent<RepairTool>() ??
item.GetComponent<Holdable>() as ItemComponent;
private float GetWeaponPriority(ItemComponent weapon, bool prioritizeMelee, bool isCloseToEnemy, out float lethalDmg)
/// <summary>
/// Normal range of combat priority is 0-100, but the value is not clamped.
/// </summary>
private float GetWeaponPriority(ItemComponent weapon, bool prioritizeMelee, bool canSeekAmmo, out float lethalDmg)
{
lethalDmg = -1;
float priority = weapon.CombatPriority;
if (priority <= 0) { return 0; }
if (weapon is RepairTool repairTool)
{
switch (repairTool.UsableIn)
@@ -531,9 +616,9 @@ namespace Barotrauma
}
if (weapon.IsEmpty(character))
{
if (weapon is RangedWeapon && isCloseToEnemy)
if (weapon is RangedWeapon && !canSeekAmmo)
{
// Ignore weapons that don't have any ammunition (-> Don't seek ammo).
// Ignore weapons that don't have any ammunition, when we are not allowed to seek more ammo.
return 0;
}
else
@@ -605,7 +690,45 @@ namespace Barotrauma
Attack attack = GetAttackDefinition(weapon);
priority = attack?.GetTotalDamage() ?? priority / 2;
}
// Reduce the priority of the weapon, if we don't have requires skills to use it.
float startPriority = priority;
var skillRequirementHints = weapon.Item.Prefab.SkillRequirementHints;
if (skillRequirementHints != null)
{
// If there are any skill requirement hints defined, let's use them.
// This should be the most accurate (manually defined) representation of the requirements (taking into account property conditionals etc).
foreach (SkillRequirementHint hint in skillRequirementHints)
{
float skillLevel = character.GetSkillLevel(hint.Skill);
float targetLevel = hint.Level;
priority = ReducePriority(priority, skillLevel, targetLevel);
}
}
else
{
// If no skill requirement hints are defined, let's rely on the required skill definition.
// This can be inaccurate in some cases (hmg, rifle), but in those cases there should be a skill requirement hint defined for the weapon.
foreach (Skill skill in weapon.RequiredSkills)
{
float skillLevel = character.GetSkillLevel(skill.Identifier);
// Skill multiplier is currently always 1, so it's not really needed, but that could change(?)
float targetLevel = skill.Level * weapon.GetSkillMultiplier();
priority = ReducePriority(priority, skillLevel, targetLevel);
}
}
// Don't allow to reduce more than half, because an assault rifle is still an assault rifle, even in untrained hands.
priority = Math.Max(priority, startPriority / 2);
return priority;
float ReducePriority(float prio, float skillLevel, float targetLevel)
{
float diff = targetLevel - skillLevel;
if (diff > 0)
{
prio -= diff;
}
return prio;
}
}
private float ApproximateStunDamage(ItemComponent weapon, Attack attack)
@@ -632,12 +755,12 @@ namespace Barotrauma
return attack.Stun + afflictionsStun + effectsStun;
}
private bool CanMeleeStunnerStun(ItemComponent weapon)
private static bool CanMeleeStunnerStun(ItemComponent weapon)
{
// If there's an item container that takes a battery,
// assume that it's required for the stun effect
// as we can't check the status effect conditions here.
var mobileBatteryTag = Tags.MobileBattery;
Identifier mobileBatteryTag = Tags.MobileBattery;
var containers = weapon.Item.Components.Where(ic =>
ic is ItemContainer container &&
container.ContainableItemIdentifiers.Contains(mobileBatteryTag));
@@ -651,11 +774,11 @@ namespace Barotrauma
weaponComponent = null;
float bestPriority = 0;
float lethalDmg = -1;
bool isCloseToEnemy = IsEnemyCloserThan(300);
bool prioritizeMelee = IsEnemyCloserThan(50) || EnemyAIController.IsLatchedTo(Enemy, character);
bool prioritizeMelee = IsEnemyClose(50) || EnemyAIController.IsLatchedTo(Enemy, character);
bool isCloseToEnemy = prioritizeMelee || IsEnemyClose(closeDistanceThreshold);
foreach (var weapon in weaponList)
{
float priority = GetWeaponPriority(weapon, prioritizeMelee, isCloseToEnemy, out lethalDmg);
float priority = GetWeaponPriority(weapon, prioritizeMelee, canSeekAmmo: !isCloseToEnemy, out lethalDmg);
if (priority > bestPriority)
{
weaponComponent = weapon;
@@ -678,7 +801,7 @@ namespace Barotrauma
}
isLethalWeapon = lethalDmg > 1;
}
if (allowHoldFire && !hasAimed && holdFireTimer <= 0)
if (AllowHoldFire && !hasAimed && holdFireTimer <= 0)
{
holdFireTimer = arrestHoldFireTime * Rand.Range(0.75f, 1.25f);
}
@@ -699,15 +822,12 @@ namespace Barotrauma
private static Attack GetAttackDefinition(ItemComponent weapon)
{
Attack attack = null;
if (weapon is MeleeWeapon meleeWeapon)
Attack attack = weapon switch
{
attack = meleeWeapon.Attack;
}
else if (weapon is RangedWeapon rangedWeapon)
{
attack = rangedWeapon.FindProjectile(triggerOnUseOnContainers: false)?.Attack;
}
MeleeWeapon meleeWeapon => meleeWeapon.Attack,
RangedWeapon rangedWeapon => rangedWeapon.FindProjectile(triggerOnUseOnContainers: false)?.Attack,
_ => null
};
return attack;
}
@@ -726,7 +846,7 @@ namespace Barotrauma
return weapons;
}
private void GetWeapons(Item item, ICollection<ItemComponent> weaponList)
private static void GetWeapons(Item item, ICollection<ItemComponent> weaponList)
{
if (item == null) { return; }
foreach (var component in item.Components)
@@ -765,14 +885,13 @@ namespace Barotrauma
}
if (!character.HasEquippedItem(Weapon, predicate: CharacterInventory.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);
ClearInputs();
Weapon.TryInteract(character, forceSelectKey: true);
var slots = Weapon.AllowedSlots.Where(s => CharacterInventory.IsHandSlotType(s));
var slots = Weapon.AllowedSlots.Where(CharacterInventory.IsHandSlotType);
if (character.Inventory.TryPutItem(Weapon, character, slots))
{
SetAimTimer(Rand.Range(0.2f, 0.4f) / AimSpeed);
SetReloadTime(WeaponComponent);
}
else
{
@@ -786,7 +905,7 @@ namespace Barotrauma
}
private float findHullTimer;
private readonly float findHullInterval = 1.0f;
private const float findHullInterval = 1.0f;
private void Retreat(float deltaTime)
{
@@ -796,6 +915,18 @@ namespace Barotrauma
}
RemoveFollowTarget();
RemoveSubObjective(ref seekAmmunitionObjective);
if (retreatTarget != null)
{
if (HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull))
{
// In the same hull with the enemy
if (retreatTarget == character.CurrentHull)
{
// Go elsewhere
retreatTarget = null;
}
}
}
if (retreatObjective != null && retreatObjective.Target != retreatTarget)
{
RemoveSubObjective(ref retreatObjective);
@@ -809,7 +940,7 @@ namespace Barotrauma
SteeringManager.SteeringAvoid(deltaTime, 5, weight: 2);
return;
}
if (retreatTarget == null || (retreatObjective != null && !retreatObjective.CanBeCompleted))
if (retreatTarget == null || retreatObjective is { CanBeCompleted: false })
{
if (findHullTimer > 0)
{
@@ -942,9 +1073,13 @@ namespace Barotrauma
if (!arrestingRegistered && followTargetObjective != null)
{
followTargetObjective.CloseEnough =
WeaponComponent is RangedWeapon ? 1000 :
WeaponComponent is MeleeWeapon mw ? mw.Range :
WeaponComponent is RepairTool rt ? rt.Range : 50;
WeaponComponent switch
{
RangedWeapon => 1000,
MeleeWeapon mw => mw.Range,
RepairTool rt => rt.Range,
_ => 50
};
}
}
@@ -976,9 +1111,8 @@ namespace Barotrauma
foreach (var item in Enemy.Inventory.AllItemsMod)
{
if (character.TeamID == CharacterTeamType.FriendlyNPC && item.StolenDuringRound ||
item.HasTag(Tags.Weapon) ||
item.GetComponent<MeleeWeapon>() != null ||
item.GetComponent<RangedWeapon>() != null)
item.HasTag(Tags.Weapon) || item.HasTag(Tags.Poison) ||
GetWeaponComponent(item) is { CombatPriority: > 0 })
{
item.Drop(character);
character.Inventory.TryPutItem(item, character, CharacterInventory.AnySlot);
@@ -1024,10 +1158,11 @@ namespace Barotrauma
RemoveSubObjective(ref retreatObjective);
RemoveSubObjective(ref seekWeaponObjective);
RemoveFollowTarget();
var itemContainer = Weapon.GetComponent<ItemContainer>();
TryAddSubObjective(ref seekAmmunitionObjective,
constructor: () => new AIObjectiveContainItem(character, ammunitionIdentifiers, Weapon.GetComponent<ItemContainer>(), objectiveManager)
constructor: () => new AIObjectiveContainItem(character, ammunitionIdentifiers, itemContainer, objectiveManager)
{
ItemCount = Weapon.GetComponent<ItemContainer>().Capacity * Weapon.GetComponent<ItemContainer>().MaxStackSize,
ItemCount = itemContainer.MainContainerCapacity * itemContainer.MaxStackSize,
checkInventory = false,
MoveWholeStack = true
},
@@ -1052,9 +1187,9 @@ namespace Barotrauma
// Eject empty ammo
HumanAIController.UnequipEmptyItems(Weapon);
ImmutableHashSet<Identifier> ammunitionIdentifiers = null;
if (WeaponComponent.requiredItems.ContainsKey(RelatedItem.RelationType.Contained))
if (WeaponComponent.RequiredItems.ContainsKey(RelatedItem.RelationType.Contained))
{
foreach (RelatedItem requiredItem in WeaponComponent.requiredItems[RelatedItem.RelationType.Contained])
foreach (RelatedItem requiredItem in WeaponComponent.RequiredItems[RelatedItem.RelationType.Contained])
{
if (Weapon.OwnInventory.AllItems.Any(it => it.Condition > 0 && requiredItem.MatchesItem(it))) { continue; }
ammunitionIdentifiers = requiredItem.Identifiers;
@@ -1075,12 +1210,14 @@ namespace Barotrauma
if (ammunition != null)
{
var container = Weapon.GetComponent<ItemContainer>();
if (!container.Inventory.TryPutItem(ammunition, user: character))
if (container.Inventory.TryPutItem(ammunition, user: character))
{
if (ammunition.ParentInventory == character.Inventory)
{
ammunition.Drop(character);
}
ClearInputs();
SetReloadTime(WeaponComponent);
}
else if (ammunition.ParentInventory == character.Inventory)
{
ammunition.Drop(character);
}
}
}
@@ -1127,7 +1264,7 @@ namespace Barotrauma
}
if (Weapon.RequireAimToUse)
{
character.SetInput(InputType.Aim, false, true);
character.SetInput(InputType.Aim, hit: false, held: true);
}
hasAimed = true;
if (holdFireTimer > 0)
@@ -1194,23 +1331,17 @@ namespace Barotrauma
float aimFactor = MathHelper.PiOver2 * (1 - AimAccuracy);
if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.WorldPosition - Weapon.WorldPosition) < MathHelper.PiOver4 + aimFactor)
{
if (myBodies == null)
{
myBodies = character.AnimController.Limbs.Select(l => l.body.FarseerBody);
}
myBodies ??= character.AnimController.Limbs.Select(l => l.body.FarseerBody);
// Check that we don't hit friendlies. No need to check the walls, because there's a separate check for that at 1096 (which intentionally has a small delay)
var pickedBodies = Submarine.PickBodies(Weapon.SimPosition, Submarine.GetRelativeSimPosition(from: Weapon, to: Enemy), myBodies, Physics.CollisionCharacter);
foreach (var body in pickedBodies)
{
Character target = null;
if (body.UserData is Character c)
Character target = body.UserData switch
{
target = c;
}
else if (body.UserData is Limb limb)
{
target = limb.character;
}
Character c => c,
Limb limb => limb.character,
_ => null
};
if (target != null && target != Enemy && HumanAIController.IsFriendly(target))
{
return;
@@ -1225,26 +1356,48 @@ namespace Barotrauma
{
// Never allow to attack characters with deadly weapons while trying to arrest.
if (Mode == CombatMode.Arrest && isLethalWeapon) { return; }
float reloadTime = 0;
if (WeaponComponent is RangedWeapon rangedWeapon)
{
// If the weapon is just equipped, we can't shoot just yet.
if (rangedWeapon.ReloadTimer <= 0 && !rangedWeapon.HoldTrigger)
{
reloadTime = rangedWeapon.Reload;
}
}
if (WeaponComponent is MeleeWeapon mw)
{
if (!((HumanoidAnimController)character.AnimController).Crouching)
{
reloadTime = mw.Reload;
}
}
character.SetInput(InputType.Shoot, false, true);
character.SetInput(InputType.Shoot, hit: false, held: true);
Weapon.Use(deltaTime, user: character);
SetReloadTime(WeaponComponent);
}
private float GetReloadTime(ItemComponent weaponComponent)
{
float reloadTime = 0;
switch (weaponComponent)
{
case RangedWeapon rangedWeapon:
{
if (rangedWeapon.ReloadTimer <= 0 && !rangedWeapon.HoldTrigger)
{
reloadTime = rangedWeapon.Reload;
}
break;
}
case MeleeWeapon mw:
{
if (character.AnimController is HumanoidAnimController { Crouching: false })
{
reloadTime = mw.Reload;
}
break;
}
}
return reloadTime;
}
private void SetReloadTime(ItemComponent weaponComponent)
{
float reloadTime = GetReloadTime(weaponComponent);
reloadTimer = Math.Max(reloadTime, reloadTime * Rand.Range(1f, 1.25f) / AimSpeed);
}
private void ClearInputs()
{
//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);
}
private bool ShouldUnequipWeapon =>
Weapon != null &&
@@ -0,0 +1,116 @@
using Barotrauma.Items.Components;
using System.Linq;
namespace Barotrauma
{
class AIObjectiveDeconstructItem : AIObjective
{
public override Identifier Identifier { get; set; } = "deconstruct item".ToIdentifier();
public override bool AllowWhileHandcuffed => false;
public override bool AllowInFriendlySubs => true;
public readonly Item Item;
private Deconstructor deconstructor;
private AIObjectiveDecontainItem decontainObjective;
public AIObjectiveDeconstructItem(Item item, Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
Item = item;
}
protected override void Act(float deltaTime)
{
if (subObjectives.Any()) { return; }
if (deconstructor == null)
{
deconstructor = FindDeconstructor();
if (deconstructor == null)
{
Abandon = true;
return;
}
}
TryAddSubObjective(ref decontainObjective,
constructor: () => new AIObjectiveDecontainItem(character, Item, objectiveManager,
sourceContainer: Item.Container?.GetComponent<ItemContainer>(), targetContainer: deconstructor.InputContainer, priorityModifier: PriorityModifier)
{
Equip = true,
RemoveExistingWhenNecessary = true
},
onCompleted: () =>
{
StartDeconstructor();
//make sure the item gets moved to the main sub if the crew leaves while a bot is deconstructing something in the outpost
if (deconstructor.Item.Submarine is { Info.IsOutpost: true })
{
HumanAIController.HandleRelocation(Item);
deconstructor.RelocateOutputToMainSub = true;
}
IsCompleted = true;
RemoveSubObjective(ref decontainObjective);
},
onAbandon: () =>
{
Abandon = true;
});
}
private Deconstructor FindDeconstructor()
{
Deconstructor closestDeconstructor = null;
float bestDistFactor = 0;
foreach (var otherItem in Item.ItemList)
{
var potentialDeconstructor = otherItem.GetComponent<Deconstructor>();
if (potentialDeconstructor?.InputContainer == null) { continue; }
if (!potentialDeconstructor.InputContainer.Inventory.CanBePut(Item)) { continue; }
if (!potentialDeconstructor.Item.HasAccess(character)) { continue; }
float distFactor = GetDistanceFactor(Item.WorldPosition, potentialDeconstructor.Item.WorldPosition, factorAtMaxDistance: 0.2f);
if (distFactor > bestDistFactor)
{
closestDeconstructor = potentialDeconstructor;
bestDistFactor = distFactor;
}
}
return closestDeconstructor;
}
private void StartDeconstructor()
{
deconstructor.SetActive(active: true, user: character, createNetworkEvent: true);
}
protected override bool CheckObjectiveSpecific()
{
if (Item.IgnoreByAI(character))
{
Abandon = true;
}
else if (deconstructor != null && deconstructor.Item.IgnoreByAI(character))
{
Abandon = true;
}
return !Abandon && IsCompleted;
}
public override void Reset()
{
base.Reset();
decontainObjective = null;
}
public void DropTarget()
{
if (Item != null && character.HasItem(Item))
{
Item.Drop(character);
}
}
}
}
@@ -0,0 +1,122 @@
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using System.Collections.Generic;
namespace Barotrauma
{
class AIObjectiveDeconstructItems : AIObjectiveLoop<Item>
{
public override Identifier Identifier { get; set; } = "deconstruct items".ToIdentifier();
//Clear periodically, because we may ending up ignoring items when all deconstructors are full
protected override float IgnoreListClearInterval => 30;
public override bool AllowInFriendlySubs => true;
protected override int MaxTargets => 10;
private bool checkedDeconstructorExists;
public AIObjectiveDeconstructItems(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
}
public override void OnSelected()
{
base.OnSelected();
if (!checkedDeconstructorExists)
{
if (character.Submarine == null ||
Item.ItemList.None(it =>
it.GetComponent<Deconstructor>() != null &&
it.IsInteractable(character) &&
character.Submarine.IsEntityFoundOnThisSub(it, includingConnectedSubs: true, allowDifferentTeam: true, allowDifferentType: true)))
{
character.Speak(TextManager.Get("orderdialogself.deconstructitem.nodeconstructor").Value, delay: 5.0f,
identifier: "nodeconstructor".ToIdentifier(), minDurationBetweenSimilar: 30.0f);
Abandon = true;
}
checkedDeconstructorExists = true;
}
}
public override void Reset()
{
base.Reset();
checkedDeconstructorExists = false;
}
protected override float TargetEvaluation()
{
if (Targets.None()) { return 0; }
if (objectiveManager.IsOrder(this))
{
return objectiveManager.GetOrderPriority(this);
}
return AIObjectiveManager.RunPriority - 0.5f;
}
protected override bool Filter(Item target)
{
// If the target was selected as a valid target, we'll have to accept it so that the objective can be completed.
// The validity changes when a character picks the item up.
if (!IsValidTarget(target, character, checkInventory: true))
{
return Objectives.ContainsKey(target) && AIObjectiveCleanupItems.IsItemInsideValidSubmarine(target, character);
}
if (target.CurrentHull.FireSources.Count > 0) { return false; }
foreach (Character c in Character.CharacterList)
{
if (c == character || !HumanAIController.IsActive(c)) { continue; }
if (c.CurrentHull == target.CurrentHull && !HumanAIController.IsFriendly(c))
{
// Don't deconstruct items in rooms that have enemies inside.
return false;
}
else if (c.TeamID == character.TeamID && c.AIController is HumanAIController humanAi)
{
if (humanAi.ObjectiveManager.CurrentObjective is AIObjectiveDeconstructItem deconstruct && deconstruct.Item == target)
{
return false;
}
}
}
return true;
}
protected override IEnumerable<Item> GetList() => Item.DeconstructItems;
protected override AIObjective ObjectiveConstructor(Item item)
=> new AIObjectiveDeconstructItem(item, character, objectiveManager, priorityModifier: PriorityModifier);
protected override void OnObjectiveCompleted(AIObjective objective, Item target)
=> HumanAIController.RemoveTargets<AIObjectiveDeconstructItems, Item>(character, target);
private static bool IsValidTarget(Item item, Character character, bool checkInventory)
{
if (item == null) { return false; }
if (item.GetRootInventoryOwner() == character) { return true; }
return AIObjectiveCleanupItems.IsValidTarget(
item,
character,
checkInventory,
allowUnloading: true,
requireValidContainer: false,
ignoreItemsMarkedForDeconstruction: false);
}
public override void OnDeselected()
{
base.OnDeselected();
foreach (var subObjective in SubObjectives)
{
if (subObjective is AIObjectiveDeconstructItem deconstructObjective)
{
deconstructObjective.DropTarget();
}
}
}
}
}
@@ -39,6 +39,9 @@ namespace Barotrauma
/// </summary>
public bool DropIfFails { get; set; } = true;
/// <summary>
/// Should existing item(s) be removed from the targetContainer if the targetItem won't fit otherwise?
/// </summary>
public bool RemoveExistingWhenNecessary { get; set; }
public Func<Item, bool> RemoveExistingPredicate { get; set; }
public int? RemoveExistingMax { get; set; }
@@ -45,13 +45,18 @@ namespace Barotrauma
else
{
float characterY = character.CurrentHull?.WorldPosition.Y ?? character.WorldPosition.Y;
float yDist = Math.Abs(characterY - targetHull.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 3 : 0;
float dist = Math.Abs(character.WorldPosition.X - targetHull.WorldPosition.X) + yDist;
float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, dist));
if (targetHull == character.CurrentHull || HumanAIController.VisibleHulls.Contains(targetHull))
float distanceFactor = 1.0f;
if (targetHull != character.CurrentHull &&
!HumanAIController.VisibleHulls.Contains(targetHull))
{
distanceFactor = 1;
distanceFactor =
GetDistanceFactor(
new Vector2(character.WorldPosition.Y, characterY),
targetHull.WorldPosition,
verticalDistanceMultiplier: 3,
maxDistance: 5000,
factorAtMaxDistance: 0.1f);
}
float severity = AIObjectiveExtinguishFires.GetFireSeverity(targetHull);
if (severity > 0.75f && !isOrder &&
@@ -12,7 +12,7 @@ namespace Barotrauma
protected override float TargetUpdateTimeMultiplier => 0.2f;
public bool TargetCharactersInOtherSubs { get; set; }
public bool TargetCharactersInOtherSubs { get; init; }
public AIObjectiveFightIntruders(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier) { }
@@ -33,20 +33,22 @@ namespace Barotrauma
protected override AIObjective ObjectiveConstructor(Character target)
{
AIObjectiveCombat.CombatMode combatMode = ShouldArrest(target, character) ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Offensive;
var combatObjective = new AIObjectiveCombat(character, target, combatMode, objectiveManager, PriorityModifier);
if (character.TeamID == CharacterTeamType.FriendlyNPC && target.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign)
AIObjectiveCombat.CombatMode combatMode = AIObjectiveCombat.CombatMode.Offensive;
if (character.IsOnPlayerTeam && target is { IsEscorted: true })
{
if (campaign.CurrentLocation is { IsFactionHostile: true })
// Try to arrest escorted characters, instead of killing them.
combatMode = AIObjectiveCombat.CombatMode.Arrest;
}
var combatObjective = new AIObjectiveCombat(character, target, combatMode, objectiveManager, PriorityModifier);
if (character.TeamID == CharacterTeamType.FriendlyNPC && target.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode { CurrentLocation.IsFactionHostile: true })
{
combatObjective.holdFireCondition = () =>
{
combatObjective.holdFireCondition = () =>
{
//hold fire while the enemy is in the airlock (except if they've attacked us)
if (character.GetDamageDoneByAttacker(target) > 0.0f) { return false; }
return target.CurrentHull == null || target.CurrentHull.OutpostModuleTags.Any(t => t == "airlock");
};
character.Speak(TextManager.Get("dialogenteroutpostwarning").Value, null, Rand.Range(0.5f, 1.0f), "leaveoutpostwarning".ToIdentifier(), 30.0f);
}
//hold fire while the enemy is in the airlock (except if they've attacked us)
if (character.GetDamageDoneByAttacker(target) > 0.0f) { return false; }
return target.CurrentHull == null || target.CurrentHull.OutpostModuleTags.Any(t => t == "airlock");
};
character.Speak(TextManager.Get("dialogenteroutpostwarning").Value, null, Rand.Range(0.5f, 1.0f), "leaveoutpostwarning".ToIdentifier(), 30.0f);
}
return combatObjective;
}
@@ -77,10 +79,5 @@ namespace Barotrauma
if (EnemyAIController.IsLatchedToSomeoneElse(target, character)) { return false; }
return true;
}
public static bool ShouldArrest(Character target, Character character)
{
return target != null && target.IsEscorted && character.TeamID == CharacterTeamType.Team1;
}
}
}
@@ -33,22 +33,27 @@ namespace Barotrauma
protected override void Act(float deltaTime)
{
TrySetTargetItem(character.Inventory.FindItemByTag(gearTag, true));
TrySetTargetItem(character.Inventory.FindItem(it => it.HasTag(gearTag) && IsSuitablePressureProtection(it, gearTag, character), true));
if (targetItem == null && gearTag == Tags.LightDivingGear)
{
TrySetTargetItem(character.Inventory.FindItemByTag(Tags.HeavyDivingGear, true));
TrySetTargetItem(character.Inventory.FindItem(
it => it.HasTag(Tags.HeavyDivingGear) && IsSuitablePressureProtection(it, Tags.HeavyDivingGear, character), recursive: true));
}
if (targetItem == null ||
!character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head) &&
targetItem.ContainedItems.Any(it => IsSuitableContainedOxygenSource(it)))
{
bool mustFindMorePressureProtection =
!objectiveManager.FailedToFindDivingGearForDepth &&
character.Inventory.FindItem(
it => it.HasTag(Tags.HeavyDivingGear) && !IsSuitablePressureProtection(it, Tags.HeavyDivingGear, character), recursive: true) != null;
TryAddSubObjective(ref getDivingGear, () =>
{
if (targetItem == null && character.IsOnPlayerTeam)
{
character.Speak(TextManager.Get("DialogGetDivingGear").Value, null, 0.0f, "getdivinggear".ToIdentifier(), 30.0f);
}
return new AIObjectiveGetItem(character, gearTag, objectiveManager, equip: true)
var getItemObjective = new AIObjectiveGetItem(character, gearTag, objectiveManager, equip: true)
{
AllowStealing = HumanAIController.NeedsDivingGear(character.CurrentHull, out _),
AllowToFindDivingGear = false,
@@ -56,8 +61,42 @@ namespace Barotrauma
EquipSlotType = InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head,
Wear = true
};
if (gearTag == Tags.HeavyDivingGear)
{
if (mustFindMorePressureProtection)
{
//if we're looking for a suit specifically because the current suit isn't enough,
//let's ignore unsuitable suits altogether...
getItemObjective.ItemFilter = it => IsSuitablePressureProtection(it, gearTag, character);
}
else
{
//...Otherwise it's fine to give a very small priority
//to inadequate suits (a suit not adequate for the depth is better than no suit)
getItemObjective.GetItemPriority = it => IsSuitablePressureProtection(it, gearTag, character) ? 1000.0f : 1.0f;
}
getItemObjective.GetItemPriority = it =>
{
if (IsSuitablePressureProtection(it, gearTag, character))
{
return 1000.0f;
}
else
{
//if we're looking for a suit specifically because the current suit isn't enough,
//let's ignore unsuitable suits altogether. Otherwise it's fine to give a very small priority
//to inadequate suits (a suit not adequate for the depth is better than no suit)
return mustFindMorePressureProtection ? 0.0f : 1.0f;
}
};
}
return getItemObjective;
},
onAbandon: () => Abandon = true,
onAbandon: () =>
{
if (mustFindMorePressureProtection) { objectiveManager.FailedToFindDivingGearForDepth = true; }
Abandon = true;
},
onCompleted: () =>
{
RemoveSubObjective(ref getDivingGear);
@@ -160,6 +199,20 @@ namespace Barotrauma
}
}
public static bool IsSuitablePressureProtection(Item item, Identifier tag, Character character)
{
if (tag == Tags.HeavyDivingGear)
{
float realWorldDepth = Level.Loaded?.GetRealWorldDepth(character.WorldPosition.Y) ?? 0.0f;
if (item.GetComponent<Wearable>() is not { } wearable || wearable.PressureProtection < realWorldDepth + Steering.PressureWarningThreshold)
{
return false;
}
}
return true;
}
private bool IsSuitableContainedOxygenSource(Item item)
{
return
@@ -52,12 +52,26 @@ namespace Barotrauma
{
bool isSuffocatingInDivingSuit = character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false);
static bool IsSuffocatingWithoutDivingGear(Character c) => c.IsLowInOxygen && c.AnimController.HeadInWater && !HumanAIController.HasDivingGear(c, requireOxygenTank: true);
if (isSuffocatingInDivingSuit ||
NeedMoreDivingGear(character.CurrentHull, AIObjectiveFindDivingGear.GetMinOxygen(character)) ||
(!objectiveManager.HasActiveObjective<AIObjectiveFindDivingGear>() && IsSuffocatingWithoutDivingGear(character)))
if (isSuffocatingInDivingSuit || (!objectiveManager.HasActiveObjective<AIObjectiveFindDivingGear>() && IsSuffocatingWithoutDivingGear(character)))
{
Priority = AIObjectiveManager.MaxObjectivePriority;
}
else if (NeedMoreDivingGear(character.CurrentHull, AIObjectiveFindDivingGear.GetMinOxygen(character)))
{
if (objectiveManager.FailedToFindDivingGearForDepth &&
HumanAIController.HasDivingSuit(character, requireSuitablePressureProtection: false))
{
//we have a suit that's not suitable for the pressure,
//but we've failed to find a better one
// shit, not much we can do here, let's just allow the bot to get on with their current objective
Priority = 0;
}
else
{
Priority = AIObjectiveManager.MaxObjectivePriority;
}
}
else if ((objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() || objectiveManager.IsCurrentOrder<AIObjectiveReturn>()) &&
character.Submarine != null && !character.IsOnFriendlyTeam(character.Submarine.TeamID))
{
@@ -259,7 +273,7 @@ namespace Barotrauma
bool inFriendlySub =
character.IsInFriendlySub ||
(character.IsEscorted && character.IsInPlayerSub);
if (cannotFindSafeHull && !inFriendlySub && objectiveManager.Objectives.None(o => o is AIObjectiveReturn))
if (cannotFindSafeHull && !inFriendlySub && character.IsOnPlayerTeam && objectiveManager.Objectives.None(o => o is AIObjectiveReturn))
{
if (OrderPrefab.Prefabs.TryGet("return".ToIdentifier(), out OrderPrefab orderPrefab))
{
@@ -401,10 +415,7 @@ namespace Barotrauma
if (isCharacterInside)
{
hullSafety = HumanAIController.GetHullSafety(potentialHull, potentialHull.GetConnectedHulls(true, 1), character);
float yDist = Math.Abs(character.WorldPosition.Y - potentialHull.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 3 : 0;
float dist = Math.Abs(character.WorldPosition.X - potentialHull.WorldPosition.X) + yDist;
float distanceFactor = MathHelper.Lerp(1, 0.9f, MathUtils.InverseLerp(0, 10000, dist));
float distanceFactor = GetDistanceFactor(potentialHull.WorldPosition, factorAtMaxDistance: 0.9f);
hullSafety *= distanceFactor;
//skip the hull if the safety is already less than the best hull
//(no need to do the expensive pathfinding if we already know we're not going to choose this hull)
@@ -446,16 +457,13 @@ namespace Barotrauma
hullSafety = 100;
hullIsAirlock = true;
}
else if(!bestHullIsAirlock && potentialHull.LeadsOutside(character))
else if (!bestHullIsAirlock && potentialHull.LeadsOutside(character))
{
hullSafety = 100;
}
float characterY = character.CurrentHull?.WorldPosition.Y ?? character.WorldPosition.Y;
float yDist = Math.Abs(characterY - potentialHull.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 3 : 0;
float distance = Math.Abs(character.WorldPosition.X - potentialHull.WorldPosition.X) + yDist;
// Huge preference for closer targets
float distanceFactor = MathHelper.Lerp(1, 0.2f, MathUtils.InverseLerp(0, 10000, distance));
float distanceFactor = GetDistanceFactor(new Vector2(character.WorldPosition.X, characterY), potentialHull.WorldPosition, factorAtMaxDistance: 0.2f);
hullSafety *= distanceFactor;
// If the target is not inside a friendly submarine, considerably reduce the hull safety.
// Intentionally exclude wrecks from this check
@@ -39,8 +39,8 @@ namespace Barotrauma
if (campaign.Map?.CurrentLocation?.Reputation is { } reputation)
{
return MathHelper.Lerp(
campaign.Settings.MaxStolenItemInspectionProbability,
campaign.Settings.MinStolenItemInspectionProbability,
campaign.Settings.PatdownProbabilityMax,
campaign.Settings.PatdownProbabilityMin,
reputation.NormalizedValue);
}
}
@@ -120,7 +120,7 @@ namespace Barotrauma
Abandon = true;
return;
}
if (weldingTool.OwnInventory == null && repairTool.requiredItems.Any(r => r.Key == RelatedItem.RelationType.Contained))
if (weldingTool.OwnInventory == null && repairTool.RequiredItems.Any(r => r.Key == RelatedItem.RelationType.Contained))
{
#if DEBUG
DebugConsole.ThrowError($"{character.Name}: AIObjectiveFixLeak failed - the item \"{weldingTool}\" has no proper inventory");
@@ -159,13 +159,14 @@ namespace Barotrauma
protected override void Act(float deltaTime)
{
if (IdentifiersOrTags != null && !isDoneSeeking)
if (IdentifiersOrTags != null)
{
if (checkInventory)
{
if (CheckInventory())
{
isDoneSeeking = true;
itemCandidates.Clear();
}
}
if (!isDoneSeeking)
@@ -189,7 +190,14 @@ namespace Barotrauma
}
}
FindTargetItem();
if (!objectiveManager.IsCurrentOrder<AIObjectiveGoTo>())
}
if (targetItem == null)
{
if (isDoneSeeking)
{
HandlePotentialItems();
}
if (objectiveManager.CurrentOrder is not AIObjectiveGoTo)
{
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
}
@@ -201,20 +209,28 @@ namespace Barotrauma
Abandon = true;
return;
}
if (targetItem == null || targetItem.Removed)
bool ShouldAbort() => IdentifiersOrTags is null || isDoneSeeking && itemCandidates.None();
if (targetItem is null or { Removed: true })
{
if (ShouldAbort())
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Target null or removed. Aborting.", Color.Red);
DebugConsole.NewMessage($"{character.Name}: Target null or removed. Aborting.", Color.Red);
#endif
Abandon = true;
Abandon = true;
}
return;
}
else if (isDoneSeeking && moveToTarget == null)
if (moveToTarget is null)
{
if (ShouldAbort())
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Move target null. Aborting.", Color.Red);
DebugConsole.NewMessage($"{character.Name}: Move target null. Aborting.", Color.Red);
#endif
Abandon = true;
Abandon = true;
return;
}
return;
}
if (character.IsItemTakenBySomeoneElse(targetItem))
@@ -399,16 +415,8 @@ namespace Barotrauma
{
StopWatch.Restart();
}
float priority = Math.Clamp(objectiveManager.GetCurrentPriority(), 10, 100);
if (!CheckPathForEachItem)
{
// While following the player, let's ensure that there's a valid path to the target before accepting it.
// Otherwise it will take some time for us to find a valid item when there are multiple items that we can't reach and some that we can.
// This is relatively expensive, so let's do this only when it significantly improves the behavior.
// Only allow one path find call per frame.
CheckPathForEachItem = priority >= AIObjectiveManager.LowestOrderPriority && (objectiveManager.IsCurrentOrder<AIObjectiveFixLeaks>() || objectiveManager.CurrentOrder is AIObjectiveGoTo gotoOrder && gotoOrder.IsFollowOrder);
}
bool checkPath = CheckPathForEachItem;
float priority = objectiveManager.GetCurrentPriority();
bool checkPath = CheckPathForEachItem || priority >= AIObjectiveManager.RunPriority || ItemCount > 1;
// Reset if the character has switched subs.
if (itemList != null && !character.Submarine.IsEntityFoundOnThisSub(itemList.FirstOrDefault(), includingConnectedSubs: true))
{
@@ -434,9 +442,9 @@ namespace Barotrauma
// Ignore items in the inventory when defined not to check it.
if (item.IsOwnedBy(character)) { continue; }
}
if (!AllowStealing)
if (!AllowStealing && character.IsOnPlayerTeam)
{
if (character.TeamID == CharacterTeamType.FriendlyNPC != item.SpawnedInCurrentOutpost) { continue; }
if (item.SpawnedInCurrentOutpost && !item.AllowStealing) { continue; }
}
if (!CheckItem(item)) { continue; }
if (item.Container != null)
@@ -454,11 +462,11 @@ namespace Barotrauma
if (!itemInventory.Container.HasRequiredItems(character, addMessage: false)) { continue; }
}
float itemPriority = item.Prefab.BotPriority;
if (itemPriority <= 0) { continue; }
if (GetItemPriority != null)
{
itemPriority *= GetItemPriority(item);
}
if (itemPriority <= 0) { continue; }
Entity rootInventoryOwner = item.GetRootInventoryOwner();
if (rootInventoryOwner is Item ownerItem)
{
@@ -474,11 +482,13 @@ namespace Barotrauma
}
}
Vector2 itemPos = (rootInventoryOwner ?? item).WorldPosition;
float yDist = Math.Abs(character.WorldPosition.Y - itemPos.Y);
yDist = yDist > 100 ? yDist * 5 : 0;
float dist = Math.Abs(character.WorldPosition.X - itemPos.X) + yDist;
float minDistFactor = EvaluateCombatPriority ? 0.1f : 0;
float distanceFactor = MathHelper.Lerp(1, minDistFactor, MathUtils.InverseLerp(100, 10000, dist));
float distanceFactor =
GetDistanceFactor(
itemPos,
verticalDistanceMultiplier: 5,
maxDistance: 10000,
factorAtMinDistance: 1.0f,
factorAtMaxDistance: EvaluateCombatPriority ? 0.1f : 0);
itemPriority *= distanceFactor;
if (EvaluateCombatPriority)
{
@@ -510,7 +520,7 @@ namespace Barotrauma
}
else
{
combatFactor = Math.Min(item.Components.Sum(ic => AIObjectiveCombat.GetLethalDamage(ic)) / 1000, 0.1f);
combatFactor = Math.Min(item.Components.Sum(AIObjectiveCombat.GetLethalDamage) / 1000, 0.1f);
}
itemPriority *= combatFactor;
}
@@ -518,10 +528,6 @@ namespace Barotrauma
{
itemPriority *= item.Condition / item.MaxCondition;
}
if (checkPath)
{
itemCandidates.Add((item, itemPriority));
}
// Ignore if the item has a lower priority than the currently selected one
if (itemPriority < currItemPriority) { continue; }
if (EvaluateCombatPriority && itemPriority <= 0)
@@ -529,23 +535,27 @@ namespace Barotrauma
// Not good enough
continue;
}
currItemPriority = itemPriority;
targetItem = item;
moveToTarget = rootInventoryOwner ?? item;
if (checkPath)
{
itemCandidates.Add((item, itemPriority));
}
else
{
currItemPriority = itemPriority;
targetItem = item;
moveToTarget = rootInventoryOwner ?? item;
}
}
if (currentSearchIndex >= itemList.Count - 1)
{
isDoneSeeking = true;
}
if (checkedItems > 0)
{
if (isDoneSeeking && itemCandidates.Any())
if (itemCandidates.Any())
{
itemCandidates.Sort((x, y) => y.priority.CompareTo(x.priority));
}
if (HumanAIController.DebugAI && targetItem != null && StopWatch.ElapsedMilliseconds > 2)
{
var msg = $"Went through {checkedItems} of total {itemList.Count} items. Found item {targetItem.Name} in {StopWatch.ElapsedMilliseconds} ms. Completed: {isDoneSeeking}";
if (HumanAIController.DebugAI && StopWatch.ElapsedMilliseconds > 2)
{
string msg = $"Went through {checkedItems} of total {itemList.Count} items. Found item {targetItem?.Name ?? "NULL"} in {StopWatch.ElapsedMilliseconds} ms. Completed: {isDoneSeeking}";
if (StopWatch.ElapsedMilliseconds > 5)
{
DebugConsole.ThrowError(msg);
@@ -557,60 +567,66 @@ namespace Barotrauma
}
}
}
if (isDoneSeeking)
}
private void HandlePotentialItems()
{
Debug.Assert(isDoneSeeking);
if (itemCandidates.Any())
{
if (PathSteering == null)
{
itemCandidates.Clear();
Abandon = true;
return;
}
if (itemCandidates.Any())
if (itemCandidates.FirstOrDefault() is var itemCandidate)
{
if (itemCandidates.FirstOrDefault() is { } itemCandidate)
var path = PathSteering.PathFinder.FindPath(character.SimPosition, character.GetRelativeSimPosition(itemCandidate.item), character.Submarine, errorMsgStr: $"AIObjectiveGetItem {character.DisplayName}", nodeFilter: node => node.Waypoint.CurrentHull != null);
if (path.Unreachable)
{
var path = PathSteering.PathFinder.FindPath(character.SimPosition, character.GetRelativeSimPosition(itemCandidate.item), character.Submarine, errorMsgStr: $"AIObjectiveGetItem {character.DisplayName}", nodeFilter: node => node.Waypoint.CurrentHull != null);
if (path.Unreachable)
{
// Remove the invalid candidates and continue on the next frame.
itemCandidates.Remove(itemCandidate);
}
else
{
// The path was valid -> we are done.
itemCandidates.Clear();
}
}
}
if (targetItem == null && itemCandidates.None())
{
if (spawnItemIfNotFound)
{
ItemPrefab prefab = FindItemToSpawn();
if (prefab == null)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot find an item with the following identifier(s) or tag(s): {string.Join(", ", IdentifiersOrTags)}, tried to spawn the item but no matching item prefabs were found.", Color.Yellow);
#endif
Abandon = true;
}
else
{
Entity.Spawner.AddItemToSpawnQueue(prefab, character.Inventory, onSpawned: (Item spawnedItem) =>
{
targetItem = spawnedItem;
if (character.TeamID == CharacterTeamType.FriendlyNPC && (character.Submarine?.Info.IsOutpost ?? false))
{
spawnedItem.SpawnedInCurrentOutpost = true;
}
});
}
// Remove the invalid candidates and continue on the next frame.
itemCandidates.Remove(itemCandidate);
}
else
{
// The path was valid -> we are done.
itemCandidates.Clear();
targetItem = itemCandidate.item;
moveToTarget = targetItem.GetRootInventoryOwner() ?? targetItem;
}
}
}
if (targetItem == null)
{
if (spawnItemIfNotFound)
{
ItemPrefab prefab = FindItemToSpawn();
if (prefab == null)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot find an item with the following identifier(s) or tag(s): {string.Join(", ", IdentifiersOrTags)}", Color.Yellow);
DebugConsole.NewMessage($"{character.Name}: Cannot find an item with the following identifier(s) or tag(s): {string.Join(", ", IdentifiersOrTags)}, tried to spawn the item but no matching item prefabs were found.", Color.Yellow);
#endif
Abandon = true;
}
else
{
Entity.Spawner.AddItemToSpawnQueue(prefab, character.Inventory, onSpawned: (Item spawnedItem) =>
{
targetItem = spawnedItem;
if (character.TeamID == CharacterTeamType.FriendlyNPC && (character.Submarine?.Info.IsOutpost ?? false))
{
spawnedItem.SpawnedInCurrentOutpost = true;
}
});
}
}
else
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot find an item with the following identifier(s) or tag(s): {string.Join(", ", IdentifiersOrTags)}", Color.Yellow);
#endif
Abandon = true;
}
}
}
@@ -3,6 +3,7 @@ using Barotrauma.Extensions;
using System.Linq;
using System.Collections.Generic;
using System.Collections.Immutable;
using System;
namespace Barotrauma
{
@@ -24,6 +25,12 @@ namespace Barotrauma
public bool CheckPathForEachItem { get; set; }
public bool RequireNonEmpty { get; set; }
public bool RequireAllItems { get; set; }
public bool RequireDivingSuitAdequate { get; set; }
/// <summary>
/// T1 = item to check, T2 = tag we're trying to find a suitable item for
/// </summary>
public Func<Item, Identifier, bool>? ItemFilter;
private readonly ImmutableArray<Identifier> gearTags;
private readonly ImmutableHashSet<Identifier> ignoredTags;
@@ -48,7 +55,8 @@ namespace Barotrauma
int count = gearTags.Count(t => t == tag);
AIObjectiveGetItem? getItem = null;
TryAddSubObjective(ref getItem, () =>
new AIObjectiveGetItem(character, tag, objectiveManager, Equip, CheckInventory && count <= 1)
{
var getItem = new AIObjectiveGetItem(character, tag, objectiveManager, Equip, CheckInventory && count <= 1)
{
AllowVariants = AllowVariants,
Wear = Wear,
@@ -58,29 +66,36 @@ namespace Barotrauma
CheckPathForEachItem = CheckPathForEachItem,
RequireNonEmpty = RequireNonEmpty,
ItemCount = count,
SpeakIfFails = RequireAllItems
},
onCompleted: () =>
SpeakIfFails = RequireAllItems,
};
if (ItemFilter != null)
{
var item = getItem?.TargetItem;
if (item?.IsOwnedBy(character) != null)
{
achievedItems.Add(item);
}
},
onAbandon: () =>
getItem.ItemFilter = (Item it) => ItemFilter(it, tag);
}
return getItem;
},
onCompleted: () =>
{
var item = getItem?.TargetItem;
if (item?.IsOwnedBy(character) != null)
{
var item = getItem?.TargetItem;
if (item != null)
{
achievedItems.Remove(item);
}
RemoveSubObjective(ref getItem);
if (RequireAllItems)
{
Abandon = true;
}
});
achievedItems.Add(item);
}
},
onAbandon: () =>
{
var item = getItem?.TargetItem;
if (item != null)
{
achievedItems.Remove(item);
}
RemoveSubObjective(ref getItem);
if (RequireAllItems)
{
Abandon = true;
}
});
}
subObjectivesCreated = true;
}
@@ -85,6 +85,8 @@ namespace Barotrauma
public bool IgnoreIfTargetDead { get; set; }
public bool AllowGoingOutside { get; set; }
public bool FaceTargetOnCompleted { get; set; } = true;
public bool AlwaysUseEuclideanDistance { get; set; } = true;
/// <summary>
@@ -324,7 +326,7 @@ namespace Barotrauma
float minOxygen = AIObjectiveFindDivingGear.GetMinOxygen(character);
if (tryToGetDivingSuit)
{
needsEquipment = !HumanAIController.HasDivingSuit(character, minOxygen);
needsEquipment = !HumanAIController.HasDivingSuit(character, minOxygen, requireSuitablePressureProtection: !objectiveManager.FailedToFindDivingGearForDepth);
}
else if (tryToGetDivingGear)
{
@@ -346,26 +348,26 @@ namespace Barotrauma
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: tryToGetDivingSuit, objectiveManager),
onAbandon: () =>
{
cantFindDivingGear = true;
if (needsDivingSuit)
{
// Shouldn't try to reach the target without a suit, because it's lethal.
Abandon = true;
}
else
{
// Try again without requiring the diving suit
RemoveSubObjective(ref findDivingGear);
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager),
onAbandon: () =>
{
Abandon = character.CurrentHull != null && (objectiveManager.CurrentOrder != this || Target.Submarine == null);
RemoveSubObjective(ref findDivingGear);
},
onCompleted: () =>
{
RemoveSubObjective(ref findDivingGear);
});
cantFindDivingGear = true;
if (needsDivingSuit)
{
// Shouldn't try to reach the target without a suit, because it's lethal.
Abandon = true;
}
else
{
// Try again without requiring the diving suit
RemoveSubObjective(ref findDivingGear);
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager),
onAbandon: () =>
{
Abandon = character.CurrentHull != null && (objectiveManager.CurrentOrder != this || Target.Submarine == null);
RemoveSubObjective(ref findDivingGear);
},
onCompleted: () =>
{
RemoveSubObjective(ref findDivingGear);
});
}
},
onCompleted: () => RemoveSubObjective(ref findDivingGear));
@@ -450,10 +452,8 @@ namespace Barotrauma
{
useScooter = false;
checkScooterTimer = checkScooterTime * Rand.Range(0.75f, 1.25f);
Identifier scooterTag = "scooter".ToIdentifier();
Identifier batteryTag = "mobilebattery".ToIdentifier();
Item scooter = null;
bool shouldUseScooter = Mimic && targetCharacter != null && targetCharacter.HasEquippedItem(scooterTag, allowBroken: false);
bool shouldUseScooter = Mimic && targetCharacter != null && targetCharacter.HasEquippedItem(Tags.Scooter, allowBroken: false);
if (!shouldUseScooter)
{
float threshold = 500;
@@ -467,7 +467,7 @@ namespace Barotrauma
shouldUseScooter = Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition) > threshold * threshold;
}
}
if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> equippedScooters, recursive: false, requireEquipped: true))
if (HumanAIController.HasItem(character, Tags.Scooter, out IEnumerable<Item> equippedScooters, recursive: false, requireEquipped: true))
{
// Currently equipped scooter
scooter = equippedScooters.FirstOrDefault();
@@ -477,23 +477,23 @@ namespace Barotrauma
var leftHandItem = character.GetEquippedItem(slotType: InvSlotType.LeftHand);
var rightHandItem = character.GetEquippedItem(slotType: InvSlotType.RightHand);
bool handsFull =
(leftHandItem != null && !character.Inventory.IsAnySlotAvailable(leftHandItem)) ||
(rightHandItem != null && !character.Inventory.IsAnySlotAvailable(rightHandItem));
(leftHandItem != null && !character.Inventory.IsAnySlotAvailable(leftHandItem) && !character.Inventory.TryPutItem(leftHandItem, character, InvSlotType.Bag.ToEnumerable())) ||
(rightHandItem != null && !character.Inventory.IsAnySlotAvailable(rightHandItem) && !character.Inventory.TryPutItem(rightHandItem, character, InvSlotType.Bag.ToEnumerable()));
if (!handsFull)
{
bool hasBattery = false;
if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> nonEquippedScooters, containedTag: batteryTag, conditionPercentage: 1, requireEquipped: false))
if (HumanAIController.HasItem(character, Tags.Scooter, out IEnumerable<Item> nonEquippedScooters, containedTag: Tags.MobileBattery, conditionPercentage: 1, requireEquipped: false))
{
// Non-equipped scooter with a battery
scooter = nonEquippedScooters.FirstOrDefault();
hasBattery = true;
}
else if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> _nonEquippedScooters, requireEquipped: false))
else if (HumanAIController.HasItem(character, Tags.Scooter, out IEnumerable<Item> _nonEquippedScooters, requireEquipped: false))
{
// Non-equipped scooter without a battery
scooter = _nonEquippedScooters.FirstOrDefault();
// Non-recursive so that the bots won't take batteries from other items. Also means that they can't find batteries inside containers. Not sure how to solve this.
hasBattery = HumanAIController.HasItem(character, batteryTag, out _, requireEquipped: false, conditionPercentage: 1, recursive: false);
hasBattery = HumanAIController.HasItem(character, Tags.MobileBattery, out _, requireEquipped: false, conditionPercentage: 1, recursive: false);
}
if (scooter != null && hasBattery)
{
@@ -511,7 +511,7 @@ namespace Barotrauma
if (scooter.ContainedItems.None(i => i.Condition > 0))
{
// Try to switch batteries
if (HumanAIController.HasItem(character, batteryTag, out IEnumerable<Item> batteries, conditionPercentage: 1, recursive: false))
if (HumanAIController.HasItem(character, Tags.MobileBattery, out IEnumerable<Item> batteries, conditionPercentage: 1, recursive: false))
{
scooter.ContainedItems.ForEachMod(emptyBattery => character.Inventory.TryPutItem(emptyBattery, character, CharacterInventory.AnySlot));
if (!scooter.Combine(batteries.OrderByDescending(b => b.Condition).First(), character))
@@ -811,16 +811,15 @@ namespace Barotrauma
private void StopMovement()
{
SteeringManager?.Reset();
if (Target != null)
if (FaceTargetOnCompleted && Target is Entity { Removed: false })
{
character.AnimController.TargetDir = Target.WorldPosition.X > character.WorldPosition.X ? Direction.Right : Direction.Left;
HumanAIController.FaceTarget(Target);
}
}
protected override void OnCompleted()
{
StopMovement();
HumanAIController.FaceTarget(Target);
if (Target is WayPoint { Ladders: null })
{
// Release ladders when ordered to wait at a spawnpoint.
@@ -454,14 +454,16 @@ namespace Barotrauma
{
targetHulls.Add(hull);
float weight = hull.RectWidth;
// Prefer rooms that are closer. Avoid rooms that are not in the same level.
// If the behavior is active, prefer rooms that are not close.
float yDist = Math.Abs(character.WorldPosition.Y - hull.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 5 : 0;
float dist = Math.Abs(character.WorldPosition.X - hull.WorldPosition.X) + yDist;
float distanceFactor = behavior == BehaviorType.Patrol ? MathHelper.Lerp(1, 0, MathUtils.InverseLerp(2500, 0, dist)) : MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, 2500, dist));
float distanceFactor = GetDistanceFactor(hull.WorldPosition, verticalDistanceMultiplier: 5, maxDistance: 2500,
factorAtMinDistance: 1, factorAtMaxDistance: 0);
if (behavior == BehaviorType.Patrol)
{
//invert when patrolling (= prefer travelling to far-away hulls)
distanceFactor = 1.0f - distanceFactor;
}
float waterFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, 100, hull.WaterPercentage * 2));
weight *= distanceFactor * waterFactor;
System.Diagnostics.Debug.Assert(weight >= 0);
hullWeights.Add(weight);
}
}
@@ -193,7 +193,10 @@ namespace Barotrauma
if (yDist > 100) { dist += yDist * 5; }
dist += Math.Abs(character.WorldPosition.X - targetPos.X);
}
float distanceFactor = dist > 0.0f ? MathHelper.Lerp(0.9f, 0, MathUtils.InverseLerp(0, 5000, dist)) : 0.9f;
float distanceFactor =
GetDistanceFactor(targetItem.WorldPosition, verticalDistanceMultiplier: 5, maxDistance: 5000, factorAtMinDistance: 0.9f, factorAtMaxDistance: 0);
bool hasContainable = character.HasItem(targetItem);
float devotion = (CumulatedDevotion + (hasContainable ? 100 - MaxDevotion : 0)) / 100;
float max = AIObjectiveManager.LowestOrderPriority - (hasContainable ? 1 : 2);
@@ -67,6 +67,10 @@ namespace Barotrauma
}
private AIObjective currentOrder;
public AIObjective ForcedOrder { get; private set; }
/// <summary>
/// Includes orders.
/// </summary>
public AIObjective CurrentObjective { get; private set; }
public AIObjectiveManager(Character character)
@@ -104,6 +108,8 @@ namespace Barotrauma
public Dictionary<AIObjective, CoroutineHandle> DelayedObjectives { get; private set; } = new Dictionary<AIObjective, CoroutineHandle>();
public bool FailedAutonomousObjectives { get; private set; }
public bool FailedToFindDivingGearForDepth;
private void ClearIgnored()
{
if (character.AIController is HumanAIController humanAi)
@@ -220,8 +226,11 @@ namespace Barotrauma
if (previousObjective == CurrentObjective) { return CurrentObjective; }
previousObjective?.OnDeselected();
CurrentObjective?.OnSelected();
GetObjective<AIObjectiveIdle>().CalculatePriority(Math.Max(CurrentObjective.Priority - 10, 0));
if (CurrentObjective != null)
{
CurrentObjective.OnSelected();
GetObjective<AIObjectiveIdle>().CalculatePriority(Math.Max(CurrentObjective.Priority - 10, 0));
}
if (GameMain.NetworkMember is { IsServer: true })
{
GameMain.NetworkMember.CreateEntityEvent(character,
@@ -230,9 +239,14 @@ namespace Barotrauma
return CurrentObjective;
}
/// <summary>
/// Returns the highest priority of the current objective and its subobjectives.
/// </summary>
public float GetCurrentPriority()
{
return CurrentObjective == null ? 0.0f : CurrentObjective.Priority;
if (CurrentObjective == null) { return 0; }
float subObjectivePriority = CurrentObjective.SubObjectives.Any() ? CurrentObjective.SubObjectives.Max(so => so.Priority) : 0;
return Math.Max(CurrentObjective.Priority, subObjectivePriority);
}
public void UpdateObjectives(float deltaTime)
@@ -241,7 +255,7 @@ namespace Barotrauma
if (CurrentOrders.Any())
{
foreach(var order in CurrentOrders)
foreach (var order in CurrentOrders)
{
var orderObjective = order.Objective;
UpdateOrderObjective(orderObjective);
@@ -396,6 +410,9 @@ namespace Barotrauma
}
}
//reset this here so the bots can retry finding a better suit if it's needed for the new order
FailedToFindDivingGearForDepth = false;
var newCurrentObjective = CreateObjective(order);
if (newCurrentObjective != null)
{
@@ -592,6 +609,9 @@ namespace Barotrauma
case "loaditems":
newObjective = new AIObjectiveLoadItems(character, this, order.Option, order.GetTargetItems(order.Option), order.TargetEntity as Item, priorityModifier);
break;
case "deconstructitems":
newObjective = new AIObjectiveDeconstructItems(character, this, priorityModifier);
break;
default:
if (order.TargetItemComponent == null) { return null; }
if (!order.TargetItemComponent.Item.IsInteractable(character)) { return null; }
@@ -613,6 +633,11 @@ namespace Barotrauma
return newObjective;
}
/// <summary>
/// Sets the order as dismissed, and enables the option to reissue the order on the crew list.
/// Note that this is not the same thing as just removing the order entirely!
/// </summary>
/// <param name="order"></param>
private void DismissSelf(Order order)
{
var currentOrder = CurrentOrders.FirstOrDefault(oi => oi.MatchesOrder(order.Identifier, order.Option));
@@ -651,13 +676,27 @@ namespace Barotrauma
return true;
}
/// <summary>
/// Only checks the current order. Deprecated, use pattern matching instead.
/// </summary>
public bool IsCurrentOrder<T>() where T : AIObjective => CurrentOrder is T;
/// <summary>
/// Checks the current objective (which can be an order too). Deprecated, use pattern matching instead.
/// </summary>
public bool IsCurrentObjective<T>() where T : AIObjective => CurrentObjective is T;
public bool IsActiveObjective<T>() where T : AIObjective => GetActiveObjective() is T;
public AIObjective GetActiveObjective() => CurrentObjective?.GetActiveObjective();
/// <summary>
/// Return the first order whose objective is of the given type. Can return null.
/// </summary>
public T GetOrder<T>() where T : AIObjective => CurrentOrders.FirstOrDefault(o => o.Objective is T)?.Objective as T;
/// <summary>
/// Return the first order with the specified objective. Can return null.
/// </summary>
public Order GetOrder(AIObjective objective) => CurrentOrders.FirstOrDefault(o => o.Objective == objective);
public T GetLastActiveObjective<T>() where T : AIObjective
=> CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).LastOrDefault(so => so is T) as T;
@@ -665,12 +704,12 @@ namespace Barotrauma
=> CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).FirstOrDefault(so => so is T) as T;
/// <summary>
/// Returns all active objectives of the specific type. Creates a new collection -> don't use too frequently.
/// Returns all active objectives of the specific type.
/// </summary>
public IEnumerable<T> GetActiveObjectives<T>() where T : AIObjective
{
if (CurrentObjective == null) { return Enumerable.Empty<T>(); }
return CurrentObjective.GetSubObjectivesRecursive(includingSelf: true).Where(so => so is T).Select(so => so as T);
return CurrentObjective.GetSubObjectivesRecursive(includingSelf: true).OfType<T>();
}
public bool HasActiveObjective<T>() where T : AIObjective => CurrentObjective is T || CurrentObjective != null && CurrentObjective.GetSubObjectivesRecursive().Any(so => so is T);
@@ -211,6 +211,10 @@ namespace Barotrauma
return;
}
}
//the character shouldn't be grabbing anyone if it's trying to operate an item
character.SelectedCharacter = null;
if (target.CanBeSelected)
{
if (!character.IsClimbing && character.CanInteractWith(target.Item, out _, checkLinked: false))
@@ -87,13 +87,29 @@ namespace Barotrauma
AIObjectiveGetItems CreateObjectives(IEnumerable<Identifier> itemTags, bool requireAll)
{
AIObjectiveGetItems objectiveReference = null;
if (!TryAddSubObjective(ref objectiveReference, () => new AIObjectiveGetItems(character, objectiveManager, itemTags)
if (!TryAddSubObjective(ref objectiveReference, () =>
{
CheckInventory = CheckInventory,
Equip = Equip,
EvaluateCombatPriority = EvaluateCombatPriority,
RequireNonEmpty = RequireNonEmpty,
RequireAllItems = requireAll
var getItems = new AIObjectiveGetItems(character, objectiveManager, itemTags)
{
CheckInventory = CheckInventory,
Equip = Equip,
EvaluateCombatPriority = EvaluateCombatPriority,
RequireNonEmpty = RequireNonEmpty,
RequireAllItems = requireAll
};
if (itemTags.Contains(Tags.HeavyDivingGear))
{
getItems.ItemFilter = (Item it, Identifier tag) =>
{
if (tag == Tags.HeavyDivingGear)
{
return AIObjectiveFindDivingGear.IsSuitablePressureProtection(it, tag, character);
}
return true;
};
}
return getItems;
},
onCompleted: () =>
{
@@ -64,10 +64,7 @@ namespace Barotrauma
float distanceFactor = 1;
if (!isPriority && Item.CurrentHull != character.CurrentHull)
{
float yDist = Math.Abs(character.WorldPosition.Y - Item.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 5 : 0;
float dist = Math.Abs(character.WorldPosition.X - Item.WorldPosition.X) + yDist;
distanceFactor = MathHelper.Lerp(1, 0.25f, MathUtils.InverseLerp(0, 4000, dist));
distanceFactor = GetDistanceFactor(Item.WorldPosition, factorAtMaxDistance: 0.25f, verticalDistanceMultiplier: 5, maxDistance: 4000);
}
float requiredSuccessFactor = objectiveManager.HasOrder<AIObjectiveRepairItems>() ? 0 : AIObjectiveRepairItems.RequiredSuccessFactor;
float severity = isPriority ? 1 : AIObjectiveRepairItems.GetTargetPriority(Item, character, requiredSuccessFactor) / 100;
@@ -113,7 +110,7 @@ namespace Barotrauma
if (!repairable.HasRequiredItems(character, false))
{
//make sure we have all the items required to fix the target item
foreach (var kvp in repairable.requiredItems)
foreach (var kvp in repairable.RequiredItems)
{
foreach (RelatedItem requiredItem in kvp.Value)
{
@@ -140,7 +137,7 @@ namespace Barotrauma
}
if (repairTool != null)
{
if (repairTool.requiredItems.TryGetValue(RelatedItem.RelationType.Contained, out var requiredItems))
if (repairTool.RequiredItems.TryGetValue(RelatedItem.RelationType.Contained, out var requiredItems))
{
if (repairTool.Item.OwnInventory == null)
{
@@ -282,7 +279,7 @@ namespace Barotrauma
{
foreach (Repairable repairable in Item.Repairables)
{
foreach (var kvp in repairable.requiredItems)
foreach (var kvp in repairable.RequiredItems)
{
foreach (RelatedItem requiredItem in kvp.Value)
{
@@ -74,7 +74,7 @@ namespace Barotrauma
}
if (!RelevantSkill.IsEmpty)
{
if (item.Repairables.None(r => r.requiredSkills.Any(s => s.Identifier == RelevantSkill))) { return false; }
if (item.Repairables.None(r => r.RequiredSkills.Any(s => s.Identifier == RelevantSkill))) { return false; }
}
return !HumanAIController.IsItemRepairedByAnother(item, out _);
}
@@ -278,52 +278,66 @@ namespace Barotrauma
float cprSuitability = Target.Oxygen < 0.0f ? -Target.Oxygen * 100.0f : 0.0f;
//find which treatments are the most suitable to treat the character's current condition
Target.CharacterHealth.GetSuitableTreatments(currentTreatmentSuitabilities, user: character, normalize: false, predictFutureDuration: 10.0f);
//check if we already have a suitable treatment for any of the afflictions
float bestSuitability = 0.0f;
Item bestItem = null;
Affliction afflictionToTreat = null;
foreach (Affliction affliction in GetSortedAfflictions(Target))
{
if (affliction == null) { throw new Exception("Affliction was null"); }
if (affliction.Prefab == null) { throw new Exception("Affliction prefab was null"); }
float bestSuitability = 0.0f;
Item bestItem = null;
foreach (KeyValuePair<Identifier, float> treatmentSuitability in affliction.Prefab.TreatmentSuitabilities)
//find which treatments are the most suitable to treat the character's current condition
Target.CharacterHealth.GetSuitableTreatments(
currentTreatmentSuitabilities,
limb: Target.CharacterHealth.GetAfflictionLimb(affliction),
user: character,
predictFutureDuration: 10.0f);
foreach (KeyValuePair<Identifier, float> treatmentSuitability in currentTreatmentSuitabilities)
{
if (currentTreatmentSuitabilities.ContainsKey(treatmentSuitability.Key) &&
currentTreatmentSuitabilities[treatmentSuitability.Key] > bestSuitability)
float thisSuitability = currentTreatmentSuitabilities[treatmentSuitability.Key];
if (thisSuitability <= 0) { continue; }
Item matchingItem = FindMedicalItem(character.Inventory, treatmentSuitability.Key);
//allow taking items from the target's inventory too if the target is unconscious
if (matchingItem == null && Target.IsIncapacitated)
{
Item matchingItem = character.Inventory.FindItemByIdentifier(treatmentSuitability.Key, true);
//allow taking items from the target's inventory too if the target is unconscious
if (matchingItem == null && Target.IsIncapacitated)
{
matchingItem ??= Target.Inventory?.FindItemByIdentifier(treatmentSuitability.Key, true);
}
if (matchingItem != null)
{
bestItem = matchingItem;
bestSuitability = currentTreatmentSuitabilities[treatmentSuitability.Key];
}
matchingItem = FindMedicalItem(Target.Inventory, treatmentSuitability.Key);
}
}
if (bestItem != null)
{
if (Target != character) { character.SelectCharacter(Target); }
ApplyTreatment(affliction, bestItem);
//wait a bit longer after applying a treatment to wait for potential side-effects to manifest
treatmentTimer = TreatmentDelay * 4;
return;
if (matchingItem == null) { continue; }
//also check how suitable the treatment is for the specific affliction we're now checking
//we don't want to e.g. give fentanyl for oxygen low just because the character has burns on other limbs
//that would also be healed by it!
float suitabilityForThisAffliction = affliction.Prefab.GetTreatmentSuitability(matchingItem);
float totalSuitability = thisSuitability * suitabilityForThisAffliction;
if (matchingItem != null && totalSuitability > bestSuitability)
{
bestItem = matchingItem;
afflictionToTreat = affliction;
bestSuitability = totalSuitability;
}
}
}
if (bestItem != null && bestSuitability > cprSuitability)
{
if (Target != character) { character.SelectCharacter(Target); }
ApplyTreatment(afflictionToTreat, bestItem);
//wait a bit longer after applying a treatment to wait for potential side-effects to manifest
treatmentTimer = TreatmentDelay * 4;
return;
}
// Find treatments outside of own inventory only if inside the own sub.
if (character.Submarine != null && character.Submarine.TeamID == character.TeamID)
{
//get "overall" suitability for no specific limb at this point
Target.CharacterHealth.GetSuitableTreatments(
currentTreatmentSuitabilities, user: character, predictFutureDuration: 10.0f);
//didn't have any suitable treatments available, try to find some medical items
if (currentTreatmentSuitabilities.Any(s => s.Value > cprSuitability))
{
itemNameList.Clear();
suitableItemIdentifiers.Clear();
foreach (KeyValuePair<Identifier, float> treatmentSuitability in currentTreatmentSuitabilities)
foreach (KeyValuePair<Identifier, float> treatmentSuitability in currentTreatmentSuitabilities.OrderByDescending(s => s.Value))
{
if (treatmentSuitability.Value <= cprSuitability) { continue; }
if (ItemPrefab.Prefabs.TryGet(treatmentSuitability.Key, out ItemPrefab itemPrefab))
@@ -420,6 +434,28 @@ namespace Barotrauma
}
}
public static Item FindMedicalItem(Inventory inventory, Identifier itemIdentifier)
{
return FindMedicalItem(inventory, it => it.Prefab.Identifier == itemIdentifier);
}
public static Item FindMedicalItem(Inventory inventory, Func<Item, bool> predicate)
{
if (inventory == null) { return null; }
//prefer items not in a container
Item match = inventory.FindItem(predicate, recursive: false);
if (match != null) { return match; }
//start from the inventories with most slots
//= prefer taking items from things like toolbelts or doctor's uniforms, as opposed to e.g. autoinjectors which tend to have one or two slots
foreach (var potentialContainer in inventory.AllItems.OrderByDescending(it => it.OwnInventory?.Capacity ?? -1))
{
match = potentialContainer.OwnInventory?.FindItem(predicate, recursive: true);
if (match != null) { return match; }
}
return null;
}
private void SpeakCannotTreat()
{
LocalizedString msg = character == Target ?
@@ -122,6 +122,11 @@ namespace Barotrauma
public bool HasOptions => Options.Length > 1;
public readonly bool MustManuallyAssign;
/// <summary>
/// If enabled and this is an Operate order, it will remove Operate orders of the same item from other characters.
/// If this is a Movement order, removes other Movement orders from the character who receives the order.
/// </summary>
public readonly bool AutoDismiss;
/// <summary>
@@ -137,7 +142,9 @@ namespace Barotrauma
}
public OrderTargetType TargetType { get; }
public int? WallSectionIndex { get; }
public bool IsIgnoreOrder => Identifier == "ignorethis" || Identifier == "unignorethis";
public bool IsIgnoreOrder => Identifier == Tags.IgnoreThis || Identifier == Tags.UnignoreThis;
public bool IsDeconstructOrder => Identifier == Tags.DeconstructThis || Identifier == Tags.DontDeconstructThis;
/// <summary>
/// Should the order icon be drawn when the order target is inside a container
@@ -273,7 +280,7 @@ namespace Barotrauma
public bool HasPreferredJob(Character character) => HasSpecifiedJob(character, PreferredJobs);
public string GetChatMessage(string targetCharacterName, string targetRoomName, bool givingOrderToSelf, Identifier orderOption = default, bool isNewOrder = true)
public string GetChatMessage(string targetCharacterName, string targetRoomName, Entity targetEntity, bool givingOrderToSelf, Identifier orderOption = default, bool isNewOrder = true)
{
if (!TargetAllCharacters && !isNewOrder && Identifier != "dismissed")
{
@@ -304,8 +311,27 @@ namespace Barotrauma
}
}
}
LocalizedString targetEntityName = string.Empty;
switch (targetEntity)
{
case Item item:
targetEntityName = item.Name;
break;
case Hull hull:
targetEntityName = hull.DisplayName;
break;
case Structure structure:
targetEntityName = structure.Name;
break;
case Character character:
targetEntityName = character.DisplayName;
break;
}
return TextManager.GetWithVariables(messageTag,
("[name]", targetCharacterName ?? string.Empty, FormatCapitals.No),
("[target]", targetEntityName, FormatCapitals.No),
("[roomname]", targetRoomName ?? string.Empty, FormatCapitals.Yes)).Fallback("").Value;
}
@@ -413,6 +439,8 @@ namespace Barotrauma
public bool TargetItemsMatchItem(Item item, Identifier option = default)
{
if (item == null) { return false; }
if (Identifier == Tags.DeconstructThis && item.AllowDeconstruct && !Item.DeconstructItems.Contains(item)) { return true; }
if (Identifier == Tags.DontDeconstructThis && Item.DeconstructItems.Contains(item)) { return true; }
ImmutableArray<Identifier> targetItems = GetTargetItems(option);
return TargetItemsMatchItem(targetItems, item);
}
@@ -528,6 +556,7 @@ namespace Barotrauma
public OrderCategory? Category => Prefab.Category;
public bool MustManuallyAssign => Prefab.MustManuallyAssign;
public bool IsIgnoreOrder => Prefab.IsIgnoreOrder;
public bool IsDeconstructOrder => Prefab.IsDeconstructOrder;
public bool DrawIconWhenContained => Prefab.DrawIconWhenContained;
public bool Hidden => Prefab.Hidden;
public bool IgnoreAtOutpost => Prefab.IgnoreAtOutpost;
@@ -538,7 +567,6 @@ namespace Barotrauma
public bool ColoredWhenControllingGiver => Prefab.ColoredWhenControllingGiver;
public bool DisplayGiverInTooltip => Prefab.DisplayGiverInTooltip;
public readonly bool UseController;
/// <summary>
@@ -762,7 +790,7 @@ namespace Barotrauma
public string GetChatMessage(
string targetCharacterName, string targetRoomName, bool givingOrderToSelf, Identifier orderOption = default, bool isNewOrder = true)
=> Prefab.GetChatMessage(targetCharacterName, targetRoomName, givingOrderToSelf, orderOption, isNewOrder);
=> Prefab.GetChatMessage(targetCharacterName, targetRoomName, TargetEntity, givingOrderToSelf, orderOption, isNewOrder);
/// <summary>
/// Get the target item component based on the target item type
@@ -52,7 +52,7 @@ namespace Barotrauma
{
if (orderedCharacter != CommandingCharacter)
{
CommandingCharacter.Speak(SuggestedOrder.GetChatMessage(OrderedCharacter.Name, "", false), minDurationBetweenSimilar: 5);
CommandingCharacter.Speak(SuggestedOrder.GetChatMessage(OrderedCharacter.Name, "", givingOrderToSelf: false), minDurationBetweenSimilar: 5);
}
CurrentOrder = SuggestedOrder
.WithOption(Option)
@@ -75,7 +75,8 @@ namespace Barotrauma
{
steering.Y = 0.0f;
}
/// <param name="speed">Update speed for the steering. Should normally match the characters current animation speed.</param>
public virtual void Update(float speed)
{
if (steering == Vector2.Zero || !MathUtils.IsValid(steering))
@@ -86,6 +87,7 @@ namespace Barotrauma
}
if (steering.LengthSquared() > speed * speed)
{
// Can't steer faster than the max speed.
steering = Vector2.Normalize(steering) * Math.Abs(speed);
}
if (host is AIController aiController && aiController?.Character.CharacterHealth.GetAfflictionOfType("invertcontrols".ToIdentifier()) != null)
@@ -32,6 +32,48 @@ namespace Barotrauma
public abstract GroundedMovementParams RunParams { get; set; }
public abstract SwimParams SwimSlowParams { get; set; }
public abstract SwimParams SwimFastParams { get; set; }
protected class AnimSwap
{
public readonly AnimationType AnimationType;
public readonly AnimationParams TemporaryAnimation;
public readonly float Priority;
public bool IsActive
{
get { return _isActive; }
set
{
if (value)
{
expirationTimer = expirationTime;
}
_isActive = value;
}
}
private bool _isActive;
private float expirationTimer;
private const float expirationTime = 0.1f;
public AnimSwap(AnimationParams temporaryAnimation, float priority)
{
AnimationType = temporaryAnimation.AnimationType;
TemporaryAnimation = temporaryAnimation;
Priority = priority;
IsActive = true;
}
public void Update(float deltaTime)
{
expirationTimer -= deltaTime;
if (expirationTimer <= 0)
{
IsActive = false;
}
}
}
protected readonly Dictionary<AnimationType, AnimSwap> tempAnimations = new Dictionary<AnimationType, AnimSwap>();
protected readonly HashSet<AnimationType> expiredAnimations = new HashSet<AnimationType>();
public AnimationParams CurrentAnimationParams
{
@@ -87,7 +129,11 @@ namespace Barotrauma
}
public bool CanWalk => RagdollParams.CanWalk;
public bool IsMovingBackwards => !InWater && Math.Sign(targetMovement.X) == -Math.Sign(Dir) && CurrentAnimationParams is not FishGroundedParams { Flip: false };
public bool IsMovingBackwards =>
!InWater &&
Math.Sign(targetMovement.X) == -Math.Sign(Dir) &&
CurrentAnimationParams is not FishGroundedParams { Flip: false } &&
Anim != Animation.Climbing;
// TODO: define death anim duration in XML
protected float deathAnimTimer, deathAnimDuration = 5.0f;
@@ -177,8 +223,14 @@ namespace Barotrauma
public float WalkPos { get; protected set; }
public AnimController(Character character, string seed, RagdollParams ragdollParams = null) : base(character, seed, ragdollParams) { }
public void UpdateAnimations(float deltaTime)
{
UpdateTemporaryAnimations(deltaTime);
UpdateAnim(deltaTime);
}
public abstract void UpdateAnim(float deltaTime);
protected abstract void UpdateAnim(float deltaTime);
public abstract void DragCharacter(Character target, float deltaTime);
@@ -253,23 +305,26 @@ namespace Barotrauma
switch (type)
{
case AnimationType.Walk:
return WalkParams;
return CanWalk ? WalkParams : null;
case AnimationType.Run:
return RunParams;
return CanWalk ? RunParams : null;
case AnimationType.Crouch:
if (this is HumanoidAnimController humanAnimController)
{
return humanAnimController.HumanCrouchParams;
}
throw new NotImplementedException(type.ToString());
else
{
DebugConsole.ThrowError($"Animation params of type {type} not implemented for non-humanoids!");
return null;
}
case AnimationType.SwimSlow:
return SwimSlowParams;
case AnimationType.SwimFast:
return SwimFastParams;
case AnimationType.NotDefined:
return null;
default:
throw new NotImplementedException(type.ToString());
return null;
}
}
@@ -376,7 +431,7 @@ namespace Barotrauma
private Direction previousDirection;
private readonly Vector2[] transformedHandlePos = new Vector2[2];
//TODO: refactor this method, it's way too convoluted
public void HoldItem(float deltaTime, Item item, Vector2[] handlePos, Vector2 holdPos, Vector2 aimPos, bool aim, float holdAngle, float itemAngleRelativeToHoldAngle = 0.0f, bool aimMelee = false)
public void HoldItem(float deltaTime, Item item, Vector2[] handlePos, Vector2 itemPos, bool aim, float holdAngle, float itemAngleRelativeToHoldAngle = 0.0f, bool aimMelee = false, Vector2? targetPos = null)
{
aimingMelee = aimMelee;
if (character.Stun > 0.0f || character.IsIncapacitated)
@@ -385,22 +440,20 @@ namespace Barotrauma
}
//calculate the handle positions
Matrix itemTransfrom = Matrix.CreateRotationZ(item.body.Rotation);
transformedHandlePos[0] = Vector2.Transform(handlePos[0], itemTransfrom);
transformedHandlePos[1] = Vector2.Transform(handlePos[1], itemTransfrom);
Matrix itemTransform = Matrix.CreateRotationZ(item.body.Rotation);
transformedHandlePos[0] = Vector2.Transform(handlePos[0], itemTransform);
transformedHandlePos[1] = Vector2.Transform(handlePos[1], itemTransform);
Limb torso = GetLimb(LimbType.Torso) ?? MainLimb;
Limb leftHand = GetLimb(LimbType.LeftHand);
Limb rightHand = GetLimb(LimbType.RightHand);
Vector2 itemPos = aim ? aimPos : holdPos;
var controller = character.SelectedItem?.GetComponent<Controller>();
bool usingController = controller != null && !controller.AllowAiming;
bool usingController = controller is { AllowAiming: false };
if (!usingController)
{
controller = character.SelectedSecondaryItem?.GetComponent<Controller>();
usingController = controller != null && !controller.AllowAiming;
usingController = controller is { AllowAiming: false };
}
bool isClimbing = character.IsClimbing && Math.Abs(character.AnimController.TargetMovement.Y) > 0.01f;
float itemAngle;
@@ -408,15 +461,17 @@ namespace Barotrauma
float torsoRotation = torso.Rotation;
Item rightHandItem = character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand);
bool equippedInRightHand = rightHandItem == item && rightHand != null && !rightHand.IsSevered;
bool equippedInRightHand = rightHandItem == item && rightHand is { IsSevered: false };
Item leftHandItem = character.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand);
bool equippedInLefthand = leftHandItem == item && leftHand != null && !leftHand.IsSevered;
bool equippedInLeftHand = leftHandItem == item && leftHand is { IsSevered: false };
if (aim && !isClimbing && !usingController && character.Stun <= 0.0f && itemPos != Vector2.Zero && !character.IsIncapacitated)
{
Vector2 mousePos = ConvertUnits.ToSimUnits(character.SmoothedCursorPosition);
Vector2 diff = holdable.Aimable ?
(mousePos - AimSourceSimPos) * Dir :
targetPos ??= ConvertUnits.ToSimUnits(character.SmoothedCursorPosition);
Vector2 diff = holdable.Aimable ?
(targetPos.Value - AimSourceSimPos) * Dir :
MathUtils.RotatePoint(Vector2.UnitX, torsoRotation);
holdAngle = MathUtils.VectorToAngle(new Vector2(diff.X, diff.Y * Dir)) - torsoRotation * Dir;
holdAngle += GetAimWobble(rightHand, leftHand, item);
itemAngle = torsoRotation + holdAngle * Dir;
@@ -424,7 +479,7 @@ namespace Barotrauma
if (holdable.ControlPose)
{
//if holding two items that should control the characters' pose, let the item in the right hand do it
bool anotherItemControlsPose = equippedInLefthand && rightHandItem != item && (rightHandItem?.GetComponent<Holdable>()?.ControlPose ?? false);
bool anotherItemControlsPose = equippedInLeftHand && rightHandItem != item && (rightHandItem?.GetComponent<Holdable>()?.ControlPose ?? false);
if (!anotherItemControlsPose && TargetMovement == Vector2.Zero && inWater)
{
torso.body.AngularVelocity -= torso.body.AngularVelocity * 0.1f;
@@ -441,7 +496,7 @@ namespace Barotrauma
{
itemAngle = rightHand.Rotation + holdAngle * Dir;
}
else if (equippedInLefthand)
else if (equippedInLeftHand)
{
itemAngle = leftHand.Rotation + holdAngle * Dir;
}
@@ -465,7 +520,7 @@ namespace Barotrauma
transformedHoldPos = rightHand.PullJointWorldAnchorA - transformedHandlePos[0];
itemAngle = rightHand.Rotation + (holdAngle - rightHand.Params.GetSpriteOrientation() + MathHelper.PiOver2) * Dir;
}
else if (equippedInLefthand)
else if (equippedInLeftHand)
{
transformedHoldPos = leftHand.PullJointWorldAnchorA - transformedHandlePos[1];
itemAngle = leftHand.Rotation + (holdAngle - leftHand.Params.GetSpriteOrientation() + MathHelper.PiOver2) * Dir;
@@ -478,7 +533,7 @@ namespace Barotrauma
transformedHoldPos = rightShoulder.WorldAnchorA;
rightHand.Disabled = true;
}
if (equippedInLefthand)
if (equippedInLeftHand)
{
if (leftShoulder == null) { return; }
transformedHoldPos = leftShoulder.WorldAnchorA;
@@ -803,5 +858,260 @@ namespace Barotrauma
public void StopUsingItem() => StopAnimation(Animation.UsingItem);
public void StopClimbing() => StopAnimation(Animation.Climbing);
private readonly Dictionary<AnimationType, AnimationParams> defaultAnimations = new Dictionary<AnimationType, AnimationParams>();
/// <summary>
/// Loads an animation (variation) that automatically resets in 0.1s, unless triggered again.
/// Meant e.g. for triggering animations in status effects, without having to worry about resetting them.
/// </summary>
public bool TryLoadTemporaryAnimation(StatusEffect.AnimLoadInfo animLoadInfo, bool throwErrors)
{
AnimationType animType = animLoadInfo.Type;
if (tempAnimations.TryGetValue(animType, out AnimSwap animSwap))
{
if (animLoadInfo.File.TryGet(out string fileName) && animSwap.TemporaryAnimation.FileNameWithoutExtension.Equals(fileName, StringComparison.OrdinalIgnoreCase))
{
// Already loaded, keep active
animSwap.IsActive = true;
return true;
}
else if (animLoadInfo.File.TryGet(out ContentPath contentPath) && animSwap.TemporaryAnimation.Path == contentPath)
{
// Already loaded, keep active
animSwap.IsActive = true;
return true;
}
else
{
if (animSwap.Priority >= animLoadInfo.Priority)
{
// If the priority of the current animation is higher than the new animation, just return and do nothing.
// Returning false would tell the status effect to not try again, which is not what we want here, which is why we fake a bit with the return value.
return true;
}
else
{
// Override any previous animations of the same type.
tempAnimations.Remove(animType);
}
}
}
AnimationParams defaultAnimation = GetAnimationParamsFromType(animType);
if (defaultAnimation == null) { return false; }
if (!TryLoadAnimation(animType, animLoadInfo.File, out AnimationParams tempParams, throwErrors)) { return false; }
// Store the default animation, if not yet stored. There should always be just one of the same type.
defaultAnimations.TryAdd(animType, defaultAnimation);
tempAnimations.Add(animType, new AnimSwap(tempParams, animLoadInfo.Priority));
return true;
}
private void UpdateTemporaryAnimations(float deltaTime)
{
if (tempAnimations.None()) { return; }
foreach ((AnimationType animationType, AnimSwap animSwap) in tempAnimations)
{
if (!animSwap.IsActive)
{
if (defaultAnimations.TryGetValue(animSwap.AnimationType, out AnimationParams defaultAnimation))
{
TrySwapAnimParams(defaultAnimation);
expiredAnimations.Add(animationType);
}
else
{
DebugConsole.ThrowError($"[AnimController] Failed to find the default animation parameters of type {animSwap.AnimationType}. Cannot swap back the default animations!");
tempAnimations.Clear();
}
}
}
foreach (AnimationType anim in expiredAnimations)
{
tempAnimations.Remove(anim);
}
expiredAnimations.Clear();
foreach (AnimSwap animSwap in tempAnimations.Values)
{
animSwap.Update(deltaTime);
}
}
/// <summary>
/// Loads animations. Non-permanent (= resets on load).
/// </summary>
public bool TryLoadAnimation(AnimationType animationType, Either<string, ContentPath> file, out AnimationParams animParams, bool throwErrors)
{
animParams = null;
if (character.IsHumanoid && this is HumanoidAnimController humanAnimController)
{
switch (animationType)
{
case AnimationType.Walk:
humanAnimController.WalkParams = HumanWalkParams.GetAnimParams(character, file, throwErrors);
animParams = humanAnimController.WalkParams;
break;
case AnimationType.Run:
humanAnimController.RunParams = HumanRunParams.GetAnimParams(character, file, throwErrors);
animParams = humanAnimController.RunParams;
break;
case AnimationType.Crouch:
humanAnimController.HumanCrouchParams = HumanCrouchParams.GetAnimParams(character, file, throwErrors);
animParams = humanAnimController.HumanCrouchParams;
break;
case AnimationType.SwimSlow:
humanAnimController.SwimSlowParams = HumanSwimSlowParams.GetAnimParams(character, file, throwErrors);
animParams = humanAnimController.SwimSlowParams;
break;
case AnimationType.SwimFast:
humanAnimController.SwimFastParams = HumanSwimFastParams.GetAnimParams(character, file, throwErrors);
animParams = humanAnimController.SwimFastParams;
break;
default:
DebugConsole.ThrowError($"[AnimController] Animation of type {animationType} not implemented!");
break;
}
}
else
{
switch (animationType)
{
case AnimationType.Walk:
if (CanWalk)
{
character.AnimController.WalkParams = FishWalkParams.GetAnimParams(character, file, throwErrors);
animParams = character.AnimController.WalkParams;
}
break;
case AnimationType.Run:
if (CanWalk)
{
character.AnimController.RunParams = FishRunParams.GetAnimParams(character, file, throwErrors);
animParams = character.AnimController.RunParams;
}
break;
case AnimationType.SwimSlow:
character.AnimController.SwimSlowParams = FishSwimSlowParams.GetAnimParams(character, file, throwErrors);
animParams = character.AnimController.SwimSlowParams;
break;
case AnimationType.SwimFast:
character.AnimController.SwimFastParams = FishSwimFastParams.GetAnimParams(character, file, throwErrors);
animParams = character.AnimController.SwimFastParams;
break;
default:
DebugConsole.ThrowError($"[AnimController] Animation of type {animationType} not implemented!");
break;
}
}
bool success = animParams != null;
if (!file.TryGet(out string fileName))
{
if (file.TryGet(out ContentPath contentPath))
{
fileName = contentPath.Value;
if (success)
{
success = contentPath == animParams.Path;
}
}
}
else
{
if (success)
{
success = animParams.FileNameWithoutExtension.Equals(fileName, StringComparison.OrdinalIgnoreCase);
}
}
if (success)
{
DebugConsole.NewMessage($"Animation {fileName} successfully loaded for {character.DisplayName}", Color.LightGreen, debugOnly: true);
}
else if (throwErrors)
{
DebugConsole.ThrowError($"Animation {fileName} for {character.DisplayName} could not be loaded!");
}
return success;
}
/// <summary>
/// Simply swaps existing animation parameters as current parameters.
/// </summary>
protected bool TrySwapAnimParams(AnimationParams newParams)
{
AnimationType animationType = newParams.AnimationType;
if (character.IsHumanoid && this is HumanoidAnimController humanAnimController)
{
switch (animationType)
{
case AnimationType.Walk:
if (newParams is HumanWalkParams newWalkParams)
{
humanAnimController.WalkParams = newWalkParams;
}
return true;
case AnimationType.Run:
if (newParams is HumanRunParams newRunParams)
{
humanAnimController.HumanRunParams = newRunParams;
}
break;
case AnimationType.Crouch:
if (newParams is HumanCrouchParams newCrouchParams)
{
humanAnimController.HumanCrouchParams = newCrouchParams;
}
return true;
case AnimationType.SwimSlow:
if (newParams is HumanSwimSlowParams newSwimSlowParams)
{
humanAnimController.HumanSwimSlowParams = newSwimSlowParams;
}
return true;
case AnimationType.SwimFast:
if (newParams is HumanSwimFastParams newSwimFastParams)
{
humanAnimController.HumanSwimFastParams = newSwimFastParams;
}
return true;
default:
DebugConsole.ThrowError($"[AnimController] Animation of type {animationType} not implemented!");
return false;
}
}
else
{
switch (animationType)
{
case AnimationType.Walk:
if (newParams is FishWalkParams walkParams)
{
character.AnimController.WalkParams = walkParams;
}
return true;
case AnimationType.Run:
if (newParams is FishRunParams runParams)
{
character.AnimController.RunParams = runParams;
}
return true;
case AnimationType.SwimSlow:
if (newParams is FishSwimSlowParams swimSlowParams)
{
character.AnimController.SwimSlowParams = swimSlowParams;
}
return true;
case AnimationType.SwimFast:
if (newParams is FishSwimFastParams swimFastParams)
{
character.AnimController.SwimFastParams = swimFastParams;
}
return true;
default:
DebugConsole.ThrowError($"[AnimController] Animation of type {animationType} not implemented!");
break;
}
}
return false;
}
}
}
@@ -22,11 +22,7 @@ namespace Barotrauma
{
if (_ragdollParams == null)
{
_ragdollParams = FishRagdollParams.GetDefaultRagdollParams(character.SpeciesName);
if (!character.VariantOf.IsEmpty)
{
_ragdollParams.ApplyVariantScale(character.Params.VariantFile);
}
_ragdollParams = FishRagdollParams.GetDefaultRagdollParams(character);
}
return _ragdollParams;
}
@@ -133,7 +129,7 @@ namespace Barotrauma
public FishAnimController(Character character, string seed, FishRagdollParams ragdollParams = null) : base(character, seed, ragdollParams) { }
public override void UpdateAnim(float deltaTime)
protected override void UpdateAnim(float deltaTime)
{
//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; }
@@ -145,7 +141,7 @@ namespace Barotrauma
}
var mainLimb = MainLimb;
levitatingCollider = !IsHanging;
levitatingCollider = !IsHangingWithRope;
if (!character.CanMove)
{
@@ -1011,15 +1007,7 @@ namespace Barotrauma
{
//make sure the angle "has the same number of revolutions" as the reference limb
//(e.g. we don't want to rotate the legs to 0 if the torso is at 360, because that'd blow up the hip joints)
while (referenceLimb.Rotation - angle > MathHelper.TwoPi)
{
angle += MathHelper.TwoPi;
}
while (referenceLimb.Rotation - angle < -MathHelper.TwoPi)
{
angle -= MathHelper.TwoPi;
}
angle = referenceLimb.body.WrapAngleToSameNumberOfRevolutions(angle);
limb?.body.SmoothRotate(angle, torque, wrapAngle: false);
}
@@ -10,7 +10,7 @@ namespace Barotrauma
{
class HumanoidAnimController : AnimController
{
private const float SteepestWalkableSlopeAngleDegrees = 50f;
private const float SteepestWalkableSlopeAngleDegrees = 55f;
private const float SlowlyWalkableSlopeAngleDegrees = 30f;
private static readonly float SteepestWalkableSlopeNormalX =
@@ -36,7 +36,7 @@ namespace Barotrauma
{
if (_ragdollParams == null)
{
_ragdollParams = RagdollParams.GetDefaultRagdollParams<HumanRagdollParams>(character.SpeciesName);
_ragdollParams = HumanRagdollParams.GetDefaultRagdollParams(character);
}
return _ragdollParams;
}
@@ -248,12 +248,12 @@ namespace Barotrauma
GetLimb(footType).PullJointLocalAnchorA);
}
public override void UpdateAnim(float deltaTime)
protected override void UpdateAnim(float deltaTime)
{
if (Frozen) { return; }
if (MainLimb == null) { return; }
levitatingCollider = !IsHanging;
levitatingCollider = !IsHangingWithRope;
if (onGround && character.CanMove)
{
if ((character.SelectedItem?.GetComponent<Controller>()?.ControlCharacterPose ?? false) ||
@@ -396,7 +396,9 @@ namespace Barotrauma
if (SimplePhysicsEnabled)
{
UpdateStandingSimple();
IsHanging = false;
StopHangingWithRope();
StopHoldingToRope();
StopGettingDraggedWithRope();
return;
}
@@ -490,7 +492,21 @@ namespace Barotrauma
aiming = false;
wasAimingMelee = aimingMelee;
aimingMelee = false;
IsHanging = IsHanging && character.IsRagdolled;
if (!shouldHangWithRope)
{
StopHangingWithRope();
}
if (!shouldHoldToRope)
{
StopHoldingToRope();
}
if (!shouldBeDraggedWithRope)
{
StopGettingDraggedWithRope();
}
shouldHoldToRope = false;
shouldHangWithRope = false;
shouldBeDraggedWithRope = false;
}
void UpdateStanding()
@@ -686,6 +702,16 @@ namespace Barotrauma
if (!onGround)
{
const float MaxFootVelocityDiff = 5.0f;
const float MaxFootVelocityDiffSqr = MaxFootVelocityDiff * MaxFootVelocityDiff;
//if the feet have a significantly different velocity from the main limb, try moving them back to a neutral pose below the torso
//this can happen e.g. when jumping over an obstacle: the feet can have a large upwards velocity during the walk/run animation,
//and just "letting go of the animations" would let them keep moving upwards, twisting the character to a weird pose
if ((leftFoot != null && (MainLimb.LinearVelocity - leftFoot.LinearVelocity).LengthSquared() > MaxFootVelocityDiffSqr) ||
(rightFoot != null && (MainLimb.LinearVelocity - rightFoot.LinearVelocity).LengthSquared() > MaxFootVelocityDiffSqr))
{
UpdateFallingProne(10.0f, moveHands: false, moveTorso: false, moveLegs: true);
}
return;
}
@@ -786,7 +812,12 @@ namespace Barotrauma
}
else
{
footPos = new Vector2(colliderPos.X + stepSize.X * i * 0.2f, colliderPos.Y - 0.1f);
float footPosX = stepSize.X * i * 0.2f;
if (CurrentGroundedParams.StepSizeWhenStanding != Vector2.Zero)
{
footPosX = Math.Sign(stepSize.X) * CurrentGroundedParams.StepSizeWhenStanding.X * i;
}
footPos = new Vector2(colliderPos.X + footPosX, colliderPos.Y - 0.1f);
}
if (Stairs == null && !onSlopeThatMakesSlow)
{
@@ -1519,6 +1550,8 @@ namespace Barotrauma
{
torso.body.ApplyLinearImpulse(new Vector2(0, -20f), maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
targetTorso.body.ApplyLinearImpulse(new Vector2(0, -20f), maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
//the pumping animation can sometimes cause impact damage, prevent that by briefly disabling it
target.DisableImpactDamageTimer = 0.15f;
cprPumpTimer = 0;
if (skill < CPRSettings.Active.DamageSkillThreshold)
@@ -1727,7 +1760,7 @@ namespace Barotrauma
if (targetLimb.type == LimbType.Torso || targetLimb == target.AnimController.MainLimb)
{
pullLimb.PullJointMaxForce = 5000.0f;
if (!character.HasAbilityFlag(AbilityFlags.MoveNormallyWhileDragging))
if (!character.CanRunWhileDragging())
{
targetMovement *= MathHelper.Clamp(Mass / target.Mass, 0.5f, 1.0f);
}
@@ -1811,7 +1844,7 @@ namespace Barotrauma
}
//limit movement if moving away from the target
if (!character.HasAbilityFlag(AbilityFlags.MoveNormallyWhileDragging) && Vector2.Dot(target.WorldPosition - WorldPosition, targetMovement) < 0)
if (!character.CanRunWhileDragging() && Vector2.Dot(target.WorldPosition - WorldPosition, targetMovement) < 0)
{
targetMovement *= MathHelper.Clamp(1.5f - dist, 0.0f, 1.0f);
}
@@ -70,8 +70,8 @@ namespace Barotrauma
public bool Frozen
{
get { return frozen; }
set
{
set
{
if (frozen == value) return;
frozen = value;
@@ -81,7 +81,7 @@ namespace Barotrauma
Collider.FarseerBody.IgnoreGravity = frozen;
//Collider.PhysEnabled = !frozen;
if (frozen && MainLimb != null) MainLimb.PullJointWorldAnchorB = MainLimb.SimPosition;
if (frozen && MainLimb != null) { MainLimb.PullJointWorldAnchorB = MainLimb.SimPosition; }
}
}
@@ -109,12 +109,12 @@ namespace Barotrauma
//a movement vector that overrides targetmovement if trying to steer
//a Character to the position sent by server in multiplayer mode
protected Vector2 overrideTargetMovement;
protected float floorY, standOnFloorY;
protected Fixture floorFixture;
protected Vector2 floorNormal = Vector2.UnitY;
protected float surfaceY;
protected bool inWater, headInWater;
protected bool onGround;
public bool OnGround => onGround;
@@ -128,7 +128,7 @@ namespace Barotrauma
public float ColliderHeightFromFloor => ConvertUnits.ToSimUnits(RagdollParams.ColliderHeightFromFloor) * RagdollParams.JointScale;
public Structure Stairs;
protected Direction dir;
public Direction TargetDir;
@@ -153,7 +153,7 @@ namespace Barotrauma
collider = null;
try
{
collider = this.collider?[index];
collider = this.collider?[index];
return true;
}
catch
@@ -322,7 +322,7 @@ namespace Barotrauma
impactTolerance = RagdollParams.ImpactTolerance;
if (character.Params.VariantFile != null)
{
float? tolerance = character.Params.VariantFile.Root.GetChildElement("ragdoll")?.GetAttributeFloat("impacttolerance", impactTolerance.Value);
float? tolerance = character.Params.VariantFile.GetRootExcludingOverride().GetChildElement("ragdoll")?.GetAttributeFloat("impacttolerance", impactTolerance.Value);
if (tolerance.HasValue)
{
impactTolerance = tolerance;
@@ -334,7 +334,8 @@ namespace Barotrauma
}
public bool Draggable => RagdollParams.Draggable;
public bool CanEnterSubmarine => RagdollParams.CanEnterSubmarine;
public CanEnterSubmarine CanEnterSubmarine => RagdollParams.CanEnterSubmarine;
public float Dir => dir == Direction.Left ? -1.0f : 1.0f;
@@ -384,6 +385,10 @@ namespace Barotrauma
if (ragdollParams != null)
{
RagdollParams = ragdollParams;
if (!character.VariantOf.IsEmpty)
{
RagdollParams.TryApplyVariantScale(character.Params.VariantFile);
}
}
else
{
@@ -696,21 +701,34 @@ namespace Barotrauma
public bool OnLimbCollision(Fixture f1, Fixture f2, Contact contact)
{
if (f2.Body.UserData is Submarine && character.Submarine == (Submarine)f2.Body.UserData) { return false; }
if (f2.UserData is Hull && character.Submarine != null) { return false; }
if (f2.Body.UserData is Submarine submarine && character.Submarine == submarine) { return false; }
if (f2.UserData is Hull)
{
if (character.Submarine != null)
{
return false;
}
if (CanEnterSubmarine == CanEnterSubmarine.Partial)
{
//collider collides with hulls to prevent the character going fully inside the sub, limbs don't
return
f1.Body == Collider.FarseerBody ||
(f1.Body.UserData is Limb limb && !limb.Params.CanEnterSubmarine);
}
}
//using the velocity of the limb would make the impact damage more realistic,
//but would also make it harder to edit the animations because the forces/torques
//would all have to be balanced in a way that prevents the character from doing
//impact damage to itself
Vector2 velocity = Collider.LinearVelocity;
if (character.Submarine == null && f2.Body.UserData is Submarine)
if (character.Submarine == null && f2.Body.UserData is Submarine sub)
{
velocity -= ((Submarine)f2.Body.UserData).Velocity;
velocity -= sub.Velocity;
}
//always collides with bodies other than structures
if (!(f2.Body.UserData is Structure structure))
if (f2.Body.UserData is not Structure structure)
{
if (!f2.IsSensor)
{
@@ -1021,7 +1039,7 @@ namespace Barotrauma
{
foreach (Ragdoll r in list)
{
r.Update(deltaTime, cam);
r.UpdateRagdoll(deltaTime, cam);
}
}
@@ -1041,7 +1059,8 @@ namespace Barotrauma
if (newHull == currentHull) { return; }
if (!CanEnterSubmarine || (character.AIController != null && !character.AIController.CanEnterSubmarine))
if (CanEnterSubmarine == CanEnterSubmarine.False ||
(character.AIController != null && character.AIController.CanEnterSubmarine == CanEnterSubmarine.False))
{
//character is inside the sub even though it shouldn't be able to enter -> teleport it out
@@ -1066,6 +1085,11 @@ namespace Barotrauma
}
}
if (CanEnterSubmarine != CanEnterSubmarine.True)
{
return;
}
if (setSubmarine)
{
//in -> out
@@ -1075,13 +1099,13 @@ namespace Barotrauma
if (Gap.FindAdjacent(Gap.GapList.Where(g => g.Submarine == currentHull.Submarine), findPos, 150.0f) != null) { return; }
if (Limbs.Any(l => Gap.FindAdjacent(currentHull.ConnectedGaps, l.WorldPosition, ConvertUnits.ToDisplayUnits(l.body.GetSize().Combine())) != null)) { return; }
character.MemLocalState?.Clear();
Teleport(ConvertUnits.ToSimUnits(currentHull.Submarine.Position), currentHull.Submarine.Velocity, detachProjectiles: false);
Teleport(ConvertUnits.ToSimUnits(currentHull.Submarine.Position), currentHull.Submarine.Velocity);
}
//out -> in
else if (currentHull == null && newHull.Submarine != null)
{
character.MemLocalState?.Clear();
Teleport(-ConvertUnits.ToSimUnits(newHull.Submarine.Position), -newHull.Submarine.Velocity, detachProjectiles: false);
Teleport(-ConvertUnits.ToSimUnits(newHull.Submarine.Position), -newHull.Submarine.Velocity);
}
//from one sub to another
else if (newHull != null && currentHull != null && newHull.Submarine != currentHull.Submarine)
@@ -1089,7 +1113,7 @@ namespace Barotrauma
character.MemLocalState?.Clear();
Vector2 newSubPos = newHull.Submarine == null ? Vector2.Zero : newHull.Submarine.Position;
Vector2 prevSubPos = currentHull.Submarine == null ? Vector2.Zero : currentHull.Submarine.Position;
Teleport(ConvertUnits.ToSimUnits(prevSubPos - newSubPos), Vector2.Zero, detachProjectiles: false);
Teleport(ConvertUnits.ToSimUnits(prevSubPos - newSubPos), Vector2.Zero);
}
}
@@ -1156,7 +1180,7 @@ namespace Barotrauma
character.DisableImpactDamageTimer = 0.25f;
SetPosition(Collider.SimPosition + moveAmount, detachProjectiles: detachProjectiles);
SetPosition(Collider.SimPosition + moveAmount);
character.CursorPosition += moveAmount;
Collider?.UpdateDrawPosition();
@@ -1221,7 +1245,7 @@ namespace Barotrauma
public bool forceStanding;
public bool forceNotStanding;
public void Update(float deltaTime, Camera cam)
public void UpdateRagdoll(float deltaTime, Camera cam)
{
if (!character.Enabled || character.Removed || Frozen || Invalid || Collider == null || Collider.Removed) { return; }
@@ -1389,7 +1413,8 @@ namespace Barotrauma
if (floorNormal.Y is > 0f and < 1f
&& Math.Sign(movement.X) == Math.Sign(floorNormal.X))
{
slopePull = Math.Abs(movement.X * floorNormal.X / floorNormal.Y) / LevitationSpeedMultiplier;
float steepness = Math.Abs(floorNormal.X);
slopePull = Math.Abs(movement.X * steepness) / LevitationSpeedMultiplier;
}
if (Math.Abs(Collider.SimPosition.Y - targetY - slopePull) > 0.01f)
@@ -1743,7 +1768,7 @@ namespace Barotrauma
return closestFraction;
}, rayStart, rayEnd, Physics.CollisionStairs | Physics.CollisionPlatform | Physics.CollisionWall | Physics.CollisionLevel);
if (standOnFloorFixture != null && !IsHanging)
if (standOnFloorFixture != null && !IsHangingWithRope)
{
floorFixture = standOnFloorFixture;
standOnFloorY = rayStart.Y + (rayEnd.Y - rayStart.Y) * standOnFloorFraction;
@@ -1845,7 +1870,7 @@ namespace Barotrauma
return (surfaceY, ceilingY);
}
public void SetPosition(Vector2 simPosition, bool lerp = false, bool ignorePlatforms = true, bool forceMainLimbToCollider = false, bool detachProjectiles = true)
public void SetPosition(Vector2 simPosition, bool lerp = false, bool ignorePlatforms = true, bool forceMainLimbToCollider = false, bool moveLatchers = true)
{
if (!MathUtils.IsValid(simPosition))
{
@@ -1858,10 +1883,14 @@ namespace Barotrauma
}
if (MainLimb == null) { return; }
Vector2 limbMoveAmount = forceMainLimbToCollider ? simPosition - MainLimb.SimPosition : simPosition - Collider.SimPosition;
// A Work-around for an issue with teleporting the characters:
// Detach every latcher when either one of the latchers or the target is teleported,
// because otherwise all the characters are teleported to invalid positions.
if (Character.AIController is EnemyAIController enemyAI && enemyAI.LatchOntoAI != null && enemyAI.LatchOntoAI.IsAttached)
// because otherwise all the characters are teleported to invalid positions.
const float ForceDeattachThreshold = 10.0f;
if (limbMoveAmount.LengthSquared() > ForceDeattachThreshold * ForceDeattachThreshold &&
Character.AIController is EnemyAIController enemyAI && enemyAI.LatchOntoAI != null && enemyAI.LatchOntoAI.IsAttached)
{
var target = enemyAI.LatchOntoAI.TargetCharacter;
if (target != null)
@@ -1874,7 +1903,6 @@ namespace Barotrauma
Character.Latchers.ForEachMod(l => l?.DeattachFromBody(reset: true));
Character.Latchers.Clear();
Vector2 limbMoveAmount = forceMainLimbToCollider ? simPosition - MainLimb.SimPosition : simPosition - Collider.SimPosition;
if (lerp)
{
Collider.TargetPosition = simPosition;
@@ -1896,15 +1924,62 @@ namespace Barotrauma
}
}
}
/// <summary>
/// Is attached to something with a rope.
/// </summary>
public bool IsHoldingToRope { get; private set; }
protected bool shouldHoldToRope;
public bool IsHanging { get; protected set; }
/// <summary>
/// Is hanging to something with a rope, so that can reel towards it. Currently only possible in water.
/// </summary>
public bool IsHangingWithRope { get; private set; }
protected bool shouldHangWithRope;
/// <summary>
/// Has someone attached to the character with a rope?
/// </summary>
public bool IsDraggedWithRope { get; private set; }
protected bool shouldBeDraggedWithRope;
public void Hang()
public void HangWithRope()
{
shouldHangWithRope = true;
IsHangingWithRope = true;
ResetPullJoints();
onGround = false;
levitatingCollider = false;
IsHanging = true;
}
public void HoldToRope()
{
shouldHoldToRope = true;
IsHoldingToRope = true;
}
public void DragWithRope()
{
shouldBeDraggedWithRope = true;
IsDraggedWithRope = true;
}
protected void StopHangingWithRope()
{
shouldHangWithRope = false;
IsHangingWithRope = false;
}
protected void StopHoldingToRope()
{
shouldHoldToRope = false;
IsHoldingToRope = false;
}
protected void StopGettingDraggedWithRope()
{
shouldBeDraggedWithRope = false;
IsDraggedWithRope = false;
}
protected void TrySetLimbPosition(Limb limb, Vector2 original, Vector2 simPosition, float rotation, bool lerp = false, bool ignorePlatforms = true)
@@ -37,7 +37,9 @@ namespace Barotrauma
FallBackUntilCanAttack,
PursueIfCanAttack,
Pursue,
Eat,
FollowThrough,
FollowThroughWithoutObstacleAvoidance,
FollowThroughUntilCanAttack,
IdleUntilCanAttack,
Reverse,
@@ -104,6 +106,13 @@ namespace Barotrauma
[Serialize(0f, IsPropertySaveable.Yes, description: "A delay before reacting after performing an attack."), Editable]
public float AfterAttackDelay { get; set; }
[Serialize(AIBehaviorAfterAttack.FallBack, IsPropertySaveable.Yes,
description: "Secondary AI behavior after the attack. The character first executes the AfterAttack behavior, then after AfterAttackSecondaryDelay passes, switches to this one. Ignored if AfterAttackSecondaryDelay is 0 or less."), Editable]
public AIBehaviorAfterAttack AfterAttackSecondary { get; set; }
[Serialize(0.0f, IsPropertySaveable.Yes, description: "How long the character executes the AfterAttack before switching to AfterAttackSecondary. The secondary behavior is ignored if this value is 0 or less."), Editable]
public float AfterAttackSecondaryDelay { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Should the AI try to turn around when aiming with this attack?"), Editable]
public bool Reverse { get; private set; }
@@ -135,10 +144,11 @@ namespace Barotrauma
[Serialize(0.25f, IsPropertySaveable.Yes, description: "An approximation of the attack duration. Effectively defines the time window in which the hit can be registered. If set to too low value, it's possible that the attack won't hit the target in time."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f, DecimalCount = 2)]
public float Duration { get; private set; }
[Serialize(5f, IsPropertySaveable.Yes, description: "How long the AI waits between the attacks."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 2)]
[Serialize(5f, IsPropertySaveable.Yes, description: "How long the AI must wait before it can use this attack again."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 2)]
public float CoolDown { get; set; } = 5;
[Serialize(0f, IsPropertySaveable.Yes, description: "Used as the attack cooldown between different kind of attacks. Does not have effect, if set to 0."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 2)]
[Serialize(0f, IsPropertySaveable.Yes, description: "When the attack cooldown is running and when there are other valid attacks possible for the character to use, the secondary cooldown is used instead of the regular cooldown. Does not have an effect, if set to 0 or less than the regular cooldown value."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 2)]
public float SecondaryCoolDown { get; set; } = 0;
[Serialize(0f, IsPropertySaveable.Yes, description: "A random factor applied to all cooldowns. Example: 0.1 -> adds a random value between -10% and 10% of the cooldown. Min 0 (default), Max 1 (could disable or double the cooldown in extreme cases)."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)]
@@ -155,6 +165,9 @@ namespace Barotrauma
set => _structureDamage = value;
}
[Serialize(false, IsPropertySaveable.Yes, description: "If the attack causes an explosion of wall damage shrapnel, should some of the shrapnel be launched as projectiles that can go through walls?"), Editable]
public bool CreateWallDamageProjectiles { get; private set; }
[Serialize(true, IsPropertySaveable.Yes, description: "Whether or not damaging structures with the attack causes damage particles to emit."), Editable]
public bool EmitStructureDamageParticles { get; private set; }
@@ -443,6 +456,12 @@ namespace Barotrauma
break;
}
}
if (SecondaryCoolDown > CoolDown)
{
DebugConsole.AddWarning($"Potentially misconfigured attack in {parentDebugName}. Secondary cooldown should not be longer than the primary cooldown.",
contentPackage: element.ContentPackage);
}
}
partial void InitProjSpecific(ContentXElement element);
@@ -461,11 +480,6 @@ namespace Barotrauma
}
affliction = afflictionPrefab.Instantiate(0.0f);
affliction.Deserialize(subElement);
//backwards compatibility
if (subElement.GetAttribute("amount") != null && subElement.GetAttribute("strength") == null)
{
affliction.Strength = subElement.GetAttributeFloat("amount", 0.0f);
}
// add the affliction anyway, so that it can be shown in the editor.
Afflictions.Add(affliction, subElement);
}
@@ -708,6 +722,8 @@ namespace Barotrauma
public float SecondaryCoolDownTimer { get; set; }
public bool IsRunning { get; private set; }
public float AfterAttackTimer { get; set; }
public void UpdateCoolDown(float deltaTime)
{
CoolDownTimer -= deltaTime;
@@ -729,6 +745,7 @@ namespace Barotrauma
public void ResetAttackTimer()
{
AfterAttackTimer = 0;
AttackTimer = 0;
IsRunning = false;
}
@@ -147,8 +147,8 @@ namespace Barotrauma
/// <summary>
/// Is the character player or does it have an active ship command manager (an AI controlled sub)? Bots in the player team are not treated as commanders.
/// </summary>
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 IsCommanding => IsPlayer || AIController is HumanAIController { ShipCommandManager.Active: true };
public bool IsBot => !IsPlayer && AIController is HumanAIController { Enabled: true };
public bool IsEscorted { get; set; }
public Identifier JobIdentifier => Info?.Job?.Prefab.Identifier ?? Identifier.Empty;
@@ -349,7 +349,13 @@ namespace Barotrauma
public bool IsFriendlyNPCTurnedHostile => originalTeamID == CharacterTeamType.FriendlyNPC && teamID == CharacterTeamType.Team2;
public bool IsInstigator => CombatAction != null && CombatAction.IsInstigator;
public bool IsInstigator => CombatAction is { IsInstigator: true };
/// <summary>
/// Set true only, if the character is turned hostile from an escort mission (See <see cref="EscortMission"/>).
/// </summary>
public bool IsHostileEscortee;
public CombatAction CombatAction;
public readonly AnimController AnimController;
@@ -393,8 +399,10 @@ namespace Barotrauma
public readonly CharacterPrefab Prefab;
public readonly CharacterParams Params;
public Identifier SpeciesName => Params?.SpeciesName ?? "null".ToIdentifier();
public Identifier GetBaseCharacterSpeciesName() => Prefab.GetBaseCharacterSpeciesName(SpeciesName);
public Identifier Group => HumanPrefab is HumanPrefab humanPrefab && !humanPrefab.Group.IsEmpty ? humanPrefab.Group : Params.Group;
@@ -444,6 +452,17 @@ namespace Barotrauma
set => Params.Visibility = value;
}
public float MaxPerceptionDistance
{
get => Params.AI?.MaxPerceptionDistance ?? 0;
set
{
if (Params.AI != null)
{
Params.AI.MaxPerceptionDistance = value;
}
}
}
public bool IsTraitor
{
get;
@@ -583,9 +602,6 @@ namespace Barotrauma
public CharacterInventory Inventory { get; private set; }
private Color speechBubbleColor;
private float speechBubbleTimer;
/// <summary>
/// Prevents the character from interacting with items or characters
/// </summary>
@@ -685,7 +701,8 @@ namespace Barotrauma
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 })
var mpCampaign = GameMain.GameSession.Campaign as MultiPlayerCampaign;
if (mpCampaign != null && GameMain.Server is { ServerSettings: { } settings })
{
switch (settings.LootedMoneyDestination)
{
@@ -698,16 +715,28 @@ namespace Barotrauma
}
}
GameServer.Log($"{GameServer.CharacterLogName(this)} grabbed {value.Name}'s body and received {grabbedWallet.Balance} mk.", ServerLog.MessageType.Money);
grabbedWallet.Deduct(balance);
//we need to save the grabbed character's wallet this at this point to ensure
//the client doesn't get to keep the money if they respawn
if (mpCampaign != null && selectedCharacter.Info != null)
{
var characterCampaignData = mpCampaign?.GetCharacterData(selectedCharacter.Info);
if (characterCampaignData!= null)
{
characterCampaignData.WalletData = grabbedWallet.Save();
characterCampaignData?.ApplyWalletData(selectedCharacter);
}
}
#elif CLIENT
if (GameMain.GameSession.Campaign is SinglePlayerCampaign spCampaign)
{
spCampaign.Bank.Give(balance);
}
#endif
grabbedWallet.Deduct(balance);
#endif
}
}
}
@@ -738,6 +767,25 @@ namespace Barotrauma
if (item2 != null && item2 != item1) { yield return item2; }
}
}
public bool IsDualWieldingRangedWeapons()
{
int rangedItemCount = 0;
foreach (var item in HeldItems)
{
if (item.GetComponent<RangedWeapon>() != null)
{
rangedItemCount++;
}
if (rangedItemCount > 1)
{
return true;
}
}
return false;
}
private float lowPassMultiplier;
public float LowPassMultiplier
@@ -844,7 +892,7 @@ namespace Barotrauma
public float Stun
{
get { return IsRagdolled && !AnimController.IsHanging ? 1.0f : CharacterHealth.Stun; }
get { return IsRagdolled && !AnimController.IsHangingWithRope ? 1.0f : CharacterHealth.Stun; }
set
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
@@ -1093,7 +1141,7 @@ namespace Barotrauma
{
get
{
return (SelectedItem == null || SelectedItem.GetComponent<Controller>() is { AllowAiming: true }) && !IsIncapacitated && !IsRagdolled;
return (SelectedItem == null || SelectedItem.GetComponent<Controller>() is { AllowAiming: true }) && !IsIncapacitated && (!IsRagdolled || AnimController.IsHoldingToRope);
}
}
@@ -1350,7 +1398,7 @@ namespace Barotrauma
}
if (Params.VariantFile != null && Params.MainElement is ContentXElement paramsMainElement)
{
var overrideElement = Params.VariantFile.Root.FromPackage(paramsMainElement.ContentPackage);
var overrideElement = Params.VariantFile.GetRootExcludingOverride().FromPackage(paramsMainElement.ContentPackage);
// Only override if the override file contains matching elements
if (overrideElement.GetChildElement("inventory") != null)
{
@@ -1432,7 +1480,7 @@ namespace Barotrauma
if (ragdollParams == null && prefab.VariantOf == null)
{
Identifier name = Params.UseHuskAppendage ? nonHuskedSpeciesName : speciesName;
ragdollParams = IsHumanoid ? RagdollParams.GetDefaultRagdollParams<HumanRagdollParams>(name) : RagdollParams.GetDefaultRagdollParams<FishRagdollParams>(name) as RagdollParams;
ragdollParams = IsHumanoid ? RagdollParams.GetDefaultRagdollParams<HumanRagdollParams>(name, Params, Prefab.ContentPackage) : RagdollParams.GetDefaultRagdollParams<FishRagdollParams>(name, Params, Prefab.ContentPackage);
}
if (Params.HasInfo && info == null)
{
@@ -1818,9 +1866,18 @@ namespace Barotrauma
// - dragging someone
// - crouching
// - moving backwards
public bool CanRun => (SelectedCharacter == null || !SelectedCharacter.CanBeDragged || HasAbilityFlag(AbilityFlags.MoveNormallyWhileDragging)) &&
(!(AnimController is HumanoidAnimController) || !((HumanoidAnimController)AnimController).Crouching) &&
!AnimController.IsMovingBackwards && !HasAbilityFlag(AbilityFlags.MustWalk);
public bool CanRun => CanRunWhileDragging() &&
AnimController is not HumanoidAnimController { Crouching: true } &&
!AnimController.IsMovingBackwards && !HasAbilityFlag(AbilityFlags.MustWalk) &&
!AnimController.IsHoldingToRope;
public bool CanRunWhileDragging()
{
if (selectedCharacter == null || !selectedCharacter.CanBeDragged) { return true; }
//if the dragged character is conscious, don't allow running (the dragged character won't keep up, and the dragging gets interrupted)
if (!selectedCharacter.IsIncapacitated && selectedCharacter.Stun <= 0.0f) { return false; }
return HasAbilityFlag(AbilityFlags.MoveNormallyWhileDragging);
}
public Vector2 ApplyMovementLimits(Vector2 targetMovement, float currentSpeed)
{
@@ -2212,6 +2269,25 @@ namespace Barotrauma
if (Inventory != null)
{
if (IsKeyHit(InputType.DropItem))
{
foreach (Item item in HeldItems)
{
if (!CanInteractWith(item)) { continue; }
if (SelectedItem?.OwnInventory != null && SelectedItem.OwnInventory.CanBePut(item))
{
SelectedItem.OwnInventory.TryPutItem(item, this);
}
else
{
item.Drop(this);
}
//only drop one held item per key hit
break;
}
}
bool CanUseItemsWhenSelected(Item item) => item == null || !item.Prefab.DisableItemUsageWhenSelected;
if (CanUseItemsWhenSelected(SelectedItem) && CanUseItemsWhenSelected(SelectedSecondaryItem))
{
@@ -2525,7 +2601,7 @@ namespace Barotrauma
if (container != null)
{
if (!container.HasRequiredItems(this, addMessage: false)) { return false; }
if (!container.DrawInventory) { return false; }
if (!container.AllowAccess) { return false; }
}
}
return true;
@@ -2587,10 +2663,7 @@ namespace Barotrauma
if (itemPriority <= 0) { continue; }
Vector2 itemPos = (rootInventoryOwner ?? item).WorldPosition;
Vector2 refPos = positionalReference != null ? positionalReference.WorldPosition : WorldPosition;
float yDist = Math.Abs(refPos.Y - itemPos.Y);
yDist = yDist > 100 ? yDist * 5 : 0;
float dist = Math.Abs(refPos.X - itemPos.X) + yDist;
float distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, maxItemDistance, dist));
float distanceFactor = AIObjective.GetDistanceFactor(refPos, itemPos, verticalDistanceMultiplier: 5, maxDistance: maxItemDistance, factorAtMaxDistance: 0);
itemPriority *= distanceFactor;
if (itemPriority > _selectedItemPriority)
{
@@ -2881,11 +2954,22 @@ namespace Barotrauma
//disable aim assist when rewiring to make it harder to accidentally select items when adding wire nodes
aimAssist = 0.0f;
}
focusedItem = CanInteract ? FindItemAtPosition(mouseSimPos, aimAssist) : null;
if (focusedItem != null && focusedItem.CampaignInteractionType != CampaignMode.InteractionType.None)
UpdateInteractablesInRange();
if (!ShowInteractionLabels) // show labels handles setting the focused item in CharacterHUD, so we can click on them boxes
{
FocusedCharacter = null;
focusedItem = CanInteract ? FindClosestItem(interactablesInRange, mouseSimPos, aimAssist) : null;
}
if (focusedItem != null)
{
if (focusedItem.CampaignInteractionType != CampaignMode.InteractionType.None ||
/*pets' "play" interaction can interfere with interacting with items, so let's remove focus from the pet if the cursor is closer to a highlighted item*/
FocusedCharacter is { IsPet: true } && Vector2.DistanceSquared(focusedItem.SimPosition, mouseSimPos) < Vector2.DistanceSquared(FocusedCharacter.SimPosition, mouseSimPos))
{
FocusedCharacter = null;
}
}
findFocusedTimer = 0.05f;
}
@@ -3068,7 +3152,7 @@ namespace Barotrauma
{
if (!c.Enabled || c.AnimController.Frozen) continue;
c.AnimController.UpdateAnim(deltaTime);
c.AnimController.UpdateAnimations(deltaTime);
}
}
@@ -3078,7 +3162,7 @@ namespace Barotrauma
{
foreach (Character c in CharacterList)
{
if (!(c is AICharacter) && !c.IsRemotePlayer) continue;
if (c is not AICharacter && !c.IsRemotePlayer) { continue; }
if (c.IsPlayer || (c.IsBot && !c.IsDead))
{
@@ -3136,8 +3220,14 @@ namespace Barotrauma
System.Diagnostics.Debug.Assert(character != null && !character.Removed);
character.Update(deltaTime, cam);
}
#if CLIENT
UpdateSpeechBubbles(deltaTime);
#endif
}
static partial void UpdateSpeechBubbles(float deltaTime);
public virtual void Update(float deltaTime, Camera cam)
{
UpdateProjSpecific(deltaTime, cam);
@@ -3175,8 +3265,6 @@ namespace Barotrauma
PreviousHull = CurrentHull;
CurrentHull = Hull.FindHull(WorldPosition, CurrentHull, useWorldCoordinates: true);
speechBubbleTimer = Math.Max(0.0f, speechBubbleTimer - deltaTime);
obstructVisionAmount = Math.Max(obstructVisionAmount - deltaTime, 0.0f);
if (Inventory != null)
@@ -3335,11 +3423,14 @@ namespace Barotrauma
else if (!tooFastToUnragdoll)
{
IsRagdolled = IsKeyDown(InputType.Ragdoll); //Handle this here instead of Control because we can stop being ragdolled ourselves
if (wasRagdolled != IsRagdolled) { ragdollingLockTimer = 0.2f; }
if (wasRagdolled != IsRagdolled && !AnimController.IsHangingWithRope)
{
ragdollingLockTimer = 0.2f;
}
}
SetInput(InputType.Ragdoll, false, IsRagdolled);
}
if (!wasRagdolled && IsRagdolled)
if (!wasRagdolled && IsRagdolled && !AnimController.IsHangingWithRope)
{
CheckTalents(AbilityEffectType.OnRagdoll);
}
@@ -3599,7 +3690,7 @@ namespace Barotrauma
Identifier despawnContainerId =
IsHuman ?
"despawncontainer".ToIdentifier() :
Tags.DespawnContainer :
Params.DespawnContainer;
if (!despawnContainerId.IsEmpty)
{
@@ -3627,6 +3718,14 @@ namespace Barotrauma
var itemContainer = item?.GetComponent<ItemContainer>();
if (itemContainer == null) { return; }
List<Item> inventoryItems = new List<Item>(Inventory.AllItemsMod);
//unequipping genetic materials normally destroys it in GeneticMaterial.Update, let's do that manually here
var geneticMaterials = Inventory.FindAllItems(it => it.GetComponent<GeneticMaterial>() != null, recursive: true);
foreach (var geneticMaterial in geneticMaterials)
{
geneticMaterial.ApplyStatusEffects(ActionType.OnSevered, 1.0f, this);
}
foreach (Item inventoryItem in inventoryItems)
{
if (!itemContainer.Inventory.TryPutItem(inventoryItem, user: null, createNetworkEvent: createNetworkEvents))
@@ -3677,13 +3776,12 @@ namespace Barotrauma
float minRange = Math.Clamp((float)Math.Sqrt(Mass) * Visibility, 250, 1000);
float massFactor = (float)Math.Sqrt(Mass / 20);
float targetRange = Math.Min(minRange + massFactor * AnimController.Collider.LinearVelocity.Length() * 2 * Visibility, maxAIRange);
targetRange *= 1.0f + GetStatValue(StatTypes.SightRangeMultiplier);
float newRange = MathHelper.SmoothStep(aiTarget.SightRange, targetRange, deltaTime * aiTargetChangeSpeed);
newRange *= 1.0f + GetStatValue(StatTypes.SightRangeMultiplier);
if (!float.IsNaN(newRange))
{
aiTarget.SightRange = newRange;
}
}
}
private void UpdateSoundRange(float deltaTime)
@@ -3697,8 +3795,8 @@ namespace Barotrauma
{
float massFactor = (float)Math.Sqrt(Mass / 10);
float targetRange = Math.Min(massFactor * AnimController.Collider.LinearVelocity.Length() * 2 * Noise, maxAIRange);
targetRange *= 1.0f + GetStatValue(StatTypes.SoundRangeMultiplier);
float newRange = MathHelper.SmoothStep(aiTarget.SoundRange, targetRange, deltaTime * aiTargetChangeSpeed);
newRange *= 1.0f + GetStatValue(StatTypes.SoundRangeMultiplier);
if (!float.IsNaN(newRange))
{
aiTarget.SoundRange = newRange;
@@ -3975,7 +4073,9 @@ namespace Barotrauma
GameMain.Server.SendChatMessage(message.Message, message.MessageType.Value, null, this);
}
#endif
ShowSpeechBubble(2.0f, ChatMessage.MessageColor[(int)message.MessageType.Value]);
#if CLIENT
ShowSpeechBubble(ChatMessage.MessageColor[(int)message.MessageType.Value], message.Message);
#endif
sentMessages.Add(message);
}
@@ -4006,13 +4106,6 @@ namespace Barotrauma
}
}
public void ShowSpeechBubble(float duration, Color color)
{
speechBubbleTimer = Math.Max(speechBubbleTimer, duration);
speechBubbleColor = color;
}
public void SetAllDamage(float damageAmount, float bleedingDamageAmount, float burnDamageAmount)
{
CharacterHealth.SetAllDamage(damageAmount, bleedingDamageAmount, burnDamageAmount);
@@ -4321,8 +4414,8 @@ namespace Barotrauma
{
if (affliction.Prefab.IsBuff) { continue; }
if (Params.IsMachine && !affliction.Prefab.AffectMachines) { continue; }
if (affliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType ||
affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType)
if (Params.Health.ImmunityIdentifiers.Contains(affliction.Identifier)) { continue; }
if (affliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType || affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType)
{
if (!Params.Health.PoisonImmunity)
{
@@ -4389,7 +4482,7 @@ namespace Barotrauma
/// Is the character knocked down regardless whether the technical state is dead, unconcious, paralyzed, or stunned.
/// With stunning, the parameter uses an one second delay before the character is treated as knocked down. The purpose of this is to ignore minor stunning. If you don't want to to ignore any stun, use the Stun property.
/// </summary>
public bool IsKnockedDown => IsRagdolled || CharacterHealth.StunTimer > 1.0f || IsIncapacitated;
public bool IsKnockedDown => (IsRagdolled && !AnimController.IsHangingWithRope) || CharacterHealth.StunTimer > 1.0f || IsIncapacitated;
public void SetStun(float newStun, bool allowStunDecrease = false, bool isNetworkMessage = false)
{
@@ -4602,6 +4695,15 @@ namespace Barotrauma
isDead = true;
// Save these resistances in the CharacterInfo object so that if they
// are needed for respawning, they will be available (because there
// will be no Character instance in the limbo/bardo state)
if (info != null)
{
info.LastResistanceMultiplierSkillLossDeath = GetAbilityResistance(Tags.SkillLossDeathResistance);
info.LastResistanceMultiplierSkillLossRespawn = GetAbilityResistance(Tags.SkillLossRespawnResistance);
}
ApplyStatusEffects(ActionType.OnDeath, 1.0f);
AnimController.Frozen = false;
@@ -4610,7 +4712,7 @@ namespace Barotrauma
causeOfDeath, causeOfDeathAffliction?.Prefab,
causeOfDeathAffliction?.Source, LastDamageSource);
if (GameAnalyticsManager.SendUserStatistics)
if (GameAnalyticsManager.SendUserStatistics && Prefab?.ContentPackage == ContentPackageManager.VanillaCorePackage)
{
string causeOfDeathStr = causeOfDeathAffliction == null ?
causeOfDeath.ToString() : causeOfDeathAffliction.Prefab.Identifier.Value.Replace(" ", "");
@@ -5096,7 +5198,7 @@ namespace Barotrauma
public bool IsImmuneToPressure => !NeedsAir || HasAbilityFlag(AbilityFlags.ImmuneToPressure);
#region Talents
#region Talents
private readonly List<CharacterTalent> characterTalents = new List<CharacterTalent>();
public IReadOnlyCollection<CharacterTalent> CharacterTalents => characterTalents;
@@ -5214,7 +5316,7 @@ namespace Barotrauma
partial void OnTalentGiven(TalentPrefab talentPrefab);
#endregion
#endregion
private readonly HashSet<Hull> sameRoomHulls = new();
@@ -5426,6 +5528,24 @@ namespace Barotrauma
private readonly Dictionary<TalentResistanceIdentifier, float> abilityResistances = new();
public float GetAbilityResistance(Identifier resistanceId)
{
float resistance = 0f;
bool hadResistance = false;
foreach (var (key, value) in abilityResistances)
{
if (key.ResistanceIdentifier == resistanceId)
{
resistance += value;
hadResistance = true;
}
}
// NOTE: Resistance is handled as a multiplier here, so 1.0 == 0% resistance
return hadResistance ? resistance : 1f;
}
public float GetAbilityResistance(AfflictionPrefab affliction)
{
float resistance = 0f;
@@ -5441,6 +5561,7 @@ namespace Barotrauma
}
}
// NOTE: Resistance is handled as a multiplier here, so 1.0 == 0% resistance
return hadResistance ? resistance : 1f;
}
@@ -4,6 +4,7 @@ using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Voronoi2;
namespace Barotrauma
{
@@ -27,9 +28,10 @@ namespace Barotrauma
UpdateMoney = 13,
UpdatePermanentStats = 14,
RemoveFromCrew = 15,
LatchOntoTarget = 16,
MinValue = 0,
MaxValue = 15
MaxValue = 16
}
private interface IEventData : NetEntityEvent.IData
@@ -135,7 +137,56 @@ namespace Barotrauma
ObjectiveType = objectiveType;
}
}
public readonly struct LatchedOntoTargetEventData : IEventData
{
public EventType EventType => EventType.LatchOntoTarget;
public readonly bool IsLatched;
public readonly UInt16 TargetCharacterID = NullEntityID;
public readonly UInt16 TargetStructureID = NullEntityID;
public readonly int TargetLevelWallIndex = -1;
public readonly Vector2 AttachSurfaceNormal = Vector2.Zero;
public readonly Vector2 AttachPos = Vector2.Zero;
public readonly Vector2 CharacterSimPos;
private LatchedOntoTargetEventData(Character character, Vector2 attachSurfaceNormal, Vector2 attachPos)
{
CharacterSimPos = character.SimPosition;
IsLatched = true;
AttachSurfaceNormal = attachSurfaceNormal;
AttachPos = attachPos;
}
public LatchedOntoTargetEventData(Character character, Character targetCharacter, Vector2 attachSurfaceNormal, Vector2 attachPos)
: this(character, attachSurfaceNormal, attachPos)
{
TargetCharacterID = targetCharacter.ID;
}
public LatchedOntoTargetEventData(Character character, Structure targetStructure, Vector2 attachSurfaceNormal, Vector2 attachPos)
: this(character, attachSurfaceNormal, attachPos)
{
TargetStructureID = targetStructure.ID;
}
public LatchedOntoTargetEventData(Character character, VoronoiCell levelWall, Vector2 attachSurfaceNormal, Vector2 attachPos)
: this(character, attachSurfaceNormal, attachPos)
{
TargetLevelWallIndex = Level.Loaded.GetAllCells().IndexOf(levelWall);
}
/// <summary>
/// Signifies detaching (not attached to any target)
/// </summary>
public LatchedOntoTargetEventData()
{
CharacterSimPos = Vector2.Zero;
IsLatched = false;
}
}
private struct TeamChangeEventData : IEventData
{
public EventType EventType => EventType.TeamChange;
@@ -490,8 +490,6 @@ namespace Barotrauma
public ContentXElement CharacterConfigElement { get; set; }
public readonly string ragdollFileName = string.Empty;
public bool StartItemsGiven;
public bool IsNewHire;
@@ -553,12 +551,11 @@ namespace Barotrauma
{
if (ragdoll == null)
{
// TODO: support for variants
Identifier speciesName = SpeciesName;
bool isHumanoid = CharacterConfigElement.GetAttributeBool("humanoid", speciesName == CharacterPrefab.HumanSpeciesName);
ragdoll = isHumanoid
? HumanRagdollParams.GetRagdollParams(speciesName, ragdollFileName)
: RagdollParams.GetRagdollParams<FishRagdollParams>(speciesName, ragdollFileName) as RagdollParams;
? RagdollParams.GetDefaultRagdollParams<HumanRagdollParams>(SpeciesName, CharacterConfigElement, CharacterConfigElement.ContentPackage)
: RagdollParams.GetDefaultRagdollParams<FishRagdollParams>(SpeciesName, CharacterConfigElement, CharacterConfigElement.ContentPackage);
}
return ragdoll;
}
@@ -652,7 +649,6 @@ namespace Barotrauma
string name = "",
string originalName = "",
Either<Job, JobPrefab> jobOrJobPrefab = null,
string ragdollFileName = null,
int variant = 0,
Rand.RandSync randSync = Rand.RandSync.Unsynced,
Identifier npcIdentifier = default)
@@ -703,14 +699,10 @@ namespace Barotrauma
Salary = CalculateSalary();
}
OriginalName = !string.IsNullOrEmpty(originalName) ? originalName : Name;
if (ragdollFileName != null)
{
this.ragdollFileName = ragdollFileName;
}
}
private void SetPersonalityTrait()
=> PersonalityTrait = NPCPersonalityTrait.GetRandom(Name + string.Concat(Head.Preset.TagSet));
=> PersonalityTrait = NPCPersonalityTrait.GetRandom(Name + string.Concat(Head.Preset.TagSet.OrderBy(tag => tag)));
public string GetRandomName(Rand.RandSync randSync)
{
@@ -832,7 +824,6 @@ namespace Barotrauma
StartItemsGiven = infoElement.GetAttributeBool("startitemsgiven", false);
Identifier personalityName = infoElement.GetAttributeIdentifier("personality", "");
ragdollFileName = infoElement.GetAttributeString("ragdoll", string.Empty);
if (personalityName != Identifier.Empty)
{
if (NPCPersonalityTrait.Traits.TryGet(personalityName, out var trait) ||
@@ -1086,7 +1077,26 @@ namespace Barotrauma
partial void LoadHeadSpriteProjectSpecific(ContentXElement limbElement);
private bool spriteTagsLoaded;
public void VerifySpriteTagsLoaded()
{
if (!spriteTagsLoaded)
{
LoadSpriteTags();
}
}
private void LoadHeadSprite()
{
LoadHeadElement(loadHeadSprite: true, loadHeadSpriteTags: true);
}
private void LoadSpriteTags()
{
LoadHeadElement(loadHeadSprite: false, loadHeadSpriteTags: true);
}
private void LoadHeadElement(bool loadHeadSprite, bool loadHeadSpriteTags)
{
if (Ragdoll?.MainElement == null) { return; }
foreach (var limbElement in Ragdoll.MainElement.Elements())
@@ -1116,20 +1126,30 @@ namespace Barotrauma
fileWithoutTags = fileWithoutTags.Split('[', ']').First();
if (fileWithoutTags != fileName) { continue; }
HeadSprite = new Sprite(spriteElement, "", file);
Portrait = new Sprite(spriteElement, "", file) { RelativeOrigin = Vector2.Zero };
//extract the tags out of the filename
SpriteTags = file.Split('[', ']').Skip(1).Select(id => id.ToIdentifier()).ToList();
if (SpriteTags.Any())
if (loadHeadSprite)
{
SpriteTags.RemoveAt(SpriteTags.Count - 1);
HeadSprite = new Sprite(spriteElement, "", file);
Portrait = new Sprite(spriteElement, "", file) { RelativeOrigin = Vector2.Zero };
}
if (loadHeadSpriteTags)
{
//extract the tags out of the filename
SpriteTags = file.Split('[', ']').Skip(1).Select(id => id.ToIdentifier()).ToList();
if (SpriteTags.Any())
{
SpriteTags.RemoveAt(SpriteTags.Count - 1);
}
spriteTagsLoaded = true;
}
break;
}
LoadHeadSpriteProjectSpecific(limbElement);
if (loadHeadSprite)
{
LoadHeadSpriteProjectSpecific(limbElement);
}
break;
}
@@ -1446,9 +1466,7 @@ namespace Barotrauma
new XAttribute("haircolor", XMLExtensions.ColorToString(Head.HairColor)),
new XAttribute("facialhaircolor", XMLExtensions.ColorToString(Head.FacialHairColor)),
new XAttribute("startitemsgiven", StartItemsGiven),
new XAttribute("ragdoll", ragdollFileName),
new XAttribute("personality", PersonalityTrait?.Identifier ?? Identifier.Empty));
// TODO: animations?
if (HumanPrefabIds != default)
{
@@ -1663,8 +1681,7 @@ namespace Barotrauma
{
Order order = null;
string orderIdentifier = orderElement.GetAttributeString("id", "");
var orderPrefab = OrderPrefab.Prefabs[orderIdentifier];
if (orderPrefab == null)
if (!OrderPrefab.Prefabs.TryGet(orderIdentifier, out OrderPrefab orderPrefab))
{
DebugConsole.ThrowError($"Error loading a previously saved order - can't find an order prefab with the identifier \"{orderIdentifier}\"");
priorityIncrease++;
@@ -1968,6 +1985,21 @@ namespace Barotrauma
}
if (changed) { OnPermanentStatChanged(statType); }
}
/// <summary>
/// Used to store the last known resistance against skill loss on death
/// when the character dies, so it can be correctly applied before
/// reinstantiating the Character object (if respawning).
/// NOTE: The resistances are handled as multipliers here, so 1.0 == 0% resistance
/// </summary>
public float LastResistanceMultiplierSkillLossDeath = 1.0f;
/// <summary>
/// Used to store the last known resistance against skill loss on respawn
/// when the character dies, so it can be correctly applied before
/// reinstantiating the Character object (if respawning).
/// NOTE: The resistances are handled as multipliers here, so 1.0 == 0% resistance
/// </summary>
public float LastResistanceMultiplierSkillLossRespawn = 1.0f;
}
internal sealed class SavedStatValue
@@ -18,6 +18,19 @@ namespace Barotrauma
public string Name => Identifier.Value;
public Identifier VariantOf { get; }
public CharacterPrefab ParentPrefab { get; set; }
public Identifier GetBaseCharacterSpeciesName(Identifier speciesName)
{
if (!VariantOf.IsEmpty)
{
speciesName = VariantOf;
if (ParentPrefab is { VariantOf.IsEmpty: false } parentPrefab)
{
speciesName = parentPrefab.GetBaseCharacterSpeciesName(speciesName);
}
}
return speciesName;
}
public void InheritFrom(CharacterPrefab parent)
{
@@ -68,7 +68,7 @@ namespace Barotrauma
public bool DivideByLimbCount { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Is the damage relative to the max vitality (percentage) or absolute (normal)"), Editable]
public bool MultiplyByMaxVitality { get; private set; }
public bool MultiplyByMaxVitality { get; set; }
public float DamagePerSecond;
public float DamagePerSecondTimer;
@@ -130,11 +130,16 @@ namespace Barotrauma
public void Deserialize(XElement element)
{
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
//backwards compatibility
if (element.GetAttribute("amount") != null && element.GetAttribute("strength") == null)
{
Strength = element.GetAttributeFloat("amount", 0.0f);
}
}
public Affliction CreateMultiplied(float multiplier, Affliction affliction)
{
var instance = Prefab.Instantiate(NonClampedStrength * multiplier, Source);
Affliction instance = Prefab.Instantiate(NonClampedStrength * multiplier, Source);
instance.CopyProperties(affliction);
return instance;
}
@@ -183,7 +188,7 @@ namespace Barotrauma
if (currentEffect.MultiplyByMaxVitality)
{
currVitalityDecrease *= characterHealth == null ? 100.0f : characterHealth.MaxVitality;
currVitalityDecrease *= characterHealth?.MaxVitality ?? 100.0f;
}
return currVitalityDecrease;
@@ -141,6 +141,9 @@ namespace Barotrauma
if (prevDisplayedMessage.HasValue && prevDisplayedMessage.Value == State) { return; }
if (highestStrength > Strength) { return; }
// Show initial husk warning by default, and disable it only if campaign difficulty settings explicitly disable it
bool showHuskWarning = GameMain.GameSession?.Campaign?.Settings.ShowHuskWarning ?? true;
switch (State)
{
case InfectionState.Dormant:
@@ -148,15 +151,18 @@ namespace Barotrauma
{
return;
}
if (character == Character.Controlled)
if (showHuskWarning)
{
if (character == Character.Controlled)
{
#if CLIENT
GUI.AddMessage(TextManager.Get("HuskDormant"), GUIStyle.Red);
GUI.AddMessage(TextManager.Get("HuskDormant"), GUIStyle.Red);
#endif
}
else if (character.IsBot)
{
character.Speak(TextManager.Get("dialoghuskdormant").Value, delay: Rand.Range(0.5f, 5.0f), identifier: "huskdormant".ToIdentifier());
}
else if (character.IsBot)
{
character.Speak(TextManager.Get("dialoghuskdormant").Value, delay: Rand.Range(0.5f, 5.0f), identifier: "huskdormant".ToIdentifier());
}
}
break;
case InfectionState.Transition:
@@ -173,6 +173,12 @@ namespace Barotrauma
max += Character.Info.Job.Prefab.VitalityModifier;
}
max *= Character.HumanPrefabHealthMultiplier;
if (GameMain.GameSession?.Campaign is CampaignMode campaign)
{
max *= Character.IsOnPlayerTeam
? campaign.Settings.CrewVitalityMultiplier
: campaign.Settings.NonCrewVitalityMultiplier;
}
max *= 1f + Character.GetStatValue(StatTypes.MaximumHealthMultiplier);
return max * Character.HealthMultiplier;
}
@@ -270,9 +276,9 @@ namespace Barotrauma
this.Character = character;
InitIrremovableAfflictions();
vitality = UnmodifiedMaxVitality;
vitality = UnmodifiedMaxVitality;
minVitality = character.IsHuman ? -100.0f : 0.0f;
minVitality = element.GetAttributeFloat(nameof(MinVitality), character.IsHuman ? -100.0f : 0.0f);
limbHealths.Clear();
limbHealthElement ??= element;
@@ -369,7 +375,7 @@ namespace Barotrauma
public Limb GetAfflictionLimb(Affliction affliction)
{
if (afflictions.TryGetValue(affliction, out LimbHealth limbHealth))
if (affliction != null && afflictions.TryGetValue(affliction, out LimbHealth limbHealth))
{
if (limbHealth == null) { return null; }
int limbHealthIndex = limbHealths.IndexOf(limbHealth);
@@ -466,13 +472,17 @@ namespace Barotrauma
public float GetResistance(AfflictionPrefab afflictionPrefab)
{
// This is a % resistance (0 to 1.0)
float resistance = 0.0f;
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
{
var affliction = kvp.Key;
resistance += affliction.GetResistance(afflictionPrefab.Identifier);
}
return 1 - ((1 - resistance) * Character.GetAbilityResistance(afflictionPrefab));
// This is a multiplier, ie. 0.0 = 100% resistance and 1.0 = 0% resistance
float abilityResistanceMultiplier = Character.GetAbilityResistance(afflictionPrefab);
// The returned value is calculated to be a % resistance again
return 1 - ((1 - resistance) * abilityResistanceMultiplier);
}
public float GetStatValue(StatTypes statType)
@@ -506,7 +516,7 @@ namespace Barotrauma
ReduceMatchingAfflictions(amount, treatmentAction);
}
public void ReduceAfflictionOnAllLimbs(Identifier afflictionIdOrType, float amount, ActionType? treatmentAction = null)
public void ReduceAfflictionOnAllLimbs(Identifier afflictionIdOrType, float amount, ActionType? treatmentAction = null, Character attacker = null)
{
if (afflictionIdOrType.IsEmpty) { throw new ArgumentException($"{nameof(afflictionIdOrType)} is empty"); }
@@ -519,7 +529,7 @@ namespace Barotrauma
}
}
ReduceMatchingAfflictions(amount, treatmentAction);
ReduceMatchingAfflictions(amount, treatmentAction, attacker);
}
private IEnumerable<Affliction> GetAfflictionsForLimb(Limb targetLimb)
@@ -531,11 +541,11 @@ namespace Barotrauma
matchingAfflictions.Clear();
matchingAfflictions.AddRange(GetAfflictionsForLimb(targetLimb));
ReduceMatchingAfflictions(amount, treatmentAction);
}
public void ReduceAfflictionOnLimb(Limb targetLimb, Identifier afflictionIdOrType, float amount, ActionType? treatmentAction = null)
public void ReduceAfflictionOnLimb(Limb targetLimb, Identifier afflictionIdOrType, float amount, ActionType? treatmentAction = null, Character attacker = null)
{
if (afflictionIdOrType.IsEmpty) { throw new ArgumentException($"{nameof(afflictionIdOrType)} is empty"); }
if (targetLimb is null) { throw new ArgumentNullException(nameof(targetLimb)); }
@@ -550,14 +560,22 @@ namespace Barotrauma
matchingAfflictions.Add(affliction.Key);
}
}
ReduceMatchingAfflictions(amount, treatmentAction);
ReduceMatchingAfflictions(amount, treatmentAction, attacker);
}
private void ReduceMatchingAfflictions(float amount, ActionType? treatmentAction)
private void ReduceMatchingAfflictions(float amount, ActionType? treatmentAction, Character attacker = null)
{
if (matchingAfflictions.Count == 0) { return; }
float reduceAmount = amount / matchingAfflictions.Count;
if (reduceAmount > 0f)
{
var abilityReduceAffliction = new AbilityReduceAffliction(Character, reduceAmount);
attacker?.CheckTalents(AbilityEffectType.OnReduceAffliction, abilityReduceAffliction);
reduceAmount = abilityReduceAffliction.Value;
}
for (int i = matchingAfflictions.Count - 1; i >= 0; i--)
{
var matchingAffliction = matchingAfflictions[i];
@@ -737,18 +755,23 @@ namespace Barotrauma
return;
}
}
if (Character.Params.Health.PoisonImmunity &&
(newAffliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType || newAffliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType)) { return; }
if (Character.Params.Health.PoisonImmunity)
{
if (newAffliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType || newAffliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType)
{
return;
}
}
if (Character.EmpVulnerability <= 0 && newAffliction.Prefab.AfflictionType == AfflictionPrefab.EMPType) { return; }
if (newAffliction.Prefab.TargetSpecies.Any() && newAffliction.Prefab.TargetSpecies.None(s => s == Character.SpeciesName)) { return; }
if (Character.Params.Health.ImmunityIdentifiers.Contains(newAffliction.Identifier)) { return; }
Affliction existingAffliction = null;
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
foreach ((Affliction affliction, LimbHealth value) in afflictions)
{
var affliction = kvp.Key;
if (kvp.Value == limbHealth && kvp.Key.Prefab == newAffliction.Prefab)
if (value == limbHealth && affliction.Prefab == newAffliction.Prefab)
{
existingAffliction = kvp.Key;
existingAffliction = affliction;
break;
}
}
@@ -974,10 +997,8 @@ namespace Barotrauma
IsParalyzed = false;
if (Unkillable || Character.GodMode) { return; }
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
foreach (var (affliction, limbHealth) in afflictions)
{
var affliction = kvp.Key;
var limbHealth = kvp.Value;
float vitalityDecrease = affliction.GetVitalityDecrease(this);
if (limbHealth != null)
{
@@ -1117,9 +1138,8 @@ namespace Barotrauma
/// and negative treatment suitabilities (e.g. a medicine that causes oxygen loss may not be suitable if the character is already suffocating)
/// </summary>
/// <param name="treatmentSuitability">A dictionary where the key is the identifier of the item and the value the suitability</param>
/// <param name="normalize">If true, the suitability values are normalized between 0 and 1. If not, they're arbitrary values defined in the medical item XML, where negative values are unsuitable, and positive ones suitable.</param>
/// <param name="predictFutureDuration">If above 0, the method will take into account how much currently active status effects while affect the afflictions in the next x seconds.</param>
public void GetSuitableTreatments(Dictionary<Identifier, float> treatmentSuitability, bool normalize, Character user, Limb limb = null, bool ignoreHiddenAfflictions = false, float predictFutureDuration = 0.0f)
public void GetSuitableTreatments(Dictionary<Identifier, float> treatmentSuitability, Character user, Limb limb = null, bool ignoreHiddenAfflictions = false, float predictFutureDuration = 0.0f)
{
//key = item identifier
//float = suitability
@@ -1129,7 +1149,9 @@ namespace Barotrauma
{
var affliction = kvp.Key;
var limbHealth = kvp.Value;
if (limb != null && affliction.Prefab.IndicatorLimb != limb.type)
if (limb != null &&
affliction.Prefab.LimbSpecific &&
GetMatchingLimbHealth(affliction) != GetMatchingLimbHealth(limb))
{
if (limbHealth == null) { continue; }
int healthIndex = limbHealths.IndexOf(limbHealth);
@@ -1145,8 +1167,7 @@ namespace Barotrauma
//other afflictions of the same type increase the "treatability"
// e.g. we might want to ignore burns below 5%, but not if the character has them on all limbs
float totalAfflictionStrength = strength + GetTotalAdjustedAfflictionStrength(affliction, includeSameAffliction: false);
if (totalAfflictionStrength < affliction.Prefab.TreatmentThreshold) { continue; }
if (afflictions.Any(otherAffliction => affliction.Prefab.IgnoreTreatmentIfAfflictedBy.Contains(otherAffliction.Key.Identifier))) { continue; }
if (ignoreHiddenAfflictions)
@@ -1164,6 +1185,13 @@ namespace Barotrauma
foreach (KeyValuePair<Identifier, float> treatment in affliction.Prefab.TreatmentSuitabilities)
{
float suitability = treatment.Value * strength;
if (suitability > 0)
{
//if this a suitable treatment, ignore it if the affliction isn't severe enough to treat
//if the suitability is negative though, we need to take it into account!
//otherwise we may end up e.g. giving too much opiates to someone already close to overdosing
if (totalAfflictionStrength < affliction.Prefab.TreatmentThreshold) { continue; }
}
if (treatment.Value > strength)
{
//avoid using very effective meds on small injuries
@@ -1182,14 +1210,6 @@ namespace Barotrauma
maxSuitability = Math.Max(treatmentSuitability[treatment.Key], maxSuitability);
}
}
//normalize the suitabilities to a range of 0 to 1
if (normalize)
{
foreach (Identifier treatment in treatmentSuitability.Keys.ToList())
{
treatmentSuitability[treatment] = (treatmentSuitability[treatment] - minSuitability) / (maxSuitability - minSuitability);
}
}
}
/// <summary>
@@ -102,6 +102,9 @@ namespace Barotrauma
[Serialize(float.PositiveInfinity, IsPropertySaveable.No)]
public float ReportRange { get; protected set; }
[Serialize(float.PositiveInfinity, IsPropertySaveable.No)]
public float FindWeaponsRange { get; protected set; }
public Identifier[] PreferredOutpostModuleTypes { get; protected set; }
@@ -172,6 +175,7 @@ namespace Barotrauma
}
}
humanAI.ReportRange = ReportRange;
humanAI.FindWeaponsRange = FindWeaponsRange;
humanAI.AimSpeed = AimSpeed;
humanAI.AimAccuracy = AimAccuracy;
}
@@ -182,6 +186,7 @@ namespace Barotrauma
{
humanAI.ObjectiveManager.SetForcedOrder(new AIObjectiveGoTo(positionToStayIn, npc, humanAI.ObjectiveManager, repeat: true, getDivingGearIfNeeded: false, closeEnough: 200)
{
FaceTargetOnCompleted = false,
DebugLogWhenFails = false,
IsWaitOrder = true,
CloseEnough = 100
@@ -419,6 +419,24 @@ namespace Barotrauma
}
}
public Vector2 DrawPosition
{
get
{
if (Removed)
{
#if DEBUG
DebugConsole.ThrowError("Attempted to access a removed limb.\n" + Environment.StackTrace.CleanupStackTrace());
#endif
GameAnalyticsManager.AddErrorEventOnce("Limb.LinearVelocity:DrawPosition", GameAnalyticsManager.ErrorSeverity.Error,
"Attempted to access a removed limb.\n" + Environment.StackTrace.CleanupStackTrace());
return Vector2.Zero;
}
return body.DrawPosition;
}
}
public float Rotation
{
get
@@ -488,6 +506,19 @@ namespace Barotrauma
}
}
private float _alpha = 1.0f;
/// <summary>
/// Can be used by status effects
/// </summary>
public float Alpha
{
get => _alpha;
set
{
_alpha = MathHelper.Clamp(value, 0.0f, 1.0f);
}
}
public int RefJointIndex => Params.RefJoint;
public readonly List<WearableSprite> WearingItems = new List<WearableSprite>();
@@ -680,9 +711,9 @@ namespace Barotrauma
}
attack.DamageRange = ConvertUnits.ToDisplayUnits(attack.DamageRange);
}
if (character is { VariantOf: { IsEmpty: false } })
if (character is { VariantOf.IsEmpty: false })
{
var attackElement = character.Params.VariantFile.Root.GetChildElement("attack");
var attackElement = character.Params.VariantFile.GetRootExcludingOverride().GetChildElement("attack");
if (attackElement != null)
{
attack.DamageMultiplier = attackElement.GetAttributeFloat("damagemultiplier", 1f);
@@ -695,7 +726,7 @@ namespace Barotrauma
DamageModifiers.Add(new DamageModifier(subElement, character.Name));
break;
case "statuseffect":
var statusEffect = StatusEffect.Load(subElement, Name);
var statusEffect = StatusEffect.Load(subElement, character.Name + ", " + Name);
if (statusEffect != null)
{
if (!statusEffects.ContainsKey(statusEffect.type))
@@ -793,10 +824,12 @@ namespace Barotrauma
{
finalDamageModifier *= character.EmpVulnerability;
}
if (!character.Params.Health.PoisonImmunity &&
(affliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType || affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType))
if (!character.Params.Health.PoisonImmunity)
{
finalDamageModifier *= character.PoisonVulnerability;
if (affliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType || affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType)
{
finalDamageModifier *= character.PoisonVulnerability;
}
}
foreach (DamageModifier damageModifier in tempModifiers)
{
@@ -908,13 +941,13 @@ namespace Barotrauma
{
if (Params.BlinkFrequency > 0)
{
if (blinkTimer > -TotalBlinkDurationOut)
if (BlinkTimer > -TotalBlinkDurationOut)
{
blinkTimer -= deltaTime;
BlinkTimer -= deltaTime;
}
else
{
blinkTimer = Params.BlinkFrequency;
BlinkTimer = Params.BlinkFrequency;
}
}
if (reEnableTimer > 0)
@@ -932,13 +965,14 @@ namespace Barotrauma
private bool temporarilyDisabled;
private float reEnableTimer = -1;
private bool originalIgnoreCollisions;
public void HideAndDisable(float duration = 0, bool ignoreCollisions = true)
{
if (Hidden || Disabled) { return; }
if (ignoreCollisions && IgnoreCollisions) { return; }
temporarilyDisabled = true;
Hidden = true;
Disabled = true;
originalIgnoreCollisions = IgnoreCollisions;
IgnoreCollisions = ignoreCollisions;
if (duration > 0)
{
@@ -957,7 +991,7 @@ namespace Barotrauma
if (!temporarilyDisabled) { return; }
Hidden = false;
Disabled = false;
IgnoreCollisions = false;
IgnoreCollisions = originalIgnoreCollisions;
reEnableTimer = -1;
}
@@ -1001,11 +1035,14 @@ namespace Barotrauma
case HitDetection.Distance:
if (dist < attack.DamageRange)
{
structureBody = Submarine.PickBody(simPos, attackSimPos, collisionCategory: Physics.CollisionWall | Physics.CollisionLevel, allowInsideFixture: true, customPredicate:
(Fixture f) =>
Vector2 rayStart = simPos;
Vector2 rayEnd = attackSimPos;
if (Submarine == null && damageTarget is ISpatialEntity spatialEntity && spatialEntity.Submarine != null)
{
return f?.Body?.UserData as string != "ruinroom";
});
rayStart -= spatialEntity.Submarine.SimPosition;
rayEnd -= spatialEntity.Submarine.SimPosition;
}
structureBody = Submarine.CheckVisibility(rayStart, rayEnd);
if (damageTarget is Item i && i.GetComponent<Items.Components.Door>() != null)
{
// If the attack is aimed to an item and hits an item, it's successful.
@@ -1228,6 +1265,8 @@ namespace Barotrauma
if (!statusEffects.TryGetValue(actionType, out var statusEffectList)) { return; }
foreach (StatusEffect statusEffect in statusEffectList)
{
if (statusEffect.ShouldWaitForInterval(character, deltaTime)) { return; }
statusEffect.sourceBody = body;
if (statusEffect.type == ActionType.OnDamaged)
{
@@ -1308,20 +1347,21 @@ namespace Barotrauma
}
}
private float blinkTimer;
public float BlinkPhase;
public float BlinkTimer { get; private set; }
public float BlinkPhase { get; set; }
public bool FreezeBlinkState;
private float TotalBlinkDurationOut => Params.BlinkDurationOut + Params.BlinkHoldTime;
public void Blink()
{
blinkTimer = -TotalBlinkDurationOut;
BlinkTimer = -TotalBlinkDurationOut;
}
public void UpdateBlink(float deltaTime, float referenceRotation)
{
if (blinkTimer > -TotalBlinkDurationOut)
if (BlinkTimer > -TotalBlinkDurationOut)
{
if (!FreezeBlinkState)
{
@@ -1431,4 +1471,16 @@ namespace Barotrauma
public Affliction Affliction { get; set; }
}
class AbilityReduceAffliction : AbilityObject, IAbilityCharacter, IAbilityValue
{
public AbilityReduceAffliction(Character character, float value)
{
Character = character;
Value = value;
}
public Character Character { get; set; }
public float Value { get; set; }
}
}
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Globalization;
using Barotrauma.IO;
using System;
using System.Diagnostics;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Extensions;
@@ -65,22 +66,19 @@ namespace Barotrauma
abstract class AnimationParams : EditableParams, IMemorizable<AnimationParams>
{
public Identifier SpeciesName { get; private set; }
public bool IsGroundedAnimation => AnimationType == AnimationType.Walk || AnimationType == AnimationType.Run || AnimationType == AnimationType.Crouch;
public bool IsSwimAnimation => AnimationType == AnimationType.SwimSlow || AnimationType == AnimationType.SwimFast;
public bool IsGroundedAnimation => AnimationType is AnimationType.Walk or AnimationType.Run or AnimationType.Crouch;
public bool IsSwimAnimation => AnimationType is AnimationType.SwimSlow or AnimationType.SwimFast;
protected static Dictionary<Identifier, Dictionary<string, AnimationParams>> allAnimations = new Dictionary<Identifier, Dictionary<string, AnimationParams>>();
/// allAnimations[speciesName][fileName]
/// <summary>
/// The cached animations of all the characters that have been loaded.
/// </summary>
private static readonly Dictionary<Identifier, Dictionary<string, AnimationParams>> allAnimations = new Dictionary<Identifier, Dictionary<string, AnimationParams>>();
private float _movementSpeed;
[Serialize(1.0f, IsPropertySaveable.Yes), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = Ragdoll.MAX_SPEED, ValueStep = 0.1f)]
public float MovementSpeed
{
get => _movementSpeed;
set => _movementSpeed = value;
}
[Serialize(1.0f, IsPropertySaveable.Yes, description: "The speed of the \"animation cycle\", i.e. how fast the character takes steps or moves the tail/legs/arms (the outcome depends what the clip is about)"),
Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2, ValueStep = 0.01f)]
public float MovementSpeed { get; set; }
[Serialize(1.0f, IsPropertySaveable.Yes, description: "The speed of the \"animation cycle\", i.e. how fast the character takes steps or moves the tail/legs/arms (the outcome depends what the clip is about)"),
Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2, ValueStep = 0.01f)]
public float CycleSpeed { get; set; }
/// <summary>
@@ -152,169 +150,214 @@ namespace Barotrauma
private static string GetFolder(ContentXElement root, string filePath)
{
var folder = root?.GetChildElement("animations")?.GetAttributeContentPath("folder")?.Value;
Debug.Assert(filePath != null);
Debug.Assert(root != null);
string folder = root.GetChildElement("animations")?.GetAttributeContentPath("folder")?.Value;
if (string.IsNullOrEmpty(folder) || folder.Equals("default", StringComparison.OrdinalIgnoreCase))
{
folder = IO.Path.Combine(IO.Path.GetDirectoryName(filePath), "Animations");
}
return folder.CleanUpPathCrossPlatform(true);
return folder.CleanUpPathCrossPlatform(correctFilenameCase: true);
}
/// <summary>
/// Selects a random filepath from multiple paths, matching the specified animation type.
/// Selects all file paths that match the specified animation type and filters them alphabetically.
/// </summary>
public static string GetRandomFilePath(IReadOnlyList<string> filePaths, AnimationType type)
public static IEnumerable<string> FilterAndSortFiles(IEnumerable<string> filePaths, AnimationType type)
{
return filePaths.GetRandom(f => AnimationPredicate(f, type), Rand.RandSync.ServerAndClient);
}
/// <summary>
/// Selects all file paths that match the specified animation type.
/// </summary>
public static IEnumerable<string> FilterFilesByType(IEnumerable<string> filePaths, AnimationType type)
{
return filePaths.Where(f => AnimationPredicate(f, type));
}
private static bool AnimationPredicate(string filePath, AnimationType type)
{
var doc = XMLExtensions.TryLoadXml(filePath);
if (doc == null) { return false; }
var typeString = doc.Root.GetAttributeString("animationtype", null);
if (string.IsNullOrWhiteSpace(typeString))
return filePaths.Where(f => AnimationPredicate(f, type)).OrderBy(f => f, StringComparer.OrdinalIgnoreCase);
static bool AnimationPredicate(string filePath, AnimationType type)
{
typeString = doc.Root.GetAttributeString("AnimationType", "NotDefined");
XDocument doc = XMLExtensions.TryLoadXml(filePath);
if (doc == null) { return false; }
return doc.GetRootExcludingOverride().GetAttributeEnum("animationtype", AnimationType.NotDefined) == type;
}
return Enum.TryParse(typeString, out AnimationType fileType) && fileType == type;
}
public static T GetDefaultAnimParams<T>(Character character, AnimationType animType) where T : AnimationParams, new()
protected static T GetDefaultAnimParams<T>(Character character, AnimationType animType) where T : AnimationParams, new()
{
// Using a null file definition means we are taking a first matching file from the folder.
return GetAnimParams<T>(character, animType, file: null, throwErrors: true);
}
protected static T GetAnimParams<T>(Character character, AnimationType animType, Either<string, ContentPath> file, bool throwErrors = true) where T : AnimationParams, new()
{
Identifier speciesName = character.SpeciesName;
if (!character.VariantOf.IsEmpty
&& (character.Params.VariantFile?.Root?.GetChildElement("animations")?.GetAttributeStringUnrestricted("folder", null)).IsNullOrEmpty())
Identifier animSpecies = speciesName;
if (!character.VariantOf.IsEmpty)
{
// Use the base animations defined in the base definition file.
speciesName = character.VariantOf;
}
return GetAnimParams<T>(speciesName, animType, GetDefaultFileName(speciesName, animType));
}
/// <summary>
/// If the file name is left null, default file is selected. If fails, will select the default file. Note: Use the filename without the extensions, don't use the full path!
/// If a custom folder is used, it's defined in the character info file.
/// </summary>
public static T GetAnimParams<T>(Identifier speciesName, AnimationType animType, string fileName = null) where T : AnimationParams, new()
{
if (!allAnimations.TryGetValue(speciesName, out Dictionary<string, AnimationParams> anims))
{
anims = new Dictionary<string, AnimationParams>();
allAnimations.Add(speciesName, anims);
}
if (fileName == null || !anims.TryGetValue(fileName, out AnimationParams anim))
{
string selectedFile = null;
string folder = GetFolder(speciesName);
if (Directory.Exists(folder))
string folder = character.Params.VariantFile?.GetRootExcludingOverride().GetChildElement("animations")?.GetAttributeContentPath("folder", character.Prefab.ContentPackage)?.Value;
if (folder.IsNullOrEmpty() || folder.Equals("default", StringComparison.OrdinalIgnoreCase))
{
var files = Directory.GetFiles(folder);
if (files.None())
// Use the animations defined in the base definition file.
animSpecies = character.Prefab.GetBaseCharacterSpeciesName(speciesName);
}
}
return GetAnimParams<T>(speciesName, animSpecies, fallbackSpecies: character.Prefab.GetBaseCharacterSpeciesName(speciesName), animType, file, throwErrors);
}
private static readonly List<string> errorMessages = new List<string>();
private static T GetAnimParams<T>(Identifier speciesName, Identifier animSpecies, Identifier fallbackSpecies, AnimationType animType, Either<string, ContentPath> file, bool throwErrors = true) where T : AnimationParams, new()
{
Debug.Assert(!speciesName.IsEmpty);
Debug.Assert(!animSpecies.IsEmpty);
ContentPath contentPath = null;
string fileName = null;
if (file != null)
{
if (!file.TryGet(out fileName))
{
file.TryGet(out contentPath);
}
Debug.Assert(!fileName.IsNullOrWhiteSpace() || !contentPath.IsNullOrWhiteSpace());
}
ContentPackage contentPackage = contentPath?.ContentPackage ?? CharacterPrefab.FindBySpeciesName(speciesName)?.ContentPackage;
Debug.Assert(contentPackage != null);
if (!allAnimations.TryGetValue(speciesName, out Dictionary<string, AnimationParams> animations))
{
animations = new Dictionary<string, AnimationParams>();
allAnimations.Add(speciesName, animations);
}
string key = fileName ?? contentPath?.Value ?? GetDefaultFileName(animSpecies, animType);
if (animations.TryGetValue(key, out AnimationParams anim) && anim.AnimationType == animType)
{
// Already cached.
return (T)anim;
}
if (!contentPath.IsNullOrEmpty())
{
// Load the animation from path.
T animInstance = new T();
if (animInstance.Load(contentPath, speciesName))
{
if (animInstance.AnimationType == animType)
{
DebugConsole.ThrowError($"[AnimationParams] Could not find any animation files from the folder: {folder}. Using the default animation.");
selectedFile = GetDefaultFile(speciesName, animType);
}
var filteredFiles = FilterFilesByType(files, animType);
if (filteredFiles.None())
{
DebugConsole.ThrowError($"[AnimationParams] Could not find any animation files that match the animation type {animType} from the folder: {folder}. Using the default animation.");
selectedFile = GetDefaultFile(speciesName, animType);
}
else if (string.IsNullOrEmpty(fileName))
{
// Files found, but none specified.
selectedFile = GetDefaultFile(speciesName, animType);
animations.TryAdd(contentPath.Value, animInstance);
return animInstance;
}
else
{
selectedFile = filteredFiles.FirstOrDefault(f => IO.Path.GetFileNameWithoutExtension(f).Equals(fileName, StringComparison.OrdinalIgnoreCase));
errorMessages.Add($"[AnimationParams] Animation type mismatch. Expected: {animType}, Actual: {animInstance.AnimationType}. Using the default animation.");
}
}
else
{
errorMessages.Add($"[AnimationParams] Failed to load an animation {animInstance} of type {animType} from {contentPath.Value} for the character {speciesName}. Using the default animation.");
}
}
// Seek the correct animation from the character's animation folder.
string selectedFile = null;
string folder = GetFolder(animSpecies);
if (Directory.Exists(folder))
{
string[] files = Directory.GetFiles(folder);
if (files.None())
{
errorMessages.Add($"[AnimationParams] Could not find any animation files from the folder: {folder}. Using the default animation.");
}
else
{
var filteredFiles = FilterAndSortFiles(files, animType);
if (filteredFiles.None())
{
errorMessages.Add($"[AnimationParams] Could not find any animation files that match the animation type {animType} from the folder: {folder}. Using the default animation.");
}
else if (string.IsNullOrEmpty(fileName))
{
// Files found, but none specified -> Get a matching animation from the specified folder.
// First try to find a file that matches the default file name. If that fails, just take any file.
string defaultFileName = GetDefaultFileName(animSpecies, animType);
selectedFile = filteredFiles.FirstOrDefault(path => PathMatchesFile(path, defaultFileName)) ?? filteredFiles.First();
}
else
{
selectedFile = filteredFiles.FirstOrDefault(path => PathMatchesFile(path, fileName));
if (selectedFile == null)
{
DebugConsole.ThrowError($"[AnimationParams] Could not find an animation file that matches the name {fileName} and the animation type {animType}. Using the default animations.");
selectedFile = GetDefaultFile(speciesName, animType);
errorMessages.Add($"[AnimationParams] Could not find an animation file that matches the name {fileName} and the animation type {animType}. Using the default animations.");
}
}
}
}
else
{
DebugConsole.ThrowError($"[Animationparams] Invalid directory: {folder}. Using the default animation.");
selectedFile = GetDefaultFile(speciesName, animType);
}
if (selectedFile == null)
{
throw new Exception("[AnimationParams] Selected file null!");
}
DebugConsole.Log($"[AnimationParams] Loading animations from {selectedFile}.");
var characterPrefab = CharacterPrefab.Prefabs[speciesName];
T a = new T();
if (a.Load(ContentPath.FromRaw(characterPrefab.ContentPackage, selectedFile), speciesName))
{
fileName = IO.Path.GetFileNameWithoutExtension(selectedFile);
if (!anims.ContainsKey(fileName))
{
anims.Add(fileName, a);
}
}
else
{
DebugConsole.ThrowError($"[AnimationParams] Failed to load an animation {a} at {selectedFile} of type {animType} for the character {speciesName}",
contentPackage: characterPrefab.ContentPackage);
}
return a;
}
return (T)anim;
else
{
errorMessages.Add($"[AnimationParams] Invalid directory: {folder}. Using the default animation.");
}
selectedFile ??= GetDefaultFile(fallbackSpecies, animType);
Debug.Assert(selectedFile != null);
if (errorMessages.None())
{
DebugConsole.Log($"[AnimationParams] Loading animations from {selectedFile}.");
}
T animationInstance = new T();
if (animationInstance.Load(ContentPath.FromRaw(contentPackage, selectedFile), speciesName))
{
animations.TryAdd(key, animationInstance);
}
else
{
errorMessages.Add($"[AnimationParams] Failed to load an animation {animationInstance} at {selectedFile} of type {animType} for the character {speciesName}");
}
foreach (string errorMsg in errorMessages)
{
if (throwErrors)
{
DebugConsole.ThrowError(errorMsg, contentPackage: contentPackage);
}
else
{
DebugConsole.Log("Logging a supressed (potential) error: " + errorMsg);
}
}
errorMessages.Clear();
return animationInstance;
static bool PathMatchesFile(string p, string f) => IO.Path.GetFileNameWithoutExtension(p).Equals(f, StringComparison.OrdinalIgnoreCase);
}
public static void ClearCache() => allAnimations.Clear();
public static AnimationParams Create(string fullPath, Identifier speciesName, AnimationType animationType, Type type)
public static AnimationParams Create(string fullPath, Identifier speciesName, AnimationType animationType, Type animationParamsType)
{
if (type == typeof(HumanWalkParams))
if (animationParamsType == typeof(HumanWalkParams))
{
return Create<HumanWalkParams>(fullPath, speciesName, animationType);
}
if (type == typeof(HumanRunParams))
if (animationParamsType == typeof(HumanRunParams))
{
return Create<HumanRunParams>(fullPath, speciesName, animationType);
}
if (type == typeof(HumanSwimSlowParams))
if (animationParamsType == typeof(HumanSwimSlowParams))
{
return Create<HumanSwimSlowParams>(fullPath, speciesName, animationType);
}
if (type == typeof(HumanSwimFastParams))
if (animationParamsType == typeof(HumanSwimFastParams))
{
return Create<HumanSwimFastParams>(fullPath, speciesName, animationType);
}
if (type == typeof(HumanCrouchParams))
if (animationParamsType == typeof(HumanCrouchParams))
{
return Create<HumanCrouchParams>(fullPath, speciesName, animationType);
}
if (type == typeof(FishWalkParams))
if (animationParamsType == typeof(FishWalkParams))
{
return Create<FishWalkParams>(fullPath, speciesName, animationType);
}
if (type == typeof(FishRunParams))
if (animationParamsType == typeof(FishRunParams))
{
return Create<FishRunParams>(fullPath, speciesName, animationType);
}
if (type == typeof(FishSwimSlowParams))
if (animationParamsType == typeof(FishSwimSlowParams))
{
return Create<FishSwimSlowParams>(fullPath, speciesName, animationType);
}
if (type == typeof(FishSwimFastParams))
if (animationParamsType == typeof(FishSwimFastParams))
{
return Create<FishSwimFastParams>(fullPath, speciesName, animationType);
}
throw new NotImplementedException(type.ToString());
throw new NotImplementedException(animationParamsType.ToString());
}
/// <summary>
@@ -331,7 +374,7 @@ namespace Barotrauma
anims = new Dictionary<string, AnimationParams>();
allAnimations.Add(speciesName, anims);
}
var fileName = IO.Path.GetFileNameWithoutExtension(fullPath);
string fileName = IO.Path.GetFileNameWithoutExtension(fullPath);
if (anims.ContainsKey(fileName))
{
DebugConsole.NewMessage($"[AnimationParams] Removing the old animation of type {animationType}.", Color.Red);
@@ -340,7 +383,8 @@ namespace Barotrauma
var instance = new T();
XElement animationElement = new XElement(GetDefaultFileName(speciesName, animationType), new XAttribute("animationtype", animationType.ToString()));
instance.doc = new XDocument(animationElement);
var characterPrefab = CharacterPrefab.Prefabs[speciesName];
var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName);
Debug.Assert(characterPrefab != null);
var contentPath = ContentPath.FromRaw(characterPrefab.ContentPackage, fullPath);
instance.UpdatePath(contentPath);
instance.IsLoaded = instance.Deserialize(animationElement);
@@ -373,16 +417,17 @@ namespace Barotrauma
else
{
// Update the key by removing and re-adding the animation.
string fileName = FileNameWithoutExtension;
if (allAnimations.TryGetValue(SpeciesName, out Dictionary<string, AnimationParams> animations))
{
animations.Remove(Name);
animations.Remove(fileName);
}
base.UpdatePath(newPath);
if (animations != null)
{
if (!animations.ContainsKey(Name))
if (!animations.ContainsKey(fileName))
{
animations.Add(Name, this);
animations.Add(fileName, this);
}
}
}
@@ -421,37 +466,26 @@ namespace Barotrauma
{
if (isHumanoid)
{
switch (type)
return type switch
{
case AnimationType.Walk:
return typeof(HumanWalkParams);
case AnimationType.Run:
return typeof(HumanRunParams);
case AnimationType.Crouch:
return typeof(HumanCrouchParams);
case AnimationType.SwimSlow:
return typeof(HumanSwimSlowParams);
case AnimationType.SwimFast:
return typeof(HumanSwimFastParams);
default:
throw new NotImplementedException(type.ToString());
}
AnimationType.Walk => typeof(HumanWalkParams),
AnimationType.Run => typeof(HumanRunParams),
AnimationType.Crouch => typeof(HumanCrouchParams),
AnimationType.SwimSlow => typeof(HumanSwimSlowParams),
AnimationType.SwimFast => typeof(HumanSwimFastParams),
_ => throw new NotImplementedException(type.ToString())
};
}
else
{
switch (type)
return type switch
{
case AnimationType.Walk:
return typeof(FishWalkParams);
case AnimationType.Run:
return typeof(FishRunParams);
case AnimationType.SwimSlow:
return typeof(FishSwimSlowParams);
case AnimationType.SwimFast:
return typeof(FishSwimFastParams);
default:
throw new NotImplementedException(type.ToString());
}
AnimationType.Walk => typeof(FishWalkParams),
AnimationType.Run => typeof(FishRunParams),
AnimationType.SwimSlow => typeof(FishSwimSlowParams),
AnimationType.SwimFast => typeof(FishSwimFastParams),
_ => throw new NotImplementedException(type.ToString())
};
}
}
@@ -9,12 +9,12 @@ namespace Barotrauma
{
return Check(character) ? GetDefaultAnimParams<FishWalkParams>(character, AnimationType.Walk) : Empty;
}
public static FishWalkParams GetAnimParams(Character character, string fileName = null)
public static FishWalkParams GetAnimParams(Character character, Either<string, ContentPath> file, bool throwErrors = true)
{
return Check(character) ? GetAnimParams<FishWalkParams>(character.SpeciesName, AnimationType.Walk, fileName) : Empty;
return Check(character) ? GetAnimParams<FishWalkParams>(character, AnimationType.Walk, file, throwErrors) : null;
}
protected static FishWalkParams Empty = new FishWalkParams();
protected static readonly FishWalkParams Empty = new FishWalkParams();
public override void StoreSnapshot() => StoreSnapshot<FishWalkParams>();
}
@@ -25,12 +25,12 @@ namespace Barotrauma
{
return Check(character) ? GetDefaultAnimParams<FishRunParams>(character, AnimationType.Run) : Empty;
}
public static FishRunParams GetAnimParams(Character character, string fileName = null)
public static FishRunParams GetAnimParams(Character character, Either<string, ContentPath> file, bool throwErrors = true)
{
return Check(character) ? GetAnimParams<FishRunParams>(character.SpeciesName, AnimationType.Run, fileName) : Empty;
return Check(character) ? GetAnimParams<FishRunParams>(character, AnimationType.Run, file, throwErrors) : null;
}
protected static FishRunParams Empty = new FishRunParams();
protected static readonly FishRunParams Empty = new FishRunParams();
public override void StoreSnapshot() => StoreSnapshot<FishRunParams>();
}
@@ -38,9 +38,9 @@ namespace Barotrauma
class FishSwimFastParams : FishSwimParams
{
public static FishSwimFastParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams<FishSwimFastParams>(character, AnimationType.SwimFast);
public static FishSwimFastParams GetAnimParams(Character character, string fileName = null)
public static FishSwimFastParams GetAnimParams(Character character, Either<string, ContentPath> file, bool throwErrors = true)
{
return GetAnimParams<FishSwimFastParams>(character.SpeciesName, AnimationType.SwimFast, fileName);
return GetAnimParams<FishSwimFastParams>(character, AnimationType.SwimFast, file, throwErrors);
}
public override void StoreSnapshot() => StoreSnapshot<FishSwimFastParams>();
@@ -49,9 +49,9 @@ namespace Barotrauma
class FishSwimSlowParams : FishSwimParams
{
public static FishSwimSlowParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams<FishSwimSlowParams>(character, AnimationType.SwimSlow);
public static FishSwimSlowParams GetAnimParams(Character character, string fileName = null)
public static FishSwimSlowParams GetAnimParams(Character character, Either<string, ContentPath> file, bool throwErrors = true)
{
return GetAnimParams<FishSwimSlowParams>(character.SpeciesName, AnimationType.SwimSlow, fileName);
return GetAnimParams<FishSwimSlowParams>(character, AnimationType.SwimSlow, file, throwErrors);
}
public override void StoreSnapshot() => StoreSnapshot<FishSwimSlowParams>();
@@ -5,9 +5,9 @@ namespace Barotrauma
class HumanWalkParams : HumanGroundedParams
{
public static HumanWalkParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams<HumanWalkParams>(character, AnimationType.Walk);
public static HumanWalkParams GetAnimParams(Character character, string fileName = null)
public static HumanWalkParams GetAnimParams(Character character, Either<string, ContentPath> file, bool throwErrors = true)
{
return GetAnimParams<HumanWalkParams>(character.SpeciesName, AnimationType.Walk, fileName);
return GetAnimParams<HumanWalkParams>(character, AnimationType.Walk, file, throwErrors);
}
public override void StoreSnapshot() => StoreSnapshot<HumanWalkParams>();
@@ -16,9 +16,9 @@ namespace Barotrauma
class HumanRunParams : HumanGroundedParams
{
public static HumanRunParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams<HumanRunParams>(character, AnimationType.Run);
public static HumanRunParams GetAnimParams(Character character, string fileName = null)
public static HumanRunParams GetAnimParams(Character character, Either<string, ContentPath> file, bool throwErrors = true)
{
return GetAnimParams<HumanRunParams>(character.SpeciesName, AnimationType.Run, fileName);
return GetAnimParams<HumanRunParams>(character, AnimationType.Run, file, throwErrors);
}
public override void StoreSnapshot() => StoreSnapshot<HumanRunParams>();
@@ -36,9 +36,9 @@ namespace Barotrauma
public float ExtraTorsoAngleWhenStationary { get; set; }
public static HumanCrouchParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams<HumanCrouchParams>(character, AnimationType.Crouch);
public static HumanCrouchParams GetAnimParams(Character character, string fileName = null)
public static HumanCrouchParams GetAnimParams(Character character, Either<string, ContentPath> file, bool throwErrors = true)
{
return GetAnimParams<HumanCrouchParams>(character.SpeciesName, AnimationType.Crouch, fileName);
return GetAnimParams<HumanCrouchParams>(character, AnimationType.Crouch, file, throwErrors);
}
public override void StoreSnapshot() => StoreSnapshot<HumanCrouchParams>();
@@ -47,9 +47,9 @@ namespace Barotrauma
class HumanSwimFastParams: HumanSwimParams
{
public static HumanSwimFastParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams<HumanSwimFastParams>(character, AnimationType.SwimFast);
public static HumanSwimFastParams GetAnimParams(Character character, string fileName = null)
public static HumanSwimFastParams GetAnimParams(Character character, Either<string, ContentPath> file, bool throwErrors = true)
{
return GetAnimParams<HumanSwimFastParams>(character.SpeciesName, AnimationType.SwimFast, fileName);
return GetAnimParams<HumanSwimFastParams>(character, AnimationType.SwimFast, file, throwErrors);
}
@@ -59,9 +59,9 @@ namespace Barotrauma
class HumanSwimSlowParams : HumanSwimParams
{
public static HumanSwimSlowParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams<HumanSwimSlowParams>(character, AnimationType.SwimSlow);
public static HumanSwimSlowParams GetAnimParams(Character character, string fileName = null)
public static HumanSwimSlowParams GetAnimParams(Character character, Either<string, ContentPath> file, bool throwErrors = true)
{
return GetAnimParams<HumanSwimSlowParams>(character.SpeciesName, AnimationType.SwimSlow, fileName);
return GetAnimParams<HumanSwimSlowParams>(character, AnimationType.SwimSlow, file, throwErrors);
}
public override void StoreSnapshot() => StoreSnapshot<HumanSwimSlowParams>();
@@ -125,6 +125,13 @@ namespace Barotrauma
[Serialize(0f, IsPropertySaveable.Yes, description: "How much the horizontal difference of waist and the foot positions has an effect to lifting the foot."), Editable(DecimalCount = 2, ValueStep = 0.1f, MinValueFloat = 0f, MaxValueFloat = 1f)]
public float FootLiftHorizontalFactor { get; set; }
[Serialize("0,0", IsPropertySaveable.Yes, description: "Normally the character's feet are positioned at a scaled-down version of it's normal step position - this can be used to override that value if you want to e.g. make the character to spread out it's feet more when standing."), Editable(DecimalCount = 2, ValueStep = 0.01f)]
public Vector2 StepSizeWhenStanding
{
get;
set;
}
/// <summary>
/// In degrees.
/// </summary>
@@ -20,31 +20,31 @@ namespace Barotrauma
[Serialize("", IsPropertySaveable.Yes), Editable]
public Identifier SpeciesName { get; private set; }
[Serialize("", IsPropertySaveable.Yes, description: "If the creature is a variant that needs to use a pre-existing translation."), Editable]
[Serialize("", IsPropertySaveable.Yes, description: "References to another species. Define only if the creature is a variant that needs to use a pre-existing translation."), Editable]
public Identifier SpeciesTranslationOverride { get; private set; }
[Serialize("", IsPropertySaveable.Yes, description: "If the display name is not defined, the game first tries to find the translated name. If that is not found, the species name will be used."), Editable]
[Serialize("", IsPropertySaveable.Yes, description: "Overrides the name of the character, shown to the player. If the display name is not defined, the game first tries to find the translated name. If that is not found, the species name will be used."), Editable]
public string DisplayName { get; private set; }
[Serialize("", IsPropertySaveable.Yes, description: "If defined, different species of the same group are considered like the characters of the same species by the AI."), Editable]
[Serialize("", IsPropertySaveable.Yes, description: "If defined, different species of the same group consider each other friendly and do not attack each other."), Editable]
public Identifier Group { get; private set; }
[Serialize(false, IsPropertySaveable.Yes), Editable(ReadOnly = true)]
[Serialize(false, IsPropertySaveable.Yes, description: "If enabled, the character is a humanoid and has different animation constraints relative to non-humanoid characters."), Editable(ReadOnly = true)]
public bool Humanoid { get; private set; }
[Serialize(false, IsPropertySaveable.Yes), Editable(ReadOnly = true)]
[Serialize(false, IsPropertySaveable.Yes, description: "If enabled, jobs can be assigned to characters of this species. Should be true for the player characters."), Editable(ReadOnly = true)]
public bool HasInfo { get; private set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Can the creature interact with items?"), Editable]
public bool CanInteract { get; private set; }
[Serialize(false, IsPropertySaveable.Yes), Editable]
[Serialize(false, IsPropertySaveable.Yes, description: "Should this character be treated as a husk?"), Editable]
public bool Husk { get; private set; }
[Serialize(false, IsPropertySaveable.Yes), Editable]
[Serialize(false, IsPropertySaveable.Yes, description:"Should this character use a special husk appendage, attached to the ragdoll, when it turns into a husk?"), Editable]
public bool UseHuskAppendage { get; private set; }
[Serialize(false, IsPropertySaveable.Yes), Editable]
[Serialize(false, IsPropertySaveable.Yes, description: "Does this character need oxygen to survive? Enabling this also makes the character vulnerable to high pressure when swimming outside of the submarine."), Editable]
public bool NeedsAir { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Can the creature live without water or does it die on dry land?"), Editable]
@@ -56,13 +56,13 @@ namespace Barotrauma
[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]
[Serialize(false, IsPropertySaveable.No, description:"Is the character able to send messages in the chat?"), Editable]
public bool CanSpeak { get; set; }
[Serialize(true, IsPropertySaveable.Yes), Editable]
[Serialize(true, IsPropertySaveable.Yes, description:"Is there a health bar shown above the character when it takes damage? Defaults to true."), Editable]
public bool ShowHealthBar { get; private set; }
[Serialize(false, IsPropertySaveable.Yes), Editable]
[Serialize(false, IsPropertySaveable.Yes, description: "Is this character's health shown at the top of the player's screen when they are in an active encounter?"), Editable]
public bool UseBossHealthBar { get; private set; }
[Serialize(100f, IsPropertySaveable.Yes, description: "How much noise the character makes when moving?"), Editable(minValue: 0f, maxValue: 100000f)]
@@ -80,7 +80,7 @@ namespace Barotrauma
[Serialize("waterblood", IsPropertySaveable.Yes), Editable]
public string BleedParticleWater { get; private set; }
[Serialize(1f, IsPropertySaveable.Yes), Editable]
[Serialize(1f, IsPropertySaveable.Yes, description: "A multiplier to increase or decrease the number of bleeding particles to create."), Editable]
public float BleedParticleMultiplier { get; private set; }
[Serialize(true, IsPropertySaveable.Yes, description: "Can the creature eat bodies? Used by player controlled creatures to allow them to eat. Currently applicable only to non-humanoids. To allow an AI controller to eat, just add an ai target with the state \"eat\""), Editable]
@@ -89,22 +89,22 @@ namespace Barotrauma
[Serialize(10f, IsPropertySaveable.Yes, description: "How effectively/easily the character eats other characters. Affects the forces, the amount of particles, and the time required before the target is eaten away"), Editable(MinValueFloat = 1, MaxValueFloat = 1000, ValueStep = 1)]
public float EatingSpeed { get; set; }
[Serialize(true, IsPropertySaveable.Yes), Editable]
[Serialize(true, IsPropertySaveable.Yes, description: "Should the character AI use waypoints defined in the level to find a path to its targets?"), Editable]
public bool UsePathFinding { get; set; }
[Serialize(1f, IsPropertySaveable.Yes, "Decreases the intensive path finding call frequency. Set to a lower value for insignificant creatures to improve performance."), Editable(minValue: 0f, maxValue: 1f)]
public float PathFinderPriority { get; set; }
[Serialize(false, IsPropertySaveable.Yes), Editable]
[Serialize(false, IsPropertySaveable.Yes, description: "Should the character be hidden in the sonar?"), Editable]
public bool HideInSonar { get; set; }
[Serialize(false, IsPropertySaveable.Yes), Editable]
[Serialize(false, IsPropertySaveable.Yes, description: "Should the character be hidden when using thermal goggles?"), Editable]
public bool HideInThermalGoggles { get; set; }
[Serialize(0f, IsPropertySaveable.Yes), Editable]
[Serialize(0f, IsPropertySaveable.Yes, description: "If set to a value greater than zero, this character creates disrupting noise on the sonar when within range."), Editable]
public float SonarDisruption { get; set; }
[Serialize(0f, IsPropertySaveable.Yes), Editable]
[Serialize(0f, IsPropertySaveable.Yes, description: "Range at which \"long distance\" blips for this character will appear on the sonar (used on some of the Abyss monsters)."), Editable]
public float DistantSonarRange { get; set; }
[Serialize(25000f, IsPropertySaveable.Yes, "If the character is farther than this (in pixels) from the sub and the players, it will be disabled. The halved value is used for triggering simple physics where the ragdoll is disabled and only the main collider is updated."), Editable(MinValueFloat = 10000f, MaxValueFloat = 100000f)]
@@ -113,7 +113,7 @@ namespace Barotrauma
[Serialize(10f, IsPropertySaveable.Yes, "How frequent the recurring idle and attack sounds are?"), Editable(MinValueFloat = 1f, MaxValueFloat = 100f)]
public float SoundInterval { get; set; }
[Serialize(false, IsPropertySaveable.Yes), Editable]
[Serialize(false, IsPropertySaveable.Yes, description: "Should the character be drawn on top of characters that do not have this set? This currently has no effect if the character has no deformable sprites."), Editable]
public bool DrawLast { get; set; }
[Serialize(1.0f, IsPropertySaveable.Yes, "Tells the bots how much they should prefer targeting this character with submarine weapons. Defaults to 1. Set 0 to tell the bots not to target this character at all. Distance to the target affects the decision making."), Editable]
@@ -543,7 +543,7 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool PoisonImmunity { get; set; }
[Serialize(1f, IsPropertySaveable.Yes, description: "1 = default, 0 = immune."), Editable(MinValueFloat = 0f, MaxValueFloat = 1000, DecimalCount = 1)]
public float PoisonVulnerability { get; set; }
@@ -552,6 +552,19 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.Yes, description: "Can afflictions affect the face/body tint of the character."), Editable]
public bool ApplyAfflictionColors { get; private set; }
[Serialize("", IsPropertySaveable.Yes, description:"A comma-separated list of identifiers of afflictions that the creature is immune to."), Editable]
public string Immunities { get; private set; }
private ImmutableHashSet<Identifier> _immunityIdentifiers;
public IEnumerable<Identifier> ImmunityIdentifiers
{
get
{
_immunityIdentifiers ??= Element.GetAttributeIdentifierArray("immunities", Array.Empty<Identifier>()).ToImmutableHashSet();
return _immunityIdentifiers;
}
}
// TODO: limbhealths, sprite?
@@ -634,6 +647,9 @@ namespace Barotrauma
[Serialize(1.0f, IsPropertySaveable.Yes, description: "Affects how far the character can hear the targets. Used as a multiplier."), Editable(minValue: 0f, maxValue: 10f)]
public float Hearing { get; private set; }
[Serialize(-1.0f, IsPropertySaveable.Yes, description: "Hard limit to how far the character can spot targets from, regardless of the sight/hearing or how visible or how much noise the target is making. Not used if set to negative."), Editable]
public float MaxPerceptionDistance { get; set; }
[Serialize(100f, IsPropertySaveable.Yes, description: "How much the targeting priority increases each time the character takes damage. Works like the greed value, described above. The default value is 100."), Editable(minValue: -1000f, maxValue: 1000f)]
public float AggressionHurt { get; private set; }
@@ -673,7 +689,7 @@ 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]
[Serialize(false, IsPropertySaveable.Yes, description:"Unlike human AI, monsters normally only use pathfinding when they are inside the submarine. When this is enabled, the monsters can also use pathfinding to get inside the sub. In practice, via doors and hatches."), 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]
@@ -690,17 +706,18 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.Yes, "Does the creature patrol the dry hulls while idling inside a friendly submarine?"), Editable]
public bool PatrolDry { get; set; }
[Serialize(0f, IsPropertySaveable.Yes, description: ""), Editable]
[Serialize(0f, IsPropertySaveable.Yes, description: "Initial aggression used in the circle attack pattern (0-100). The aggression affects how close and how fast to the target the monster circles."), Editable]
public float StartAggression { get; private set; }
[Serialize(100f, IsPropertySaveable.Yes, description: ""), Editable]
[Serialize(100f, IsPropertySaveable.Yes, description: "Maximum aggression used in the circle attack pattern (0-100). The aggression affects how close and how fast to the target the monster circles."), Editable]
public float MaxAggression { get; private set; }
[Serialize(0f, IsPropertySaveable.Yes, description: ""), Editable]
[Serialize(0f, IsPropertySaveable.Yes, description: "How quickly the aggression level increases from StartAggression to MaxAggression when using the circle attack pattern. Artificial amount, applied once per attack cycle."), Editable]
public float AggressionCumulation { get; private set; }
[Serialize(WallTargetingMethod.Target, IsPropertySaveable.Yes, description: ""), Editable]
[Serialize(WallTargetingMethod.Target, IsPropertySaveable.Yes, description: "Defines the method of checking whether there's a blocking (submarine) wall."), Editable]
public WallTargetingMethod WallTargetingMethod { get; private set; }
public IEnumerable<TargetParams> Targets => targets;
@@ -851,6 +868,12 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.Yes, description: "Should the target be ignored while the creature is outside. Doesn't matter where the target is."), Editable]
public bool IgnoreOutside { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Should the target be ignored if it's inside. Doesn't matter where the creature itself is."), Editable]
public bool IgnoreTargetInside { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Should the target be ignored if it's outside. Doesn't matter where the creature itself is."), Editable]
public bool IgnoreTargetOutside { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Should the target be ignored if it's inside a different submarine than us? Normally only some targets are ignored when they are not inside the same sub."), Editable]
public bool IgnoreIfNotInSameSub { get; set; }
@@ -866,10 +889,16 @@ namespace Barotrauma
[Serialize(-1f, IsPropertySaveable.Yes, description: "A generic max threshold. Not used if set to negative."), Editable]
public float ThresholdMax { get; private set; }
[Serialize("0.0, 0.0", IsPropertySaveable.Yes), Editable]
[Serialize(1.0f, IsPropertySaveable.Yes, description: "Can be used to make the monster perceive the target further than it normally can."), Editable]
public float PerceptionDistanceMultiplier { get; private set; }
[Serialize(-1.0f, IsPropertySaveable.Yes, description: "Maximum distance at which the monster can perceive the target, regardless of the sight/hearing or how visible or how much noise the target is making. Not used if set to negative."), Editable]
public float MaxPerceptionDistance { get; private set; }
[Serialize("0.0, 0.0", IsPropertySaveable.Yes, description: "A generic offset. Used for example for offsetting the react distance (vector length) and for offsetting the target position when a guardian flees to a pod."), Editable]
public Vector2 Offset { get; private set; }
[Serialize(AttackPattern.Straight, IsPropertySaveable.Yes), Editable]
[Serialize(AttackPattern.Straight, IsPropertySaveable.Yes, description: "Defines the movement pattern of the character when approaching a target."), Editable]
public AttackPattern AttackPattern { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "If enabled, the AI will give more priority to targets close to the horizontal middle of the sub. Only applies to walls, hulls, and items like sonar. Circle and Sweep always does this regardless of this property."), Editable]
@@ -887,31 +916,48 @@ namespace Barotrauma
#endregion
#region Circle
[Serialize(5000f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 20000f)]
[Serialize(5000f, IsPropertySaveable.Yes, description:"How close to the target the character should be, before they start using the circle pattern instead of directional approaching."), Editable(MinValueFloat = 0f, MaxValueFloat = 20000f)]
public float CircleStartDistance { get; private set; }
[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)]
[Serialize(1f, IsPropertySaveable.Yes, description:"Determines the rate how quickly the target movement position is rotated towards the attack target. The actual rotation is calculated once per each attack cycle, based on the current aggression level."), Editable(MinValueFloat = 0f, MaxValueFloat = 100f)]
public float CircleRotationSpeed { get; private set; }
[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)]
[Serialize(0f, IsPropertySaveable.Yes, description:"How much the turn speed can differ between attack cycles (stays constant during the cycle)"), Editable(MinValueFloat = 0f, MaxValueFloat = 1f)]
public float CircleRandomRotationFactor { get; private set; }
[Serialize(5f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 10f)]
[Serialize(5f, IsPropertySaveable.Yes, description:"Affects how close to the target the character has to be before the strike phase of the circle behavior triggers. In the strike phase, the creature moves directly towards the target."), Editable(MinValueFloat = 0f, MaxValueFloat = 10f)]
public float CircleStrikeDistanceMultiplier { get; private set; }
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 50f)]
[Serialize(0f, IsPropertySaveable.Yes, description:"How much the target position is offset at maximum. Low values make the character hit the target earlier/always, higher values make it miss the target when the aggression intensity is low (early in the encounter)."), Editable(MinValueFloat = 0f, MaxValueFloat = 50f)]
public float CircleMaxRandomOffset { get; private set; }
#endregion
public TargetParams(ContentXElement element, CharacterParams character) : base(element, character) { }
/// <summary>
/// Conditionals that must be met for the character to be able to use these targeting parameters.
/// </summary>
public List<PropertyConditional> Conditionals { get; private set; } = new List<PropertyConditional>();
public TargetParams(string tag, AIState state, float priority, CharacterParams character) : base(CreateNewElement(character, tag, state, priority), character) { }
public TargetParams(string tag, AIState state, float priority, CharacterParams character) :
this(CreateNewElement(character, tag, state, priority), character) { }
public TargetParams(ContentXElement element, CharacterParams character) : base(element, character)
{
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "conditional":
Conditionals.AddRange(PropertyConditional.FromXElement(subElement));
break;
}
}
}
public static ContentXElement CreateNewElement(CharacterParams character, Identifier tag, AIState state, float priority) =>
CreateNewElement(character, tag.Value, state, priority);
@@ -16,6 +16,7 @@ namespace Barotrauma
public bool IsLoaded { get; protected set; }
public string Name { get; private set; }
public string FileName { get; private set; }
public string FileNameWithoutExtension { get; private set; }
public string Folder { get; private set; }
public ContentPath Path { get; protected set; } = ContentPath.Empty;
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; protected set; }
@@ -103,8 +104,9 @@ namespace Barotrauma
{
Path = fullPath;
Name = GetName();
FileName = System.IO.Path.GetFileName(Path.Value);
Folder = System.IO.Path.GetDirectoryName(Path.Value);
FileName = Barotrauma.IO.Path.GetFileName(Path.Value);
FileNameWithoutExtension = Barotrauma.IO.Path.GetFileNameWithoutExtension(Path.Value);
Folder = Barotrauma.IO.Path.GetDirectoryName(Path.Value);
}
public virtual bool Save(string fileNameWithoutExtension = null, System.Xml.XmlWriterSettings settings = null)
@@ -1,6 +1,7 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Xml.Linq;
using System.Linq;
using Barotrauma.IO;
@@ -13,15 +14,31 @@ using Barotrauma.SpriteDeformations;
namespace Barotrauma
{
public enum CanEnterSubmarine
{
/// <summary>
/// No part of the ragdoll can go inside a submarine
/// </summary>
False,
/// <summary>
/// Can fully enter a submarine. Make sure to only allow this on small/medium sized creatures that can reasonably fit inside rooms.
/// </summary>
True,
/// <summary>
/// The ragdoll's limbs can enter the sub, but the collider can't.
/// Can be used to e.g. allow the monster's head to poke into the sub to bite characters, even if the whole monster can't fit in the sub.
/// </summary>
Partial
}
class HumanRagdollParams : RagdollParams
{
public static HumanRagdollParams GetRagdollParams(Identifier speciesName, string fileName = null) => GetRagdollParams<HumanRagdollParams>(speciesName, fileName);
public static HumanRagdollParams GetDefaultRagdollParams(Identifier speciesName) => GetDefaultRagdollParams<HumanRagdollParams>(speciesName);
public static HumanRagdollParams GetDefaultRagdollParams(Character character) => GetDefaultRagdollParams<HumanRagdollParams>(character);
}
class FishRagdollParams : RagdollParams
{
public static FishRagdollParams GetDefaultRagdollParams(Identifier speciesName) => GetDefaultRagdollParams<FishRagdollParams>(speciesName);
public static FishRagdollParams GetDefaultRagdollParams(Character character) => GetDefaultRagdollParams<FishRagdollParams>(character);
}
class RagdollParams : EditableParams, IMemorizable<RagdollParams>
@@ -37,8 +54,11 @@ namespace Barotrauma
[Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes), Editable()]
public Color Color { get; set; }
[Serialize(0.0f, IsPropertySaveable.Yes, description: "The orientation of the sprites as drawn on the sprite sheet. Can be overridden by setting a value for Limb's 'Sprite Orientation'."), Editable(-360, 360)]
[Serialize(0.0f, IsPropertySaveable.Yes, description: "General orientation of the sprites as drawn on the spritesheet. " +
"Defines the \"forward direction\" of the sprites. Should be configured as the direction pointing outwards from the main limb. " +
"Incorrectly defined orientations may lead to limbs being rotated incorrectly when e.g. when the character aims or flips to face a different direction. " +
"Can be overridden per sprite by setting a value for Limb's 'Sprite Orientation'."), Editable(-360, 360)]
public float SpritesheetOrientation { get; set; }
public bool IsSpritesheetOrientationHorizontal
@@ -53,11 +73,19 @@ namespace Barotrauma
private float limbScale;
[Serialize(1.0f, IsPropertySaveable.Yes), Editable(MIN_SCALE, MAX_SCALE, DecimalCount = 3)]
public float LimbScale { get { return limbScale; } set { limbScale = MathHelper.Clamp(value, MIN_SCALE, MAX_SCALE); } }
public float LimbScale
{
get { return limbScale; }
set { limbScale = MathHelper.Clamp(value, MIN_SCALE, MAX_SCALE); }
}
private float jointScale;
[Serialize(1.0f, IsPropertySaveable.Yes), Editable(MIN_SCALE, MAX_SCALE, DecimalCount = 3)]
public float JointScale { get { return jointScale; } set { jointScale = MathHelper.Clamp(value, MIN_SCALE, MAX_SCALE); } }
public float JointScale
{
get { return jointScale; }
set { jointScale = MathHelper.Clamp(value, MIN_SCALE, MAX_SCALE); }
}
// Don't show in the editor, because shouldn't be edited in runtime. Requires that the limb scale and the collider sizes are adjusted. TODO: automatize?
[Serialize(1f, IsPropertySaveable.No)]
@@ -69,8 +97,8 @@ namespace Barotrauma
[Serialize(50f, IsPropertySaveable.Yes, description: "How much impact is required before the character takes impact damage?"), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
public float ImpactTolerance { get; set; }
[Serialize(true, IsPropertySaveable.Yes, description: "Can the creature enter submarine. Creatures that cannot enter submarines, always collide with it, even when there is a gap."), Editable()]
public bool CanEnterSubmarine { get; set; }
[Serialize(CanEnterSubmarine.True, IsPropertySaveable.Yes, description: "Can the creature enter submarine. Creatures that cannot enter submarines, always collide with it, even when there is a gap."), Editable()]
public CanEnterSubmarine CanEnterSubmarine { get; set; }
[Serialize(true, IsPropertySaveable.Yes), Editable]
public bool CanWalk { get; set; }
@@ -86,7 +114,7 @@ namespace Barotrauma
/// key2: File path
/// value: Ragdoll parameters
/// </summary>
private readonly static Dictionary<Identifier, Dictionary<string, RagdollParams>> allRagdolls = new Dictionary<Identifier, Dictionary<string, RagdollParams>>();
private static readonly Dictionary<Identifier, Dictionary<string, RagdollParams>> allRagdolls = new Dictionary<Identifier, Dictionary<string, RagdollParams>>();
public List<ColliderParams> Colliders { get; private set; } = new List<ColliderParams>();
public List<LimbParams> Limbs { get; private set; } = new List<LimbParams>();
@@ -106,8 +134,7 @@ namespace Barotrauma
CharacterPrefab prefab = CharacterPrefab.Find(p => p.Identifier == speciesName && (contentPackage == null || p.ContentFile.ContentPackage == contentPackage));
if (prefab?.ConfigElement == null)
{
DebugConsole.ThrowError($"Failed to find config file for '{speciesName}'",
contentPackage: contentPackage);
DebugConsole.ThrowError($"Failed to find config file for '{speciesName}'", contentPackage: contentPackage);
return string.Empty;
}
return GetFolder(prefab.ConfigElement, prefab.ContentFile.Path.Value);
@@ -115,99 +142,151 @@ namespace Barotrauma
private static string GetFolder(ContentXElement root, string filePath)
{
var folder = root?.GetChildElement("ragdolls")?.GetAttributeContentPath("folder")?.Value;
Debug.Assert(filePath != null);
Debug.Assert(root != null);
string folder = (root.GetChildElement("ragdolls") ?? root.GetChildElement("ragdoll"))?.GetAttributeContentPath("folder")?.Value;
if (folder.IsNullOrEmpty() || folder.Equals("default", StringComparison.OrdinalIgnoreCase))
{
folder = IO.Path.Combine(IO.Path.GetDirectoryName(filePath), "Ragdolls") + IO.Path.DirectorySeparatorChar;
}
return folder.CleanUpPathCrossPlatform(correctFilenameCase: true);
}
public static T GetDefaultRagdollParams<T>(Identifier speciesName) where T : RagdollParams, new() => GetRagdollParams<T>(speciesName);
/// <summary>
/// If the file name is left null, default file is selected. If fails, will select the default file. Note: Use the filename without the extensions, don't use the full path!
/// If a custom folder is used, it's defined in the character info file.
/// </summary>
public static T GetRagdollParams<T>(Identifier speciesName, string fileName = null) where T : RagdollParams, new()
public static T GetDefaultRagdollParams<T>(Character character) where T : RagdollParams, new() => GetDefaultRagdollParams<T>(character.SpeciesName, character.Params, character.Prefab.ContentPackage);
public static T GetDefaultRagdollParams<T>(Identifier speciesName, CharacterParams characterParams, ContentPackage contentPackage) where T : RagdollParams, new()
{
if (speciesName.IsEmpty)
XElement mainElement = characterParams.VariantFile?.Root ?? characterParams.MainElement;
return GetDefaultRagdollParams<T>(speciesName, mainElement, contentPackage);
}
public static T GetDefaultRagdollParams<T>(Identifier speciesName, XElement characterRootElement, ContentPackage contentPackage) where T : RagdollParams, new()
{
Debug.Assert(contentPackage != null);
if (characterRootElement.IsOverride())
{
throw new Exception($"Species name null or empty!");
characterRootElement = characterRootElement.FirstElement();
}
Identifier ragdollSpecies = speciesName;
Identifier variantOf = characterRootElement.VariantOf();
if (characterRootElement != null && (characterRootElement.GetChildElement("ragdolls") ?? characterRootElement.GetChildElement("ragdoll")) is XElement ragdollElement)
{
if ((ragdollElement.GetAttributeContentPath("path", contentPackage) ?? ragdollElement.GetAttributeContentPath("file", contentPackage)) is ContentPath path)
{
return GetRagdollParams<T>(speciesName, ragdollSpecies, file: path, contentPackage);
}
else if (!variantOf.IsEmpty)
{
string folder = ragdollElement.GetAttributeContentPath("folder", contentPackage)?.Value;
if (folder.IsNullOrEmpty() || folder.Equals("default", StringComparison.OrdinalIgnoreCase))
{
// Folder attribute not defined or set to default -> use the ragdoll defined in the base definition file.
if (CharacterPrefab.FindBySpeciesName(variantOf) is CharacterPrefab prefab)
{
ragdollSpecies = prefab.GetBaseCharacterSpeciesName(variantOf);
}
}
}
}
else if (!variantOf.IsEmpty && CharacterPrefab.FindBySpeciesName(variantOf) is CharacterPrefab prefab)
{
// Ragdoll element not defined -> use the ragdoll defined in the base definition file.
ragdollSpecies = prefab.GetBaseCharacterSpeciesName(variantOf);
}
// Using a null file definition means we use the default animations found in the Ragdolls folder.
return GetRagdollParams<T>(speciesName, ragdollSpecies, file: null, contentPackage);
}
public static T GetRagdollParams<T>(Identifier speciesName, Identifier ragdollSpecies, Either<string, ContentPath> file, ContentPackage contentPackage) where T : RagdollParams, new()
{
Debug.Assert(!speciesName.IsEmpty);
Debug.Assert(!ragdollSpecies.IsEmpty);
ContentPath contentPath = null;
string fileName = null;
if (file != null)
{
if (!file.TryGet(out fileName))
{
file.TryGet(out contentPath);
}
Debug.Assert(!fileName.IsNullOrWhiteSpace() || !contentPath.IsNullOrWhiteSpace());
}
Debug.Assert(contentPackage != null);
if (!allRagdolls.TryGetValue(speciesName, out Dictionary<string, RagdollParams> ragdolls))
{
ragdolls = new Dictionary<string, RagdollParams>();
allRagdolls.Add(speciesName, ragdolls);
}
if (!string.IsNullOrEmpty(fileName) && ragdolls.TryGetValue(fileName, out RagdollParams ragdoll))
string key = fileName ?? contentPath?.Value ?? GetDefaultFileName(ragdollSpecies);
if (ragdolls.TryGetValue(key, out RagdollParams ragdoll))
{
// Already cached.
return (T)ragdoll;
}
string selectedFile = null;
Identifier ragdollSpecies = speciesName;
if (CharacterPrefab.Prefabs.TryGet(speciesName, out var prefab))
if (!contentPath.IsNullOrEmpty())
{
if (!prefab.VariantOf.IsEmpty)
// Load the ragdoll from path.
T ragdollInstance = new T();
if (ragdollInstance.Load(contentPath, ragdollSpecies))
{
ragdollSpecies = prefab.VariantOf;
ragdolls.TryAdd(contentPath.Value, ragdollInstance);
return ragdollInstance;
}
string error = null;
string folder = GetFolder(ragdollSpecies);
if (!Directory.Exists(folder))
else
{
error = $"[RagdollParams] Invalid directory: {folder}. Using the default ragdoll.";
DebugConsole.ThrowError($"[AnimationParams] Failed to load an animation {ragdollInstance} from {contentPath.Value} for the character {speciesName}. Using the default ragdoll.", contentPackage: contentPackage);
}
}
// Seek the default ragdoll from the character's ragdoll folder.
string selectedFile;
string folder = GetFolder(ragdollSpecies);
if (Directory.Exists(folder))
{
var files = Directory.GetFiles(folder).OrderBy(f => f, StringComparer.OrdinalIgnoreCase);
if (files.None())
{
DebugConsole.ThrowError($"[RagdollParams] Could not find any ragdoll files from the folder: {folder}. Using the default ragdoll.", contentPackage: contentPackage);
selectedFile = GetDefaultFile(ragdollSpecies);
}
else
{
string[] files = Directory.GetFiles(folder);
if (files.None())
if (string.IsNullOrEmpty(fileName))
{
error = $"[RagdollParams] Could not find any ragdoll files from the folder: {folder}. Using the default ragdoll.";
selectedFile = GetDefaultFile(ragdollSpecies);
}
else if (string.IsNullOrEmpty(fileName))
{
// Files found, but none specified
selectedFile = GetDefaultFile(ragdollSpecies);
// Files found, but none specified -> Get a matching ragdoll from the specified folder.
// First try to find a file that matches the default file name. If that fails, just take any file.
string defaultFileName = GetDefaultFileName(ragdollSpecies);
selectedFile = files.FirstOrDefault(f => f.Contains(defaultFileName, StringComparison.OrdinalIgnoreCase)) ?? files.First();
}
else
{
selectedFile = files.FirstOrDefault(f => IO.Path.GetFileNameWithoutExtension(f).Equals(fileName, StringComparison.OrdinalIgnoreCase));
if (selectedFile == null)
{
error = $"[RagdollParams] Could not find a ragdoll file that matches the name {fileName}. Using the default ragdoll.";
DebugConsole.ThrowError($"[RagdollParams] Could not find a ragdoll file that matches the name {fileName}. Using the default ragdoll.", contentPackage: contentPackage);
selectedFile = GetDefaultFile(ragdollSpecies);
}
}
}
}
if (error != null)
{
DebugConsole.ThrowError(error,
contentPackage: prefab?.ContentPackage);
}
}
if (selectedFile == null)
{
throw new Exception("[RagdollParams] Selected file null!");
}
DebugConsole.Log($"[RagdollParams] Loading ragdoll from {selectedFile}.");
var characterPrefab = CharacterPrefab.Prefabs[speciesName];
T r = new T();
if (r.Load(ContentPath.FromRaw(characterPrefab.ContentPackage, selectedFile), ragdollSpecies))
{
if (!ragdolls.ContainsKey(r.Name))
{
ragdolls.Add(r.Name, r);
}
return r;
}
else
{
// Failing to create a ragdoll causes so many issues that cannot be handled. Dummy ragdoll just seems to make things harded to debug. It's better to fail early.
DebugConsole.ThrowError($"[RagdollParams] Invalid directory: {folder}. Using the default ragdoll.", contentPackage: contentPackage);
selectedFile = GetDefaultFile(ragdollSpecies);
}
Debug.Assert(selectedFile != null);
DebugConsole.Log($"[RagdollParams] Loading the ragdoll from {selectedFile}.");
T r = new T();
if (r.Load(ContentPath.FromRaw(contentPackage, selectedFile), speciesName))
{
ragdolls.TryAdd(key, r);
}
else
{
// Failing to create a ragdoll causes so many issues that cannot be handled. Dummy ragdoll just seems to make things harder to debug. It's better to fail early.
throw new Exception($"[RagdollParams] Failed to load ragdoll {r.Name} from {selectedFile} for the character {speciesName}.");
}
return r;
}
/// <summary>
@@ -234,9 +313,9 @@ namespace Barotrauma
instance.IsLoaded = instance.Deserialize(mainElement);
instance.Save();
instance.Load(contentPath, speciesName);
ragdolls.Add(instance.Name, instance);
ragdolls.Add(instance.FileNameWithoutExtension, instance);
DebugConsole.NewMessage("[RagdollParams] New default ragdoll params successfully created at " + fullPath, Color.NavajoWhite);
return instance as T;
return instance;
}
public static void ClearCache() => allRagdolls.Clear();
@@ -250,16 +329,17 @@ namespace Barotrauma
else
{
// Update the key by removing and re-adding the ragdoll.
string fileName = FileNameWithoutExtension;
if (allRagdolls.TryGetValue(SpeciesName, out Dictionary<string, RagdollParams> ragdolls))
{
ragdolls.Remove(Name);
ragdolls.Remove(fileName);
}
base.UpdatePath(fullPath);
if (ragdolls != null)
{
if (!ragdolls.ContainsKey(Name))
if (!ragdolls.ContainsKey(fileName))
{
ragdolls.Add(Name, this);
ragdolls.Add(fileName, this);
}
}
}
@@ -282,6 +362,7 @@ namespace Barotrauma
{
if (Load(file))
{
isVariantScaleApplied = false;
SpeciesName = speciesName;
CreateColliders();
CreateLimbs();
@@ -398,18 +479,21 @@ namespace Barotrauma
}
#endif
private bool variantScaleApplied;
public void ApplyVariantScale(XDocument variantFile)
private bool isVariantScaleApplied;
public void TryApplyVariantScale(XDocument variantFile)
{
if (variantScaleApplied) { return; }
if (isVariantScaleApplied) { return; }
if (variantFile == null) { return; }
var scaleMultiplier = variantFile.Root.GetChildElement("ragdoll")?.GetAttributeFloat("scalemultiplier", 1f);
if (scaleMultiplier.HasValue)
if (variantFile.GetRootExcludingOverride() is XElement root)
{
JointScale *= scaleMultiplier.Value;
LimbScale *= scaleMultiplier.Value;
if ((root.GetChildElement("ragdoll") ?? root.GetChildElement("ragdolls")) is XElement ragdollElement)
{
float scaleMultiplier = ragdollElement.GetAttributeFloat("scalemultiplier", 1f);
JointScale *= scaleMultiplier;
LimbScale *= scaleMultiplier;
}
}
variantScaleApplied = true;
isVariantScaleApplied = true;
}
#endregion
@@ -623,8 +707,11 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.Yes, description: "Disable drawing for this limb."), Editable()]
public bool Hide { get; set; }
[Serialize(float.NaN, IsPropertySaveable.Yes, description: "The orientation of the sprite as drawn on the sprite sheet. Overrides the value defined in the Ragdoll settings."), Editable(-360, 360, ValueStep = 90, DecimalCount = 0)]
[Serialize(float.NaN, IsPropertySaveable.Yes, description: "Orientation of the sprite as drawn on the spritesheet. " +
"Defines the \"forward direction\" of the sprite. Should be configured as the direction pointing outwards from the main limb." +
"Incorrectly defined orientations may lead to limbs being rotated incorrectly when e.g. when the character aims or flips to face a different direction. " +
"Overrides the value of 'Spritesheet Orientation' for this limb."), Editable(-360, 360, ValueStep = 90, DecimalCount = 0)]
public float SpriteOrientation { get; set; }
[Serialize(LimbType.None, IsPropertySaveable.Yes, description: "If set, the limb sprite will use the same sprite depth as the specified limb. Generally only useful for limbs that get added on the ragdoll on the fly (e.g. extra limbs added via gene splicing).")]
@@ -744,6 +831,9 @@ namespace Barotrauma
[Serialize(0.05f, IsPropertySaveable.Yes)]
public float Restitution { get; set; }
[Serialize(true, IsPropertySaveable.Yes, description: "Can the limb enter submarines? Only valid if the ragdoll's CanEnterSubmarine is set to Partial, otherwise the limb can enter if the ragdoll can."), Editable]
public bool CanEnterSubmarine { get; private set; }
public LimbParams(ContentXElement element, RagdollParams ragdoll) : base(element, ragdoll)
{
var spriteElement = element.GetChildElement("sprite");
@@ -12,9 +12,9 @@ namespace Barotrauma.Abilities
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
if ((abilityObject as IAbilityAffliction)?.Affliction is Affliction affliction)
if (abilityObject is IAbilityAffliction { Affliction: Affliction affliction })
{
return afflictions.Any(a => a == affliction.Identifier);
return afflictions.Any(a => a == affliction.Identifier || a == affliction.Prefab.AfflictionType);
}
else
{
@@ -1,16 +1,15 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class AbilityConditionItemInSubmarine : AbilityConditionData
[TypePreviouslyKnownAs("AbilityConditionItemInSubmarine")]
class AbilityConditionInSubmarine : AbilityConditionData
{
private readonly SubmarineType? submarineType;
public AbilityConditionItemInSubmarine(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
public AbilityConditionInSubmarine(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
if (conditionElement.GetAttribute("submarinetype") != null)
{
submarineType = conditionElement.GetAttributeEnum<SubmarineType>("submarinetype", SubmarineType.Player);
submarineType = conditionElement.GetAttributeEnum("submarinetype", SubmarineType.Player);
}
}
@@ -30,9 +29,15 @@ namespace Barotrauma.Abilities
}
else
{
LogAbilityConditionError(abilityObject, typeof(IAbilityItem));
return false;
return MatchesCondition();
}
}
public override bool MatchesCondition()
{
if (character.Submarine is null) { return false; }
return character.Submarine?.Info?.Type == submarineType;
}
}
}
@@ -8,6 +8,7 @@ namespace Barotrauma.Abilities
{
private readonly Option<int> matchedLevel;
private readonly Option<int> minLevel;
private readonly Option<int> maxLevel;
public AbilityConditionHasLevel(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
@@ -18,23 +19,33 @@ namespace Barotrauma.Abilities
minLevel = conditionElement.GetAttributeInt("minlevel", 0) is var min and not 0
? Option<int>.Some(min)
: Option<int>.None();
maxLevel = conditionElement.GetAttributeInt("maxlevel", 0) is var max and not 0
? Option<int>.Some(max)
: Option<int>.None();
if (matchedLevel.IsNone() && minLevel.IsNone())
if (matchedLevel.IsNone() && minLevel.IsNone() && maxLevel.IsNone())
{
throw new Exception($"{nameof(AbilityConditionHasLevel)} must have either \"levelequals\" or \"minlevel\" attribute.");
throw new Exception($"{nameof(AbilityConditionHasLevel)} must have either \"levelequals\", \"minlevel\" or \"maxlevel\" attribute.");
}
}
protected override bool MatchesConditionSpecific()
{
var currentLevel = character.Info.GetCurrentLevel();
if (matchedLevel.TryUnwrap(out int match))
{
return character.Info.GetCurrentLevel() == match;
return currentLevel == match;
}
if (minLevel.TryUnwrap(out int min))
{
return character.Info.GetCurrentLevel() >= min;
return currentLevel >= min;
}
if (maxLevel.TryUnwrap(out int max))
{
return currentLevel <= max;
}
return false;
@@ -12,7 +12,9 @@ namespace Barotrauma.Abilities
protected override bool MatchesConditionSpecific()
{
return character.IsRagdolled || character.Stun > 0f || character.IsIncapacitated;
// TODO: Should we only check whether the target is ragdolling here?
// Or should we use character.IsKnockedDown instead?
return (character.IsRagdolled && !character.AnimController.IsHangingWithRope) || character.Stun > 0f || character.IsIncapacitated;
}
}
}
@@ -100,7 +100,7 @@ namespace Barotrauma.Abilities
string type = abilityElement.Name.ToString().ToLowerInvariant();
try
{
abilityType = Type.GetType("Barotrauma.Abilities." + type + "", false, true);
abilityType = ReflectionUtils.GetTypeWithBackwardsCompatibility("Barotrauma.Abilities", type, false, true);
if (abilityType == null)
{
if (errorMessages) DebugConsole.ThrowError("Could not find the CharacterAbility \"" + type + "\" (" + characterAbilityGroup.CharacterTalent.DebugIdentifier + ")",
@@ -21,24 +21,42 @@
}
}
protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate)
{
if (conditionsMatched)
{
ApplyEffect();
}
}
protected override void ApplyEffect()
{
ApplyAfflictionToCharacter(Character);
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
if (abilityObject is IAbilityCharacter character)
{
var afflictionPrefab = AfflictionPrefab.Prefabs.Find(a => a.Identifier == afflictionId);
if (afflictionPrefab == null)
{
DebugConsole.ThrowError($"Error in CharacterAbilityGiveAffliction - could not find an affliction with the identifier \"{afflictionId}\".",
contentPackage: CharacterTalent.Prefab.ContentPackage);
return;
}
float strength = this.strength;
if (!string.IsNullOrEmpty(multiplyStrengthBySkill))
{
strength *= Character.GetSkillLevel(multiplyStrengthBySkill);
}
character.Character.CharacterHealth.ApplyAffliction(null, afflictionPrefab.Instantiate(strength), allowStacking: !setValue);
ApplyAfflictionToCharacter(character.Character);
}
}
private void ApplyAfflictionToCharacter(Character character)
{
var afflictionPrefab = AfflictionPrefab.Prefabs.Find(a => a.Identifier == afflictionId);
if (afflictionPrefab == null)
{
DebugConsole.ThrowError($"Error in CharacterAbilityGiveAffliction - could not find an affliction with the identifier \"{afflictionId}\".",
contentPackage: CharacterTalent.Prefab.ContentPackage);
return;
}
float strength = this.strength;
if (!string.IsNullOrEmpty(multiplyStrengthBySkill))
{
strength *= Character.GetSkillLevel(multiplyStrengthBySkill);
}
character.CharacterHealth.ApplyAffliction(null, afflictionPrefab.Instantiate(strength), allowStacking: !setValue);
}
}
}
@@ -15,12 +15,13 @@
DebugConsole.ThrowError("Error in CharacterAbilityGiveResistance - resistance identifier not set.",
contentPackage: abilityElement.ContentPackage);
}
// NOTE: The resistance value is a multiplier here, so 1.0 == 0% resistance
if (MathUtils.NearlyEqual(multiplier, 1))
{
DebugConsole.AddWarning($"Possible error in talent {CharacterTalent.DebugIdentifier} - multiplier set to 1, which will do nothing.",
contentPackage: abilityElement.ContentPackage);
}
}
public override void InitializeAbility(bool addingFirstTime)
@@ -22,7 +22,7 @@ namespace Barotrauma.Abilities
protected override void ApplyEffect(AbilityObject abilityObject)
{
if (abilityObject is not IAbilityCharacter character) { return; }
character.Character.CharacterHealth.ReduceAfflictionOnAllLimbs(afflictionId, amount);
character.Character.CharacterHealth.ReduceAfflictionOnAllLimbs(afflictionId, amount, attacker: Character);
}
}
}
@@ -0,0 +1,57 @@
#nullable enable
namespace Barotrauma.Abilities;
internal class CharacterAbilityUpgradeSubmarine : CharacterAbility
{
private readonly UpgradePrefab? upgradePrefab;
private readonly UpgradeCategory? upgradeCategory;
public readonly int level;
public override bool AllowClientSimulation => true;
public CharacterAbilityUpgradeSubmarine(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
var prefabIdentifier = abilityElement.GetAttributeIdentifier(nameof(upgradePrefab), Identifier.Empty);
var categoryIdentifier = abilityElement.GetAttributeIdentifier(nameof(upgradeCategory), Identifier.Empty);
if (UpgradePrefab.Find(prefabIdentifier) is not { } foundUpgradePrefab)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, {nameof(CharacterAbilityUpgradeSubmarine)} - {nameof(upgradePrefab)} not found.",
contentPackage: abilityElement.ContentPackage);
}
else
{
upgradePrefab = foundUpgradePrefab;
}
if (UpgradeCategory.Find(categoryIdentifier) is not { } foundUpgradeCategory)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, {nameof(CharacterAbilityUpgradeSubmarine)} - {nameof(upgradeCategory)} not found.",
contentPackage: abilityElement.ContentPackage);
}
else
{
upgradeCategory = foundUpgradeCategory;
}
level = abilityElement.GetAttributeInt(nameof(level), 1);
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
ApplyEffectSpecific();
}
protected override void ApplyEffect()
{
ApplyEffectSpecific();
}
private void ApplyEffectSpecific()
{
if (upgradePrefab == null || upgradeCategory == null) { return; }
if (GameMain.GameSession?.Campaign?.UpgradeManager is not { } upgradeManager) { return; }
upgradeManager.AddUpgradeExternally(upgradePrefab, upgradeCategory, level);
}
}
@@ -0,0 +1,63 @@
namespace Barotrauma.Abilities;
/// <summary>
/// Hardcoded ability for the "War Stories" talent.
/// Spawns an item and sets the health multiplier to the target stat value.
///
/// The item spawned should have a default health of 1 because we set the multiplier.
/// This is because we already had existing Item.HealthMultiplier that gets synced and
/// everything but not one for setting the max health directly to some value and I didn't
/// want to add a new one just for this.
/// </summary>
internal class CharacterAbilityWarStories : CharacterAbility
{
private readonly Identifier targetStat;
private readonly float minCondition;
private readonly ItemPrefab prefab;
public CharacterAbilityWarStories(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
targetStat = abilityElement.GetAttributeIdentifier("target", Identifier.Empty);
minCondition = abilityElement.GetAttributeFloat("mincondition", 1);
if (targetStat.IsEmpty)
{
DebugConsole.ThrowError($"{nameof(CharacterAbilityWarStories)}: target stat is not defined", contentPackage: abilityElement.ContentPackage);
}
Identifier spawnedItem = abilityElement.GetAttributeIdentifier("item", Identifier.Empty);
if (!ItemPrefab.Prefabs.TryGet(spawnedItem, out prefab))
{
DebugConsole.ThrowError($"{nameof(CharacterAbilityWarStories)}: spawned item \"{spawnedItem}\" could not be found.", contentPackage: abilityElement.ContentPackage);
}
}
protected override void ApplyEffect()
{
if (prefab is null || Character is null) { return; }
float condition = Character.Info?.GetSavedStatValue(StatTypes.None, targetStat) ?? 0;
if (condition < minCondition) { return; }
if (GameMain.GameSession?.RoundEnding ?? true)
{
Item item = new(prefab, Character.WorldPosition, Character.Submarine)
{
Condition = condition,
HealthMultiplier = condition
};
Character.Inventory.TryPutItem(item, Character, item.AllowedSlots);
}
else
{
Entity.Spawner?.AddItemToSpawnQueue(prefab, Character.Inventory, condition: condition, onSpawned: item =>
{
item.HealthMultiplier = condition;
});
}
}
protected override void ApplyEffect(AbilityObject abilityObject)
=> ApplyEffect();
}
@@ -140,7 +140,7 @@ namespace Barotrauma.Abilities
string type = conditionElement.Name.ToString().ToLowerInvariant();
try
{
conditionType = Type.GetType("Barotrauma.Abilities." + type + "", false, true);
conditionType = ReflectionUtils.GetTypeWithBackwardsCompatibility("Barotrauma.Abilities", type, false, true);
if (conditionType == null)
{
if (errorMessages)
@@ -22,6 +22,12 @@ namespace Barotrauma
public readonly Sprite Icon;
/// <summary>
/// When set to a value the talent tooltip will display a text showing the current value of the stat and the max value.
/// For example "Progress: 37/100".
/// </summary>
public readonly Option<(Identifier PermanentStatIdentifier, int Max)> TrackedStat;
#if CLIENT
public readonly Option<Color> ColorOverride;
#endif
@@ -44,6 +50,12 @@ namespace Barotrauma
AbilityEffectsStackWithSameTalent = element.GetAttributeBool("abilityeffectsstackwithsametalent", true);
var trackedStat = element.GetAttributeIdentifier("trackedstat", Identifier.Empty);
var trackedMax = element.GetAttributeInt("trackedmax", 100);
TrackedStat = !trackedStat.IsEmpty
? Option.Some((trackedStat, trackedMax))
: Option.None;
Identifier nameIdentifier = element.GetAttributeIdentifier("nameidentifier", Identifier.Empty);
if (!nameIdentifier.IsEmpty)
{
@@ -82,18 +82,17 @@ namespace Barotrauma
TalentSubTree subTree = talentTree!.TalentSubTrees.FirstOrDefault(tst => tst.Identifier == subTreeIdentifier);
if (subTree is null) { return TalentStages.Invalid; }
if (!TalentTreeMeetsRequirements(talentTree, subTree, selectedTalents))
{
return TalentStages.Locked;
}
TalentOption targetTalentOption = subTree.TalentOptionStages[index];
if (targetTalentOption.HasEnoughTalents(character.Info))
{
return TalentStages.Unlocked;
}
if (!TalentTreeMeetsRequirements(talentTree, subTree, selectedTalents))
{
return TalentStages.Locked;
}
if (targetTalentOption.HasSelectedTalent(selectedTalents))
{
return TalentStages.Highlighted;
@@ -0,0 +1,83 @@
#nullable enable
using System.Xml.Linq;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
internal sealed partial class CircuitBoxLabelNode : CircuitBoxNode, ICircuitBoxIdentifiable
{
public Color Color;
public ushort ID { get; }
public override bool IsResizable => true;
public static NetLimitedString DefaultHeaderText => new("label");
public NetLimitedString BodyText = NetLimitedString.Empty;
public NetLimitedString HeaderText = DefaultHeaderText;
public static Vector2 MinSize = new(128, 8);
public CircuitBoxLabelNode(ushort id, Color color, Vector2 pos, CircuitBox circuitBox) : base(circuitBox)
{
Size = new Vector2(256);
Position = pos;
ID = id;
Color = color;
UpdatePositions();
#if CLIENT
bodyLabel = new GUITextBlock(new RectTransform(Point.Zero), text: string.Empty, font: GUIStyle.Font, textAlignment: Alignment.TopLeft, wrap: true);
headerLabel = new CircuitBoxLabel(HeaderText.Value, GUIStyle.LargeFont);
UpdateDrawRects();
UpdateTextSizes(DrawRect);
#endif
}
public void EditText(NetLimitedString header, NetLimitedString body)
{
HeaderText = header;
BodyText = body;
#if CLIENT
UpdateTextSizes(DrawRect);
#endif
}
public XElement Save()
{
var element = new XElement("Label",
new XAttribute("id", ID),
new XAttribute("color", Color.ToStringHex()),
new XAttribute("position", XMLExtensions.Vector2ToString(Position)),
new XAttribute("size", XMLExtensions.Vector2ToString(Size)),
new XAttribute("header", HeaderText),
new XAttribute("body", BodyText));
return element;
}
public static CircuitBoxLabelNode LoadFromXML(ContentXElement element, CircuitBox circuitBox)
{
ushort id = element.GetAttributeUInt16("id", ICircuitBoxIdentifiable.NullComponentID);
Vector2 position = element.GetAttributeVector2("position", Vector2.Zero);
Vector2 size = element.GetAttributeVector2("size", Vector2.Zero);
Color color = element.GetAttributeColor("color", Color.White);
string header = element.GetAttributeString("header", string.Empty);
string body = element.GetAttributeString("body", string.Empty);
var labelNode = new CircuitBoxLabelNode(id, color, position, circuitBox)
{
Size = size,
HeaderText = new NetLimitedString(header),
BodyText = new NetLimitedString(body)
};
// proc a edit to force the sizes to be updated
labelNode.EditText(new NetLimitedString(header), new NetLimitedString(body));
labelNode.UpdatePositions();
#if CLIENT
labelNode.UpdateTextSizes(labelNode.Rect);
#endif
return labelNode;
}
}
}
@@ -8,6 +8,19 @@ using Microsoft.Xna.Framework;
namespace Barotrauma
{
[Flags]
internal enum CircuitBoxResizeDirection
{
None = 0,
Down = 1,
Right = 2,
Left = 4
}
// TODO this needs to be refactored at some point for reasons:
// 1. We need to send 4 different ImmutableArray<short> for some network packets
// 2. We have 3 identical remove events that are identical in signature
// 3. We have 3 different events for selecting. nodes, wires, and server broadcast
public enum CircuitBoxOpcode
{
Error,
@@ -20,6 +33,10 @@ namespace Barotrauma
SelectWires,
UpdateSelection,
DeleteComponent,
RenameLabel,
AddLabel,
RemoveLabel,
ResizeLabel,
ServerInitialize
}
@@ -88,6 +105,18 @@ namespace Barotrauma
=> $"{{Name: {SignalConnection}, ID: {(TargetId.TryUnwrap(out var value) ? value.ToString() : "N/A")}}}";
}
[NetworkSerialize]
internal readonly record struct CircuitBoxAddLabelEvent(Vector2 Position, Color Color, NetLimitedString Header, NetLimitedString Body) : INetSerializableStruct;
[NetworkSerialize]
internal readonly record struct CircuitBoxServerAddLabelEvent(ushort ID, Vector2 Position, Vector2 Size, Color Color, NetLimitedString Header, NetLimitedString Body) : INetSerializableStruct;
[NetworkSerialize]
internal readonly record struct CircuitBoxResizeLabelEvent(ushort ID, Vector2 Position, Vector2 Size) : INetSerializableStruct;
[NetworkSerialize]
internal readonly record struct CircuitBoxRemoveLabelEvent(ImmutableArray<ushort> TargetIDs) : INetSerializableStruct;
[NetworkSerialize]
internal readonly record struct CircuitBoxAddComponentEvent(UInt32 PrefabIdentifier, Vector2 Position) : INetSerializableStruct;
@@ -98,13 +127,13 @@ namespace Barotrauma
internal readonly record struct CircuitBoxRemoveComponentEvent(ImmutableArray<ushort> TargetIDs) : INetSerializableStruct;
[NetworkSerialize]
internal readonly record struct CircuitBoxMoveComponentEvent(ImmutableArray<ushort> TargetIDs, ImmutableArray<CircuitBoxInputOutputNode.Type> IOs, Vector2 MoveAmount) : INetSerializableStruct;
internal readonly record struct CircuitBoxMoveComponentEvent(ImmutableArray<ushort> TargetIDs, ImmutableArray<CircuitBoxInputOutputNode.Type> IOs, ImmutableArray<ushort> LabelIDs, Vector2 MoveAmount) : INetSerializableStruct;
[NetworkSerialize]
internal readonly record struct CircuitBoxSelectNodesEvent(ImmutableArray<ushort> TargetIDs, ImmutableArray<CircuitBoxInputOutputNode.Type> IOs, bool Overwrite, ushort CharacterID) : INetSerializableStruct;
internal readonly record struct CircuitBoxSelectNodesEvent(ImmutableArray<ushort> TargetIDs, ImmutableArray<CircuitBoxInputOutputNode.Type> IOs, ImmutableArray<ushort> LabelIDs, bool Overwrite, ushort CharacterID) : INetSerializableStruct;
[NetworkSerialize]
internal readonly record struct CircuitBoxServerUpdateSelection(ImmutableArray<CircuitBoxIdSelectionPair> ComponentIds, ImmutableArray<CircuitBoxIdSelectionPair> WireIds, ImmutableArray<CircuitBoxTypeSelectionPair> InputOutputs) : INetSerializableStruct;
internal readonly record struct CircuitBoxServerUpdateSelection(ImmutableArray<CircuitBoxIdSelectionPair> ComponentIds, ImmutableArray<CircuitBoxIdSelectionPair> WireIds, ImmutableArray<CircuitBoxTypeSelectionPair> InputOutputs, ImmutableArray<CircuitBoxIdSelectionPair> LabelIds) : INetSerializableStruct;
[NetworkSerialize]
internal readonly record struct CircuitBoxIdSelectionPair(ushort ID, Option<ushort> SelectedBy) : INetSerializableStruct;
@@ -124,6 +153,9 @@ namespace Barotrauma
[NetworkSerialize]
internal readonly record struct CircuitBoxRemoveWireEvent(ImmutableArray<ushort> TargetIDs) : INetSerializableStruct;
[NetworkSerialize]
internal readonly record struct CircuitBoxRenameLabelEvent(ushort LabelId, Color Color, NetLimitedString NewHeader, NetLimitedString NewBody) : INetSerializableStruct;
[NetworkSerialize]
internal readonly record struct CircuitBoxErrorEvent(string Message) : INetSerializableStruct;
@@ -131,6 +163,7 @@ namespace Barotrauma
internal readonly record struct CircuitBoxInitializeStateFromServerEvent(
ImmutableArray<CircuitBoxServerCreateComponentEvent> Components,
ImmutableArray<CircuitBoxServerCreateWireEvent> Wires,
ImmutableArray<CircuitBoxServerAddLabelEvent> Labels,
Vector2 InputPos,
Vector2 OutputPos) : INetSerializableStruct;
@@ -157,6 +190,14 @@ namespace Barotrauma
=> CircuitBoxOpcode.RemoveWire,
CircuitBoxInitializeStateFromServerEvent
=> CircuitBoxOpcode.ServerInitialize,
CircuitBoxRenameLabelEvent
=> CircuitBoxOpcode.RenameLabel,
(CircuitBoxAddLabelEvent or CircuitBoxServerAddLabelEvent)
=> CircuitBoxOpcode.AddLabel,
CircuitBoxRemoveLabelEvent
=> CircuitBoxOpcode.RemoveLabel,
CircuitBoxResizeLabelEvent
=> CircuitBoxOpcode.ResizeLabel,
_ => throw new ArgumentOutOfRangeException(nameof(Data))
};
}
@@ -14,6 +14,8 @@ namespace Barotrauma
public RectangleF Rect;
private Vector2 position;
public virtual bool IsResizable => false;
public Vector2 Position
{
get => position;
@@ -22,12 +24,12 @@ namespace Barotrauma
const float clampSize = CircuitBoxSizes.PlayableAreaSize / 2f;
position = new Vector2(Math.Clamp(value.X, -clampSize, clampSize),
Math.Clamp(value.Y, -clampSize, clampSize));
Math.Clamp(value.Y, -clampSize, clampSize));
UpdatePositions();
}
}
public ImmutableArray<CircuitBoxConnection> Connectors;
public ImmutableArray<CircuitBoxConnection> Connectors = ImmutableArray<CircuitBoxConnection>.Empty;
public static float Opacity = 0.8f;
@@ -38,6 +40,47 @@ namespace Barotrauma
CircuitBox = circuitBox;
}
public (Vector2 Size, Vector2 Pos) ResizeBy(CircuitBoxResizeDirection directions, Vector2 amount)
{
Vector2 newSize = Size;
Vector2 newPos = Position;
amount.Y = -amount.Y;
if (directions.HasFlag(CircuitBoxResizeDirection.Down))
{
newSize.Y += amount.Y;
newSize.Y = Math.Max(newSize.Y, CircuitBoxLabelNode.MinSize.Y);
newPos = new Vector2(newPos.X, newPos.Y - (newSize.Y - Size.Y) / 2f);
}
if (directions.HasFlag(CircuitBoxResizeDirection.Right))
{
newSize.X += amount.X;
newSize.X = Math.Max(newSize.X, CircuitBoxLabelNode.MinSize.X);
newPos = new Vector2(newPos.X + (newSize.X - Size.X) / 2f, newPos.Y);
}
if (directions.HasFlag(CircuitBoxResizeDirection.Left))
{
newSize.X -= amount.X;
newSize.X = Math.Max(newSize.X, CircuitBoxLabelNode.MinSize.X);
newPos = new Vector2(newPos.X + (Size.X - newSize.X) / 2f, newPos.Y);
}
return (newSize, newPos);
}
public void ApplyResize(Vector2 newSize, Vector2 newPos)
{
if (!MathUtils.IsValid(newSize)) { return; }
Size = newSize;
Position = newPos;
UpdatePositions();
#if CLIENT
OnResized(DrawRect);
#endif
}
public static Vector2 CalculateSize(IReadOnlyList<CircuitBoxConnection> conns)
{
Vector2 leftSize = Vector2.Zero,
@@ -11,6 +11,7 @@ namespace Barotrauma
public const int WireWidth = 10;
public const int WireKnobLength = 16;
public const int NodeHeaderTextPadding = 8;
public const int NodeBodyTextPadding = 8;
public const float PlayableAreaSize = 8192f;
}
@@ -148,8 +148,14 @@ namespace Barotrauma
}
case CircuitBoxNodeConnection node when two is CircuitBoxInputConnection input:
{
if (node.ExternallyConnectedFrom.Contains(input)) { break; }
node.ExternallyConnectedFrom.Add(input);
if (!node.Connection.CircuitBoxConnections.Contains(input))
{
node.Connection.CircuitBoxConnections.Add(input);
}
if (!node.ExternallyConnectedFrom.Contains(input))
{
node.ExternallyConnectedFrom.Add(input);
}
break;
}
}
@@ -14,6 +14,7 @@ namespace Barotrauma
public override void LoadFile()
{
ClearCaches();
XDocument doc = XMLExtensions.TryLoadXml(Path);
if (doc == null)
{
@@ -36,6 +37,15 @@ namespace Barotrauma
public override void UnloadFile()
{
CharacterPrefab.Prefabs.RemoveByFile(this);
ClearCaches();
}
private static void ClearCaches()
{
// Clear the caches to get rid of any overrides.
// Variants should have their own params instances, but let's keep it simple and play safe.
RagdollParams.ClearCache();
AnimationParams.ClearCache();
}
public override void Sort()
@@ -60,11 +70,11 @@ namespace Barotrauma
{
if (humanoid)
{
ragdollParams = RagdollParams.GetRagdollParams<HumanRagdollParams>(speciesName);
ragdollParams = RagdollParams.GetDefaultRagdollParams<HumanRagdollParams>(speciesName, mainElement, ContentPackage);
}
else
{
ragdollParams = RagdollParams.GetRagdollParams<FishRagdollParams>(speciesName);
ragdollParams = RagdollParams.GetDefaultRagdollParams<FishRagdollParams>(speciesName, mainElement, ContentPackage);
}
}
catch (Exception e)
@@ -0,0 +1,16 @@
#nullable enable
namespace Barotrauma
{
internal sealed class ContainerTagFile : GenericPrefabFile<ContainerTagPrefab>
{
public ContainerTagFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
protected override bool MatchesSingular(Identifier identifier) => identifier == "containertag";
protected override bool MatchesPlural(Identifier identifier) => identifier == "containertags";
protected override PrefabCollection<ContainerTagPrefab> Prefabs => ContainerTagPrefab.Prefabs;
protected override ContainerTagPrefab CreatePrefab(ContentXElement element)
=> new(element, this);
}
}
@@ -1,4 +1,4 @@
using System.Collections.Immutable;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;
@@ -37,7 +37,8 @@ namespace Barotrauma
if (newList.Count != 0) { TextManager.TextPacks.TryAdd(kvp.Key, newList); }
}
TextManager.IncrementLanguageVersion();
if (!TextManager.TextPacks.ContainsKey(GameSettings.CurrentConfig.Language))
if (!TextManager.TextPacks.ContainsKey(GameSettings.CurrentConfig.Language) &&
GameSettings.CurrentConfig.Language != TextManager.DefaultLanguage)
{
DebugConsole.AddWarning($"The language {GameSettings.CurrentConfig.Language} is no longer available. Switching to {TextManager.DefaultLanguage}...");
var config = GameSettings.CurrentConfig;
@@ -65,8 +65,13 @@ namespace Barotrauma
public static void ReloadCore()
{
if (Core == null) { return; }
Core.UnloadContent();
Core.LoadContent();
ReloadPackage(Core);
}
public static void ReloadPackage(ContentPackage p)
{
p.UnloadContent();
p.LoadContent();
SortContent();
}
@@ -22,7 +22,7 @@ namespace Barotrauma
private string? cachedValue;
private string? cachedFullPath;
public string Value
{
get
@@ -107,11 +107,6 @@ namespace Barotrauma
prevCreatedRaw = newRaw;
return newRaw;
}
public static ContentPath FromEvaluated(ContentPackage? contentPackage, string? evaluatedValue)
{
throw new NotImplementedException();
}
private static bool StringEquality(string? a, string? b)
{
@@ -119,8 +114,8 @@ namespace Barotrauma
{
return a.IsNullOrEmpty() == b.IsNullOrEmpty();
}
return string.Equals(Path.GetFullPath(a.CleanUpPathCrossPlatform(false) ?? ""),
Path.GetFullPath(b.CleanUpPathCrossPlatform(false) ?? ""), StringComparison.OrdinalIgnoreCase);
return string.Equals(Path.GetFullPath(a.CleanUpPathCrossPlatform(correctFilenameCase: false) ?? ""),
Path.GetFullPath(b.CleanUpPathCrossPlatform(correctFilenameCase: false) ?? ""), StringComparison.OrdinalIgnoreCase);
}
public static bool operator==(ContentPath a, ContentPath b)
@@ -145,9 +140,9 @@ namespace Barotrauma
public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
if (ReferenceEquals(null, obj)) { return false; }
if (ReferenceEquals(this, obj)) { return true; }
if (obj.GetType() != this.GetType()) { return false; }
return Equals((ContentPath)obj);
}
@@ -739,6 +739,35 @@ namespace Barotrauma
};
}, isCheat: true));
commands.Add(new Command("listsuitabletreatments", "listsuitabletreatments [character name]: List which items are the most suitable for treating the specified character. Useful for debugging medic AI.", (string[] args) =>
{
Character character = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args);
if (character != null)
{
Dictionary<Identifier, float> treatments = new Dictionary<Identifier, float>();
character.CharacterHealth.GetSuitableTreatments(treatments, user: null);
foreach (var treatment in treatments.OrderByDescending(t => t.Value))
{
Color color = Color.White;
#if CLIENT
color = ToolBox.GradientLerp(
MathUtils.InverseLerp(-1000, 1000, treatment.Value),
Color.Red, Color.Yellow, Color.White, Color.LightGreen);
#endif
NewMessage((int)treatment.Value + ": " + treatment.Key, color);
}
}
},
() =>
{
return new string[][]
{
Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray()
};
}, isCheat: true));
commands.Add(new Command("revive", "revive [character name]: Bring the specified character back from the dead. If the name parameter is omitted, the controlled character will be revived.", (string[] args) =>
{
Character revivedCharacter = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args);
@@ -827,7 +856,7 @@ namespace Barotrauma
}
else if (eventPrefab != null)
{
var newEvent = eventPrefab.CreateInstance();
var newEvent = eventPrefab.CreateInstance(GameMain.GameSession.EventManager.RandomSeed);
if (newEvent == null)
{
NewMessage($"Could not initialize event {args[0]} because level did not meet requirements");
@@ -2296,6 +2325,7 @@ namespace Barotrauma
#endif
}
spawnedCharacter.GiveJobItems(spawnPoint);
spawnedCharacter.GiveIdCardTags(spawnPoint);
spawnedCharacter.Info.StartItemsGiven = true;
}
else
@@ -152,6 +152,7 @@ namespace Barotrauma
OnAllyGainMissionExperience,
OnGainMissionExperience,
OnGainMissionMoney,
OnCrewGainMissionReputation,
OnLocationDiscovered,
OnItemDeconstructed,
OnItemDeconstructedByAlly,
@@ -329,6 +330,11 @@ namespace Barotrauma
/// Increases the repair speed of the character when repairing mechanical items by a percentage.
/// </summary>
MechanicalRepairSpeed,
/// <summary>
/// Increases the repair speed of the character when repairing electrical items by a percentage.
/// </summary>
ElectricalRepairSpeed,
/// <summary>
/// Increase deconstruction speed of deconstructor operated by the character by a percentage.
@@ -572,7 +578,12 @@ namespace Barotrauma
/// <summary>
/// Modifies how far the character can be seen from (can be used to make the character easier or more difficult for monsters to see)
/// </summary>
SightRangeMultiplier
SightRangeMultiplier,
/// <summary>
/// Reduces the dual wielding penalty by a percentage.
/// </summary>
DualWieldingPenaltyReduction
}
internal enum ItemTalentStats
@@ -672,18 +683,26 @@ namespace Barotrauma
Both = Bot | Player
}
public enum StartingBalanceAmount
public enum StartingBalanceAmountOption
{
Low,
Medium,
High,
}
public enum GameDifficulty
public enum PatdownProbabilityOption
{
Easy,
Off,
Low,
Medium,
Hard,
High,
}
public enum WorldHostilityOption
{
Low,
Medium,
High,
Hellish
}
@@ -29,8 +29,8 @@ namespace Barotrauma
return $"ArtifactEvent ({(itemPrefab == null ? "null" : itemPrefab.Name)})";
}
public ArtifactEvent(EventPrefab prefab)
: base(prefab)
public ArtifactEvent(EventPrefab prefab, int seed)
: base(prefab, seed)
{
if (prefab.ConfigElement.GetAttribute("itemname") != null)
{
@@ -55,9 +55,8 @@ namespace Barotrauma
}
}
public override void Init(EventSet parentSet)
protected override void InitEventSpecific(EventSet parentSet)
{
base.Init(parentSet);
spawnPos = Level.Loaded.GetRandomItemPos(
(Rand.Value(Rand.RandSync.ServerAndClient) < 0.5f) ?
Level.PositionType.MainPath | Level.PositionType.SidePath :
@@ -9,7 +9,7 @@ namespace Barotrauma
public event Action Finished;
protected bool isFinished;
public int RandomSeed;
public readonly int RandomSeed;
protected readonly EventPrefab prefab;
@@ -17,6 +17,8 @@ namespace Barotrauma
public EventSet ParentSet { get; private set; }
public bool Initialized { get; private set; }
public Func<Level.InterestingPosition, bool> SpawnPosFilter;
public bool IsFinished
@@ -37,8 +39,9 @@ namespace Barotrauma
}
}
public Event(EventPrefab prefab)
public Event(EventPrefab prefab, int seed)
{
RandomSeed = seed;
this.prefab = prefab ?? throw new ArgumentNullException(nameof(prefab));
}
@@ -47,9 +50,15 @@ namespace Barotrauma
yield break;
}
public virtual void Init(EventSet parentSet = null)
public void Init(EventSet parentSet = null)
{
Initialized = true;
ParentSet = parentSet;
InitEventSpecific(parentSet);
}
protected virtual void InitEventSpecific(EventSet parentSet = null)
{
}
public virtual string GetDebugInfo()
@@ -1,22 +1,35 @@
using System.Linq;
using System.Linq;
namespace Barotrauma
{
/// <summary>
/// Gives an affliction to a specific character.
/// </summary>
class AfflictionAction : EventAction
{
[Serialize("", IsPropertySaveable.Yes)]
[Serialize("", IsPropertySaveable.Yes, description: "Identifier of the affliction.")]
public Identifier Affliction { get; set; }
[Serialize(0.0f, IsPropertySaveable.Yes)]
[Serialize(0.0f, IsPropertySaveable.Yes, description: "Strength of the affliction.")]
public float Strength { get; set; }
[Serialize(LimbType.None, IsPropertySaveable.Yes)]
[Serialize(LimbType.None, IsPropertySaveable.Yes, description: "Type of the limb(s) to apply the affliction on. Only valid if the affliction is limb-specific.")]
public LimbType LimbType { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
[Serialize("", IsPropertySaveable.Yes, description: "Tag of the character to apply the affliction on.")]
public Identifier TargetTag { get; set; }
public AfflictionAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
[Serialize(false, IsPropertySaveable.Yes, description: "Should the strength be multiplied by the maximum vitality of the target?")]
public bool MultiplyByMaxVitality { get; set; }
public AfflictionAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
if (Affliction.IsEmpty)
{
DebugConsole.ThrowError($"Error in {nameof(AfflictionAction)}: affliction not defined (use the attribute \"{nameof(Affliction)}\").",
contentPackage: element.ContentPackage);
}
}
private bool isFinished = false;
@@ -40,27 +53,32 @@ namespace Barotrauma
{
if (target != null && target is Character character)
{
float strength = Strength;
if (MultiplyByMaxVitality)
{
strength *= character.MaxVitality;
}
if (LimbType != LimbType.None)
{
var limb = character.AnimController.GetLimb(LimbType);
if (Strength > 0.0f)
if (strength > 0.0f)
{
character.CharacterHealth.ApplyAffliction(limb, afflictionPrefab.Instantiate(Strength), ignoreUnkillability: true);
character.CharacterHealth.ApplyAffliction(limb, afflictionPrefab.Instantiate(strength), ignoreUnkillability: true);
}
else if (Strength < 0.0f)
else if (strength < 0.0f)
{
character.CharacterHealth.ReduceAfflictionOnLimb(limb, Affliction, -Strength);
character.CharacterHealth.ReduceAfflictionOnLimb(limb, Affliction, -strength);
}
}
else
{
if (Strength > 0.0f)
if (strength > 0.0f)
{
character.CharacterHealth.ApplyAffliction(null, afflictionPrefab.Instantiate(Strength), ignoreUnkillability: true);
character.CharacterHealth.ApplyAffliction(null, afflictionPrefab.Instantiate(strength), ignoreUnkillability: true);
}
else if (Strength < 0.0f)
else if (strength < 0.0f)
{
character.CharacterHealth.ReduceAfflictionOnAllLimbs(Affliction, -Strength);
character.CharacterHealth.ReduceAfflictionOnAllLimbs(Affliction, -strength);
}
}
}
@@ -4,24 +4,27 @@ using System.Linq;
namespace Barotrauma
{
/// <summary>
/// Check whether a target has a specific affliction.
/// </summary>
internal class CheckAfflictionAction : BinaryOptionAction
{
[Serialize("", IsPropertySaveable.Yes)]
[Serialize("", IsPropertySaveable.Yes, description: "Identifier of the affliction.")]
public Identifier Identifier { get; set; } = Identifier.Empty;
[Serialize("", IsPropertySaveable.Yes)]
[Serialize("", IsPropertySaveable.Yes, description: "Tag of the character to check.")]
public Identifier TargetTag { get; set; } = Identifier.Empty;
[Serialize("", IsPropertySaveable.Yes, description: "Tag referring to the character who caused the affliction.")]
[Serialize("", IsPropertySaveable.Yes, description: "Tag referring to the character who caused the affliction. Can be used to require the affliction to be caused by a specific character.")]
public Identifier SourceCharacter { get; set; } = Identifier.Empty;
[Serialize(LimbType.None, IsPropertySaveable.Yes, "Only check afflictions on the specified limb type")]
[Serialize(LimbType.None, IsPropertySaveable.Yes, "Only check afflictions on the specified limb type.")]
public LimbType TargetLimb { get; set; }
[Serialize(true, IsPropertySaveable.Yes, "When set to false when TargetLimb is not specified prevent checking limb-specific afflictions")]
[Serialize(true, IsPropertySaveable.Yes, "When set to false, limb-specific afflictions are ignored when not checking a specific limb.")]
public bool AllowLimbAfflictions { get; set; }
[Serialize(0.0f, IsPropertySaveable.Yes, "Minimum strength of the affliction")]
[Serialize(0.0f, IsPropertySaveable.Yes, "Minimum strength of the affliction.")]
public float MinStrength { get; set; }
public CheckAfflictionAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
@@ -6,20 +6,24 @@ using System.Xml.Linq;
namespace Barotrauma
{
/// <summary>
/// Checks whether an arbitrary condition is met. The conditionals work the same way as they do in StatusEffects.
/// </summary>
class CheckConditionalAction : BinaryOptionAction
{
[Serialize("", IsPropertySaveable.Yes)]
[Serialize("", IsPropertySaveable.Yes, description: "Tag of the target to check.")]
public Identifier TargetTag { get; set; }
[Serialize(PropertyConditional.LogicalOperatorType.Or, IsPropertySaveable.Yes)]
[Serialize(PropertyConditional.LogicalOperatorType.Or, IsPropertySaveable.Yes, description: "Do all of the conditions need to be met, or is it enough if at least one is? Only valid if there are multiple conditionals.")]
public PropertyConditional.LogicalOperatorType LogicalOperator { get; set; }
private ImmutableArray<PropertyConditional> Conditionals { get; }
[Serialize("", IsPropertySaveable.Yes)]
[Serialize("", IsPropertySaveable.Yes, description: "A tag to apply to the hull the target is currently in when the check succeeds, as well as all the hulls linked to it.")]
public Identifier ApplyTagToLinkedHulls { get; set; }
[Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the hull the target item is inside when the item is used.")]
[Serialize("", IsPropertySaveable.Yes, description: "A tag to apply to the hull the target is currently in when the check succeeds.")]
public Identifier ApplyTagToHull { get; set; }
public CheckConditionalAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
@@ -4,21 +4,24 @@ using System.Linq;
namespace Barotrauma;
/// <summary>
/// Check whether a specific connection of an item is wired to a specific kind of connection.
/// </summary>
class CheckConnectionAction : BinaryOptionAction
{
[Serialize("", IsPropertySaveable.Yes)]
[Serialize("", IsPropertySaveable.Yes, description: "Tag of the item to check.")]
public Identifier ItemTag { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
[Serialize("", IsPropertySaveable.Yes, description: "The name of the connection to check on the target item.")]
public Identifier ConnectionName { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
[Serialize("", IsPropertySaveable.Yes, description: "Tag of the item the connection must be wired to. If omitted, it doesn't matter what the connection is wired to.")]
public Identifier ConnectedItemTag { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
[Serialize("", IsPropertySaveable.Yes, description: "The name of the other connection the connection must be wired to. If omitted, it doesn't matter what the connection is wired to.")]
public Identifier OtherConnectionName { get; set; }
[Serialize(1, IsPropertySaveable.Yes)]
[Serialize(1, IsPropertySaveable.Yes, description: "Minimum number of matching connections for the check to succeed.")]
public int MinAmount { get; set; }
public CheckConnectionAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
@@ -37,7 +40,7 @@ class CheckConnectionAction : BinaryOptionAction
if (!IsCorrectConnection(connection, ConnectionName)) { continue; }
if (ConnectedItemTag.IsEmpty && OtherConnectionName.IsEmpty)
{
amount += connection.Wires.Count();
amount += connection.Wires.Count;
if (amount >= MinAmount) { return true; }
continue;
}
@@ -1,21 +1,24 @@
#nullable enable
using System;
using System.Linq;
namespace Barotrauma
{
/// <summary>
/// Can be used to check arbitrary campaign metadata set using <see cref="SetDataAction"/>.
/// </summary>
class CheckDataAction : BinaryOptionAction
{
[Serialize("", IsPropertySaveable.Yes)]
[Serialize("", IsPropertySaveable.Yes, description: "Identifier of the data to check.")]
public Identifier Identifier { get; set; } = Identifier.Empty;
[Serialize("", IsPropertySaveable.Yes)]
[Serialize("", IsPropertySaveable.Yes, description: "The condition that must be met for the check to succeed. Uses the same formatting as conditionals (for example, \"gt 5.2\", \"true\", \"lt 10\".)")]
public string Condition { get; set; } = "";
[Serialize(false, IsPropertySaveable.Yes, "Forces the comparison to use string instead of attempting to parse it as a boolean or a float first")]
[Serialize(false, IsPropertySaveable.Yes, "Forces the comparison to use string instead of attempting to parse it as a boolean or a float first. Use this if you know the value is a string.")]
public bool ForceString { get; set; }
[Serialize(false, IsPropertySaveable.Yes, "Performs the comparison against a metadata by identifier instead of a constant value")]
[Serialize(false, IsPropertySaveable.Yes, "Performs the comparison against a metadata by identifier instead of a constant value. Meaning that you could for example check whether the value of \"progress_of_some_event\" is larger than \"progress_of_some_other_event\".")]
public bool CheckAgainstMetadata { get; set; }
protected object? value2;
@@ -6,18 +6,22 @@ using System.Linq;
namespace Barotrauma
{
/// <summary>
/// Can be used to do various kinds of checks on items: whether a specific kind of item exists,
/// if it's in a specific character's inventory or in a container, or whether some conditions are met on the item.
/// </summary>
class CheckItemAction : BinaryOptionAction
{
[Serialize("", IsPropertySaveable.Yes)]
[Serialize("", IsPropertySaveable.Yes, description: "Either the tag of the item(s) we want to check, or a character/container the items are inside.")]
public Identifier TargetTag { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
[Serialize("", IsPropertySaveable.Yes, description: "The target item must have one of these identifiers.")]
public string ItemIdentifiers { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
[Serialize("", IsPropertySaveable.Yes, description: "The target item must have at least one of these tags.")]
public string ItemTags { get; set; }
[Serialize(1, IsPropertySaveable.Yes)]
[Serialize(1, IsPropertySaveable.Yes, description: "The minimum number of matching items for the check to succeed.")]
public int Amount { get; set; }
[Serialize("", IsPropertySaveable.Yes, description: "Optional tag of a hull the target must be inside.")]
@@ -29,31 +33,27 @@ namespace Barotrauma
[Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the found item(s) when the check succeeds.")]
public Identifier ApplyTagToItem { get; set; }
[Serialize(false, IsPropertySaveable.Yes)]
[Serialize(false, IsPropertySaveable.Yes, description: "Does the item need to be equipped for the check to succeed?")]
public bool RequireEquipped { get; set; }
[Serialize(true, IsPropertySaveable.Yes)]
[Serialize(true, IsPropertySaveable.Yes, description: "If enabled, the doesn't need to be directly inside the container/character we're checking, but can be nested inside multiple containers (e.g. in a toolbelt in a character's inventory).")]
public bool Recursive { get; set; }
[Serialize(-1, IsPropertySaveable.Yes)]
[Serialize(-1, IsPropertySaveable.Yes, description: "Can be used to require the item to be in a specific ItemContainer of the target container. For example, the input slots of a fabricator (the first ItemContainer of the fabricator, with an index of 0).")]
public int ItemContainerIndex { get; set; }
private readonly bool checkPercentage;
private float requiredConditionalMatchPercentage;
[Serialize(100.0f, IsPropertySaveable.Yes)]
/// <summary>
/// What percentage of targets do the conditionals need to match for the check to succeed?
/// </summary>
[Serialize(100.0f, IsPropertySaveable.Yes, description: "What percentage of targets do the conditionals need to match for the check to succeed?")]
public float RequiredConditionalMatchPercentage
{
get { return requiredConditionalMatchPercentage; }
set { requiredConditionalMatchPercentage = MathHelper.Clamp(value, 0.0f, 100.0f); }
}
[Serialize(false, IsPropertySaveable.Yes)]
[Serialize(false, IsPropertySaveable.Yes, description: "When enabled, the number of matching items is compared to the number of matching items there were at the start of the round. Only valid if RequiredConditionalMatchPercentage is set.")]
public bool CompareToInitialAmount { get; set; }
private readonly IReadOnlyList<PropertyConditional> conditionals;
@@ -94,12 +94,12 @@ namespace Barotrauma
private bool EnoughTargets(int totalTargets, int targetsWithConditionalsMatched)
{
if (CompareToInitialAmount)
{
totalTargets = ParentEvent.GetInitialTargetCount(TargetTag);
}
if (checkPercentage)
{
if (CompareToInitialAmount)
{
totalTargets = ParentEvent.GetInitialTargetCount(TargetTag);
}
return MathUtils.Percentage(targetsWithConditionalsMatched, totalTargets) >= RequiredConditionalMatchPercentage;
}
else
@@ -3,6 +3,9 @@ using System.Linq;
namespace Barotrauma;
/// <summary>
/// Check whether a specific mission is currently active, selected for the next round or available.
/// </summary>
class CheckMissionAction : BinaryOptionAction
{
public enum MissionType
@@ -12,16 +15,16 @@ class CheckMissionAction : BinaryOptionAction
Available
}
[Serialize(MissionType.Current, IsPropertySaveable.Yes)]
[Serialize(MissionType.Current, IsPropertySaveable.Yes, description: "Does the mission need to be currently active, selected for the next round or available.")]
public MissionType Type { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
[Serialize("", IsPropertySaveable.Yes, description: "Identifier of the mission.")]
public Identifier MissionIdentifier { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
[Serialize("", IsPropertySaveable.Yes, description: "Tag of the mission. Ignored if MissionIdentifier is set.")]
public Identifier MissionTag { get; set; }
[Serialize(1, IsPropertySaveable.Yes)]
[Serialize(1, IsPropertySaveable.Yes, description: "Minimum number of matching missions for the check to succeed.")]
public int MissionCount { get; set; }
public CheckMissionAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
@@ -1,16 +1,18 @@
using Barotrauma.Networking;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Networking;
namespace Barotrauma
{
/// <summary>
/// Check whether the crew or a specific player has enough money.
/// </summary>
class CheckMoneyAction : BinaryOptionAction
{
[Serialize(0, IsPropertySaveable.Yes)]
[Serialize(0, IsPropertySaveable.Yes, description: "Minimum amount of money the crew or the player must have.")]
public int Amount { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
[Serialize("", IsPropertySaveable.Yes, description: "Tag of the player to check. If omitted, the crew's shared wallet is checked instead.")]
public Identifier TargetTag { get; set; }
public CheckMoneyAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
@@ -1,5 +1,8 @@
namespace Barotrauma;
/// <summary>
/// Checks the state of an Objective created using <see cref="EventObjectiveAction"/>.
/// </summary>
partial class CheckObjectiveAction : BinaryOptionAction
{
public CheckObjectiveAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
@@ -2,6 +2,9 @@ using Barotrauma.Extensions;
namespace Barotrauma
{
/// <summary>
/// Check whether a specific character has been given a specific order.
/// </summary>
class CheckOrderAction : BinaryOptionAction
{
public enum OrderPriority
@@ -10,19 +13,19 @@ namespace Barotrauma
Any
}
[Serialize("", IsPropertySaveable.Yes)]
[Serialize("", IsPropertySaveable.Yes, description: "Tag of the character to check.")]
public Identifier TargetTag { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
[Serialize("", IsPropertySaveable.Yes, description: "Identifier of the order the target character must have.")]
public Identifier OrderIdentifier { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
[Serialize("", IsPropertySaveable.Yes, description: "The option that must be selected for the order. If the order has multiple options (such as turning on or turning off a reactor).")]
public Identifier OrderOption { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
[Serialize("", IsPropertySaveable.Yes, description: "Tag of the entity the order must be targeting. Only valid for orders that can target a specific entity (such as orders to operate a specific turret).")]
public Identifier OrderTargetTag { get; set; }
[Serialize(OrderPriority.Any, IsPropertySaveable.Yes)]
[Serialize(OrderPriority.Any, IsPropertySaveable.Yes, description: "Does the order need to have top priority, or is any priority fine?")]
public OrderPriority Priority { get; set; }
public CheckOrderAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
@@ -3,6 +3,9 @@ using System.Linq;
namespace Barotrauma;
/// <summary>
/// Check whether specific kinds of items have been purchased or sold during the round.
/// </summary>
class CheckPurchasedItemsAction : BinaryOptionAction
{
public enum TransactionType
@@ -11,16 +14,16 @@ class CheckPurchasedItemsAction : BinaryOptionAction
Sold
}
[Serialize(TransactionType.Purchased, IsPropertySaveable.Yes)]
[Serialize(TransactionType.Purchased, IsPropertySaveable.Yes, description: "Do the items need to have been purchased or sold?")]
public TransactionType Type { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
[Serialize("", IsPropertySaveable.Yes, description: "Identifier of the item that must have been purchased or sold.")]
public Identifier ItemIdentifier { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
[Serialize("", IsPropertySaveable.Yes, description: "Tag of the item that must have been purchased or sold.")]
public Identifier ItemTag { get; set; }
[Serialize(1, IsPropertySaveable.Yes)]
[Serialize(1, IsPropertySaveable.Yes, description: "Minimum number of matching items that must have been purchased or sold.")]
public int MinCount { get; set; }
public CheckPurchasedItemsAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
@@ -3,9 +3,12 @@ using System.Diagnostics;
namespace Barotrauma
{
/// <summary>
/// Check whether the reputation of the crew for a specific faction meets some criteria (e.g. equal to, larger than or less than some value).
/// </summary>
class CheckReputationAction : CheckDataAction
{
[Serialize(ReputationAction.ReputationType.None, IsPropertySaveable.Yes)]
[Serialize(ReputationAction.ReputationType.None, IsPropertySaveable.Yes, description: "Should the action check the reputation for a given faction, or whichever faction owns the current location.")]
public ReputationAction.ReputationType TargetType { get; set; }
public CheckReputationAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
@@ -3,17 +3,20 @@ using System.Collections.Generic;
namespace Barotrauma
{
/// <summary>
/// Check whether a specific character has selected a specific kind of item.
/// </summary>
class CheckSelectedAction : BinaryOptionAction
{
public enum SelectedItemType { Primary, Secondary, Any };
[Serialize("", IsPropertySaveable.Yes)]
[Serialize("", IsPropertySaveable.Yes, description: "Tag of the character to check.")]
public Identifier CharacterTag { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
[Serialize("", IsPropertySaveable.Yes, description: "If specified, only items that have been given this tag using TagAction are considered valid.")]
public Identifier TargetTag { get; set; }
[Serialize(SelectedItemType.Any, IsPropertySaveable.Yes)]
[Serialize(SelectedItemType.Any, IsPropertySaveable.Yes, description: "How does the item need to be selected? Primary item (i.e. any device you're interacting with), secondary item (such as ladders or chairs which allow interacting with a primary item at the same time), or either?")]
public SelectedItemType ItemType { get; set; }
public CheckSelectedAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
@@ -2,12 +2,15 @@
namespace Barotrauma
{
/// <summary>
/// Check whether a specific character has a specific talent.
/// </summary>
internal sealed class CheckTalentAction : BinaryOptionAction
{
[Serialize("", IsPropertySaveable.Yes)]
[Serialize("", IsPropertySaveable.Yes, description: "Identifier of the talent to check for.")]
public Identifier TalentIdentifier { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
[Serialize("", IsPropertySaveable.Yes, description: "Tag of the character to check.")]
public Identifier TargetTag { get; set; }
public CheckTalentAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
@@ -2,9 +2,12 @@
namespace Barotrauma
{
/// <summary>
/// Check the state of the traitor event the action is defined in. Only valid for traitor events.
/// </summary>
class CheckTraitorEventStateAction : BinaryOptionAction
{
[Serialize(TraitorEvent.State.Completed, IsPropertySaveable.Yes)]
[Serialize(TraitorEvent.State.Completed, IsPropertySaveable.Yes, description: "What does the state of the event need to be for the check to succeed?")]
public TraitorEvent.State State { get; set; }
private readonly TraitorEvent? traitorEvent;
@@ -9,7 +9,7 @@ namespace Barotrauma
/// </summary>
class CheckTraitorVoteAction : BinaryOptionAction
{
[Serialize("", IsPropertySaveable.Yes)]
[Serialize("", IsPropertySaveable.Yes, description: "Tag of the character to check.")]
public Identifier Target { get; set; }
public CheckTraitorVoteAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
@@ -4,6 +4,9 @@ using System.Linq;
namespace Barotrauma
{
/// <summary>
/// Check whether a specific entity is visible from the perspective of another entity.
/// </summary>
class CheckVisibilityAction : BinaryOptionAction
{
[Serialize("", IsPropertySaveable.Yes, description: "Tag of the entity to do the visibility check from.")]
@@ -1,10 +1,12 @@
using System.Xml.Linq;
namespace Barotrauma
{
/// <summary>
/// Clears the specific tag from the event (i.e. untagging all the entities that have been previously given the tag).
/// </summary>
class ClearTagAction : EventAction
{
[Serialize("", IsPropertySaveable.Yes)]
[Serialize("", IsPropertySaveable.Yes, description: "The tag to clear.")]
public Identifier Tag { get; set; }
private bool isFinished;
@@ -1,31 +1,33 @@
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
/// <summary>
/// Makes an NPC switch to a combat state (with options for different kinds of behaviors, such as offensive, arresting or retreating).
/// </summary>
class CombatAction : EventAction
{
[Serialize(AIObjectiveCombat.CombatMode.Offensive, IsPropertySaveable.Yes)]
[Serialize(AIObjectiveCombat.CombatMode.Offensive, IsPropertySaveable.Yes, description: $"What kind of combat mode should the NPC switch to (Defensive, Offensive, Arrest, Retreat, None)?")]
public AIObjectiveCombat.CombatMode CombatMode { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Did this NPC start the fight (as an aggressor)?")]
[Serialize(false, IsPropertySaveable.Yes, description: "Did this NPC start the fight (as an aggressor)? Attacking instigators doesn't reduce reputation or trigger outpost security.")]
public bool IsInstigator { get; set; }
[Serialize(AIObjectiveCombat.CombatMode.None, IsPropertySaveable.Yes)]
[Serialize(AIObjectiveCombat.CombatMode.None, IsPropertySaveable.Yes, description: "How do guards react to this character attacking others?")]
public AIObjectiveCombat.CombatMode GuardReaction { get; set; }
[Serialize(AIObjectiveCombat.CombatMode.None, IsPropertySaveable.Yes)]
[Serialize(AIObjectiveCombat.CombatMode.None, IsPropertySaveable.Yes, description: "How do other NPCs react to this character attacking others?")]
public AIObjectiveCombat.CombatMode WitnessReaction { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
[Serialize("", IsPropertySaveable.Yes, description: "The tag of the NPC to switch to combat mode.")]
public Identifier NPCTag { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
[Serialize("", IsPropertySaveable.Yes, description: "Tag of the character the NPC should attack.")]
public Identifier EnemyTag { get; set; }
[Serialize(120.0f, IsPropertySaveable.Yes)]
[Serialize(120.0f, IsPropertySaveable.Yes, description: "How long it takes for the NPC to \"cool down\" (stop attacking).")]
public float CoolDown { get; set; }
private bool isFinished = false;
@@ -8,6 +8,10 @@ using System.Xml.Linq;
namespace Barotrauma
{
/// <summary>
/// Triggers a "conversation popup" with text and support for different branching options.
/// </summary>
partial class ConversationAction : EventAction
{
@@ -26,43 +30,40 @@ namespace Barotrauma
/// </summary>
const float BlockOtherConversationsDuration = 5.0f;
[Serialize("", IsPropertySaveable.Yes)]
[Serialize("", IsPropertySaveable.Yes, description: "The text to display in the prompt. Can be the text as-is, or a tag referring to a line in a text file.")]
public string Text { get; set; }
[Serialize(0, IsPropertySaveable.Yes)]
public int DefaultOption { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
[Serialize("", IsPropertySaveable.Yes, description: "Tag of the character who's speaking. Makes a speech bubble icon appear above the character to indicate you can speak with them, and stops the character in place when the conversation triggers. Also allows the conversation to be interrupted if the speaker dies or becomes incapacitated mid-conversation.")]
public Identifier SpeakerTag { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
[Serialize("", IsPropertySaveable.Yes, description: "Tag of the player the conversation is shown to. If empty, the conversation is shown to everyone. If SpeakerTag is defined, the conversation is always only shown to the player who interacts with the speaker.")]
public Identifier TargetTag { get; set; }
[Serialize(true, IsPropertySaveable.Yes)]
[Serialize(true, IsPropertySaveable.Yes, "Should someone interact with the speaker for the conversation to trigger?")]
public bool WaitForInteraction { get; set; }
[Serialize("", IsPropertySaveable.Yes, "Tag to assign to whoever invokes the conversation")]
[Serialize("", IsPropertySaveable.Yes, "Tag to assign to whoever invokes the conversation.")]
public Identifier InvokerTag { get; set; }
[Serialize(false, IsPropertySaveable.Yes)]
[Serialize(false, IsPropertySaveable.Yes, description: "Should the screen fade to black when the conversation is active?")]
public bool FadeToBlack { get; set; }
[Serialize(true, IsPropertySaveable.Yes, "Should the event end if the conversations is interrupted (e.g. if the speaker dies or falls unconscious mid-conversation). Defaults to true.")]
public bool EndEventIfInterrupted { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
[Serialize("", IsPropertySaveable.Yes, description: "Identifier of an event sprite to display in the corner of the conversation prompt.")]
public string EventSprite { get; set; }
[Serialize(DialogTypes.Regular, IsPropertySaveable.Yes)]
[Serialize(DialogTypes.Regular, IsPropertySaveable.Yes, description: "Type of the dialog prompt.")]
public DialogTypes DialogType { get; set; }
[Serialize(false, IsPropertySaveable.Yes)]
[Serialize(false, IsPropertySaveable.Yes, description: "Does this conversation continue after this ConversationAction? If you have multiple successive ConversationActions, perhaps with some actions happening in between, you can enable this to prevent the dialog prompt from closing between the actions. Not necessary if the ConversationActions are nested inside each other: those are always considered parts of the same conversation, and shown in the same prompt.")]
public bool ContinueConversation { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "If enabled, the event will not stop to wait for the conversation to be dismissed.")]
public bool ContinueAutomatically { get; set; }
[Serialize(false, IsPropertySaveable.Yes)]
[Serialize(false, IsPropertySaveable.Yes, description: "If SpeakerTag is defined, the conversation is interrupted by default if the speaker and the target end up too far from each other. This can be used to disable that behavior, keeping the dialog prompt open regardless of the distance.")]
public bool IgnoreInterruptDistance { get; set; }
public Character Speaker
@@ -72,6 +73,7 @@ namespace Barotrauma
}
private AIObjective prevIdleObjective, prevGotoObjective;
private AIObjective npcWaitObjective;
public List<SubactionGroup> Options { get; private set; }
@@ -275,6 +277,10 @@ namespace Barotrauma
if (!SpeakerTag.IsEmpty)
{
if (npcWaitObjective != null)
{
npcWaitObjective.ForceHighestPriority = true;
}
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)
@@ -386,11 +392,11 @@ namespace Barotrauma
{
prevIdleObjective = humanAI.ObjectiveManager.GetObjective<AIObjectiveIdle>();
prevGotoObjective = humanAI.ObjectiveManager.GetObjective<AIObjectiveGoTo>();
humanAI.SetForcedOrder(
npcWaitObjective = humanAI.SetForcedOrder(
new Order(OrderPrefab.Prefabs["wait"], Barotrauma.Identifier.Empty, null, orderGiver: null));
if (targets.Any())
if (targets.Any() || targetCharacter != null)
{
Entity closestTarget = null;
Entity closestTarget = targetCharacter;
float closestDist = float.MaxValue;
foreach (Entity entity in targets)
{
@@ -5,9 +5,12 @@ using System.Linq;
namespace Barotrauma
{
/// <summary>
/// Check whether there's at least / at most some number of entities matching some specific criteria.
/// </summary>
class CountTargetsAction : BinaryOptionAction
{
[Serialize("", IsPropertySaveable.Yes)]
[Serialize("", IsPropertySaveable.Yes, description: "Tag of the entities to check.")]
public Identifier TargetTag { get; set; }
[Serialize("", IsPropertySaveable.Yes, description: "Optional second tag. Can be used if the target must have two different tags.")]
@@ -16,29 +19,19 @@ namespace Barotrauma
[Serialize("", IsPropertySaveable.Yes, description: "Optional tag of a hull the target must be inside.")]
public Identifier HullTag { get; set; }
[Serialize(-1, IsPropertySaveable.Yes)]
[Serialize(-1, IsPropertySaveable.Yes, description: "Minimum number of matching entities for the check to succeed. If omitted or negative, there is no minimum amount.")]
public int MinAmount { get; set; }
[Serialize(-1, IsPropertySaveable.Yes)]
[Serialize(-1, IsPropertySaveable.Yes, description: "Maximum number of matching entities for the check to succeed. If omitted or negative, there is no maximum amount.")]
public int MaxAmount { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
[Serialize("", IsPropertySaveable.Yes, description: "Tag of some other entities to compare the number of targets to. E.g. you could compare the number of entities tagged as \"discoveredhull\" to entities tagged as \"anyhull\". The minimum/maximum amount of entities there must be relative to the other entities is configured using MinPercentageRelativeToTarget and MaxPercentageRelativeToTarget.")]
public Identifier CompareToTarget { get; set; }
[Serialize(-1.0f, IsPropertySaveable.Yes)]
/// <summary>
/// Minimum amount of targets, as a percentage of the number of entities tagged with CompareToTarget
/// E.g. you could compare the number of entities tagged as "discoveredhull" to entities tagged as "anyhull" to require 50% of hulls to be discovered.
/// </summary>
[Serialize(-1.0f, IsPropertySaveable.Yes, description: "Minimum amount of targets, as a percentage of the number of entities tagged with CompareToTarget. E.g. you could compare the number of entities tagged as \"discoveredhull\" to entities tagged as \"anyhull\" to require 50% of hulls to be discovered.")]
public float MinPercentageRelativeToTarget { get; set; }
[Serialize(-1.0f, IsPropertySaveable.Yes)]
/// <summary>
/// Maximum amount of targets, as a percentage of the number of entities tagged with CompareToTarget
/// E.g. you could compare the number of entities tagged as "floodedhull" to entities tagged as "anyhull" to require less than 50% of hulls to be flooded.
/// </summary>
[Serialize(-1.0f, IsPropertySaveable.Yes, description: "Maximum amount of targets, as a percentage of the number of entities tagged with CompareToTarget. E.g. you could compare the number of entities tagged as \"floodedhull\" to entities tagged as \"anyhull\" to require less than 50% of hulls to be flooded.")]
public float MaxPercentageRelativeToTarget { get; set; }
private readonly IReadOnlyList<PropertyConditional> conditionals;
@@ -5,15 +5,19 @@ using System.Xml.Linq;
namespace Barotrauma
{
/// <summary>
/// Adds an entry to the "event log" displayed in the mission tab of the tab menu.
/// </summary>
partial class EventLogAction : EventAction
{
[Serialize("", IsPropertySaveable.Yes)]
[Serialize("", IsPropertySaveable.Yes, description: "Identifier of the entry. If there's already an entry with the same id, it gets overwritten.")]
public Identifier Id { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
[Serialize("", IsPropertySaveable.Yes, description: "Text to add to the event log. Can be the text as-is, or a tag referring to a line in a text file.")]
public string Text { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
[Serialize("", IsPropertySaveable.Yes, description: "Tag of the character(s) who should see the entry. If empty, the entry is shown to everyone.")]
public Identifier TargetTag { get; set; }
public bool ShowInServerLog { get; set; }

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