(6eeea9b7c) v0.9.10.0.0

This commit is contained in:
Joonas Rikkonen
2020-06-04 16:41:07 +03:00
parent ce4ccd99ac
commit eeac247a8e
366 changed files with 7772 additions and 3692 deletions
@@ -3,7 +3,7 @@ using System.Collections.Generic;
namespace Barotrauma
{
public enum AIState { Idle, Attack, Escape, Eat, Flee, Avoid, Aggressive, PassiveAggressive }
public enum AIState { Idle, Attack, Escape, Eat, Flee, Avoid, Aggressive, PassiveAggressive, Protect }
abstract partial class AIController : ISteerable
{
@@ -40,6 +40,7 @@ namespace Barotrauma
private readonly float avoidLookAheadDistance;
private IndoorsSteeringManager PathSteering => insideSteering as IndoorsSteeringManager;
private SteeringManager outsideSteering, insideSteering;
private float updateTargetsTimer;
@@ -85,8 +86,9 @@ namespace Barotrauma
private readonly float colliderWidth;
private readonly float colliderLength;
private readonly int requiredHoleCount;
private readonly bool canAttackSub;
private readonly bool canAttackCharacters;
private bool canAttackWalls;
private bool canAttackDoors;
private bool canAttackCharacters;
private readonly float priorityFearIncreasement = 2;
private readonly float memoryFadeTime = 0.5f;
@@ -94,9 +96,13 @@ namespace Barotrauma
private float avoidTimer;
public bool StayInsideLevel = true;
public LatchOntoAI LatchOntoAI { get; private set; }
public SwarmBehavior SwarmBehavior { get; private set; }
public CharacterParams.TargetParams SelectedTargetingParams { get { return selectedTargetingParams; } }
public bool AttackHumans
{
get
@@ -186,24 +192,9 @@ namespace Barotrauma
}
}
bool canBreakDoors = false;
if (GetTarget("room")?.Priority > 0.0f)
{
var currentContexts = Character.GetAttackContexts();
foreach (Limb limb in Character.AnimController.Limbs)
{
if (limb.attack == null) { continue; }
if (!limb.attack.IsValidTarget(AttackTarget.Structure)) { continue; }
if (limb.attack.IsValidContext(currentContexts) && limb.attack.StructureDamage > 0.0f)
{
canBreakDoors = true;
break;
}
}
}
ReevaluateAttacks();
outsideSteering = new SteeringManager(this);
insideSteering = new IndoorsSteeringManager(this, false, canBreakDoors);
insideSteering = new IndoorsSteeringManager(this, false, canAttackDoors);
steeringManager = outsideSteering;
State = AIState.Idle;
@@ -213,9 +204,6 @@ namespace Barotrauma
requiredHoleCount = (int)Math.Ceiling(ConvertUnits.ToDisplayUnits(colliderWidth) / Structure.WallSectionSize);
avoidLookAheadDistance = Math.Max(colliderWidth * 3, 1.5f);
canAttackSub = Character.AnimController.CanAttackSubmarine;
canAttackCharacters = Character.AnimController.CanAttackCharacters;
}
private CharacterParams.AIParams AIParams => Character.Params.AI;
@@ -226,31 +214,32 @@ namespace Barotrauma
public void SelectTarget(AITarget target, float priority)
{
SelectedAiTarget = target;
selectedTargetMemory = GetTargetMemory(target);
selectedTargetMemory = GetTargetMemory(target, true);
selectedTargetMemory.Priority = priority;
}
private float escapeMargin;
private float movementMargin;
public override void Update(float deltaTime)
{
if (DisableEnemyAI) { return; }
base.Update(deltaTime);
bool ignorePlatforms = (-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X));
if (steeringManager is IndoorsSteeringManager)
bool ignorePlatforms = Character.AnimController.TargetMovement.Y < -0.5f && (-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X));
if (steeringManager == insideSteering)
{
var currPath = ((IndoorsSteeringManager)steeringManager).CurrentPath;
var currPath = PathSteering.CurrentPath;
if (currPath != null && currPath.CurrentNode != null)
{
if (currPath.CurrentNode.SimPosition.Y < Character.AnimController.GetColliderBottom().Y)
{
ignorePlatforms = true;
// Don't allow to jump from too high.
float allowedJumpHeight = Character.AnimController.ImpactTolerance / 2;
float height = Math.Abs(currPath.CurrentNode.SimPosition.Y - Character.SimPosition.Y);
ignorePlatforms = height < allowedJumpHeight;
}
}
}
Character.AnimController.IgnorePlatforms = ignorePlatforms;
//clients get the facing direction from the server
@@ -396,7 +385,7 @@ namespace Barotrauma
{
bool isBeingChased = IsBeingChased;
float reactDistance = !isBeingChased && selectedTargetingParams != null && selectedTargetingParams.ReactDistance > 0 ? selectedTargetingParams.ReactDistance : GetPerceivingRange(SelectedAiTarget);
if (squaredDistance <= Math.Pow(reactDistance + escapeMargin, 2))
if (squaredDistance <= Math.Pow(reactDistance + movementMargin, 2))
{
float halfReactDistance = reactDistance / 2;
float attackDistance = selectedTargetingParams != null && selectedTargetingParams.AttackDistance > 0 ? selectedTargetingParams.AttackDistance : halfReactDistance;
@@ -408,26 +397,56 @@ namespace Barotrauma
else
{
run = isBeingChased ? true : squaredDistance < Math.Pow(halfReactDistance, 2);
if (escapeMargin <= 0)
if (movementMargin <= 0)
{
escapeMargin = halfReactDistance;
movementMargin = halfReactDistance;
}
escapeMargin = MathHelper.Clamp(escapeMargin += deltaTime, halfReactDistance, reactDistance);
movementMargin = MathHelper.Clamp(movementMargin += deltaTime, halfReactDistance, reactDistance);
UpdateEscape(deltaTime);
}
}
else
{
escapeMargin = 0;
movementMargin = 0;
UpdateIdle(deltaTime);
}
}
break;
case AIState.Protect:
if (SelectedAiTarget == null || SelectedAiTarget.Entity == null || SelectedAiTarget.Entity.Removed)
{
State = AIState.Idle;
return;
}
if (SelectedAiTarget.Entity is Character targetCharacter && targetCharacter.LastAttacker is Character attacker)
{
// Attack the character that attacked the target we are protecting
ChangeTargetState(attacker, AIState.Attack, selectedTargetingParams.Priority * 2);
SelectTarget(attacker.AiTarget);
return;
}
float sqrDist = Vector2.DistanceSquared(WorldPosition, SelectedAiTarget.WorldPosition);
float reactDist = selectedTargetingParams != null && selectedTargetingParams.ReactDistance > 0 ? selectedTargetingParams.ReactDistance : GetPerceivingRange(SelectedAiTarget);
if (sqrDist > Math.Pow(reactDist + movementMargin, 2))
{
movementMargin = reactDist;
run = true;
UpdateFollow(deltaTime);
}
else
{
movementMargin = MathHelper.Clamp(movementMargin -= deltaTime, 0, reactDist);
UpdateIdle(deltaTime);
}
break;
default:
throw new NotImplementedException();
}
LatchOntoAI?.Update(this, deltaTime);
if (!Character.AnimController.SimplePhysicsEnabled)
{
LatchOntoAI?.Update(this, deltaTime);
}
IsSteeringThroughGap = false;
if (SwarmBehavior != null)
{
@@ -435,6 +454,8 @@ namespace Barotrauma
SwarmBehavior.Refresh();
SwarmBehavior.UpdateSteering(deltaTime);
}
// Ensure that the creature keeps inside the level
SteerInsideLevel(deltaTime);
float speed = Character.AnimController.GetCurrentSpeed(run && Character.CanRun);
steeringManager.Update(speed);
Character.AnimController.TargetMovement = Character.ApplyMovementLimits(Steering, State == AIState.Idle && Character.AnimController.InWater ? Steering.Length() : speed);
@@ -459,13 +480,12 @@ namespace Barotrauma
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 5);
return;
}
SteerInsideLevel(deltaTime);
}
var target = SelectedAiTarget ?? _lastAiTarget;
if (target?.Entity != null && !target.Entity.Removed && PreviousState == AIState.Attack && Character.CurrentHull == null)
{
// Keep heading to the last known position of the target
var memory = GetTargetMemory(target);
var memory = GetTargetMemory(target, false);
if (memory != null)
{
var location = memory.Location;
@@ -579,10 +599,10 @@ namespace Barotrauma
}
else if (pathSteering != null)
{
if (canAttackSub && hasValidPath)
if (canAttackDoors && hasValidPath)
{
var door = pathSteering.CurrentPath.CurrentNode?.ConnectedDoor ?? pathSteering.CurrentPath.NextNode?.ConnectedDoor;
if (door != null && !door.IsOpen)
if (door != null && !door.IsOpen && !door.IsBroken)
{
if (SelectedAiTarget != door.Item.AiTarget)
{
@@ -618,7 +638,6 @@ namespace Barotrauma
{
SteeringManager.SteeringWander();
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 5);
SteerInsideLevel(deltaTime);
}
}
}
@@ -644,17 +663,18 @@ namespace Barotrauma
if (SelectedAiTarget.Entity is Item item)
{
// If the item is held by a character, attack the character instead.
var pickable = item.GetComponent<Pickable>();
if (pickable != null)
Character owner = GetOwner(item);
if (owner != null)
{
Entity owner = pickable.Picker ?? item.ParentInventory?.Owner;
if (owner != null)
if (IsFriendly(Character, owner))
{
var target = owner.AiTarget;
if (target?.Entity != null && !target.Entity.Removed)
{
SelectedAiTarget = target;
}
ResetAITarget();
State = AIState.Idle;
return;
}
else
{
SelectedAiTarget = owner.AiTarget;
}
}
}
@@ -666,7 +686,8 @@ namespace Barotrauma
{
attackWorldPos += wallTarget.Structure.Submarine.Position;
}
attackSimPos = ConvertUnits.ToSimUnits(attackWorldPos);
attackSimPos = Character.Submarine == wallTarget.Structure.Submarine ? wallTarget.Position : attackWorldPos;
attackSimPos = ConvertUnits.ToSimUnits(attackSimPos);
}
else
{
@@ -704,7 +725,7 @@ namespace Barotrauma
var door = i.GetComponent<Door>();
// Steer through the door manually if it's open or broken
// Don't try to enter dry hulls if cannot walk or if the gap is too narrow
if (door?.LinkedGap?.FlowTargetHull != null && !door.LinkedGap.IsRoomToRoom && door.IsOpen)
if (door?.LinkedGap?.FlowTargetHull != null && !door.LinkedGap.IsRoomToRoom && (door.IsOpen || door.IsBroken))
{
if (Character.AnimController.CanWalk || door.LinkedGap.FlowTargetHull.WaterPercentage > 25)
{
@@ -722,7 +743,6 @@ namespace Barotrauma
}
else if (SelectedAiTarget.Entity is Structure w && wallTarget == null)
{
// Targeting only the outer walls
bool isBroken = true;
for (int i = 0; i < w.Sections.Length; i++)
{
@@ -893,35 +913,23 @@ namespace Barotrauma
}
canAttack = AttackingLimb != null && AttackingLimb.attack.CoolDownTimer <= 0;
}
if (!canAttack && SelectedAiTarget.Entity.Submarine != null && !canAttackSub)
if (!Character.AnimController.SimplePhysicsEnabled && SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null && (!canAttackDoors || !canAttackWalls || !AIParams.TargetOuterWalls))
{
float dist = Vector2.Distance(Character.AnimController.MainLimb.WorldPosition, attackWorldPos);
if (wallTarget != null)
if (Vector2.DistanceSquared(Character.WorldPosition, attackWorldPos) < 2000 * 2000)
{
// Steer towards the target, but turn away if a wall is blocking the way
if (dist < ConvertUnits.ToDisplayUnits(colliderLength) * 3)
{
State = AIState.Idle;
IgnoreTarget(SelectedAiTarget);
// Resetting the ai target prevents the character from chasing it
ResetAITarget();
return;
}
}
else if (dist < 1000)
{
// Check that we are not bumping into a door
// Check that we are not bumping into a door or a wall
Vector2 rayStart = SimPosition;
if (Character.Submarine == null)
{
rayStart -= SelectedAiTarget.Entity.Submarine.SimPosition;
}
Vector2 toTarget = SelectedAiTarget.WorldPosition - WorldPosition;
Vector2 rayEnd = rayStart + toTarget.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 2);
Vector2 dir = SelectedAiTarget.WorldPosition - WorldPosition;
Vector2 rayEnd = rayStart + dir.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 2);
Body closestBody = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true);
if (Submarine.LastPickedFraction != 1.0f && closestBody != null && closestBody.UserData is Item i && i.Submarine != null && i.GetComponent<Door>() != null)
if (Submarine.LastPickedFraction != 1.0f && closestBody != null &&
(!AIParams.TargetOuterWalls || !canAttackWalls && closestBody.UserData is Structure s && s.Submarine != null || !canAttackDoors && closestBody.UserData is Item i && i.Submarine != null && i.GetComponent<Door>() != null))
{
// Target is unreachable, there's a door ahead
// Target is unreachable, there's a door or wall ahead
State = AIState.Idle;
IgnoreTarget(SelectedAiTarget);
ResetAITarget();
@@ -934,23 +942,28 @@ namespace Barotrauma
Character targetCharacter = SelectedAiTarget.Entity as Character;
if (canAttack)
{
// Target a specific limb instead of the target center position
if (wallTarget == null && targetCharacter != null)
if (!Character.AnimController.SimplePhysicsEnabled)
{
var targetLimbType = AttackingLimb.Params.Attack.Attack.TargetLimbType;
attackTargetLimb = GetTargetLimb(AttackingLimb, targetCharacter, targetLimbType);
if (attackTargetLimb == null)
// Target a specific limb instead of the target center position
if (wallTarget == null && targetCharacter != null)
{
State = AIState.Idle;
IgnoreTarget(SelectedAiTarget);
ResetAITarget();
return;
var targetLimbType = AttackingLimb.Params.Attack.Attack.TargetLimbType;
attackTargetLimb = GetTargetLimb(AttackingLimb, targetCharacter, targetLimbType);
if (attackTargetLimb == null)
{
State = AIState.Idle;
IgnoreTarget(SelectedAiTarget);
ResetAITarget();
return;
}
attackWorldPos = attackTargetLimb.WorldPosition;
attackSimPos = Character.GetRelativeSimPosition(attackTargetLimb);
}
attackWorldPos = attackTargetLimb.WorldPosition;
attackSimPos = Character.GetRelativeSimPosition(attackTargetLimb);
}
// Check that we can reach the target
Vector2 toTarget = attackWorldPos - AttackingLimb.WorldPosition;
Vector2 attackLimbPos = Character.AnimController.SimplePhysicsEnabled ? Character.WorldPosition : AttackingLimb.WorldPosition;
Vector2 toTarget = attackWorldPos - attackLimbPos;
// Add a margin when the target is moving away, because otherwise it might be difficult to reach it (the attack takes some time to perform)
if (wallTarget != null)
{
if (wallTarget.Structure.Submarine != null)
@@ -961,7 +974,6 @@ namespace Barotrauma
}
else if (targetCharacter != null)
{
// Add a margin when the target is moving away, because otherwise it might be difficult to reach it (the attack takes some time to perform)
Vector2 margin = CalculateMargin(targetCharacter.AnimController.Collider.LinearVelocity);
toTarget += margin;
}
@@ -981,6 +993,7 @@ namespace Barotrauma
return ConvertUnits.ToDisplayUnits(targetVelocity) * AttackingLimb.attack.Duration * dot;
}
// Check that we can reach the target
distance = toTarget.Length();
canAttack = distance < AttackingLimb.attack.Range;
if (!canAttack && !IsCoolDownRunning)
@@ -1035,21 +1048,25 @@ namespace Barotrauma
}
else
{
Vector2 offset = Character.SimPosition - steeringLimb.SimPosition;
// Offset so that we don't overshoot the movement
Vector2 steerPos = attackSimPos + offset;
Vector2 steerPos = attackSimPos;
if (!Character.AnimController.SimplePhysicsEnabled)
{
// Offset so that we don't overshoot the movement
Vector2 offset = Character.SimPosition - steeringLimb.SimPosition;
steerPos += offset;
}
if (SteeringManager is IndoorsSteeringManager pathSteering)
{
if (pathSteering.CurrentPath != null)
{
// Attack doors
if (canAttackSub)
if (canAttackDoors)
{
// If the target is in the same hull, there shouldn't be any doors blocking the path
if (targetCharacter == null || targetCharacter.CurrentHull != Character.CurrentHull)
{
var door = pathSteering.CurrentPath.CurrentNode?.ConnectedDoor ?? pathSteering.CurrentPath.NextNode?.ConnectedDoor;
if (door != null && !door.IsOpen)
if (door != null && !door.IsOpen && !door.IsBroken)
{
if (door.Item.AiTarget != null && SelectedAiTarget != door.Item.AiTarget)
{
@@ -1063,13 +1080,14 @@ namespace Barotrauma
if ((Character.AnimController.InWater || pursue || !Character.AnimController.CanWalk) &&
(targetCharacter != null && VisibleHulls.Contains(targetCharacter.CurrentHull) || Character.CanSeeTarget(SelectedAiTarget.Entity)))
{
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(attackSimPos - steeringLimb.SimPosition));
Vector2 myPos = Character.AnimController.SimplePhysicsEnabled ? Character.SimPosition : steeringLimb.SimPosition;
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(steerPos - myPos));
}
else
{
SteeringManager.SteeringSeek(steerPos, 2);
// Switch to Idle when cannot reach the target and if cannot damage the walls
if ((!canAttackSub || wallTarget == null) && !pathSteering.IsPathDirty && pathSteering.CurrentPath.Unreachable)
if ((!canAttackWalls || wallTarget == null) && !pathSteering.IsPathDirty && pathSteering.CurrentPath.Unreachable)
{
State = AIState.Idle;
return;
@@ -1145,7 +1163,7 @@ namespace Barotrauma
if (attack == null) { continue; }
if (attack.CoolDownTimer > 0) { continue; }
if (!attack.IsValidContext(currentContexts)) { continue; }
if (!attack.IsValidTarget(target)) { continue; }
if (!attack.IsValidTarget(target as IDamageable)) { continue; }
if (target is ISerializableEntity se && target is Character)
{
if (attack.Conditionals.Any(c => !c.Matches(se))) { continue; }
@@ -1176,6 +1194,7 @@ namespace Barotrauma
float CalculatePriority(Limb limb, Vector2 attackPos)
{
if (Character.AnimController.SimplePhysicsEnabled) { return 1 + limb.attack.Priority; }
float dist = Vector2.Distance(limb.WorldPosition, attackPos);
// The limb is ignored if the target is not close. Prevents character going in reverse if very far away from it.
// We also need a max value that is more than the actual range.
@@ -1242,7 +1261,10 @@ namespace Barotrauma
LatchOntoAI?.SetAttachTarget(wall.Submarine.PhysicsBody.FarseerBody, wall.Submarine, ConvertUnits.ToSimUnits(sectionPos), attachTargetNormal);
if (Character.AnimController.CanEnterSubmarine || !wall.SectionBodyDisabled(sectionIndex) && !IsWallDisabled(wall))
{
wallTarget = new WallTarget(sectionPos, wall, sectionIndex);
if (AIParams.TargetOuterWalls || wall.prefab.Tags.Contains("inner"))
{
wallTarget = new WallTarget(sectionPos, wall, sectionIndex);
}
}
}
if (!Character.AnimController.CanEnterSubmarine && wallTarget == null)
@@ -1271,7 +1293,7 @@ namespace Barotrauma
}
return isDisabled;
}
public override void OnAttacked(Character attacker, AttackResult attackResult)
{
float reactionTime = Rand.Range(0.1f, 0.3f);
@@ -1281,7 +1303,7 @@ namespace Barotrauma
Character.AnimController.ReleaseStuckLimbs();
LatchOntoAI?.DeattachFromBody();
if (attacker == null || attacker.AiTarget == null) { return; }
bool isFriendly = attacker.SpeciesName == Character.SpeciesName || attacker.Params.Group == Character.Params.Group;
bool isFriendly = IsFriendly(Character, attacker);
if (wasLatched)
{
avoidTimer = avoidTime * Rand.Range(0.75f, 1.25f);
@@ -1302,7 +1324,7 @@ namespace Barotrauma
}
if (!isFriendly && attackResult.Damage > 0.0f)
{
bool canAttack = attacker.Submarine == Character.Submarine && canAttackCharacters || attacker.Submarine != null && canAttackSub;
bool canAttack = attacker.Submarine == Character.Submarine && canAttackCharacters || attacker.Submarine != null && canAttackWalls;
if (Character.Params.AI.AttackWhenProvoked && canAttack)
{
if (attacker.IsHusk)
@@ -1356,7 +1378,7 @@ namespace Barotrauma
}
}
AITargetMemory targetMemory = GetTargetMemory(attacker.AiTarget);
AITargetMemory targetMemory = GetTargetMemory(attacker.AiTarget, true);
targetMemory.Priority += GetRelativeDamage(attackResult.Damage, Character.Vitality) * AggressionHurt;
// Only allow to react once. Otherwise would attack the target with only a fraction of a cooldown
@@ -1399,7 +1421,7 @@ namespace Barotrauma
var aiTarget = wallTarget.Structure.AiTarget;
if (aiTarget != null && SelectedAiTarget != aiTarget)
{
SelectTarget(aiTarget, GetTargetMemory(SelectedAiTarget).Priority);
SelectTarget(aiTarget, GetTargetMemory(SelectedAiTarget, true).Priority);
}
}
IDamageable damageTarget = wallTarget != null ? wallTarget.Structure : SelectedAiTarget.Entity as IDamageable;
@@ -1464,7 +1486,7 @@ namespace Barotrauma
State = AIState.Idle;
return;
}
Vector2 mouthPos = Character.AnimController.GetMouthPosition().Value;
Vector2 mouthPos = Character.AnimController.SimplePhysicsEnabled ? SimPosition : Character.AnimController.GetMouthPosition().Value;
Vector2 attackSimPosition = Character.GetRelativeSimPosition(target);
Vector2 limbDiff = attackSimPosition - mouthPos;
float extent = Math.Max(mouthLimb.body.GetMaxExtent(), 2);
@@ -1493,6 +1515,25 @@ namespace Barotrauma
#endregion
private void UpdateFollow(float deltaTime)
{
if (SelectedAiTarget == null || SelectedAiTarget.Entity == null || SelectedAiTarget.Entity.Removed)
{
State = AIState.Idle;
return;
}
Vector2 dir = Vector2.Normalize(SelectedAiTarget.Entity.WorldPosition - Character.WorldPosition);
if (!MathUtils.IsValid(dir))
{
return;
}
steeringManager.SteeringManual(deltaTime, dir);
if (Character.AnimController.InWater)
{
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15);
}
}
#region Targeting
private bool IsLatchedOnSub => LatchOntoAI != null && LatchOntoAI.IsAttachedToSub;
@@ -1619,13 +1660,16 @@ namespace Barotrauma
Door door = null;
if (aiTarget.Entity is Item item)
{
//item inside and we're outside -> attack the hull
if (item.CurrentHull != null && character.CurrentHull == null)
{
targetingTag = "room";
}
door = item.GetComponent<Door>();
bool targetingFromOutsideToInside = item.CurrentHull != null && character.CurrentHull == null;
if (targetingFromOutsideToInside)
{
if (door != null && !canAttackDoors || !canAttackWalls)
{
// Can't reach
continue;
}
}
foreach (var prio in AIParams.Targets)
{
if (item.HasTag(prio.Tag))
@@ -1634,7 +1678,25 @@ namespace Barotrauma
break;
}
}
if (door == null && targetingTag == null)
{
if (item.GetComponent<Sonar>() != null)
{
targetingTag = "sonar";
}
else if (targetingFromOutsideToInside)
{
targetingTag = "room";
}
}
else if (targetingTag == "nasonov")
{
if ((item.Submarine == null || !item.Submarine.Info.IsPlayer) && item.ParentInventory == null)
{
// Only target nasonovartifacts when they are held be a player or inside the playersub
continue;
}
}
// Ignore the target if it's a decoy and the character is already inside a sub
if (character.CurrentHull != null && targetingTag == "decoy")
{
@@ -1649,15 +1711,13 @@ namespace Barotrauma
// Ignore structures that doesn't have a body (not walls)
continue;
}
if (s.IsPlatform)
if (s.IsPlatform) { continue; }
if (s.Submarine == null) { continue; }
bool isCharacterInside = character.CurrentHull != null;
bool isInnerWall = s.prefab.Tags.Contains("inner");
if (isInnerWall && !isCharacterInside)
{
continue;
}
bool isCharacterOutside = s.Submarine == null || character.CurrentHull == null;
bool targetInnerWalls = AIParams.TargetInnerWalls;
if (!isCharacterOutside && !targetInnerWalls)
{
// Ignore walls when inside (walltargets still work)
// Ignore inner walls when outside (walltargets still work)
continue;
}
valueModifier = 1;
@@ -1670,48 +1730,71 @@ namespace Barotrauma
var section = s.Sections[i];
if (section.gap == null) { continue; }
bool leadsInside = !section.gap.IsRoomToRoom && section.gap.FlowTargetHull != null;
isInnerWall = isInnerWall || !leadsInside;
if (Character.AnimController.CanEnterSubmarine)
{
if (isCharacterOutside)
if (!isCharacterInside)
{
if (CanPassThroughHole(s, i))
{
valueModifier *= leadsInside ? (AggressiveBoarding ? 5 : 1) : (targetInnerWalls ? 1 : 0);
valueModifier *= leadsInside ? (AggressiveBoarding ? 5 : 1) : 0;
}
else
else if (AggressiveBoarding && leadsInside && canAttackWalls && AIParams.TargetOuterWalls)
{
// Ignore holes that cannot be passed through if cannot attack items/structures. Holes that are big enough should be targeted, so that we can get in
if (!canAttackSub)
// Up to 100% priority increase for every gap in the wall when an aggressive boarder is outside
valueModifier *= 1 + section.gap.Open;
}
}
else
{
// Inside
if (AggressiveBoarding)
{
if (!isInnerWall)
{
// Only interested in getting inside (aggressive boarder) -> don't target outer walls when already inside
valueModifier = 0;
break;
}
else if (CanPassThroughHole(s, i))
{
valueModifier *= isInnerWall ? 1 : 0;
}
else if (!canAttackWalls)
{
valueModifier = 0;
break;
}
if (AggressiveBoarding && leadsInside)
}
else
{
if (!canAttackWalls)
{
// Up to 100% priority increase for every gap in the wall when an aggressive boarder is outside
valueModifier *= 1 + section.gap.Open;
valueModifier = 0;
break;
}
// We are actually interested in breaking things -> reduce the priority when the wall is already broken
// (Terminalcells)
valueModifier *= 1 - section.gap.Open * 0.25f;
}
}
else if (!canAttackSub || CanPassThroughHole(s, i))
}
else
{
// Cannot enter
if (isInnerWall || !canAttackWalls)
{
// Already inside -> ignore holes in the walls and ignore walls if cannot attack the sub.
// Ignore inner walls and all walls if cannot do damage on walls.
valueModifier = 0;
break;
}
else if (canAttackSub && !AggressiveBoarding)
else if (AggressiveBoarding)
{
// We are actually interested in breaking things -> reduce the priority when the wall is already broken
valueModifier *= 1 - section.gap.Open * 0.25f;
// Up to 100% priority increase for every gap in the wall when an aggressive boarder is outside
// (Bonethreshers)
valueModifier *= 1 + section.gap.Open;
}
}
else if (!leadsInside || !canAttackSub)
{
// Can't get in, ignore inner walls
// Also ignore all walls if cannot attack the sub
valueModifier = 0;
break;
}
}
}
else
@@ -1727,31 +1810,34 @@ namespace Barotrauma
}
if (door.Item.Submarine == null) { continue;}
bool isOutdoor = door.LinkedGap?.FlowTargetHull != null && !door.LinkedGap.IsRoomToRoom;
bool isOpen = door.IsOpen;
if (!isOpen && (!canAttackSub))
bool isOpen = door.IsOpen || door.IsBroken;
if (!isOpen && !canAttackDoors || (isOutdoor && !AIParams.TargetOuterWalls))
{
// Ignore doors that are not open if cannot attack items/structures. Open doors should be targeted, so that we can get in if we are aggressive boarders
valueModifier = 0;
// Ignore doors that are not open if cannot attack doors or shouldn't target outer doors.
continue;
}
if (character.CurrentHull == null)
if (isOpen && (!Character.AnimController.CanEnterSubmarine || !AggressiveBoarding))
{
valueModifier = isOutdoor ? 1 : 0;
// Ignore broken and open doors
// Aggressive boarders don't ignore open doors, because they use them for get in.
continue;
}
else if (AggressiveBoarding)
if (AggressiveBoarding)
{
// Increase priority if the character is outside and an aggressive boarder, and the door is from outside to inside
if (character.CurrentHull == null)
// Increase the priority if the character is outside and the door is from outside to inside
if (character.CurrentHull == null && isOutdoor)
{
valueModifier *= isOpen ? 5 : 1;
}
else
{
valueModifier *= isOpen ? 0 : 1;
// Inside
valueModifier *= isOpen || isOutdoor ? 0 : 1;
}
}
else if (!Character.AnimController.CanEnterSubmarine && isOpen) //ignore broken and open doors
else if (character.CurrentHull == null)
{
continue;
valueModifier = isOutdoor ? 1 : 0;
}
}
else if (aiTarget.Entity is IDamageable targetDamageable && targetDamageable.Health <= 0.0f)
@@ -1783,7 +1869,7 @@ namespace Barotrauma
// -> just ignore the distance and attack whatever has the highest priority
dist = Math.Max(dist, 100.0f);
AITargetMemory targetMemory = GetTargetMemory(aiTarget);
AITargetMemory targetMemory = GetTargetMemory(aiTarget, true);
if (Character.CurrentHull != null && Math.Abs(toTarget.Y) > Character.CurrentHull.Size.Y)
{
// Inside the sub, treat objects that are up or down, as they were farther away.
@@ -1793,9 +1879,18 @@ namespace Barotrauma
if (valueModifier > targetValue)
{
// Don't target items that we own.
// This is a rare case, and almost entirely related to Humanhusks, so let's check it last to reduce unnecessary checks (although the check shouldn't be expensive)
if (aiTarget.Entity is Item i && i.IsOwnedBy(character)) { continue; }
if (aiTarget.Entity is Item i)
{
Character owner = GetOwner(i);
// Don't target items that we own.
// This is a rare case, and almost entirely related to Humanhusks, so let's check it last to reduce unnecessary checks (although the check shouldn't be expensive)
if (owner == character) { continue; }
if (owner != null && IsFriendly(Character, owner))
{
// If the item is held by a friendly character, ignore it.
continue;
}
}
if (targetCharacter != null)
{
if (targetCharacter.Submarine != Character.Submarine)
@@ -1820,7 +1915,7 @@ namespace Barotrauma
foreach (var gap in Character.CurrentHull.ConnectedGaps)
{
var door = gap.ConnectedDoor;
if (door == null || !door.IsOpen)
if (door == null || !door.IsOpen && !door.IsBroken)
{
var wall = gap.ConnectedWall;
if (wall != null)
@@ -1855,12 +1950,15 @@ namespace Barotrauma
return SelectedAiTarget;
}
private AITargetMemory GetTargetMemory(AITarget target)
private AITargetMemory GetTargetMemory(AITarget target, bool addIfNotFound)
{
if (!targetMemories.TryGetValue(target, out AITargetMemory memory))
{
memory = new AITargetMemory(target, 10);
targetMemories.Add(target, memory);
if (addIfNotFound)
{
memory = new AITargetMemory(target, 10);
targetMemories.Add(target, memory);
}
}
return memory;
}
@@ -1875,8 +1973,11 @@ namespace Barotrauma
}
else if (CanPerceive(_selectedAiTarget, distSquared: Vector2.DistanceSquared(Character.WorldPosition, _selectedAiTarget.WorldPosition)))
{
var memory = GetTargetMemory(_selectedAiTarget);
memory.Location = _selectedAiTarget.WorldPosition;
var memory = GetTargetMemory(_selectedAiTarget, false);
if (memory != null)
{
memory.Location = _selectedAiTarget.WorldPosition;
}
}
}
}
@@ -2014,11 +2115,17 @@ namespace Barotrauma
{
// If the target is shooting from the submarine, we might not perceive it because it doesn't move.
// --> Target the submarine too.
if (target.Submarine != null && canAttackSub)
if (target.Submarine != null && (canAttackDoors || canAttackWalls))
{
ChangeParams("room", state, priority);
ChangeParams("wall", state, priority);
ChangeParams("door", state, priority);
if (canAttackWalls)
{
ChangeParams("wall", state, priority);
}
if (canAttackDoors)
{
ChangeParams("door", state, priority);
}
}
ChangeParams("provocative", state, priority, onlyExisting: true);
ChangeParams("light", state, priority, onlyExisting: true);
@@ -2039,7 +2146,7 @@ namespace Barotrauma
Character.AnimController.ReleaseStuckLimbs();
escapeTarget = null;
AttackingLimb = null;
escapeMargin = 0;
movementMargin = 0;
allGapsSearched = false;
unreachableGaps.Clear();
if (isStateChanged && to == AIState.Idle && from != to)
@@ -2064,28 +2171,66 @@ namespace Barotrauma
}
}
public void ReevaluateAttacks()
{
canAttackWalls = LatchOntoAI != null && LatchOntoAI.AttachToSub;
canAttackDoors = false;
canAttackCharacters = false;
foreach (var limb in Character.AnimController.Limbs)
{
if (limb.IsSevered) { continue; }
if (limb.attack == null) { continue; }
if (!canAttackWalls)
{
canAttackWalls = limb.attack.IsValidTarget(AttackTarget.Structure) && limb.attack.StructureDamage > 0;
}
if (!canAttackDoors)
{
canAttackDoors = limb.attack.IsValidTarget(AttackTarget.Structure) && limb.attack.ItemDamage > 0;
}
if (!canAttackCharacters)
{
canAttackCharacters = limb.attack.IsValidTarget(AttackTarget.Character);
}
}
if (PathSteering != null)
{
PathSteering.CanBreakDoors = canAttackDoors;
}
}
private Vector2 returnDir;
private float returnTimer;
private void SteerInsideLevel(float deltaTime)
{
if (Level.Loaded == null) { return; }
Vector2 levelSimSize = new Vector2(
ConvertUnits.ToSimUnits(Level.Loaded.Size.X),
ConvertUnits.ToSimUnits(Level.Loaded.Size.Y));
float margin = 10.0f;
if (SimPosition.Y < 0.0)
if (SteeringManager is IndoorsSteeringManager || !StayInsideLevel) { return; }
if (Level.Loaded == null) { return; }
Vector2 levelSimSize = ConvertUnits.ToSimUnits(Level.Loaded.Size.X, Level.Loaded.Size.Y);
float returnTime = 3;
if (SimPosition.Y < 0)
{
steeringManager.SteeringManual(deltaTime, Vector2.UnitY * MathUtils.InverseLerp(0.0f, -margin, SimPosition.Y));
// Too far down
returnTimer = returnTime * Rand.Range(0.75f, 1.25f);
returnDir = Vector2.UnitY;
}
if (SimPosition.X < 0.0f)
if (SimPosition.X < 0)
{
steeringManager.SteeringManual(deltaTime, Vector2.UnitX * MathUtils.InverseLerp(0.0f, -margin, SimPosition.X));
// Too far left
returnTimer = returnTime * Rand.Range(0.75f, 1.25f);
returnDir = Vector2.UnitX;
}
if (SimPosition.X > levelSimSize.X)
{
steeringManager.SteeringManual(deltaTime, Vector2.UnitX * MathUtils.InverseLerp(levelSimSize.X, levelSimSize.X + margin, SimPosition.X));
}
// Too far right
returnTimer = returnTime * Rand.Range(0.75f, 1.25f);
returnDir = -Vector2.UnitX;
}
if (returnTimer > 0)
{
returnTimer -= deltaTime;
SteeringManager.Reset();
SteeringManager.SteeringManual(deltaTime, returnDir);
}
}
private bool CanPassThroughHole(Structure wall, int sectionIndex)
@@ -2116,7 +2261,6 @@ namespace Barotrauma
targetLimbs.Clear();
foreach (var limb in target.AnimController.Limbs)
{
if (limb.IsSevered) { continue; }
if (limb.type == targetLimbType || targetLimbType == LimbType.None)
{
targetLimbs.Add(limb);
@@ -2131,6 +2275,7 @@ namespace Barotrauma
Limb targetLimb = null;
foreach (Limb limb in targetLimbs)
{
if (limb.IsSevered) { continue; }
float dist = Vector2.DistanceSquared(limb.WorldPosition, attackLimb.WorldPosition) / Math.Max(limb.AttackPriority, 0.1f);
if (dist < closestDist)
{
@@ -2140,6 +2285,27 @@ namespace Barotrauma
}
return targetLimb;
}
private Character GetOwner(Item item)
{
// If the item is held by a character, attack the character instead.
var pickable = item.GetComponent<Pickable>();
if (pickable != null)
{
Character owner = pickable.Picker ?? item.FindParentInventory(i => i.Owner is Character)?.Owner as Character;
if (owner != null)
{
var target = owner.AiTarget;
if (target?.Entity != null && !target.Entity.Removed)
{
return owner;
}
}
}
return null;
}
public static bool IsFriendly(Character me, Character other) => other.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.Group);
}
//the "memory" of the Character
@@ -30,8 +30,9 @@ namespace Barotrauma
public static float HULL_SAFETY_THRESHOLD = 50;
public HashSet<Hull> UnreachableHulls { get; private set; } = new HashSet<Hull>();
public HashSet<Hull> UnsafeHulls { get; private set; } = new HashSet<Hull>();
public readonly HashSet<Hull> UnreachableHulls = new HashSet<Hull>();
public readonly HashSet<Hull> UnsafeHulls = new HashSet<Hull>();
public readonly List<Item> IgnoredItems = new List<Item>();
private SteeringManager outsideSteering, insideSteering;
@@ -55,13 +56,13 @@ namespace Barotrauma
private set;
}
public float CurrentHullSafety { get; private set; }
public float CurrentHullSafety { get; private set; } = 100;
public HumanAIController(Character c) : base(c)
{
if (!c.IsHuman)
{
throw new System.Exception($"Tried to create a human ai controller for a non-human: {c.SpeciesName}!");
throw new Exception($"Tried to create a human ai controller for a non-human: {c.SpeciesName}!");
}
insideSteering = new IndoorsSteeringManager(this, true, false);
outsideSteering = new SteeringManager(this);
@@ -85,7 +86,7 @@ namespace Barotrauma
{
unreachableClearTimer = clearUnreachableInterval;
UnreachableHulls.Clear();
ignoredContainers.Clear();
IgnoredItems.Clear();
}
// Use the pathfinding also outside of the sub, but not farther than the extents of the sub + 500 units.
@@ -175,9 +176,7 @@ namespace Barotrauma
}
steeringManager.Update(Character.AnimController.GetCurrentSpeed(run && Character.CanRun));
bool ignorePlatforms = Character.AnimController.TargetMovement.Y < -0.5f &&
(-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X));
bool ignorePlatforms = Character.AnimController.TargetMovement.Y < -0.5f && (-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X));
if (steeringManager == insideSteering)
{
var currPath = PathSteering.CurrentPath;
@@ -185,51 +184,24 @@ namespace Barotrauma
{
if (currPath.CurrentNode.SimPosition.Y < Character.AnimController.GetColliderBottom().Y)
{
// Don't allow to jump from too high. The formula might require tweaking.
// Don't allow to jump from too high.
float allowedJumpHeight = Character.AnimController.ImpactTolerance / 2;
float height = Math.Abs(currPath.CurrentNode.SimPosition.Y - Character.SimPosition.Y);
ignorePlatforms = height < allowedJumpHeight;
}
}
if (Character.IsClimbing && PathSteering.IsNextLadderSameAsCurrent)
{
Character.AnimController.TargetMovement = new Vector2(0.0f, Math.Sign(Character.AnimController.TargetMovement.Y));
}
}
Character.AnimController.IgnorePlatforms = ignorePlatforms;
Vector2 targetMovement = AnimController.TargetMovement;
if (!Character.AnimController.InWater)
{
targetMovement = new Vector2(Character.AnimController.TargetMovement.X, MathHelper.Clamp(Character.AnimController.TargetMovement.Y, -1.0f, 1.0f));
}
if (Character.AnimController.InWater && targetMovement.LengthSquared() < 0.000001f)
{
bool isAiming = false;
var holdable = Character.SelectedConstruction?.GetComponent<Holdable>();
if (holdable != null)
{
isAiming = holdable.ControlPose;
}
bool swimInPlace = !isAiming;
if (swimInPlace && ObjectiveManager.GetActiveObjective() is AIObjectiveGoTo goToObjective)
{
if (goToObjective.Target != Character)
{
swimInPlace = false;
}
}
if (swimInPlace)
{
// Swim in place so that we don't fall motionless and look dead.
targetMovement = new Vector2(targetMovement.X, Rand.Range(-0.001f, 0.001f));
}
}
Character.AnimController.TargetMovement = Character.ApplyMovementLimits(targetMovement, AnimController.GetCurrentSpeed(run));
flipTimer -= deltaTime;
@@ -280,14 +252,14 @@ namespace Barotrauma
else
{
findItemState = FindItemState.Extinguisher;
if (FindSuitableContainer(Character, extinguisher, out Item targetContainer))
if (FindSuitableContainer(extinguisher, out Item targetContainer))
{
findItemState = FindItemState.None;
itemIndex = 0;
if (targetContainer != null)
{
var decontainObjective = new AIObjectiveDecontainItem(Character, extinguisher, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
decontainObjective.Abandoned += () => ignoredContainers.Add(targetContainer);
decontainObjective.Abandoned += () => IgnoredItems.Add(targetContainer);
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
return;
}
@@ -310,42 +282,47 @@ namespace Barotrauma
|| ObjectiveManager.IsCurrentObjective<AIObjectiveFindSafety>()
|| ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn);
bool removeDivingSuit = !Character.AnimController.HeadInWater && oxygenLow;
AIObjectiveGoTo gotoObjective = ObjectiveManager.GetActiveObjective<AIObjectiveGoTo>();
bool takeMaskOff = !Character.AnimController.HeadInWater && oxygenLow;
if (!removeDivingSuit)
{
bool targetHasNoSuit = gotoObjective != null && gotoObjective.mimic && !HasDivingSuit(gotoObjective.Target as Character);
removeDivingSuit = !shouldKeepTheGearOn && (gotoObjective == null || targetHasNoSuit);
}
bool takeMaskOff = !Character.AnimController.HeadInWater && oxygenLow;
if (!takeMaskOff && Character.CurrentHull.WaterPercentage < 40)
{
bool targetHasNoMask = gotoObjective != null && gotoObjective.mimic && !HasDivingMask(gotoObjective.Target as Character);
takeMaskOff = !shouldKeepTheGearOn && (gotoObjective == null || targetHasNoMask);
}
if (gotoObjective != null)
{
if (gotoObjective.Target is Hull h)
if (shouldKeepTheGearOn)
{
if (NeedsDivingGear(Character, h, out _))
{
removeDivingSuit = false;
takeMaskOff = false;
}
removeDivingSuit = false;
}
else if (gotoObjective.Target is Character c)
}
if (!takeMaskOff)
{
if (shouldKeepTheGearOn)
{
if (NeedsDivingGear(Character, c.CurrentHull, out _))
{
removeDivingSuit = false;
takeMaskOff = false;
}
takeMaskOff = false;
}
else if (gotoObjective.Target is Item i)
}
if (!shouldKeepTheGearOn && (!takeMaskOff || !removeDivingSuit))
{
foreach (var objective in ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(includingSelf: true))
{
if (NeedsDivingGear(Character, i.CurrentHull, out _))
if (objective is AIObjectiveGoTo gotoObjective)
{
removeDivingSuit = false;
takeMaskOff = false;
bool insideSteering = SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.IsPathDirty;
Hull targetHull = gotoObjective.GetTargetHull();
bool targetIsOutside = (gotoObjective.Target != null && targetHull == null) || (insideSteering && PathSteering.CurrentPath.HasOutdoorsNodes);
if (targetIsOutside || NeedsDivingGear(Character, targetHull, out _))
{
removeDivingSuit = false;
takeMaskOff = false;
break;
}
else if (gotoObjective.mimic)
{
if (!removeDivingSuit)
{
removeDivingSuit = !HasDivingSuit(gotoObjective.Target as Character);
}
if (!takeMaskOff)
{
takeMaskOff = !HasDivingMask(gotoObjective.Target as Character);
}
}
}
}
}
@@ -363,7 +340,7 @@ namespace Barotrauma
else
{
findItemState = FindItemState.DivingSuit;
if (FindSuitableContainer(Character, divingSuit, out Item targetContainer))
if (FindSuitableContainer(divingSuit, out Item targetContainer))
{
findItemState = FindItemState.None;
itemIndex = 0;
@@ -375,7 +352,7 @@ namespace Barotrauma
};
decontainObjective.Abandoned += () =>
{
ignoredContainers.Add(targetContainer);
IgnoredItems.Add(targetContainer);
};
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
return;
@@ -405,14 +382,14 @@ namespace Barotrauma
else
{
findItemState = FindItemState.DivingMask;
if (FindSuitableContainer(Character, mask, out Item targetContainer))
if (FindSuitableContainer(mask, out Item targetContainer))
{
findItemState = FindItemState.None;
itemIndex = 0;
if (targetContainer != null)
{
var decontainObjective = new AIObjectiveDecontainItem(Character, mask, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
decontainObjective.Abandoned += () => ignoredContainers.Add(targetContainer);
decontainObjective.Abandoned += () => IgnoredItems.Add(targetContainer);
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
return;
}
@@ -442,14 +419,14 @@ namespace Barotrauma
{
if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, new List<InvSlotType>() { InvSlotType.Any }))
{
if (FindSuitableContainer(Character, item, out Item targetContainer))
if (FindSuitableContainer(item, out Item targetContainer))
{
findItemState = FindItemState.None;
itemIndex = 0;
if (targetContainer != null)
{
var decontainObjective = new AIObjectiveDecontainItem(Character, item, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
decontainObjective.Abandoned += () => ignoredContainers.Add(targetContainer);
decontainObjective.Abandoned += () => IgnoredItems.Add(targetContainer);
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
return;
}
@@ -478,11 +455,10 @@ namespace Barotrauma
}
private FindItemState findItemState;
private int itemIndex;
private List<Item> ignoredContainers = new List<Item>();
public bool FindSuitableContainer(Character character, Item containableItem, out Item suitableContainer)
public bool FindSuitableContainer(Item containableItem, out Item suitableContainer)
{
suitableContainer = null;
if (character.FindItem(ref itemIndex, out Item targetContainer, ignoredItems: ignoredContainers, customPriorityFunction: i =>
if (Character.FindItem(ref itemIndex, out Item targetContainer, ignoredItems: IgnoredItems, customPriorityFunction: i =>
{
var container = i.GetComponent<ItemContainer>();
if (container == null) { return 0; }
@@ -583,7 +559,7 @@ namespace Barotrauma
if (item.CurrentHull != hull) { continue; }
if (AIObjectiveRepairItems.IsValidTarget(item, Character))
{
if (item.Repairables.All(r => item.ConditionPercentage > r.AIRepairThreshold)) { continue; }
if (item.Repairables.All(r => item.ConditionPercentage > r.RepairThreshold)) { continue; }
if (AddTargets<AIObjectiveRepairItems, Item>(Character, item) && newOrder == null && !ObjectiveManager.HasActiveObjective<AIObjectiveRepairItem>())
{
var orderPrefab = Order.GetPrefab("reportbrokendevices");
@@ -633,9 +609,13 @@ namespace Barotrauma
if (ObjectiveManager.CurrentObjective is AIObjectiveFightIntruders) { return; }
if (attacker == null || attacker.IsDead || attacker.Removed)
{
// Don't react on the damage if there's no attacker.
// We might consider launching the retreat combat objective in some cases, so that the bot does not just stand somewhere getting damaged and dying.
// But fires and enemies should already be handled by the FindSafetyObjective.
return;
// Ignore damage from falling etc that we shouldn't react to.
if (Character.LastDamageSource == null) { return; }
AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, Rand.Range(0.5f, 1f, Rand.RandSync.Unsynced));
//if (Character.LastDamageSource == null) { return; }
//AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, Rand.Range(0.5f, 1f, Rand.RandSync.Unsynced));
}
else if (IsFriendly(attacker))
{
@@ -784,7 +764,6 @@ namespace Barotrauma
return false;
}
public static bool HasDivingGear(Character character, float conditionPercentage = 0) => HasDivingSuit(character, conditionPercentage) || HasDivingMask(character, conditionPercentage);
/// <summary>
@@ -852,7 +831,7 @@ namespace Barotrauma
if (item.CurrentHull != hull) { continue; }
if (AIObjectiveRepairItems.IsValidTarget(item, character))
{
if (item.Repairables.All(r => item.ConditionPercentage >= r.AIRepairThreshold)) { continue; }
if (item.Repairables.All(r => item.ConditionPercentage >= r.RepairThreshold)) { continue; }
AddTargets<AIObjectiveRepairItems, Item>(character, item);
}
}
@@ -12,7 +12,8 @@ namespace Barotrauma
private PathFinder pathFinder;
private SteeringPath currentPath;
private bool canOpenDoors, canBreakDoors;
private bool canOpenDoors;
public bool CanBreakDoors { get; set; }
private Character character;
@@ -50,8 +51,8 @@ namespace Barotrauma
/// </summary>
public bool InLadders =>
currentPath != null &&
currentPath.CurrentNode != null && (currentPath.CurrentNode.Ladders != null ||
(currentPath.NextNode != null && currentPath.NextNode.Ladders != null));
currentPath.CurrentNode != null && (currentPath.CurrentNode.Ladders != null && !currentPath.CurrentNode.Ladders.Item.NonInteractable ||
(currentPath.NextNode != null && currentPath.NextNode.Ladders != null && !currentPath.NextNode.Ladders.Item.NonInteractable));
/// <summary>
/// Returns true if any node in the path is in stairs
@@ -69,6 +70,7 @@ namespace Barotrauma
if (currentPath.NextNode == null) { return false; }
var currentLadder = currentPath.CurrentNode.Ladders;
if (currentLadder == null) { return false; }
if (currentLadder.Item.NonInteractable) { return false; }
var nextLadder = GetNextLadder();
return nextLadder != null && nextLadder == currentLadder;
}
@@ -80,7 +82,7 @@ namespace Barotrauma
pathFinder.GetNodePenalty = GetNodePenalty;
this.canOpenDoors = canOpenDoors;
this.canBreakDoors = canBreakDoors;
this.CanBreakDoors = canBreakDoors;
character = (host as AIController).Character;
@@ -103,6 +105,12 @@ namespace Barotrauma
IsPathDirty = false;
}
public void ResetPath()
{
currentPath = null;
IsPathDirty = true;
}
public void SteeringSeek(Vector2 target, float weight, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null)
{
steering += CalculateSteeringSeek(target, weight, startNodeFilter, endNodeFilter, nodeFilter);
@@ -115,7 +123,7 @@ namespace Barotrauma
{
if (currentPath == null) { return null; }
if (currentPath.NextNode == null) { return null; }
if (currentPath.NextNode.Ladders != null)
if (currentPath.NextNode.Ladders != null && !currentPath.NextNode.Ladders.Item.NonInteractable)
{
return currentPath.NextNode.Ladders;
}
@@ -126,7 +134,10 @@ namespace Barotrauma
{
var node = currentPath.Nodes[index];
if (node == null) { return null; }
return node.Ladders;
if (node.Ladders != null && !node.Ladders.Item.NonInteractable)
{
return node.Ladders;
}
}
return null;
}
@@ -134,7 +145,19 @@ namespace Barotrauma
private Vector2 CalculateSteeringSeek(Vector2 target, float weight, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null)
{
bool needsNewPath = character.Params.PathFinderPriority > 0.5f && (currentPath == null || currentPath.Unreachable || currentPath.Finished || Vector2.DistanceSquared(target, currentTarget) > 1);
Vector2 targetDiff = target - currentTarget;
if (currentPath != null && currentPath.Nodes.Any())
{
//current path calculated relative to a different sub than where the character is now
//take that into account when calculating if the target has moved
Submarine currentPathSub = currentPath?.Nodes.First().Submarine;
if (currentPathSub != character.Submarine && character.Submarine != null)
{
Vector2 subDiff = character.Submarine.SimPosition - currentPathSub.SimPosition;
targetDiff += subDiff;
}
}
bool needsNewPath = character.Params.PathFinderPriority > 0.5f && (currentPath == null || currentPath.Unreachable || currentPath.Finished || targetDiff.LengthSquared() > 1);
//find a new path if one hasn't been found yet or the target is different from the current target
if (needsNewPath || findPathTimer < -1.0f)
{
@@ -172,12 +195,13 @@ namespace Barotrauma
Vector2 diff = DiffToCurrentNode();
var collider = character.AnimController.Collider;
// Only humanoids can climb ladders
bool canClimb = character.AnimController is HumanoidAnimController;
//if not in water and the waypoint is between the top and bottom of the collider, no need to move vertically
if (!character.AnimController.InWater && !character.IsClimbing && diff.Y < collider.height / 2 + collider.radius)
if (canClimb && !character.AnimController.InWater && !character.IsClimbing && diff.Y < collider.height / 2 + collider.radius)
{
diff.Y = 0.0f;
}
//if (diff.LengthSquared() < 0.001f) { return -host.Steering; }
if (diff == Vector2.Zero) { return Vector2.Zero; }
return Vector2.Normalize(diff) * weight;
}
@@ -186,8 +210,10 @@ namespace Barotrauma
private Vector2 DiffToCurrentNode()
{
if (currentPath == null || currentPath.Unreachable) return Vector2.Zero;
if (currentPath == null || currentPath.Unreachable)
{
return Vector2.Zero;
}
if (currentPath.Finished)
{
Vector2 pos2 = host.SimPosition;
@@ -197,15 +223,12 @@ namespace Barotrauma
pos2 -= CurrentPath.Nodes.Last().Submarine.SimPosition;
}
return currentTarget - pos2;
}
}
if (canOpenDoors && !character.LockHands && buttonPressCooldown <= 0.0f)
{
CheckDoorsInPath();
}
}
Vector2 pos = host.SimPosition;
if (character != null && currentPath.CurrentNode != null)
{
if (CurrentPath.CurrentNode.Submarine != null)
@@ -220,19 +243,17 @@ namespace Barotrauma
}
}
}
bool isDiving = character.AnimController.InWater && character.AnimController.HeadInWater;
//only humanoids can climb ladders
if (!isDiving && character.AnimController is HumanoidAnimController && IsNextLadderSameAsCurrent)
// Only humanoids can climb ladders
bool canClimb = character.AnimController is HumanoidAnimController;
if (canClimb && !isDiving && IsNextLadderSameAsCurrent)
{
if (character.SelectedConstruction != currentPath.CurrentNode.Ladders.Item &&
currentPath.CurrentNode.Ladders.Item.IsInsideTrigger(character.WorldPosition))
var ladders = currentPath.CurrentNode.Ladders;
if (character.SelectedConstruction != ladders.Item && ladders.Item.IsInsideTrigger(character.WorldPosition))
{
currentPath.CurrentNode.Ladders.Item.TryInteract(character, false, true);
}
}
}
var collider = character.AnimController.Collider;
if (character.IsClimbing && !isDiving)
{
@@ -252,13 +273,16 @@ namespace Barotrauma
{
diff.Y = Math.Max(diff.Y, 1.0f);
}
// We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative.
float margin = 0.1f;
bool isAboveFloor = heightFromFloor > -margin && heightFromFloor < collider.height * 1.5f;
// If the next waypoint is horizontally far, we don't want to keep holding the ladders
if (nextLadder == null || Math.Abs(currentPath.CurrentNode.WorldPosition.X - currentPath.NextNode.WorldPosition.X) > 50)
if (isAboveFloor && (nextLadder == null || Math.Abs(currentPath.CurrentNode.WorldPosition.X - currentPath.NextNode.WorldPosition.X) > 50))
{
character.AnimController.Anim = AnimController.Animation.None;
character.SelectedConstruction = null;
}
else if (!nextLadderSameAsCurrent)
else if (nextLadder != null && !nextLadderSameAsCurrent)
{
// Try to change the ladder (hatches between two submarines)
if (character.SelectedConstruction != nextLadder.Item && nextLadder.Item.IsInsideTrigger(character.WorldPosition))
@@ -266,9 +290,6 @@ namespace Barotrauma
nextLadder.Item.TryInteract(character, false, true);
}
}
// We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative.
float margin = 0.1f;
bool isAboveFloor = heightFromFloor > -margin && heightFromFloor < collider.height * 1.5f;
if (nextLadder != null || isAboveFloor)
{
currentPath.SkipToNextNode();
@@ -286,7 +307,7 @@ namespace Barotrauma
}
return diff;
}
else if (character.AnimController.InWater)
else if (!canClimb || character.AnimController.InWater)
{
// If the character is underwater, we don't need the ladders anymore
if (character.IsClimbing && isDiving)
@@ -294,49 +315,59 @@ namespace Barotrauma
character.AnimController.Anim = AnimController.Animation.None;
character.SelectedConstruction = null;
}
float multiplier = MathHelper.Lerp(1, 10, MathHelper.Clamp(collider.LinearVelocity.Length() / 10, 0, 1));
float targetDistance = collider.GetSize().X * multiplier;
float horizontalDistance = Math.Abs(character.WorldPosition.X - currentPath.CurrentNode.WorldPosition.X);
float verticalDistance = Math.Abs(character.WorldPosition.Y - currentPath.CurrentNode.WorldPosition.Y);
if (character.CurrentHull != currentPath.CurrentNode.CurrentHull)
var door = currentPath.CurrentNode.ConnectedDoor;
bool blockedByDoor = door != null && !door.IsOpen && !door.IsBroken;
if (!blockedByDoor)
{
verticalDistance *= 2;
}
float distance = horizontalDistance + verticalDistance;
if (ConvertUnits.ToSimUnits(distance) < targetDistance)
{
currentPath.SkipToNextNode();
float multiplier = MathHelper.Lerp(1, 10, MathHelper.Clamp(collider.LinearVelocity.Length() / 10, 0, 1));
float targetDistance = collider.GetSize().X * multiplier;
float horizontalDistance = Math.Abs(character.WorldPosition.X - currentPath.CurrentNode.WorldPosition.X);
float verticalDistance = Math.Abs(character.WorldPosition.Y - currentPath.CurrentNode.WorldPosition.Y);
if (character.CurrentHull != currentPath.CurrentNode.CurrentHull)
{
verticalDistance *= 2;
}
float distance = horizontalDistance + verticalDistance;
if (ConvertUnits.ToSimUnits(distance) < targetDistance)
{
currentPath.SkipToNextNode();
}
}
}
else if (!IsNextLadderSameAsCurrent)
{
// Walking horizontally
Vector2 colliderBottom = character.AnimController.GetColliderBottom();
Vector2 colliderSize = collider.GetSize();
Vector2 velocity = collider.LinearVelocity;
// If the character is smaller than this, it fails to use the waypoint nodes, because they are always too high.
// If the character is smaller than this, it would fail to use the waypoint nodes because they are always too high.
float minHeight = 1;
// Cannot use the head position, because not all characters have head or it can be below the total height of the character
float characterHeight = Math.Max(colliderSize.Y + character.AnimController.ColliderHeightFromFloor, minHeight);
float horizontalDistance = Math.Abs(collider.SimPosition.X - currentPath.CurrentNode.SimPosition.X);
bool isAboveFeet = currentPath.CurrentNode.SimPosition.Y > colliderBottom.Y;
bool isNotTooHigh = currentPath.CurrentNode.SimPosition.Y < colliderBottom.Y + characterHeight;
var door = currentPath.CurrentNode.ConnectedDoor;
bool blockedByDoor = door != null && !door.IsOpen && !door.IsBroken;
float margin = MathHelper.Lerp(1, 10, MathHelper.Clamp(Math.Abs(velocity.X) / 10, 0, 1));
float targetDistance = collider.radius * margin;
if (horizontalDistance < targetDistance && isAboveFeet && isNotTooHigh)
if (horizontalDistance < targetDistance && isAboveFeet && isNotTooHigh && !blockedByDoor)
{
currentPath.SkipToNextNode();
}
}
if (currentPath.CurrentNode == null) return Vector2.Zero;
if (currentPath.CurrentNode == null)
{
return Vector2.Zero;
}
return currentPath.CurrentNode.SimPosition - pos;
}
private bool CanAccessDoor(Door door, Func<Controller, bool> buttonFilter = null)
{
if (door.IsOpen) { return true; }
if (canBreakDoors) { return true; }
if (door.Item.NonInteractable) { return false; }
if (CanBreakDoors) { return true; }
if (door.IsStuck) { return false; }
if (!canOpenDoors || character.LockHands) { return false; }
if (door.HasIntegratedButtons)
@@ -345,7 +376,7 @@ namespace Barotrauma
}
else
{
return door.Item.GetConnectedComponents<Controller>(true).Any(b => b.HasAccess(character) && (buttonFilter == null || buttonFilter(b)));
return door.Item.GetConnectedComponents<Controller>(true).Any(b => !b.Item.NonInteractable && b.HasAccess(character) && (buttonFilter == null || buttonFilter(b)));
}
}
@@ -381,7 +412,11 @@ namespace Barotrauma
{
//the node we're heading towards is the last one in the path, and at a door
//the door needs to be open for the character to reach the node
shouldBeOpen = true;
if (currentWaypoint.ConnectedDoor.LinkedGap != null && currentWaypoint.ConnectedDoor.LinkedGap.IsRoomToRoom)
{
shouldBeOpen = true;
door = currentWaypoint.ConnectedDoor;
}
}
else
{
@@ -519,9 +554,9 @@ namespace Barotrauma
//non-humanoids can't climb up ladders
if (!(character.AnimController is HumanoidAnimController))
{
if (node.Waypoint.Ladders != null && nextNode.Waypoint.Ladders != null &&
nextNode.Position.Y - node.Position.Y > 1.0f && //more than one sim unit to climb up
nextNode.Waypoint.CurrentHull != null && nextNode.Waypoint.CurrentHull.Surface < nextNode.Waypoint.Position.Y) //upper node not underwater
if (node.Waypoint.Ladders != null && nextNode.Waypoint.Ladders != null && nextNode.Waypoint.Ladders.Item.NonInteractable ||
(nextNode.Position.Y - node.Position.Y > 1.0f && //more than one sim unit to climb up
nextNode.Waypoint.CurrentHull != null && nextNode.Waypoint.CurrentHull.Surface < nextNode.Waypoint.Position.Y)) //upper node not underwater
{
return null;
}
@@ -539,7 +574,10 @@ namespace Barotrauma
}
if (character.NeedsAir && hull.WaterVolume / hull.Rect.Width > 100.0f)
{
penalty += 500.0f;
if (!HumanAIController.HasDivingSuit(character))
{
penalty += 500.0f;
}
}
if (character.PressureProtection < 10.0f && hull.WaterVolume > hull.Volume)
{
@@ -1,11 +1,11 @@
using FarseerPhysics;
using FarseerPhysics.Common;
using FarseerPhysics.Dynamics;
using FarseerPhysics.Dynamics.Joints;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Xml.Linq;
using System.Linq;
namespace Barotrauma
{
@@ -19,8 +19,8 @@ namespace Barotrauma
private Vector2 attachSurfaceNormal;
private Submarine attachTargetSubmarine;
private bool attachToSub;
private bool attachToWalls;
public bool AttachToSub { get; private set; }
public bool AttachToWalls { get; private set; }
private float minDeattachSpeed = 3.0f, maxDeattachSpeed = 10.0f;
private float damageOnDetach = 0.0f, detachStun = 0.0f;
@@ -58,8 +58,8 @@ namespace Barotrauma
public LatchOntoAI(XElement element, EnemyAIController enemyAI)
{
attachToWalls = element.GetAttributeBool("attachtowalls", false);
attachToSub = element.GetAttributeBool("attachtosub", false);
AttachToWalls = element.GetAttributeBool("attachtowalls", false);
AttachToSub = element.GetAttributeBool("attachtosub", false);
minDeattachSpeed = element.GetAttributeFloat("mindeattachspeed", 3.0f);
maxDeattachSpeed = Math.Max(minDeattachSpeed, element.GetAttributeFloat("maxdeattachspeed", 10.0f));
damageOnDetach = element.GetAttributeFloat("damageondetach", 0.0f);
@@ -67,11 +67,19 @@ namespace Barotrauma
localAttachPos = ConvertUnits.ToSimUnits(element.GetAttributeVector2("localattachpos", Vector2.Zero));
attachLimbRotation = MathHelper.ToRadians(element.GetAttributeFloat("attachlimbrotation", 0.0f));
if (Enum.TryParse(element.GetAttributeString("attachlimb", "Head"), out LimbType attachLimbType))
string limbString = element.GetAttributeString("attachlimb", null);
attachLimb = enemyAI.Character.AnimController.Limbs.FirstOrDefault(l => string.Equals(l.Name, limbString, StringComparison.OrdinalIgnoreCase));
if (attachLimb == null)
{
attachLimb = enemyAI.Character.AnimController.GetLimb(attachLimbType);
if (Enum.TryParse(limbString, out LimbType attachLimbType))
{
attachLimb = enemyAI.Character.AnimController.GetLimb(attachLimbType);
}
}
if (attachLimb == null)
{
attachLimb = enemyAI.Character.AnimController.MainLimb;
}
if (attachLimb == null) attachLimb = enemyAI.Character.AnimController.MainLimb;
enemyAI.Character.OnDeath += OnCharacterDeath;
}
@@ -108,7 +116,9 @@ namespace Barotrauma
//something went wrong, limb body is very far from the joint anchor -> deattach
if (Vector2.DistanceSquared(attachJoints[i].WorldAnchorB, attachJoints[i].BodyA.Position) > 10.0f * 10.0f)
{
#if DEBUG
DebugConsole.ThrowError("Limb body of the character \"" + character.Name + "\" is very far from the attach joint anchor -> deattach");
#endif
DeattachFromBody();
return;
}
@@ -131,7 +141,7 @@ namespace Barotrauma
switch (enemyAI.State)
{
case AIState.Idle:
if (attachToWalls && character.Submarine == null && Level.Loaded != null)
if (AttachToWalls && character.Submarine == null && Level.Loaded != null)
{
if (!IsAttached)
{
@@ -180,8 +190,9 @@ namespace Barotrauma
}
else
{
float dist = Vector2.Distance(character.SimPosition, wallAttachPos);
if (dist < Math.Max(Math.Max(character.AnimController.Collider.radius, character.AnimController.Collider.width), character.AnimController.Collider.height) * 1.2f)
float squaredDistance = Vector2.DistanceSquared(character.SimPosition, wallAttachPos);
float targetDistance = Math.Max(Math.Max(character.AnimController.Collider.radius, character.AnimController.Collider.width), character.AnimController.Collider.height) * 1.2f;
if (squaredDistance < targetDistance * targetDistance)
{
//close enough to a wall -> attach
AttachToBody(character.AnimController.Collider, attachLimb, attachTargetBody, wallAttachPos);
@@ -197,12 +208,13 @@ namespace Barotrauma
}
break;
case AIState.Attack:
case AIState.Aggressive:
if (enemyAI.AttackingLimb != null)
{
if (attachToSub && !enemyAI.IsSteeringThroughGap && wallAttachPos != Vector2.Zero && attachTargetBody != null)
if (AttachToSub && !enemyAI.IsSteeringThroughGap && wallAttachPos != Vector2.Zero && attachTargetBody != null)
{
// is not attached or is attached to something else
if (!IsAttached || IsAttached && attachJoints[0].BodyB == attachTargetBody)
if (!IsAttached || IsAttached && attachJoints[0].BodyB != attachTargetBody)
{
if (Vector2.DistanceSquared(ConvertUnits.ToDisplayUnits(transformedAttachPos), enemyAI.AttackingLimb.WorldPosition) < enemyAI.AttackingLimb.attack.DamageRange * enemyAI.AttackingLimb.attack.DamageRange)
{
@@ -247,16 +259,17 @@ namespace Barotrauma
if (attachJoints.Count > 0)
{
//already attached to the target body, no need to do anything
if (attachJoints[0].BodyB == targetBody) return;
if (attachJoints[0].BodyB == targetBody) { return; }
DeattachFromBody();
}
jointDir = attachLimb.Dir;
Vector2 transformedLocalAttachPos = localAttachPos * attachLimb.Scale * attachLimb.Params.Ragdoll.LimbScale;
if (jointDir < 0.0f) transformedLocalAttachPos.X = -transformedLocalAttachPos.X;
//transformedLocalAttachPos = Vector2.Transform(transformedLocalAttachPos, Matrix.CreateRotationZ(attachLimb.Rotation));
if (jointDir < 0.0f)
{
transformedLocalAttachPos.X = -transformedLocalAttachPos.X;
}
float angle = MathUtils.VectorToAngle(-attachSurfaceNormal) - MathHelper.PiOver2 + attachLimbRotation * attachLimb.Dir;
attachLimb.body.SetTransform(attachPos + attachSurfaceNormal * transformedLocalAttachPos.Length(), angle);
@@ -274,7 +287,10 @@ namespace Barotrauma
// Limb scale is already taken into account when creating the collider.
Vector2 colliderFront = collider.GetLocalFront();
if (jointDir < 0.0f) colliderFront.X = -colliderFront.X;
if (jointDir < 0.0f)
{
colliderFront.X = -colliderFront.X;
}
collider.SetTransform(attachPos + attachSurfaceNormal * colliderFront.Length(), MathUtils.VectorToAngle(-attachSurfaceNormal) - MathHelper.PiOver2);
var colliderJoint = new WeldJoint(collider.FarseerBody, targetBody, colliderFront, targetBody.GetLocalPoint(attachPos), false)
@@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using Barotrauma.IO;
using System.Linq;
using System.Xml.Linq;
@@ -375,9 +375,7 @@ namespace Barotrauma
}
}
StreamWriter file = new StreamWriter(@"NPCConversations.csv");
file.WriteLine(sb.ToString());
file.Close();
File.WriteAllText("NPCConversations.csv", sb.ToString());
}
private static void WriteConversation(System.Text.StringBuilder sb, NPCConversation conv, int depthIndex)
@@ -118,7 +118,6 @@ namespace Barotrauma
public void TryComplete(float deltaTime)
{
if (isCompleted) { return; }
//if (Abandon && !IsLoop && subObjectives.None()) { return; }
if (CheckState()) { return; }
// Not ready -> act (can't do foreach because it's possible that the collection is modified in event callbacks.
for (int i = 0; i < subObjectives.Count; i++)
@@ -201,7 +200,7 @@ namespace Barotrauma
}
else
{
Priority = CumulatedDevotion * PriorityModifier;
Priority = CumulatedDevotion;
}
return Priority;
}
@@ -211,7 +210,7 @@ namespace Barotrauma
var currentObjective = objectiveManager.CurrentObjective;
if (currentObjective != null && (currentObjective == this || currentObjective.subObjectives.Any(so => so == this)))
{
CumulatedDevotion += Devotion * PriorityModifier * deltaTime;
CumulatedDevotion += Devotion * deltaTime;
}
}
@@ -20,6 +20,7 @@ namespace Barotrauma
{
if (battery == null) { return false; }
var item = battery.Item;
if (item.NonInteractable) { return false; }
if (item.Submarine == null) { return false; }
if (item.CurrentHull == null) { return false; }
if (item.Submarine.TeamID != character.TeamID) { return false; }
@@ -121,6 +121,12 @@ namespace Barotrauma
protected override bool Check()
{
if (initialMode == CombatMode.Offensive && Mode != CombatMode.Offensive)
{
Abandon = true;
SteeringManager.Reset();
return false;
}
bool completed = (Enemy != null && (Enemy.Removed || Enemy.IsDead)) || (initialMode != CombatMode.Offensive && coolDownTimer <= 0);
if (completed)
{
@@ -465,7 +471,6 @@ namespace Barotrauma
SteeringManager.Reset();
return;
}
retreatTarget = null;
RemoveSubObjective(ref retreatObjective);
RemoveSubObjective(ref seekAmmunition);
@@ -482,9 +487,8 @@ namespace Barotrauma
},
onAbandon: () =>
{
Mode = CombatMode.Defensive;
Abandon = true;
SteeringManager.Reset();
RemoveSubObjective(ref followTargetObjective);
});
if (followTargetObjective != null)
{
@@ -593,10 +597,7 @@ namespace Barotrauma
private void Attack(float deltaTime)
{
float squaredDistance = Vector2.DistanceSquared(character.Position, Enemy.Position);
character.CursorPosition = Enemy.Position;
float engageDistance = 500;
if (character.CurrentHull != Enemy.CurrentHull && squaredDistance > engageDistance * engageDistance) { return; }
if (!character.CanSeeCharacter(Enemy)) { return; }
if (Weapon.RequireAimToUse)
{
@@ -604,7 +605,7 @@ namespace Barotrauma
if (SteeringManager == PathSteering)
{
var door = PathSteering.CurrentPath?.CurrentNode?.ConnectedDoor;
if (door != null && !door.IsOpen)
if (door != null && !door.IsOpen && !door.IsBroken)
{
isOperatingButtons = door.HasIntegratedButtons || door.Item.GetConnectedComponents<Controller>(true).Any();
}
@@ -626,7 +627,7 @@ namespace Barotrauma
}
if (WeaponComponent is MeleeWeapon meleeWeapon)
{
if (squaredDistance <= meleeWeapon.Range * meleeWeapon.Range)
if (Vector2.DistanceSquared(character.Position, Enemy.Position) <= meleeWeapon.Range * meleeWeapon.Range)
{
character.SetInput(InputType.Shoot, false, true);
Weapon.Use(deltaTime, character);
@@ -636,7 +637,7 @@ namespace Barotrauma
{
if (WeaponComponent is RepairTool repairTool)
{
if (squaredDistance > repairTool.Range * repairTool.Range) { return; }
if (Vector2.DistanceSquared(character.Position, Enemy.Position) > repairTool.Range * repairTool.Range) { return; }
}
if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.Position - Weapon.Position) < MathHelper.PiOver4)
{
@@ -158,7 +158,7 @@ namespace Barotrauma
Abandon = true;
}, onCompleted: () =>
{
if (getItemObjective.TargetItem != null)
if (getItemObjective?.TargetItem != null)
{
containedItems.Add(getItemObjective.TargetItem);
}
@@ -106,7 +106,7 @@ namespace Barotrauma
if (SteeringManager == PathSteering)
{
var door = PathSteering.CurrentPath?.CurrentNode?.ConnectedDoor;
if (door != null && !door.IsOpen)
if (door != null && !door.IsOpen && !door.IsBroken)
{
isOperatingButtons = door.HasIntegratedButtons || door.Item.GetConnectedComponents<Controller>(true).Any();
}
@@ -1,9 +1,4 @@
using Barotrauma.Items.Components;
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Barotrauma.Extensions;
using System.Collections.Generic;
namespace Barotrauma
{
@@ -43,7 +43,7 @@ namespace Barotrauma
}
if (character.CurrentHull == null)
{
Priority = objectiveManager.CurrentOrder is AIObjectiveGoTo ? 0 : 100;
Priority = objectiveManager.CurrentOrder is AIObjectiveGoTo && HumanAIController.HasDivingSuit(character) ? 0 : 100;
}
else
{
@@ -83,8 +83,9 @@ namespace Barotrauma
else
{
float dangerFactor = (100 - currenthullSafety) / 100;
Priority = Math.Min(Priority + dangerFactor * priorityIncrease * deltaTime, 100);
Priority += dangerFactor * priorityIncrease * deltaTime;
}
Priority = MathHelper.Clamp(Priority, 0, 100);
}
}
@@ -93,34 +94,39 @@ namespace Barotrauma
protected override void Act(float deltaTime)
{
var currentHull = character.CurrentHull;
bool needsDivingGear = HumanAIController.NeedsDivingGear(character, currentHull, out bool needsDivingSuit);
bool needsEquipment = false;
if (needsDivingSuit)
bool dangerousPressure = currentHull == null || currentHull.LethalPressure > 0;
if (!dangerousPressure)
{
needsEquipment = !HumanAIController.HasDivingSuit(character, AIObjectiveFindDivingGear.lowOxygenThreshold);
}
else if (needsDivingGear)
{
needsEquipment = !HumanAIController.HasDivingGear(character, AIObjectiveFindDivingGear.lowOxygenThreshold);
}
if (needsEquipment && divingGearObjective == null && !character.LockHands)
{
RemoveSubObjective(ref goToObjective);
TryAddSubObjective(ref divingGearObjective,
constructor: () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager),
onAbandon: () =>
{
searchHullTimer = Math.Min(1, searchHullTimer);
// Don't try to seek diving gear if the pressure is dangerous. Just get out.
bool needsDivingGear = HumanAIController.NeedsDivingGear(character, currentHull, out bool needsDivingSuit);
bool needsEquipment = false;
if (needsDivingSuit)
{
needsEquipment = !HumanAIController.HasDivingSuit(character, AIObjectiveFindDivingGear.lowOxygenThreshold);
}
else if (needsDivingGear)
{
needsEquipment = !HumanAIController.HasDivingGear(character, AIObjectiveFindDivingGear.lowOxygenThreshold);
}
if (needsEquipment && divingGearObjective == null && !character.LockHands)
{
RemoveSubObjective(ref goToObjective);
TryAddSubObjective(ref divingGearObjective,
constructor: () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager),
onAbandon: () =>
{
searchHullTimer = Math.Min(1, searchHullTimer);
// Don't reset the diving gear objective, because it's possible that there is no diving gear -> seek a safe hull and then reset so that we can check again.
},
onCompleted: () =>
{
resetPriority = true;
searchHullTimer = Math.Min(1, searchHullTimer);
RemoveSubObjective(ref divingGearObjective);
});
onCompleted: () =>
{
resetPriority = true;
searchHullTimer = Math.Min(1, searchHullTimer);
RemoveSubObjective(ref divingGearObjective);
});
}
}
else if (divingGearObjective == null || !divingGearObjective.CanBeCompleted)
if (divingGearObjective == null || !divingGearObjective.CanBeCompleted)
{
if (currenthullSafety < HumanAIController.HULL_SAFETY_THRESHOLD)
{
@@ -37,7 +37,7 @@ namespace Barotrauma
protected override float TargetEvaluation()
{
int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveFixLeaks>(), onlyBots: true);
int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveFixLeaks>() && !c.Character.IsIncapacitated, onlyBots: true);
int totalLeaks = Targets.Count();
if (totalLeaks == 0) { return 0; }
int secondaryLeaks = Targets.Count(l => l.IsRoomToRoom);
@@ -3,6 +3,7 @@ using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
@@ -20,7 +21,9 @@ namespace Barotrauma
//can be either tags or identifiers
private string[] itemIdentifiers;
public IEnumerable<string> Identifiers => itemIdentifiers;
private Item targetItem, moveToTarget, rootContainer;
private Item targetItem;
private ISpatialEntity moveToTarget;
private bool isDoneSeeking;
public Item TargetItem => targetItem;
private int currSearchIndex;
@@ -29,6 +32,8 @@ namespace Barotrauma
private float currItemPriority;
private bool checkInventory;
public static float DefaultReach = 100;
public bool AllowToFindDivingGear { get; set; } = true;
public AIObjectiveGetItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, bool equip = true, float priorityModifier = 1)
@@ -37,6 +42,7 @@ namespace Barotrauma
currSearchIndex = -1;
this.equip = equip;
this.targetItem = targetItem;
moveToTarget = targetItem?.GetRootInventoryOwner();
}
public AIObjectiveGetItem(Character character, string itemIdentifier, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1)
@@ -62,8 +68,7 @@ namespace Barotrauma
if (item != null)
{
targetItem = item;
rootContainer = item.GetRootContainer();
moveToTarget = rootContainer ?? item;
moveToTarget = item.GetRootInventoryOwner();
}
return item != null;
}
@@ -86,6 +91,15 @@ namespace Barotrauma
}
if (!isDoneSeeking)
{
bool dangerousPressure = character.CurrentHull == null || character.CurrentHull.LethalPressure > 0;
if (dangerousPressure)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Seeking item aborted, because the pressure is dangerous.", Color.Yellow);
#endif
Abandon = true;
return;
}
FindTargetItem();
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
return;
@@ -108,7 +122,26 @@ namespace Barotrauma
Reset();
return;
}
if (character.CanInteractWith(targetItem, out _, checkLinked: false))
bool canInteract = false;
if (moveToTarget is Character c)
{
if (character == c)
{
canInteract = true;
moveToTarget = null;
}
else
{
character.SelectCharacter(c);
canInteract = character.CanInteractWith(c, maxDist: DefaultReach);
character.DeselectCharacter();
}
}
else if (moveToTarget is Item parentItem)
{
canInteract = character.CanInteractWith(parentItem, out _, checkLinked: false);
}
if (canInteract)
{
var pickable = targetItem.GetComponent<Pickable>();
if (pickable == null)
@@ -173,17 +206,17 @@ namespace Barotrauma
}
}
}
else
else if (moveToTarget != null)
{
TryAddSubObjective(ref goToObjective,
constructor: () =>
{
return new AIObjectiveGoTo(moveToTarget, character, objectiveManager, repeat: false, getDivingGearIfNeeded: AllowToFindDivingGear)
return new AIObjectiveGoTo(moveToTarget, character, objectiveManager, repeat: false, getDivingGearIfNeeded: AllowToFindDivingGear, closeEnough: DefaultReach)
{
// If the root container changes, the item is no longer where it was (taken by someone -> need to find another item)
abortCondition = () => targetItem == null || targetItem.GetRootContainer() != rootContainer,
abortCondition = () => targetItem == null || targetItem.GetRootInventoryOwner() != moveToTarget,
DialogueIdentifier = "dialogcannotreachtarget",
TargetName = moveToTarget.Name
TargetName = (moveToTarget as MapEntity)?.Name ?? (moveToTarget as Character)?.Name ?? moveToTarget.ToString()
};
},
onAbandon: () =>
@@ -212,9 +245,9 @@ namespace Barotrauma
{
currSearchIndex++;
var item = Item.ItemList[currSearchIndex];
if (item.Submarine == null) { continue; }
if (item.CurrentHull == null) { continue; }
if (item.Submarine.TeamID != character.TeamID) { continue; }
Submarine itemSub = item.Submarine ?? item.ParentInventory?.Owner?.Submarine;
if (itemSub == null) { continue; }
if (itemSub.TeamID != character.TeamID) { continue; }
if (!CheckItem(item)) { continue; }
if (ignoredContainerIdentifiers != null && item.Container != null)
{
@@ -222,8 +255,8 @@ namespace Barotrauma
}
if (character.Submarine != null)
{
if (item.Submarine.Info.Type != character.Submarine.Info.Type) { continue; }
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(item, true)) { continue; }
if (itemSub.Info.Type != character.Submarine.Info.Type) { continue; }
if (character.Submarine.GetConnectedSubs().None(s => s == itemSub && itemSub.TeamID == character.TeamID && itemSub.Info.Type == character.Submarine.Info.Type)) { continue; }
}
if (character.IsItemTakenBySomeoneElse(item)) { continue; }
float itemPriority = 1;
@@ -231,8 +264,8 @@ namespace Barotrauma
{
itemPriority = GetItemPriority(item);
}
Item rootContainer = item.GetRootContainer();
Vector2 itemPos = (rootContainer ?? item).WorldPosition;
Entity rootInventoryOwner = item.GetRootInventoryOwner();
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;
@@ -243,8 +276,7 @@ namespace Barotrauma
if (itemPriority < currItemPriority) { continue; }
currItemPriority = itemPriority;
targetItem = item;
moveToTarget = rootContainer ?? item;
this.rootContainer = rootContainer;
moveToTarget = rootInventoryOwner ?? item;
}
if (currSearchIndex >= Item.ItemList.Count - 1)
{
@@ -293,7 +325,6 @@ namespace Barotrauma
RemoveSubObjective(ref goToObjective);
targetItem = null;
moveToTarget = null;
rootContainer = null;
isDoneSeeking = false;
currSearchIndex = 0;
}
@@ -80,21 +80,28 @@ namespace Barotrauma
this.repeat = repeat;
waitUntilPathUnreachable = 3.0f;
this.getDivingGearIfNeeded = getDivingGearIfNeeded;
CloseEnough = closeEnough;
if (Target is Item i)
{
CloseEnough = Math.Max(CloseEnough, i.InteractDistance + Math.Max(i.Rect.Width, i.Rect.Height) / 2);
}
else if (Target is Character)
{
CloseEnough = Math.Max(closeEnough, AIObjectiveGetItem.DefaultReach);
}
else
{
CloseEnough = closeEnough;
}
}
private void SpeakCannotReach()
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot reach the target: {Target.ToString()}", Color.Yellow);
DebugConsole.NewMessage($"{character.Name}: Cannot reach the target: {Target}", Color.Yellow);
#endif
if (objectiveManager.CurrentOrder != null && DialogueIdentifier != null)
{
string msg = TargetName == null ? TextManager.Get(DialogueIdentifier, true) : TextManager.GetWithVariable(DialogueIdentifier, "[name]", TargetName, true);
string msg = TargetName == null ? TextManager.Get(DialogueIdentifier, true) : TextManager.GetWithVariable(DialogueIdentifier, "[name]", TargetName, formatCapitals: !(Target is Character));
if (msg != null)
{
character.Speak(msg, identifier: DialogueIdentifier, minDurationBetweenSimilar: 20.0f);
@@ -213,10 +220,19 @@ namespace Barotrauma
return;
}
}
if (repeat && IsCloseEnough)
if (repeat)
{
OnCompleted();
return;
if (IsCloseEnough)
{
if (requiredCondition == null || requiredCondition())
{
if (character.CanSeeTarget(Target))
{
OnCompleted();
return;
}
}
}
}
if (SteeringManager == PathSteering)
{
@@ -244,7 +260,7 @@ namespace Barotrauma
}
}
private Hull GetTargetHull()
public Hull GetTargetHull()
{
if (Target is Hull h)
{
@@ -284,13 +300,7 @@ namespace Barotrauma
//otherwise characters can let go of the ladders too soon once they're close enough to the target
if (PathSteering.CurrentPath.NextNode != null) { return false; }
}
bool closeEnough = Vector2.DistanceSquared(Target.WorldPosition, character.WorldPosition) < CloseEnough * CloseEnough;
if (closeEnough)
{
closeEnough = !(Target is Character) || Target is Character c && c.CurrentHull == character.CurrentHull;
}
return closeEnough;
return Vector2.DistanceSquared(Target.WorldPosition, character.WorldPosition) < CloseEnough * CloseEnough;
}
}
@@ -326,7 +336,9 @@ namespace Barotrauma
}
else if (Target is Character targetCharacter)
{
if (character.CanInteractWith(targetCharacter, CloseEnough)) { IsCompleted = true; }
character.SelectCharacter(targetCharacter);
if (character.CanInteractWith(targetCharacter, skipDistanceCheck: true)) { IsCompleted = true; }
character.DeselectCharacter();
}
else
{
@@ -338,6 +350,16 @@ namespace Barotrauma
return IsCompleted;
}
protected override void OnAbandon()
{
StopMovement();
if (SteeringManager == PathSteering)
{
PathSteering.ResetPath();
}
base.OnAbandon();
}
private void StopMovement()
{
character.AIController.SteeringManager.Reset();
@@ -3,7 +3,6 @@ using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
@@ -35,41 +34,43 @@ namespace Barotrauma
{
standStillTimer = Rand.Range(-10.0f, 10.0f);
walkDuration = Rand.Range(0.0f, 10.0f);
CalculatePriority();
}
protected override bool Check() => false;
public override bool CanBeCompleted => true;
public override bool IsLoop { get => true; set => throw new System.Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace); }
public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace); }
private float randomTimer;
private float randomUpdateInterval = 5;
public float Random { get; private set; }
public void CalculatePriority()
public void CalculatePriority(float max = 0)
{
Random = Rand.Range(0.5f, 1.5f);
randomTimer = randomUpdateInterval;
float max = Math.Min(Math.Min(AIObjectiveManager.RunPriority, AIObjectiveManager.OrderPriority) - 1, 100);
float initiative = character.GetSkillLevel("initiative");
Priority = MathHelper.Lerp(1, max, MathUtils.InverseLerp(100, 0, initiative * Random));
//Random = Rand.Range(0.5f, 1.5f);
//randomTimer = randomUpdateInterval;
//max = max > 0 ? max : Math.Min(Math.Min(AIObjectiveManager.RunPriority, AIObjectiveManager.OrderPriority) - 1, 100);
//float initiative = character.GetSkillLevel("initiative");
//Priority = MathHelper.Lerp(1, max, MathUtils.InverseLerp(100, 0, initiative * Random));
Priority = 1;
}
public override float GetPriority() => Priority;
public override void Update(float deltaTime)
{
if (objectiveManager.CurrentObjective == this)
{
if (randomTimer > 0)
{
randomTimer -= deltaTime;
}
else
{
CalculatePriority();
}
}
//if (objectiveManager.CurrentObjective == this)
//{
// if (randomTimer > 0)
// {
// randomTimer -= deltaTime;
// }
// else
// {
// CalculatePriority();
// }
//}
}
protected override void Act(float deltaTime)
@@ -129,7 +130,7 @@ namespace Barotrauma
//choose a random available hull
currentTarget = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced);
bool isCurrentHullAllowed = !IsForbidden(character.CurrentHull);
var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, nodeFilter: node =>
var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, errorMsgStr: $"AIObjectiveIdle {character.DisplayName}", nodeFilter: node =>
{
if (node.Waypoint.CurrentHull == null) { return false; }
// Check that there is no unsafe or forbidden hulls on the way to the target
@@ -47,7 +47,7 @@ namespace Barotrauma
public override bool AllowSubObjectiveSorting => true;
public virtual bool InverseTargetEvaluation => false;
public override bool IsLoop { get => true; set => throw new System.Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace); }
public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace); }
public override void Update(float deltaTime)
{
@@ -204,7 +204,7 @@ namespace Barotrauma
{
Objectives.Remove(target);
ignoreList.Add(target);
targetUpdateTimer = 0;
targetUpdateTimer = Math.Min(0.1f, targetUpdateTimer);
};
}
}
@@ -14,8 +14,11 @@ namespace Barotrauma
public const float OrderPriority = 70;
public const float RunPriority = 50;
// Constantly increases the priority of the selected objective, unless overridden
public const float baseDevotion = 3;
public const float baseDevotion = 5;
/// <summary>
/// Excluding the current order.
/// </summary>
public List<AIObjective> Objectives { get; private set; } = new List<AIObjective>();
private readonly Character character;
@@ -88,8 +91,25 @@ namespace Barotrauma
public Dictionary<AIObjective, CoroutineHandle> DelayedObjectives { get; private set; } = new Dictionary<AIObjective, CoroutineHandle>();
private void ClearIgnored()
{
if (character.AIController is HumanAIController humanAi)
{
humanAi.UnreachableHulls.Clear();
humanAi.IgnoredItems.Clear();
}
}
public void CreateAutonomousObjectives()
{
if (character.IsDead)
{
#if DEBUG
DebugConsole.ThrowError("Attempted to create autonomous orders for a dead character");
#else
return;
#endif
}
foreach (var delayedObjective in DelayedObjectives)
{
CoroutineManager.StopCoroutines(delayedObjective.Value);
@@ -99,15 +119,15 @@ namespace Barotrauma
AddObjective(new AIObjectiveFindSafety(character, this));
AddObjective(new AIObjectiveIdle(character, this));
int objectiveCount = Objectives.Count;
foreach (var automaticOrder in character.Info.Job.Prefab.AutomaticOrders)
foreach (var autonomousObjective in character.Info.Job.Prefab.AutonomousObjective)
{
var orderPrefab = Order.GetPrefab(automaticOrder.identifier);
if (orderPrefab == null) { throw new Exception($"Could not find a matching prefab by the identifier: '{automaticOrder.identifier}'"); }
var orderPrefab = Order.GetPrefab(autonomousObjective.identifier);
if (orderPrefab == null) { throw new Exception($"Could not find a matching prefab by the identifier: '{autonomousObjective.identifier}'"); }
var item = orderPrefab.MustSetTarget ? orderPrefab.GetMatchingItems(character.Submarine, false)?.GetRandom() : null;
var order = new Order(orderPrefab, item ?? character.CurrentHull as Entity,
item?.Components.FirstOrDefault(ic => ic.GetType() == orderPrefab.ItemComponentType), orderGiver: character);
if (order == null) { continue; }
var objective = CreateObjective(order, automaticOrder.option, character, automaticOrder.priorityModifier);
var objective = CreateObjective(order, autonomousObjective.option, character, isAutonomous: true, autonomousObjective.priorityModifier);
if (objective != null && objective.CanBeCompleted)
{
AddObjective(objective, delay: Rand.Value() / 2);
@@ -160,7 +180,7 @@ namespace Barotrauma
{
previousObjective?.OnDeselected();
CurrentObjective?.OnSelected();
GetObjective<AIObjectiveIdle>().CalculatePriority();
GetObjective<AIObjectiveIdle>().CalculatePriority(Math.Max(CurrentObjective.Priority - 10, 0));
}
return CurrentObjective;
}
@@ -172,7 +192,21 @@ namespace Barotrauma
public void UpdateObjectives(float deltaTime)
{
CurrentOrder?.Update(deltaTime);
if (CurrentOrder != null)
{
#if DEBUG
// Note: don't automatically remove orders here. Removing orders needs to be done via dismissing.
if (CurrentOrder.IsCompleted)
{
DebugConsole.NewMessage($"{character.Name}: ORDER {CurrentOrder.DebugTag} IS COMPLETED. CURRENTLY ALL ORDERS SHOULD BE LOOPING.", Color.Red);
}
else if (!CurrentOrder.CanBeCompleted)
{
DebugConsole.NewMessage($"{character.Name}: ORDER {CurrentOrder.DebugTag}, CANNOT BE COMPLETED.", Color.Red);
}
#endif
CurrentOrder.Update(deltaTime);
}
if (WaitTimer > 0)
{
WaitTimer -= deltaTime;
@@ -195,7 +229,7 @@ namespace Barotrauma
#endif
Objectives.Remove(objective);
}
else if (objective != CurrentOrder)
else
{
objective.Update(deltaTime);
}
@@ -233,7 +267,16 @@ namespace Barotrauma
public void SetOrder(Order order, string option, Character orderGiver)
{
CurrentOrder = CreateObjective(order, option, orderGiver);
if (character.IsDead)
{
#if DEBUG
DebugConsole.ThrowError("Attempted to set an order for a dead character");
#else
return;
#endif
}
ClearIgnored();
CurrentOrder = CreateObjective(order, option, orderGiver, isAutonomous: false);
if (CurrentOrder == null)
{
// Recreate objectives, because some of them may be removed, if impossible to complete (e.g. due to path finding)
@@ -245,7 +288,7 @@ namespace Barotrauma
}
}
public AIObjective CreateObjective(Order order, string option, Character orderGiver, float priorityModifier = 1)
public AIObjective CreateObjective(Order order, string option, Character orderGiver, bool isAutonomous, float priorityModifier = 1)
{
if (order == null) { return null; }
AIObjective newObjective;
@@ -284,14 +327,21 @@ namespace Barotrauma
newObjective = new AIObjectiveRepairItems(character, this, priorityModifier: priorityModifier, prioritizedItem: order.TargetEntity as Item)
{
RelevantSkill = order.AppropriateSkill,
RequireAdequateSkills = option == "jobspecific"
RequireAdequateSkills = isAutonomous
};
break;
case "pumpwater":
if (order.TargetItemComponent is Pump targetPump)
{
newObjective = new AIObjectiveOperateItem(targetPump, character, this, option, false, priorityModifier: priorityModifier);
// newObjective.Completed += DismissSelf;
if (order.TargetItemComponent.Item.NonInteractable) { return null; }
newObjective = new AIObjectiveOperateItem(targetPump, character, this, option, false, priorityModifier: priorityModifier)
{
IsLoop = true,
Override = orderGiver != null && orderGiver.IsPlayer
};
// ItemComponent.AIOperate() returns false by default -> We'd have to set IsLoop = false and implement a custom override of AIOperate for the Pump.cs,
// if we want that the bot just switches the pump on/off and continues doing something else.
// If we want that the bot does the objective and then forgets about it, I think we could do the same plus dismiss when the bot is done.
}
else
{
@@ -306,9 +356,11 @@ namespace Barotrauma
break;
case "steer":
var steering = (order?.TargetEntity as Item)?.GetComponent<Steering>();
if (steering != null) steering.PosToMaintain = steering.Item.Submarine?.WorldPosition;
if (steering != null) { steering.PosToMaintain = steering.Item.Submarine?.WorldPosition; }
if (order.TargetItemComponent == null) { return null; }
newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option, requireEquip: false, useController: order.UseController, priorityModifier: priorityModifier)
if (order.TargetItemComponent.Item.NonInteractable) { return null; }
newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option,
requireEquip: false, useController: order.UseController, controller: order.ConnectedController, priorityModifier: priorityModifier)
{
IsLoop = true,
// Don't override unless it's an order by a player
@@ -317,12 +369,15 @@ namespace Barotrauma
break;
default:
if (order.TargetItemComponent == null) { return null; }
newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option, requireEquip: false, useController: order.UseController, priorityModifier: priorityModifier)
if (order.TargetItemComponent.Item.NonInteractable) { return null; }
newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option,
requireEquip: false, useController: order.UseController, controller: order.ConnectedController, priorityModifier: priorityModifier)
{
IsLoop = true,
// Don't override unless it's an order by a player
Override = orderGiver != null && orderGiver.IsPlayer
};
if (newObjective.Abandon) { return null; }
break;
}
return newObjective;
@@ -3,7 +3,6 @@ using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
@@ -29,6 +28,7 @@ namespace Barotrauma
public ItemComponent GetTarget() => useController ? controller : component;
public Func<bool> completionCondition;
private bool isDoneOperating;
public override float GetPriority()
{
@@ -47,17 +47,41 @@ namespace Barotrauma
{
Priority = AIObjectiveManager.OrderPriority;
}
Item targetItem = GetTarget()?.Item;
ItemComponent target = GetTarget();
Item targetItem = target?.Item;
if (targetItem == null)
{
#if DEBUG
DebugConsole.ThrowError("Item or component of AI Objective Operate item wass null. This shouldn't happen.");
DebugConsole.ThrowError("Item or component of AI Objective Operate item was null. This shouldn't happen.");
#endif
Abandon = true;
Priority = 0;
return 0.0f;
return Priority;
}
if (targetItem.CurrentHull == null || targetItem.CurrentHull.FireSources.Any() || HumanAIController.IsItemOperatedByAnother(GetTarget(), out _))
var reactor = component?.Item.GetComponent<Reactor>();
if (reactor != null)
{
switch (Option)
{
case "shutdown":
if (!reactor.PowerOn)
{
Priority = 0;
return Priority;
}
break;
case "powerup":
// Check that we don't already have another order that is targeting the same item.
// Without this the autonomous objective will tell the bot to turn the reactor on again.
if (objectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder != this && operateOrder.GetTarget() == target)
{
Priority = 0;
return Priority;
}
break;
}
}
if (targetItem.CurrentHull == null || targetItem.CurrentHull.FireSources.Any() || HumanAIController.IsItemOperatedByAnother(target, out _))
{
Priority = 0;
}
@@ -68,26 +92,33 @@ namespace Barotrauma
else
{
float value = CumulatedDevotion + (AIObjectiveManager.OrderPriority * PriorityModifier);
float max = MathHelper.Min((AIObjectiveManager.OrderPriority - 1), 90);
float max = objectiveManager.CurrentOrder == this ? MathHelper.Min(AIObjectiveManager.OrderPriority, 90) : AIObjectiveManager.RunPriority - 1;
Priority = MathHelper.Clamp(value, 0, max);
}
}
return Priority;
}
public AIObjectiveOperateItem(ItemComponent item, Character character, AIObjectiveManager objectiveManager, string option, bool requireEquip, Entity operateTarget = null, bool useController = false, float priorityModifier = 1)
public AIObjectiveOperateItem(ItemComponent item, Character character, AIObjectiveManager objectiveManager, string option, bool requireEquip,
Entity operateTarget = null, bool useController = false, ItemComponent controller = null, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier, option)
{
this.component = item ?? throw new System.ArgumentNullException("item", "Attempted to create an AIObjectiveOperateItem with a null target.");
component = item ?? throw new ArgumentNullException("item", "Attempted to create an AIObjectiveOperateItem with a null target.");
this.requireEquip = requireEquip;
this.operateTarget = operateTarget;
this.useController = useController;
if (useController)
if (useController) { this.controller = controller ?? component?.Item?.FindController(); }
var target = GetTarget();
if (target == null)
{
//try finding the controller with the simpler non-recursive method first
controller =
component.Item.GetConnectedComponents<Controller>().FirstOrDefault() ??
component.Item.GetConnectedComponents<Controller>(recursive: true).FirstOrDefault();
#if DEBUG
throw new Exception("target null");
#endif
Abandon = true;
}
else if (target.Item.NonInteractable)
{
Abandon = true;
}
}
@@ -122,7 +153,7 @@ namespace Barotrauma
}
if (component.AIOperate(deltaTime, character, this))
{
IsCompleted = completionCondition == null || completionCondition();
isDoneOperating = completionCondition == null || completionCondition();
}
}
else
@@ -189,12 +220,12 @@ namespace Barotrauma
}
if (component.AIOperate(deltaTime, character, this))
{
IsCompleted = completionCondition == null || completionCondition();
isDoneOperating = completionCondition == null || completionCondition();
}
}
}
}
protected override bool Check() => IsCompleted && !IsLoop;
protected override bool Check() => isDoneOperating && !IsLoop;
}
}
@@ -3,7 +3,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Barotrauma.Extensions;
namespace Barotrauma
{
@@ -27,6 +26,7 @@ namespace Barotrauma
protected override bool Filter(Pump pump)
{
if (pump == null) { return false; }
if (pump.Item.NonInteractable) { return false; }
if (pump.Item.HasTag("ballast")) { return false; }
if (pump.Item.Submarine == null) { return false; }
if (pump.Item.CurrentHull == null) { return false; }
@@ -3,7 +3,6 @@ using Microsoft.Xna.Framework;
using System;
using System.Linq;
using Barotrauma.Extensions;
using FarseerPhysics;
namespace Barotrauma
{
@@ -52,12 +51,11 @@ namespace Barotrauma
float dist = Math.Abs(character.WorldPosition.X - Item.WorldPosition.X) + yDist;
distanceFactor = MathHelper.Lerp(1, 0.25f, MathUtils.InverseLerp(0, 5000, dist));
}
float damagePriority = isPriority ? 1 : MathHelper.Lerp(1, 0, Item.Condition / Item.MaxCondition);
float successFactor = isPriority ? 1 : MathHelper.Lerp(0, 1, Item.Repairables.Average(r => r.DegreeOfSuccess(character)));
float severity = isPriority ? 1 : AIObjectiveRepairItems.GetTargetPriority(Item, character);
float isSelected = IsRepairing ? 50 : 0;
float devotion = (CumulatedDevotion + isSelected) / 100;
float max = MathHelper.Min(AIObjectiveManager.OrderPriority - 1, 90);
Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (damagePriority * distanceFactor * successFactor * PriorityModifier), 0, 1));
Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1));
}
return Priority;
}
@@ -150,7 +148,8 @@ namespace Barotrauma
{
if (character.SelectedConstruction != Item)
{
if (!Item.TryInteract(character, true, true))
if (!Item.TryInteract(character, ignoreRequiredItems: true, forceSelectKey: true) &&
!Item.TryInteract(character, ignoreRequiredItems: true, forceActionKey: true))
{
Abandon = true;
}
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
@@ -56,7 +57,7 @@ namespace Barotrauma
{
Objectives.Remove(item);
ignoreList.Add(item);
targetUpdateTimer = 0;
targetUpdateTimer = Math.Min(0.1f, targetUpdateTimer);
};
}
break;
@@ -75,13 +76,9 @@ namespace Barotrauma
if (item != character.SelectedConstruction)
{
float condition = item.ConditionPercentage;
if (item.Repairables.All(r => condition >= r.AIRepairThreshold)) { return false; }
if (item.Repairables.All(r => condition >= r.RepairThreshold)) { return false; }
}
}
if (RequireAdequateSkills)
{
if (item.Repairables.Any(r => !r.HasRequiredSkills(character))) { return false; }
}
if (!string.IsNullOrWhiteSpace(RelevantSkill))
{
if (item.Repairables.None(r => r.requiredSkills.Any(s => s.Identifier.Equals(RelevantSkill, StringComparison.OrdinalIgnoreCase)))) { return false; }
@@ -96,7 +93,7 @@ namespace Barotrauma
// Don't stop fixing until done
return 100;
}
int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveRepairItems>(), onlyBots: true);
int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveRepairItems>() && !c.Character.IsIncapacitated, onlyBots: true);
int items = Targets.Count;
bool anyFixers = otherFixers > 0;
float ratio = anyFixers ? items / (float)otherFixers : 1;
@@ -111,10 +108,24 @@ namespace Barotrauma
// Enough fixers
return 0;
}
return Targets.Sum(t => 100 - t.ConditionPercentage) * ratio;
if (RequireAdequateSkills)
{
return Targets.Sum(t => GetTargetPriority(t, character)) * ratio;
}
else
{
return Targets.Sum(t => 100 - t.ConditionPercentage) * ratio;
}
}
}
public static float GetTargetPriority(Item item, Character character)
{
float damagePriority = MathHelper.Lerp(1, 0, item.Condition / item.MaxCondition);
float successFactor = MathHelper.Lerp(0, 1, item.Repairables.Average(r => r.DegreeOfSuccess(character)));
return MathHelper.Lerp(0, 100, MathHelper.Clamp(damagePriority * successFactor, 0, 1));
}
protected override IEnumerable<Item> GetList() => Item.ItemList;
protected override AIObjective ObjectiveConstructor(Item item)
@@ -126,6 +137,7 @@ namespace Barotrauma
public static bool IsValidTarget(Item item, Character character)
{
if (item == null) { return false; }
if (item.NonInteractable) { return false; }
if (item.IsFullCondition) { return false; }
if (item.CurrentHull == null) { return false; }
if (item.Submarine == null) { return false; }
@@ -38,7 +38,19 @@ namespace Barotrauma
}
this.targetCharacter = targetCharacter;
}
protected override void OnAbandon()
{
character.SelectedCharacter = null;
base.OnAbandon();
}
protected override void OnCompleted()
{
character.SelectedCharacter = null;
base.OnCompleted();
}
protected override void Act(float deltaTime)
{
if (character.LockHands || targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead)
@@ -46,16 +58,13 @@ namespace Barotrauma
Abandon = true;
return;
}
if (targetCharacter.SelectedBy != null && targetCharacter.SelectedBy != character)
var otherRescuer = targetCharacter.SelectedBy;
if (otherRescuer != null && otherRescuer != character)
{
var otherCharacter = character.SelectedBy;
if (otherCharacter != null)
{
// Someone else is rescuing/holding the target.
Abandon = otherCharacter.IsPlayer || character.GetSkillLevel("medical") < otherCharacter.GetSkillLevel("medical");
}
// Someone else is rescuing/holding the target.
Abandon = otherRescuer.IsPlayer || character.GetSkillLevel("medical") < otherRescuer.GetSkillLevel("medical");
return;
}
if (targetCharacter != character)
{
// Incapacitated target is not in a safe place -> Move to a safe place first
@@ -161,13 +170,23 @@ namespace Barotrauma
private readonly List<string> suitableItemIdentifiers = new List<string>();
private readonly List<string> itemNameList = new List<string>();
private Dictionary<string, float> currentTreatmentSuitabilities = new Dictionary<string, float>();
private readonly Dictionary<string, float> currentTreatmentSuitabilities = new Dictionary<string, float>();
private void GiveTreatment(float deltaTime)
{
if (targetCharacter == null)
{
string errorMsg = $"{character.Name}: Attempted to update a Rescue objective with no target!";
DebugConsole.ThrowError(errorMsg);
Abandon = true;
return;
}
SteeringManager?.Reset();
if (!targetCharacter.IsPlayer)
{
// If the target is a bot, don't let it move
targetCharacter.AIController?.SteeringManager.Reset();
targetCharacter.AIController?.SteeringManager?.Reset();
}
if (treatmentTimer > 0.0f)
{
@@ -182,6 +201,8 @@ namespace Barotrauma
//check if we already have a suitable treatment for any of the afflictions
foreach (Affliction affliction in GetSortedAfflictions(targetCharacter))
{
if (affliction == null) { throw new Exception("Affliction was null"); }
if (affliction.Prefab == null) { throw new Exception("Affliction prefab was null"); }
foreach (KeyValuePair<string, float> treatmentSuitability in affliction.Prefab.TreatmentSuitability)
{
if (currentTreatmentSuitabilities.ContainsKey(treatmentSuitability.Key) && currentTreatmentSuitabilities[treatmentSuitability.Key] > 0.0f)
@@ -258,7 +279,7 @@ namespace Barotrauma
ic.PlaySound(ActionType.OnUse, character);
#endif
ic.WasUsed = true;
ic.ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb);
ic.ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb, user: character);
if (ic.DeleteOnUse)
{
remove = true;
@@ -33,46 +33,31 @@ namespace Barotrauma
protected override float TargetEvaluation()
{
int otherRescuers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveRescueAll>(), onlyBots: true);
int targetCount = Targets.Count;
bool anyRescuers = otherRescuers > 0;
float ratio = anyRescuers ? targetCount / (float)otherRescuers : 1;
if (objectiveManager.CurrentOrder == this)
if (objectiveManager.CurrentOrder != this)
{
return Targets.Min(t => GetVitalityFactor(t)) / ratio;
}
else
{
float multiplier = 1;
if (anyRescuers)
if (!character.IsMedic && HumanAIController.IsTrueForAnyCrewMember(c => c != HumanAIController && c.Character.IsMedic && !c.Character.IsUnconscious))
{
float mySkill = character.GetSkillLevel("medical");
int betterRescuers = HumanAIController.CountCrew(c => c != HumanAIController && c.Character.Info.Job.GetSkillLevel("medical") >= mySkill, onlyBots: true);
if (targetCount / (float)betterRescuers <= 1)
{
// Enough rescuers
return 100;
}
else
{
bool foundOtherMedics = HumanAIController.IsTrueForAnyCrewMember(c => c != HumanAIController && c.Character.Info.Job.Prefab.Identifier == "medicaldoctor");
if (foundOtherMedics)
{
if (character.Info.Job.Prefab.Identifier != "medicaldoctor")
{
// Double the vitality factor -> less likely to take action
multiplier = 2;
}
}
}
// Don't do anything if there's a medic on board and we are not a medic
return 100;
}
return Targets.Min(t => GetVitalityFactor(t)) / ratio * multiplier;
}
float worstCondition = Targets.Min(t => GetVitalityFactor(t));
if (Targets.Contains(character))
{
if (character.Bleeding > 10)
{
// Enforce the highest priority when bleeding out.
worstCondition = 0;
}
// Boost the priority when wounded.
worstCondition /= 2;
}
return worstCondition;
}
public static float GetVitalityFactor(Character character)
{
float vitality = character.HealthPercentage - character.Bleeding - character.Bloodloss + Math.Min(character.Oxygen, 0);
float vitality = character.HealthPercentage - (character.Bleeding * 2) - character.Bloodloss + Math.Min(character.Oxygen, 0);
vitality -= character.CharacterHealth.GetAfflictionStrength("paralysis");
return Math.Clamp(vitality, 0, 100);
}
@@ -92,6 +77,11 @@ namespace Barotrauma
if (GetVitalityFactor(target) >= GetVitalityThreshold(humanAI.ObjectiveManager, character, target)) { return false; }
if (!humanAI.ObjectiveManager.IsCurrentOrder<AIObjectiveRescueAll>())
{
if (!character.IsMedic && target != character)
{
// Don't allow to treat others autonomously
return false;
}
// Ignore unsafe hulls, unless ordered
if (humanAI.UnsafeHulls.Contains(target.CurrentHull))
{
@@ -111,10 +101,10 @@ namespace Barotrauma
if (target.Submarine.Info.Type != character.Submarine.Info.Type) { return false; }
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, true)) { return false; }
}
if (!target.IsPlayer && HumanAIController.IsActive(target) && target.AIController is HumanAIController targetAI)
if (target != character &&!target.IsPlayer && HumanAIController.IsActive(target) && target.AIController is HumanAIController targetAI)
{
// Ignore all concious targets that are currently fighting, fleeing or treating characters
if (targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveCombat>() ||
if (targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveCombat>() ||
targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveFindSafety>() ||
targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveRescue>())
{
@@ -85,32 +85,22 @@ namespace Barotrauma
//legacy support
public readonly string[] AppropriateJobs;
public readonly string[] Options;
public readonly string[] OptionNames;
private readonly Dictionary<string, string> OptionNames;
public readonly Dictionary<string, Sprite> OptionSprites;
private readonly Dictionary<string, Sprite> minimapIcons;
public Dictionary<string, Sprite> MinimapIcons => IsPrefab ? minimapIcons : Prefab.minimapIcons;
public readonly float Weight;
public readonly bool MustSetTarget;
public readonly string AppropriateSkill;
public bool HasOptions
{
get
{
if (IsPrefab)
{
return MustSetTarget || Options.Length > 1;
}
else
{
return Prefab.MustSetTarget || Prefab.Options.Length > 1;
}
}
}
public bool HasOptions => (IsPrefab ? Options : Prefab.Options).Length > 1;
public bool IsPrefab { get; private set; }
public readonly bool MustManuallyAssign;
static Order()
public static void Init()
{
Prefabs = new Dictionary<string, Order>();
OrderCategoryIcons = new Dictionary<OrderCategory, Tuple<Sprite, Color>>();
@@ -219,25 +209,18 @@ namespace Barotrauma
MustSetTarget = orderElement.GetAttributeBool("mustsettarget", false);
AppropriateSkill = orderElement.GetAttributeString("appropriateskill", null);
string translatedOptionNames = TextManager.Get("OrderOptions." + Identifier, true);
if (translatedOptionNames == null)
var optionNames = TextManager.Get("OrderOptions." + Identifier, true)?.Split(',', '') ??
orderElement.GetAttributeStringArray("optionnames", new string[0]);
OptionNames = new Dictionary<string, string>();
for (int i = 0; i < Options.Length && i < optionNames.Length; i++)
{
OptionNames = orderElement.GetAttributeStringArray("optionnames", new string[0]);
OptionNames.Add(Options[i], optionNames[i].Trim());
}
else
{
string[] splitOptionNames = translatedOptionNames.Split(',', '');
OptionNames = new string[Options.Length];
for (int i = 0; i < Options.Length && i < splitOptionNames.Length; i++)
{
OptionNames[i] = splitOptionNames[i].Trim();
}
}
if (OptionNames.Length != Options.Length)
if (OptionNames.Count != Options.Length)
{
DebugConsole.ThrowError("Error in Order " + Name + " - the number of option names doesn't match the number of options.");
OptionNames = Options;
OptionNames.Clear();
Options.ForEach(o => OptionNames.Add(o, o));
}
var spriteElement = orderElement.GetChildElement("sprite");
@@ -261,6 +244,15 @@ namespace Barotrauma
}
}
minimapIcons = new Dictionary<string, Sprite>();
var minimapIconElements = orderElement.GetChildElements("minimapicon");
foreach (XElement minimapIconElement in minimapIconElements)
{
var id = minimapIconElement.GetAttributeString("id", null);
if (string.IsNullOrWhiteSpace(id)) { continue; }
minimapIcons.Add(id, new Sprite(minimapIconElement.GetChildElement("sprite"), lazyLoad: true));
}
IsPrefab = true;
MustManuallyAssign = orderElement.GetAttributeBool("mustmanuallyassign", false);
}
@@ -268,7 +260,7 @@ namespace Barotrauma
/// <summary>
/// Constructor for order instances
/// </summary>
public Order(Order prefab, Entity targetEntity, ItemComponent targetItem, Character orderGiver = null)
public Order(Order prefab, Entity targetEntity, ItemComponent targetItem, Character orderGiver = null, bool isAutonomous = false)
{
Prefab = prefab;
@@ -293,27 +285,23 @@ namespace Barotrauma
TargetEntity = targetEntity;
if (targetItem != null)
{
if (UseController) { ConnectedController = FindController(targetItem); }
if (UseController)
{
ConnectedController = targetItem.Item?.FindController();
if (ConnectedController == null)
{
#if DEBUG
throw new Exception("Tried to use controller, but couldn't find one");
#endif
UseController = false;
}
}
TargetEntity = targetItem.Item;
TargetItemComponent = targetItem;
}
IsPrefab = false;
}
private Controller FindController(ItemComponent targetComponent)
{
if (targetComponent?.Item == null) { return null; }
//try finding the controller with the simpler non-recursive method first
return targetComponent.Item.GetConnectedComponents<Controller>().FirstOrDefault() ??
targetComponent.Item.GetConnectedComponents<Controller>(recursive: true).FirstOrDefault();
}
private bool TryFindController(ItemComponent targetComponent, out Controller controller)
{
controller = FindController(targetComponent);
return controller != null;
}
public bool HasAppropriateJob(Character character)
{
@@ -368,7 +356,7 @@ namespace Barotrauma
matchingItems.RemoveAll(it => it.NonInteractable);
if (UseController)
{
matchingItems.RemoveAll(i => i.Components.None(c => c.GetType() == ItemComponentType && TryFindController(c, out _)));
matchingItems.RemoveAll(i => i.Components.None(c => c.GetType() == ItemComponentType) && !i.TryFindController(out _));
}
}
return matchingItems;
@@ -381,5 +369,16 @@ namespace Barotrauma
Submarine.MainSub;
return GetMatchingItems(submarine, mustBelongToPlayerSub);
}
public string GetOptionName(string id)
{
return Prefab == null ? OptionNames[id] : Prefab.OptionNames[id];
}
public string GetOptionName(int index)
{
if (index < 0 || index >= Options.Length) { return null; }
return GetOptionName(Options[index]);
}
}
}
@@ -46,7 +46,7 @@ namespace Barotrauma
var nodes = new Dictionary<int, PathNode>();
foreach (WayPoint wayPoint in wayPoints)
{
if (wayPoint == null) continue;
if (wayPoint == null) { continue; }
if (nodes.ContainsKey(wayPoint.ID))
{
#if DEBUG
@@ -63,7 +63,7 @@ namespace Barotrauma
{
PathNode connectedNode = null;
nodes.TryGetValue(linked.ID, out connectedNode);
if (connectedNode == null) continue;
if (connectedNode == null) { continue; }
node.Value.connections.Add(connectedNode);
}
@@ -107,17 +107,17 @@ namespace Barotrauma
void WaypointLinksChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (Submarine.Unloading) return;
if (Submarine.Unloading) { return; }
var waypoints = sender as IEnumerable<MapEntity>;
foreach (MapEntity me in waypoints)
{
WayPoint wp = me as WayPoint;
if (me == null) continue;
if (me == null) { continue; }
var node = nodes.Find(n => n.Waypoint == wp);
if (node == null) return;
if (node == null) { return; }
if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Remove)
{
@@ -136,10 +136,10 @@ namespace Barotrauma
for (int i = 0; i < wp.linkedTo.Count; i++)
{
WayPoint connected = wp.linkedTo[i] as WayPoint;
if (connected == null) continue;
if (connected == null) { continue; }
//already connected, continue
if (node.connections.Any(n => n.Waypoint == connected)) continue;
if (node.connections.Any(n => n.Waypoint == connected)) { continue; }
var matchingNode = nodes.Find(n => n.Waypoint == connected);
if (matchingNode == null)
@@ -201,8 +201,8 @@ namespace Barotrauma
if (body != null)
{
//if (body.UserData is Submarine) continue;
if (body.UserData is Structure && !((Structure)body.UserData).IsPlatform) continue;
if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) continue;
if (body.UserData is Structure && !((Structure)body.UserData).IsPlatform) { continue; }
if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { continue; }
}
}
@@ -236,9 +236,9 @@ namespace Barotrauma
if (InsideSubmarine)
{
//much higher cost to waypoints that are outside
if (node.Waypoint.CurrentHull == null) dist *= 10.0f;
if (node.Waypoint.CurrentHull == null) { dist *= 10.0f; }
//avoid stopping at a doorway
if (node.Waypoint.ConnectedDoor != null) dist *= 10.0f;
if (node.Waypoint.ConnectedDoor != null) { dist *= 10.0f; }
}
if (dist < closestDist || endNode == null)
{
@@ -251,8 +251,8 @@ namespace Barotrauma
if (body != null)
{
//if (body.UserData is Submarine) continue;
if (body.UserData is Structure && !((Structure)body.UserData).IsPlatform) continue;
if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) continue;
if (body.UserData is Structure && !((Structure)body.UserData).IsPlatform) { continue; }
if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { continue; }
}
}
@@ -9,12 +9,31 @@ namespace Barotrauma
int currentIndex;
private float? totalLength;
public bool Unreachable
{
get;
set;
}
public float TotalLength
{
get
{
if (Unreachable) { return float.PositiveInfinity; }
if (!totalLength.HasValue)
{
totalLength = 0.0f;
for (int i = 0; i < nodes.Count - 1; i++)
{
totalLength += Vector2.Distance(nodes[i].WorldPosition, nodes[i + 1].WorldPosition);
}
}
return totalLength.Value;
}
}
public SteeringPath(bool unreachable = false)
{
nodes = new List<WayPoint>();
@@ -23,10 +42,10 @@ namespace Barotrauma
public void AddNode(WayPoint node)
{
if (node == null) return;
if (node == null) { return; }
nodes.Add(node);
if (node.CurrentHull == null) HasOutdoorsNodes = true;
if (node.CurrentHull == null) { HasOutdoorsNodes = true; }
}
public bool HasOutdoorsNodes
@@ -48,10 +67,10 @@ namespace Barotrauma
public WayPoint PrevNode
{
get
get
{
if (currentIndex-1 < 0 || currentIndex-1 > nodes.Count - 1) return null;
return nodes[currentIndex-1];
if (currentIndex - 1 < 0 || currentIndex - 1 > nodes.Count - 1) { return null; }
return nodes[currentIndex - 1];
}
}
@@ -59,7 +78,7 @@ namespace Barotrauma
{
get
{
if (currentIndex < 0 || currentIndex > nodes.Count - 1) return null;
if (currentIndex < 0 || currentIndex > nodes.Count - 1) { return null; }
return nodes[currentIndex];
}
}
@@ -73,7 +92,7 @@ namespace Barotrauma
{
get
{
if (currentIndex+1 < 0 || currentIndex+1 > nodes.Count - 1) return null;
if (currentIndex + 1 < 0 || currentIndex + 1 > nodes.Count - 1) { return null; }
return nodes[currentIndex+1];
}
}
@@ -90,8 +109,8 @@ namespace Barotrauma
public WayPoint CheckProgress(Vector2 simPosition, float minSimDistance = 0.1f)
{
if (nodes.Count == 0 || currentIndex>nodes.Count-1) return null;
if (Vector2.Distance(simPosition, nodes[currentIndex].SimPosition) < minSimDistance) currentIndex++;
if (nodes.Count == 0 || currentIndex > nodes.Count - 1) { return null; }
if (Vector2.Distance(simPosition, nodes[currentIndex].SimPosition) < minSimDistance) { currentIndex++; }
return CurrentNode;
}
@@ -268,7 +268,26 @@ namespace Barotrauma
{
if (Config.KillAgentsWhenEntityDies)
{
protectiveCells.ForEach(c => c.Kill(CauseOfDeathType.Unknown, null, isNetworkMessage: true));
protectiveCells.ForEach(c => c.Kill(CauseOfDeathType.Unknown, null));
if (!string.IsNullOrWhiteSpace(Config.OffensiveAgent))
{
foreach (var character in Character.CharacterList)
{
// Kills ALL offensive agents that are near the thalamus. Not the ideal solution,
// but as long as spawning is handled via status effects, I don't know if there is any better way.
// In practice there shouldn't be terminal cells from different thalamus organisms at the same time.
// And if there was, the distance check should prevent killing the agents of a different organism.
if (character.SpeciesName.Equals(Config.OffensiveAgent, StringComparison.OrdinalIgnoreCase))
{
// Sonar distance is used also for wreck positioning. No wreck should be closer to each other than this.
float maxDistance = Sonar.DefaultSonarRange;
if (Vector2.DistanceSquared(character.WorldPosition, Wreck.WorldPosition) < maxDistance * maxDistance)
{
character.Kill(CauseOfDeathType.Unknown, null);
}
}
}
}
}
}
}
@@ -18,6 +18,9 @@ namespace Barotrauma
[Serialize("", false)]
public string DefensiveAgent { get; private set; }
[Serialize("", false)]
public string OffensiveAgent { get; private set; }
[Serialize("", false)]
public string Brain { get; private set; }
@@ -47,12 +47,16 @@ namespace Barotrauma
base.Update(deltaTime, cam);
if (!Enabled) { return; }
if (IsDead || Vitality <= 0.0f || Stun > 0.0f || IsIncapacitated) { return; }
if (IsDead || Vitality <= 0.0f || Stun > 0.0f || IsIncapacitated)
{
//don't enable simple physics on dead/incapacitated characters
//the ragdoll controls the movement of incapacitated characters instead of the collider,
//but in simple physics mode the ragdoll would get disabled, causing the character to not move at all
AnimController.SimplePhysicsEnabled = false;
return;
}
//don't enable simple physics on dead/incapacitated characters
//the ragdoll controls the movement of incapacitated characters instead of the collider,
//but in simple physics mode the ragdoll would get disabled, causing the character to not move at all
if (!IsRemotePlayer)
if (!IsRemotePlayer && !(AIController is HumanAIController))
{
float characterDist = float.MaxValue;
#if CLIENT
@@ -76,7 +76,8 @@ namespace Barotrauma
{
if (InWater || !CanWalk)
{
return TargetMovement.Length() > (SwimSlowParams.MovementSpeed + SwimFastParams.MovementSpeed) / 2.0f;
float avg = (SwimSlowParams.MovementSpeed + SwimFastParams.MovementSpeed) / 2.0f;
return TargetMovement.LengthSquared() > avg * avg;
}
else
{
@@ -213,7 +213,12 @@ namespace Barotrauma
UpdateWalkAnim(deltaTime);
}
//don't flip or drag when simply physics is enabled
if (character.SelectedCharacter != null)
{
DragCharacter(character.SelectedCharacter, deltaTime);
}
//don't flip when simply physics is enabled
if (SimplePhysicsEnabled) { return; }
if (!character.IsRemotePlayer && (character.AIController == null || character.AIController.CanFlip))
@@ -248,29 +253,33 @@ namespace Barotrauma
}
}
if (character.SelectedCharacter != null)
{
DragCharacter(character.SelectedCharacter, deltaTime);
}
if (!CurrentFishAnimation.Flip) { return; }
if (IsStuck) { return; }
if (character.AIController != null && !character.AIController.CanFlip) { return; }
flipCooldown -= deltaTime;
if (TargetDir != Direction.None && TargetDir != dir)
if (TargetDir != Direction.None && TargetDir != dir)
{
flipTimer += deltaTime;
if ((flipTimer > 0.5f && flipCooldown <= 0.0f) || character.IsRemotePlayer)
// Speed reductions are not taken into account here. It's intentional: an ai character cannot flip if it's heavily paralyzed (for example).
float requiredSpeed = CurrentAnimationParams.MovementSpeed / 2;
if (CurrentHull != null)
{
// Enemy movement speeds are halved inside submarines
requiredSpeed /= 2;
}
bool isMovingFastEnough = Math.Abs(MainLimb.LinearVelocity.X) > requiredSpeed;
bool isTryingToMoveHorizontally = Math.Abs(TargetMovement.X) > Math.Abs(TargetMovement.Y);
if ((flipTimer > CurrentFishAnimation.FlipDelay && flipCooldown <= 0.0f && ((isMovingFastEnough && isTryingToMoveHorizontally) || IsMovingBackwards))
|| character.IsRemotePlayer)
{
Flip();
if (!inWater || (CurrentSwimParams != null && CurrentSwimParams.Mirror))
{
Mirror();
Mirror(CurrentSwimParams != null ? CurrentSwimParams.MirrorLerp : true);
}
flipTimer = 0.0f;
flipCooldown = 1.0f;
flipCooldown = CurrentFishAnimation.FlipCooldown;
}
}
else
@@ -295,7 +304,7 @@ namespace Barotrauma
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
{
//stop dragging if there's something between the pull limb and the target
Vector2 sourceSimPos = mouthLimb.SimPosition;
Vector2 sourceSimPos = SimplePhysicsEnabled ? character.SimPosition : mouthLimb.SimPosition;
Vector2 targetSimPos = target.SimPosition;
if (character.Submarine != null && character.SelectedCharacter.Submarine == null)
{
@@ -317,7 +326,7 @@ namespace Barotrauma
float eatSpeed = dmg / ((float)Math.Sqrt(Math.Max(target.Mass, 1)) * 10);
eatTimer += deltaTime * eatSpeed;
Vector2 mouthPos = GetMouthPosition().Value;
Vector2 mouthPos = SimplePhysicsEnabled ? character.SimPosition : GetMouthPosition().Value;
Vector2 attackSimPosition = character.Submarine == null ? ConvertUnits.ToSimUnits(target.WorldPosition) : target.SimPosition;
Vector2 limbDiff = attackSimPosition - mouthPos;
@@ -525,6 +534,7 @@ namespace Barotrauma
foreach (var limb in Limbs)
{
if (limb.IsSevered) { continue; }
if (Math.Abs(limb.Params.ConstantTorque) > 0)
{
limb.body.SmoothRotate(movementAngle + MathHelper.ToRadians(limb.Params.ConstantAngle) * Dir, limb.Params.ConstantTorque, wrapAngle: true);
@@ -550,10 +560,12 @@ namespace Barotrauma
for (int i = 0; i < Limbs.Length; i++)
{
if (Limbs[i].SteerForce <= 0.0f) { continue; }
var limb = Limbs[i];
if (limb.IsSevered) { continue; }
if (limb.SteerForce <= 0.0f) { continue; }
if (!Collider.PhysEnabled) { continue; }
Vector2 pullPos = Limbs[i].PullJointWorldAnchorA;
Limbs[i].body.ApplyForce(movement * Limbs[i].SteerForce * Limbs[i].Mass * Math.Max(character.SpeedMultiplier, 1), pullPos);
Vector2 pullPos = limb.PullJointWorldAnchorA;
limb.body.ApplyForce(movement * limb.SteerForce * limb.Mass * Math.Max(character.SpeedMultiplier, 1), pullPos);
}
Vector2 mainLimbDiff = mainLimb.PullJointWorldAnchorB - mainLimb.SimPosition;
@@ -604,6 +616,14 @@ namespace Barotrauma
float stepLift = TargetMovement.X == 0.0f ? 0 :
(float)Math.Sin(WalkPos * CurrentGroundedParams.StepLiftFrequency + MathHelper.Pi * CurrentGroundedParams.StepLiftOffset) * (CurrentGroundedParams.StepLiftAmount / 100);
float limpAmount = character.GetLegPenalty();
if (limpAmount > 0)
{
float walkPosX = (float)Math.Cos(WalkPos);
//make the footpos oscillate when limping
limpAmount = Math.Max(Math.Abs(walkPosX) * limpAmount, 0.0f) * Math.Min(Math.Abs(TargetMovement.X), 0.3f) * Dir;
}
Limb torso = GetLimb(LimbType.Torso);
if (torso != null)
{
@@ -613,7 +633,7 @@ namespace Barotrauma
}
if (TorsoPosition.HasValue)
{
Vector2 pos = colliderBottom + new Vector2(0, TorsoPosition.Value + stepLift);
Vector2 pos = colliderBottom + new Vector2(limpAmount, TorsoPosition.Value + stepLift);
if (torso != mainLimb)
{
@@ -635,7 +655,7 @@ namespace Barotrauma
}
if (HeadPosition.HasValue)
{
Vector2 pos = colliderBottom + new Vector2(0, HeadPosition.Value + stepLift * CurrentGroundedParams.StepLiftHeadMultiplier);
Vector2 pos = colliderBottom + new Vector2(limpAmount, HeadPosition.Value + stepLift * CurrentGroundedParams.StepLiftHeadMultiplier);
if (head != mainLimb)
{
@@ -670,6 +690,7 @@ namespace Barotrauma
foreach (Limb limb in Limbs)
{
if (limb.IsSevered) { continue; }
if (Math.Abs(limb.Params.ConstantTorque) > 0)
{
limb.body.SmoothRotate(movementAngle + MathHelper.ToRadians(limb.Params.ConstantAngle) * Dir, limb.Params.ConstantTorque, wrapAngle: true);
@@ -766,6 +787,7 @@ namespace Barotrauma
foreach (Limb limb in Limbs)
{
if (limb.IsSevered) { continue; }
#if CLIENT
if (limb.LightSource != null)
{
@@ -821,6 +843,7 @@ namespace Barotrauma
base.Flip();
foreach (Limb l in Limbs)
{
if (l.IsSevered) { continue; }
if (!l.DoesFlip) { continue; }
if (RagdollParams.IsSpritesheetOrientationHorizontal)
{
@@ -838,10 +861,13 @@ namespace Barotrauma
foreach (Limb l in Limbs)
{
if (l.IsSevered) { continue; }
TrySetLimbPosition(l,
centerOfMass,
new Vector2(centerOfMass.X - (l.SimPosition.X - centerOfMass.X), l.SimPosition.Y),
lerp);
l.body.PositionSmoothingFactor = 0.8f;
if (!l.DoesFlip) { continue; }
@@ -862,7 +888,7 @@ namespace Barotrauma
if (diff < 100.0f)
{
character.SelectedCharacter.AnimController.SetPosition(
new Vector2(centerOfMass.X - diff, character.SelectedCharacter.SimPosition.Y), lerp: true);
new Vector2(centerOfMass.X - diff, character.SelectedCharacter.SimPosition.Y), lerp);
}
}
}
@@ -323,7 +323,14 @@ namespace Barotrauma
levitatingCollider = true;
ColliderIndex = Crouching ? 1 : 0;
if (!Crouching && ColliderIndex == 1) Crouching = true;
if (character.SelectedConstruction?.GetComponent<Controller>()?.ControlCharacterPose ?? false)
{
Crouching = false;
}
else if (!Crouching && ColliderIndex == 1)
{
Crouching = true;
}
//stun (= disable the animations) if the ragdoll receives a large enough impact
if (strongestImpact > 0.0f)
@@ -542,12 +549,6 @@ namespace Barotrauma
Limb leftLeg = GetLimb(LimbType.LeftLeg);
Limb rightLeg = GetLimb(LimbType.RightLeg);
float limpAmount =
character.CharacterHealth.GetAfflictionStrength("damage", leftFoot, true) +
character.CharacterHealth.GetAfflictionStrength("damage", rightFoot, true) +
character.CharacterHealth.GetAfflictionStrength("spaceherpes");
limpAmount = MathHelper.Clamp(limpAmount / 100.0f, 0.0f, 1.0f);
float walkCycleMultiplier = 1.0f;
if (Stairs != null)
{
@@ -582,6 +583,11 @@ namespace Barotrauma
stepSize.Y *= walkPosY;
float footMid = colliderPos.X;
var herpes = character.CharacterHealth.GetAffliction("spaceherpes", false);
float herpesAmount = herpes == null ? 0 : herpes.Strength / herpes.Prefab.MaxStrength;
float legDamage = character.GetLegPenalty(startSum: -0.1f) * 1.1f;
float limpAmount = MathHelper.Lerp(0, 1, legDamage + herpesAmount);
if (limpAmount > 0.0f)
{
//make the footpos oscillate when limping
@@ -652,6 +658,7 @@ namespace Barotrauma
(float)Math.Sin(WalkPos * CurrentGroundedParams.StepLiftFrequency + MathHelper.Pi * CurrentGroundedParams.StepLiftOffset) * (CurrentGroundedParams.StepLiftAmount / 100);
float y = colliderPos.Y + stepLift;
if (TorsoPosition.HasValue)
{
y += TorsoPosition.Value;
@@ -690,6 +697,7 @@ namespace Barotrauma
foreach (Limb limb in Limbs)
{
if (limb.IsSevered) { continue; }
MoveLimb(limb, limb.SimPosition + move, 15.0f, true);
}
@@ -947,8 +955,6 @@ namespace Barotrauma
torso.body.MoveToPos(Collider.SimPosition + new Vector2((float)Math.Sin(-Collider.Rotation), (float)Math.Cos(-Collider.Rotation)) * 0.4f, 5.0f);
if (TargetMovement == Vector2.Zero) { return; }
movement = MathUtils.SmoothStep(movement, TargetMovement, 0.3f);
if (TorsoAngle.HasValue)
@@ -1001,29 +1007,31 @@ namespace Barotrauma
}
WalkPos += movement.Length();
legCyclePos += Vector2.Normalize(movement).Length();
legCyclePos += Math.Min(movement.LengthSquared() + Collider.AngularVelocity, 1.0f);
handCyclePos += MathHelper.ToRadians(CurrentSwimParams.HandCycleSpeed) * Math.Sign(movement.X);
var waist = GetLimb(LimbType.Waist);
footPos = waist == null ? Vector2.Zero : waist.SimPosition - new Vector2((float)Math.Sin(-Collider.Rotation), (float)Math.Cos(-Collider.Rotation)) * (upperLegLength + lowerLegLength);
Vector2 transformedFootPos = new Vector2((float)Math.Sin(legCyclePos / CurrentSwimParams.LegCycleLength / character.SpeedMultiplier) * CurrentSwimParams.LegMoveAmount, 0.0f);
Vector2 transformedFootPos = new Vector2((float)Math.Sin(legCyclePos / CurrentSwimParams.LegCycleLength) * CurrentSwimParams.LegMoveAmount, 0.0f);
transformedFootPos = Vector2.Transform(transformedFootPos, Matrix.CreateRotationZ(Collider.Rotation));
float torque = CurrentSwimParams.FootRotateStrength * character.SpeedMultiplier * (1.2f - character.GetLegPenalty());
if (rightFoot != null && !rightFoot.Disabled)
{
FootIK(rightFoot, footPos - transformedFootPos, CurrentSwimParams.FootRotateStrength, CurrentSwimParams.FootRotateStrength, CurrentSwimParams.FootAngleInRadians);
FootIK(rightFoot, footPos - transformedFootPos, torque, torque, CurrentSwimParams.FootAngleInRadians);
}
if (leftFoot != null && !leftFoot.Disabled)
{
FootIK(leftFoot, footPos + transformedFootPos, CurrentSwimParams.FootRotateStrength, CurrentSwimParams.FootRotateStrength, CurrentSwimParams.FootAngleInRadians);
FootIK(leftFoot, footPos + transformedFootPos, torque, torque, CurrentSwimParams.FootAngleInRadians);
}
handPos = (torso.SimPosition + head.SimPosition) / 2.0f;
//at the surface, not moving sideways -> hands just float around
if (!headInWater && TargetMovement.X == 0.0f && TargetMovement.Y > 0)
//at the surface, not moving sideways OR not moving at all
// -> hands just float around
if ((!headInWater && TargetMovement.X == 0.0f && TargetMovement.Y > 0) || TargetMovement.LengthSquared() < 0.001f)
{
handPos.X = handPos.X + Dir * 0.6f;
handPos += MathUtils.RotatePoint(Vector2.UnitX * Dir * 0.6f, torso.Rotation);
float wobbleAmount = 0.1f;
@@ -1060,7 +1068,7 @@ namespace Barotrauma
rightHandPos.X = (Dir == 1.0f) ? Math.Max(0.3f, rightHandPos.X) : Math.Min(-0.3f, rightHandPos.X);
rightHandPos = Vector2.Transform(rightHandPos, rotationMatrix);
HandIK(rightHand, handPos + rightHandPos, CurrentSwimParams.HandMoveStrength * character.SpeedMultiplier);
HandIK(rightHand, handPos + rightHandPos, CurrentSwimParams.HandMoveStrength * character.SpeedMultiplier * (1 - Character.GetRightHandPenalty()));
}
if (leftHand != null && !leftHand.Disabled)
@@ -1069,7 +1077,7 @@ namespace Barotrauma
leftHandPos.X = (Dir == 1.0f) ? Math.Max(0.3f, leftHandPos.X) : Math.Min(-0.3f, leftHandPos.X);
leftHandPos = Vector2.Transform(leftHandPos, rotationMatrix);
HandIK(leftHand, handPos + leftHandPos, CurrentSwimParams.HandMoveStrength * character.SpeedMultiplier);
HandIK(leftHand, handPos + leftHandPos, CurrentSwimParams.HandMoveStrength * character.SpeedMultiplier * (1 - Character.GetLeftHandPenalty()));
}
}
@@ -1251,31 +1259,39 @@ namespace Barotrauma
Limb head = GetLimb(LimbType.Head);
Limb torso = GetLimb(LimbType.Torso);
//if the head is moving, try to protect it with the hands
if (head.LinearVelocity.LengthSquared() > 1.0f && !head.IsSevered)
if (head != null && head.LinearVelocity.LengthSquared() > 1.0f && !head.IsSevered)
{
//if the head is moving, try to protect it with the hands
Limb leftHand = GetLimb(LimbType.LeftHand);
Limb rightHand = GetLimb(LimbType.RightHand);
//move hands in front of the head in the direction of the movement
Vector2 protectPos = head.SimPosition + Vector2.Normalize(head.LinearVelocity);
if (!rightHand.IsSevered) HandIK(rightHand, protectPos, strength * 0.1f);
if (!leftHand.IsSevered) HandIK(leftHand, protectPos, strength * 0.1f);
if (rightHand != null && !rightHand.IsSevered)
{
HandIK(rightHand, protectPos, strength * 0.1f);
}
if (leftHand != null && !leftHand.IsSevered)
{
HandIK(leftHand, protectPos, strength * 0.1f);
}
}
if (torso == null) { return; }
//attempt to make legs stay in a straight line with the torso to prevent the character from doing a split
for (int i = 0; i < 2; i++)
{
var thigh = i == 0 ? GetLimb(LimbType.LeftThigh) : GetLimb(LimbType.RightThigh);
if (thigh.IsSevered) continue;
if (thigh == null) { continue; }
if (thigh.IsSevered) { continue; }
float thighDiff = Math.Abs(MathUtils.GetShortestAngle(torso.Rotation, thigh.Rotation));
float thighTorque = thighDiff * thigh.Mass * Math.Sign(torso.Rotation - thigh.Rotation) * 5.0f;
thigh.body.ApplyTorque(thighTorque * strength);
var leg = i == 0 ? GetLimb(LimbType.LeftLeg) : GetLimb(LimbType.RightLeg);
if (leg.IsSevered) continue;
if (leg == null || leg.IsSevered) { continue; }
float legDiff = Math.Abs(MathUtils.GetShortestAngle(torso.Rotation, leg.Rotation));
float legTorque = legDiff * leg.Mass * Math.Sign(torso.Rotation - leg.Rotation) * 5.0f;
leg.body.ApplyTorque(legTorque * strength);
@@ -1697,7 +1713,7 @@ namespace Barotrauma
if (holdable.ControlPose)
{
head.body.SmoothRotate(itemAngle);
head?.body.SmoothRotate(itemAngle);
if (TargetMovement == Vector2.Zero && inWater)
{
@@ -1719,13 +1735,13 @@ namespace Barotrauma
{
if (character.SelectedItems[0] == item)
{
if (rightHand.IsSevered) return;
if (rightHand == null || rightHand.IsSevered) { return; }
transformedHoldPos = rightHand.PullJointWorldAnchorA - transformedHandlePos[0];
itemAngle = (rightHand.Rotation + (holdAngle - MathHelper.PiOver2) * Dir);
}
else if (character.SelectedItems[1] == item)
{
if (leftHand.IsSevered) return;
if (leftHand == null || leftHand.IsSevered) { return; }
transformedHoldPos = leftHand.PullJointWorldAnchorA - transformedHandlePos[1];
itemAngle = (leftHand.Rotation + (holdAngle - MathHelper.PiOver2) * Dir);
}
@@ -1734,12 +1750,12 @@ namespace Barotrauma
{
if (character.SelectedItems[0] == item)
{
if (rightHand.IsSevered) return;
if (rightHand == null || rightHand.IsSevered) { return; }
rightHand.Disabled = true;
}
if (character.SelectedItems[1] == item)
{
if (leftHand.IsSevered) return;
if (leftHand == null || leftHand.IsSevered) { return; }
leftHand.Disabled = true;
}
@@ -1797,17 +1813,14 @@ namespace Barotrauma
}
}
item.SetTransform(currItemPos, itemAngle + itemAngleRelativeToHoldAngle * Dir, setPrevTransform: false);
item.SetTransform(currItemPos, itemAngle + itemAngleRelativeToHoldAngle * Dir, setPrevTransform: false);
if (!isClimbing)
if (!isClimbing && !character.IsIncapacitated)
{
for (int i = 0; i < 2; i++)
{
if (character.SelectedItems[i] != item) continue;
if (itemPos == Vector2.Zero) continue;
if (character.SelectedItems[i] != item || itemPos == Vector2.Zero) { continue; }
Limb hand = (i == 0) ? rightHand : leftHand;
HandIK(hand, transformedHoldPos + transformedHandlePos[i]);
}
}
@@ -1987,6 +2000,8 @@ namespace Barotrauma
foreach (Limb limb in Limbs)
{
if (limb.IsSevered) { continue; }
bool mirror = false;
bool flipAngle = false;
bool wrapAngle = false;
@@ -47,7 +47,9 @@ namespace Barotrauma
private readonly Queue<Impact> impactQueue = new Queue<Impact>();
protected Hull currentHull;
private bool accessRemovedCharacterErrorShown;
private Limb[] limbs;
public Limb[] Limbs
{
@@ -55,16 +57,17 @@ namespace Barotrauma
{
if (limbs == null)
{
string errorMsg = "Attempted to access a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this);
#if DEBUG || UNSTABLE
errorMsg += '\n' + Environment.StackTrace;
#endif
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce(
"Ragdoll.Limbs:AccessRemoved",
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Attempted to access a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this) + "\n" + Environment.StackTrace);
if (!accessRemovedCharacterErrorShown)
{
string errorMsg = "Attempted to access a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this);
errorMsg += '\n' + Environment.StackTrace;
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce(
"Ragdoll.Limbs:AccessRemoved",
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Attempted to access a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this) + "\n" + Environment.StackTrace);
accessRemovedCharacterErrorShown = true;
}
return new Limb[0];
}
return limbs;
@@ -97,12 +100,17 @@ namespace Barotrauma
private bool simplePhysicsEnabled;
public Character Character => character;
protected Character character;
protected float strongestImpact;
private float splashSoundTimer;
//the ragdoll builds a "tolerance" to the flow force when being pushed by water.
//Allows sudden forces (breach, letting water through a door) to heavily push the character around while ensuring flowing water won't make the characters permanently stuck.
private float flowForceTolerance, flowStunTolerance;
//the movement speed of the ragdoll
public Vector2 movement;
//the target speed towards which movement is interpolated
@@ -156,8 +164,8 @@ namespace Barotrauma
}
set
{
if (value == colliderIndex || collider == null) return;
if (value >= collider.Count || value < 0) return;
if (value == colliderIndex || collider == null) { return; }
if (value >= collider.Count || value < 0) { return; }
if (collider[colliderIndex].height < collider[value].height)
{
@@ -165,10 +173,9 @@ namespace Barotrauma
pos1.Y -= collider[colliderIndex].height * ColliderHeightFromFloor;
Vector2 pos2 = pos1;
pos2.Y += collider[value].height * 1.1f;
if (GameMain.World.RayCast(pos1, pos2).Any(f => f.CollisionCategories.HasFlag(Physics.CollisionWall))) return;
if (GameMain.World.RayCast(pos1, pos2).Any(f => f.CollisionCategories.HasFlag(Physics.CollisionWall))) { return; }
}
Vector2 pos = collider[colliderIndex].SimPosition;
pos.Y -= collider[colliderIndex].height * 0.5f;
pos.Y += collider[value].height * 0.5f;
@@ -216,7 +223,7 @@ namespace Barotrauma
mainLimb = torso ?? head;
if (mainLimb == null)
{
mainLimb = Limbs.FirstOrDefault();
mainLimb = Limbs.FirstOrDefault(l => !l.IsSevered && !l.ignoreCollisions);
}
}
return mainLimb;
@@ -238,13 +245,13 @@ namespace Barotrauma
get { return simplePhysicsEnabled; }
set
{
if (value == simplePhysicsEnabled) return;
if (value == simplePhysicsEnabled) { return; }
simplePhysicsEnabled = value;
foreach (Limb limb in Limbs)
{
if (limb.IsSevered) continue;
if (limb.IsSevered) { continue; }
if (limb.body == null)
{
DebugConsole.ThrowError("Limb has no body! (" + (character != null ? character.Name : "Unknown character") + ", " + limb.type.ToString());
@@ -296,8 +303,6 @@ namespace Barotrauma
public float ImpactTolerance => RagdollParams.ImpactTolerance;
public bool Draggable => RagdollParams.Draggable;
public bool CanEnterSubmarine => RagdollParams.CanEnterSubmarine;
public bool CanAttackSubmarine => Limbs.Any(l => l.attack != null && l.attack.IsValidTarget(AttackTarget.Structure));
public bool CanAttackCharacters => Limbs.Any(l => l.attack != null && l.attack.IsValidTarget(AttackTarget.Character));
public float Dir => dir == Direction.Left ? -1.0f : 1.0f;
@@ -324,6 +329,7 @@ namespace Barotrauma
Submarine currSubmarine = currentHull?.Submarine;
foreach (Limb limb in Limbs)
{
if (limb.IsSevered) { continue; }
limb.body.Submarine = currSubmarine;
}
Collider.Submarine = currSubmarine;
@@ -377,7 +383,7 @@ namespace Barotrauma
foreach (var kvp in items)
{
int id = kvp.Key.ID;
// This can be the case if we manipulate the ragdoll in runtime (husk appendage, limb severance)
// This can be the case if we manipulate the ragdoll at runtime (husk appendage, limb removal in the character editor)
if (id > limbs.Length - 1) { continue; }
var limb = limbs[id];
var itemList = kvp.Value;
@@ -438,7 +444,7 @@ namespace Barotrauma
{
foreach (LimbJoint joint in LimbJoints)
{
if (GameMain.World.JointList.Contains(joint)) { GameMain.World.Remove(joint); }
if (GameMain.World.JointList.Contains(joint.Joint)) { GameMain.World.Remove(joint.Joint); }
}
}
DebugConsole.Log($"Creating joints from {RagdollParams.Name}.");
@@ -523,7 +529,7 @@ namespace Barotrauma
public void AddJoint(JointParams jointParams)
{
LimbJoint joint = new LimbJoint(Limbs[jointParams.Limb1], Limbs[jointParams.Limb2], jointParams, this);
GameMain.World.Add(joint);
GameMain.World.Add(joint.Joint);
for (int i = 0; i < LimbJoints.Length; i++)
{
if (LimbJoints[i] != null) continue;
@@ -606,7 +612,7 @@ namespace Barotrauma
limb.Remove();
foreach (LimbJoint limbJoint in attachedJoints)
{
GameMain.World.Remove(limbJoint);
GameMain.World.Remove(limbJoint.Joint);
}
}
@@ -706,7 +712,7 @@ namespace Barotrauma
float impactDamage = Math.Min((impact - ImpactTolerance) * ImpactDamageMultiplayer, character.MaxVitality * MaxImpactDamage);
character.LastDamageSource = null;
character.AddDamage(impactPos, new List<Affliction>() { AfflictionPrefab.InternalDamage.Instantiate(impactDamage) }, 0.0f, true);
character.AddDamage(impactPos, AfflictionPrefab.ImpactDamage.Instantiate(impactDamage).ToEnumerable(), 0.0f, true);
strongestImpact = Math.Max(strongestImpact, impact - ImpactTolerance);
character.ApplyStatusEffects(ActionType.OnImpact, 1.0f);
//briefly disable impact damage
@@ -720,12 +726,14 @@ namespace Barotrauma
ImpactProjSpecific(impact, f1.Body);
}
public void SeverLimbJoint(LimbJoint limbJoint, bool playSound = true)
private readonly List<Limb> connectedLimbs = new List<Limb>();
private readonly List<LimbJoint> checkedJoints = new List<LimbJoint>();
public bool SeverLimbJoint(LimbJoint limbJoint)
{
if (!limbJoint.CanBeSevered || limbJoint.IsSevered)
{
return;
return false;
}
limbJoint.IsSevered = true;
@@ -738,22 +746,29 @@ namespace Barotrauma
limbJoint.LimbA.body.ApplyLinearImpulse(limbDiff * mass, (limbJoint.LimbA.SimPosition + limbJoint.LimbB.SimPosition) / 2.0f);
limbJoint.LimbB.body.ApplyLinearImpulse(-limbDiff * mass, (limbJoint.LimbA.SimPosition + limbJoint.LimbB.SimPosition) / 2.0f);
List<Limb> connectedLimbs = new List<Limb>();
List<LimbJoint> checkedJoints = new List<LimbJoint>();
connectedLimbs.Clear();
checkedJoints.Clear();
GetConnectedLimbs(connectedLimbs, checkedJoints, MainLimb);
foreach (Limb limb in Limbs)
{
if (connectedLimbs.Contains(limb)) { continue; }
limb.IsSevered = true;
if (limb.type == LimbType.RightHand)
{
character.SelectedItems[0]?.Drop(character);
}
else if (limb.type == LimbType.LeftHand)
{
character.SelectedItems[1]?.Drop(character);
}
}
SeverLimbJointProjSpecific(limbJoint, playSound: true);
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
GameMain.NetworkMember.CreateEntityEvent(character, new object[] { NetEntityEvent.Type.Status });
}
return true;
}
partial void SeverLimbJointProjSpecific(LimbJoint limbJoint, bool playSound);
@@ -764,7 +779,7 @@ namespace Barotrauma
foreach (LimbJoint joint in LimbJoints)
{
if (joint.IsSevered || checkedJoints.Contains(joint)) continue;
if (joint.IsSevered || checkedJoints.Contains(joint)) { continue; }
if (joint.LimbA == limb)
{
if (!connectedLimbs.Contains(joint.LimbB))
@@ -861,7 +876,7 @@ namespace Barotrauma
{
for (int i = 0; i < Limbs.Length; i++)
{
if (Limbs[i] == null) continue;
if (Limbs[i] == null) { continue; }
Limbs[i].PullJointEnabled = false;
}
}
@@ -982,8 +997,8 @@ namespace Barotrauma
{
foreach (Limb limb in Limbs)
{
if (limb.IsSevered) continue;
if (limb.body.FarseerBody.ContactList == null) continue;
if (limb.IsSevered) { continue; }
if (limb.body.FarseerBody.ContactList == null) { continue; }
ContactEdge ce = limb.body.FarseerBody.ContactList;
while (ce != null && ce.Contact != null)
@@ -995,7 +1010,7 @@ namespace Barotrauma
foreach (Limb limb in Limbs)
{
if (limb.IsSevered) continue;
if (limb.IsSevered) { continue; }
limb.body.LinearVelocity += velocityChange;
}
@@ -1020,15 +1035,15 @@ namespace Barotrauma
Category collisionCategory = (IgnorePlatforms) ?
wall | Physics.CollisionProjectile | Physics.CollisionStairs
: wall | Physics.CollisionProjectile | Physics.CollisionPlatform | Physics.CollisionStairs;
if (collisionCategory == prevCollisionCategory) return;
if (collisionCategory == prevCollisionCategory) { return; }
prevCollisionCategory = collisionCategory;
Collider.CollidesWith = collisionCategory | Physics.CollisionItemBlocking;
foreach (Limb limb in Limbs)
{
if (limb.ignoreCollisions || limb.IsSevered) continue;
if (limb.ignoreCollisions || limb.IsSevered) { continue; }
try
{
@@ -1081,8 +1096,6 @@ namespace Barotrauma
CheckDistFromCollider();
UpdateCollisionCategories();
Vector2 flowForce = Vector2.Zero;
FindHull();
PreventOutsideCollision();
@@ -1104,10 +1117,7 @@ namespace Barotrauma
}
else
{
flowForce = GetFlowForce();
headInWater = false;
inWater = false;
if (currentHull.WaterVolume > currentHull.Volume * 0.95f)
{
@@ -1129,7 +1139,7 @@ namespace Barotrauma
if (lowerHull != null) floorY = ConvertUnits.ToSimUnits(lowerHull.Rect.Y - lowerHull.Rect.Height);
}
}
float standHeight =
float standHeight =
HeadPosition.HasValue ? HeadPosition.Value :
TorsoPosition.HasValue ? TorsoPosition.Value :
Collider.GetMaxExtent() * 0.5f;
@@ -1140,10 +1150,7 @@ namespace Barotrauma
}
}
if (flowForce.LengthSquared() > 0.001f)
{
Collider.ApplyForce(flowForce, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
}
UpdateHullFlowForces(deltaTime);
if (currentHull == null ||
currentHull.WaterVolume > currentHull.Volume * 0.95f ||
@@ -1152,7 +1159,6 @@ namespace Barotrauma
Collider.ApplyWaterForces();
}
foreach (Limb limb in Limbs)
{
//find the room which the limb is in
@@ -1177,14 +1183,7 @@ namespace Barotrauma
if (limb.Position.Y < limbHull.Surface)
{
limb.inWater = true;
if (flowForce.LengthSquared() > 0.001f)
{
limb.body.ApplyForce(flowForce, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
}
surfaceY = limbHull.Surface;
if (limb.type == LimbType.Head)
{
headInWater = true;
@@ -1256,6 +1255,12 @@ namespace Barotrauma
private int validityResets;
private bool CheckValidity()
{
if (limbs == null)
{
DebugConsole.ThrowError("Attempted to check the validity of a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this));
Invalid = true;
return false;
}
bool isColliderValid = CheckValidity(Collider);
bool limbsValid = true;
foreach (Limb limb in limbs)
@@ -1278,8 +1283,8 @@ namespace Barotrauma
Collider.SetTransform(Vector2.Zero, 0.0f);
foreach (Limb limb in Limbs)
{
limb.body.SetTransform(Collider.SimPosition, 0.0f);
limb.body.ResetDynamics();
limb.body?.SetTransform(Collider.SimPosition, 0.0f);
limb.body?.ResetDynamics();
}
Frozen = true;
}
@@ -1353,6 +1358,74 @@ namespace Barotrauma
partial void Splash(Limb limb, Hull limbHull);
private void UpdateHullFlowForces(float deltaTime)
{
if (currentHull == null) { return; }
const float StunForceThreshold = 5.0f;
const float StunDuration = 0.5f;
const float ToleranceIncreaseSpeed = 5.0f;
const float ToleranceDecreaseSpeed = 1.0f;
//how much distance to a gap affects the force it exerts on the character
const float DistanceFactor = 0.5f;
const float ForceMultiplier = 0.035f;
Vector2 flowForce = Vector2.Zero;
foreach (Gap gap in Gap.GapList)
{
if (gap.Open <= 0.0f || !gap.linkedTo.Contains(currentHull) || gap.LerpedFlowForce.LengthSquared() < 0.01f) { continue; }
float dist = Vector2.Distance(MainLimb.WorldPosition, gap.WorldPosition) * DistanceFactor;
flowForce += Vector2.Normalize(gap.LerpedFlowForce) * (Math.Max(gap.LerpedFlowForce.Length() - dist, 0.0f) * ForceMultiplier);
}
//throwing conscious/moving characters around takes more force -> double the flow force
if (character.CanMove) { flowForce *= 2.0f; }
float flowForceMagnitude = flowForce.Length();
float limbMultipier = limbs.Count(l => l.inWater) / (float)limbs.Length;
//if the force strong enough, stun the character to let it get thrown around by the water
if ((flowForceMagnitude * limbMultipier) - flowStunTolerance > StunForceThreshold)
{
character.Stun = Math.Max(character.Stun, StunDuration);
flowStunTolerance = Math.Max(flowStunTolerance, flowForceMagnitude);
}
if (character == Character.Controlled && inWater && Screen.Selected?.Cam != null)
{
float shakeStrength = Math.Min(flowForceMagnitude / 10.0f, 5.0f) * limbMultipier;
Screen.Selected.Cam.Shake = Math.Max(Screen.Selected.Cam.Shake, shakeStrength);
}
if (flowForceMagnitude > 0.0001f)
{
flowForce = Vector2.Normalize(flowForce) * Math.Max(flowForceMagnitude - flowForceTolerance, 0.0f);
}
if (flowForceTolerance <= flowForceMagnitude * 1.5f && inWater)
{
//build up "tolerance" to the flow force
//ensures the character won't get permanently stuck by forces, while allowing sudden changes in flow to push the character hard
flowForceTolerance += deltaTime * ToleranceIncreaseSpeed;
flowStunTolerance = Math.Max(flowStunTolerance, flowForceTolerance);
}
else
{
flowForceTolerance = Math.Max(flowForceTolerance - deltaTime * ToleranceDecreaseSpeed, 0.0f);
flowStunTolerance = Math.Max(flowStunTolerance - deltaTime * ToleranceDecreaseSpeed, 0.0f);
}
if (flowForce.LengthSquared() > 0.001f)
{
Collider.ApplyForce(flowForce, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
foreach (Limb limb in limbs)
{
if (!limb.inWater) { continue; }
limb.body.ApplyForce(flowForce, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
}
}
}
private void RefreshFloorY(Limb refLimb = null, bool ignoreStairs = false)
{
PhysicsBody refBody = refLimb == null ? Collider : refLimb.body;
@@ -1490,7 +1563,7 @@ namespace Barotrauma
foreach (Limb limb in Limbs)
{
if (limb.IsSevered) continue;
if (limb.IsSevered) { continue; }
//check visibility from the new position of the collider to the new position of this limb
Vector2 movePos = limb.SimPosition + limbMoveAmount;
@@ -1554,7 +1627,7 @@ namespace Barotrauma
Vector2 forceDir = diff / (float)Math.Sqrt(distSqrd);
foreach (Limb limb in Limbs)
{
if (limb.IsSevered) continue;
if (limb.IsSevered) { continue; }
limb.body.CollidesWith = Physics.CollisionNone;
limb.body.ApplyForce(forceDir * limb.Mass * 10.0f, maxVelocity: 10.0f);
}
@@ -1600,28 +1673,37 @@ namespace Barotrauma
UpdateNetPlayerPositionProjSpecific(deltaTime, lowestSubPos);
}
private Vector2 GetFlowForce()
{
Vector2 limbPos = Limbs[0].Position;
Vector2 force = Vector2.Zero;
foreach (Gap gap in Gap.GapList)
{
if (gap.Open <= 0.0f || gap.FlowTargetHull != currentHull || gap.LerpedFlowForce.LengthSquared() < 0.01f) continue;
Vector2 gapPos = gap.SimPosition;
float dist = Vector2.Distance(limbPos, gapPos);
force += Vector2.Normalize(gap.LerpedFlowForce) * (Math.Max(gap.LerpedFlowForce.Length() - dist, 0.0f) / 500.0f);
}
return force;
}
/// <summary>
/// Note that if there are multiple limbs of the same type, only the first of them is found in the dictionary.
/// </summary>
public Limb GetLimb(LimbType limbType)
public Limb GetLimb(LimbType limbType, bool excludeSevered = true)
{
limbDictionary.TryGetValue(limbType, out Limb limb);
Limb limb = null;
if (HasMultipleLimbsOfSameType)
{
for (int i = 0; i < 10; i++)
{
limbDictionary.TryGetValue(limbType, out limb);
if (limb == null)
{
// No limbs found
break;
}
if (!excludeSevered || !limb.IsSevered)
{
// Found a valid limb
break;
}
}
}
else
{
limbDictionary.TryGetValue(limbType, out limb);
}
if (excludeSevered && limb != null && limb.IsSevered)
{
limb = null;
}
return limb;
}
@@ -1664,10 +1746,15 @@ namespace Barotrauma
Limb lowestLimb = null;
foreach (Limb limb in Limbs)
{
if (limb.IsSevered) { continue; }
if (lowestLimb == null)
{
lowestLimb = limb;
}
else if (limb.SimPosition.Y < lowestLimb.SimPosition.Y)
{
lowestLimb = limb;
}
}
return lowestLimb;
@@ -1700,11 +1787,12 @@ namespace Barotrauma
if (LimbJoints != null)
{
foreach (RevoluteJoint joint in LimbJoints)
foreach (var joint in LimbJoints)
{
if (GameMain.World.JointList.Contains(joint))
var j = joint.Joint;
if (GameMain.World.JointList.Contains(j))
{
GameMain.World.Remove(joint);
GameMain.World.Remove(j);
}
}
LimbJoints = null;
@@ -316,11 +316,6 @@ namespace Barotrauma
continue;
}
}
//float afflictionStrength = subElement.GetAttributeFloat(1.0f, "amount", "strength");
//var affliction = afflictionPrefab.Instantiate(afflictionStrength);
//Afflictions.Add(affliction, subElement);
break;
case "conditional":
foreach (XAttribute attribute in subElement.Attributes())
@@ -347,14 +342,18 @@ namespace Barotrauma
afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier.Equals(afflictionIdentifier, System.StringComparison.OrdinalIgnoreCase));
if (afflictionPrefab != null)
{
float afflictionStrength = subElement.GetAttributeFloat(1.0f, "amount", "strength");
affliction = afflictionPrefab.Instantiate(afflictionStrength);
affliction = afflictionPrefab.Instantiate(0.0f);
}
else
{
affliction = new Affliction(null, 0);
}
affliction.Deserialize(subElement);
//backwards compatibility
if (subElement.Attribute("amount") != null && subElement.Attribute("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);
}
@@ -572,18 +571,14 @@ namespace Barotrauma
public bool IsValidTarget(AttackTarget targetType) => TargetType == AttackTarget.Any || TargetType == targetType;
public bool IsValidTarget(Entity target)
public bool IsValidTarget(IDamageable target)
{
switch (TargetType)
return TargetType switch
{
case AttackTarget.Character:
return target is Character;
case AttackTarget.Structure:
return !(target is Character);
case AttackTarget.Any:
default:
return true;
}
AttackTarget.Character => target is Character,
AttackTarget.Structure => !(target is Character),
_ => true,
};
}
public Vector2 CalculateAttackPhase(TransitionMode easing = TransitionMode.Linear)
@@ -3,7 +3,7 @@ using FarseerPhysics;
using FarseerPhysics.Dynamics.Joints;
using Microsoft.Xna.Framework;
using System;
using System.IO;
using Barotrauma.IO;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
@@ -43,6 +43,7 @@ namespace Barotrauma
foreach (Limb limb in AnimController.Limbs)
{
if (limb.IsSevered) { continue; }
if (limb.body != null)
{
limb.body.Enabled = enabled;
@@ -235,6 +236,7 @@ namespace Barotrauma
{
get
{
if (info != null && !string.IsNullOrWhiteSpace(info.Name)) { return info.Name; }
var displayName = Params.DisplayName;
if (string.IsNullOrWhiteSpace(displayName))
{
@@ -587,30 +589,32 @@ namespace Barotrauma
set { canInventoryBeAccessed = value; }
}
private bool accessRemovedCharacterErrorShown;
public override Vector2 SimPosition
{
get
{
if (AnimController?.Collider == null)
{
string errorMsg = "Attempted to access a potentially removed character. Character: " + Name + ", id: " + ID + ", removed: " + Removed + ".";
if (AnimController == null)
if (!accessRemovedCharacterErrorShown)
{
errorMsg += " AnimController == null";
string errorMsg = "Attempted to access a potentially removed character. Character: " + Name + ", id: " + ID + ", removed: " + Removed + ".";
if (AnimController == null)
{
errorMsg += " AnimController == null";
}
else if (AnimController.Collider == null)
{
errorMsg += " AnimController.Collider == null";
}
errorMsg += '\n' + Environment.StackTrace;
DebugConsole.NewMessage(errorMsg, Color.Red);
GameAnalyticsManager.AddErrorEventOnce(
"Character.SimPosition:AccessRemoved",
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
errorMsg + "\n" + Environment.StackTrace);
accessRemovedCharacterErrorShown = true;
}
else if (AnimController.Collider == null)
{
errorMsg += " AnimController.Collider == null";
}
#if DEBUG || UNSTABLE
errorMsg += '\n' + Environment.StackTrace;
#endif
DebugConsole.NewMessage(errorMsg, Color.Red);
GameAnalyticsManager.AddErrorEventOnce(
"Character.SimPosition:AccessRemoved",
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
errorMsg + "\n" + Environment.StackTrace);
return Vector2.Zero;
}
@@ -1140,22 +1144,86 @@ namespace Barotrauma
greatestNegativeSpeedMultiplier = 1f;
}
/// <summary>
/// Speed reduction from the current limb specific damage. Min 0, max 1.
/// </summary>
public float GetTemporarySpeedReduction()
{
float reduction = 0;
reduction = CalculateMovementPenalty(AnimController.GetLimb(LimbType.RightFoot, excludeSevered: false), reduction);
reduction = CalculateMovementPenalty(AnimController.GetLimb(LimbType.LeftFoot, excludeSevered: false), reduction);
if (AnimController is HumanoidAnimController)
{
if (AnimController.InWater)
{
// Currently only humans use hands for swimming.
reduction = CalculateMovementPenalty(AnimController.GetLimb(LimbType.RightHand, excludeSevered: false), reduction);
reduction = CalculateMovementPenalty(AnimController.GetLimb(LimbType.LeftHand, excludeSevered: false), reduction);
}
}
else
{
int totalTailLimbs = 0;
int destroyedTailLimbs = 0;
foreach (var limb in AnimController.Limbs)
{
if (limb.type == LimbType.Tail)
{
totalTailLimbs++;
if (limb.IsSevered)
{
destroyedTailLimbs++;
}
}
}
if (destroyedTailLimbs > 0)
{
reduction += MathHelper.Lerp(0, AnimController.InWater ? 1f : 0.5f, (float)destroyedTailLimbs / totalTailLimbs);
}
}
return Math.Clamp(reduction, 0, 1f);
}
private float CalculateMovementPenalty(Limb limb, float sum, float max = 0.4f)
{
if (limb != null)
{
sum += MathHelper.Lerp(0, max, CharacterHealth.GetLimbDamage(limb, afflictionType: "damage"));
}
return Math.Clamp(sum, 0, 1f);
}
public float GetRightHandPenalty() => CalculateMovementPenalty(AnimController.GetLimb(LimbType.RightHand, excludeSevered: false), 0, max: 1);
public float GetLeftHandPenalty() => CalculateMovementPenalty(AnimController.GetLimb(LimbType.LeftHand, excludeSevered: false), 0, max: 1);
public float GetLegPenalty(float startSum = 0)
{
float sum = startSum;
foreach (var limb in AnimController.Limbs)
{
switch (limb.type)
{
case LimbType.RightFoot:
case LimbType.LeftFoot:
sum += CalculateMovementPenalty(limb, sum, max: 0.5f);
break;
}
}
return Math.Clamp(sum, 0, 1f);
}
public float ApplyTemporarySpeedLimits(float speed)
{
var leftFoot = AnimController.GetLimb(LimbType.LeftFoot);
if (leftFoot != null)
float max;
if (AnimController is HumanoidAnimController)
{
float footAfflictionStrength = CharacterHealth.GetAfflictionStrength("damage", leftFoot, true);
speed *= MathHelper.Lerp(1.0f, 0.4f, MathHelper.Clamp(footAfflictionStrength / 80.0f, 0.0f, 1.0f));
max = AnimController.InWater ? 0.5f : 0.7f;
}
var rightFoot = AnimController.GetLimb(LimbType.RightFoot);
if (rightFoot != null)
else
{
float footAfflictionStrength = CharacterHealth.GetAfflictionStrength("damage", rightFoot, true);
speed *= MathHelper.Lerp(1.0f, 0.4f, MathHelper.Clamp(footAfflictionStrength / 80.0f, 0.0f, 1.0f));
max = AnimController.InWater ? 0.9f : 0.5f;
}
speed *= 1f - MathHelper.Lerp(0, max, GetTemporarySpeedReduction());
return speed;
}
@@ -1256,58 +1324,72 @@ namespace Barotrauma
}
else if (IsKeyDown(InputType.Attack) && (IsRemotePlayer || Controlled == this || (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)))
{
Vector2 attackPos = SimPosition + ConvertUnits.ToSimUnits(cursorPosition - Position);
List<Body> ignoredBodies = AnimController.Limbs.Select(l => l.body.FarseerBody).ToList();
ignoredBodies.Add(AnimController.Collider.FarseerBody);
var body = Submarine.PickBody(
SimPosition,
attackPos,
ignoredBodies,
Physics.CollisionCharacter | Physics.CollisionWall);
IDamageable attackTarget = null;
if (body != null)
{
attackPos = Submarine.LastPickedPosition;
if (body.UserData is Submarine sub)
{
body = Submarine.PickBody(
SimPosition - ((Submarine)body.UserData).SimPosition,
attackPos - ((Submarine)body.UserData).SimPosition,
ignoredBodies,
Physics.CollisionWall);
if (body != null)
{
attackPos = Submarine.LastPickedPosition + sub.SimPosition;
attackTarget = body.UserData as IDamageable;
}
}
else
{
if (body.UserData is IDamageable)
{
attackTarget = (IDamageable)body.UserData;
}
else if (body.UserData is Limb)
{
attackTarget = ((Limb)body.UserData).character;
}
}
}
var currentContexts = GetAttackContexts();
var validLimbs = AnimController.Limbs.Where(l => !l.IsSevered && !l.IsStuck && l.attack != null && l.attack.IsValidContext(currentContexts));
var validLimbs = AnimController.Limbs.Where(l =>
{
if (l.IsSevered || l.IsStuck) { return false; }
var attack = l.attack;
if (attack == null) { return false; }
if (attack.CoolDownTimer > 0) { return false; }
if (!attack.IsValidContext(currentContexts)) { return false; }
if (attackTarget != null)
{
if (!attack.IsValidTarget(attackTarget)) { return false; }
if (attackTarget is ISerializableEntity se && attackTarget is Character)
{
if (attack.Conditionals.Any(c => !c.Matches(se))) { return false; }
}
}
if (attack.Conditionals.Any(c => c.TargetSelf && !c.Matches(this))) { return false; }
return true;
});
var sortedLimbs = validLimbs.OrderBy(l => Vector2.DistanceSquared(ConvertUnits.ToDisplayUnits(l.SimPosition), cursorPosition));
// Select closest
var attackLimb = sortedLimbs.FirstOrDefault();
if (attackLimb != null)
{
Vector2 attackPos = attackLimb.SimPosition + Vector2.Normalize(cursorPosition - attackLimb.Position) * ConvertUnits.ToSimUnits(attackLimb.attack.Range);
List<Body> ignoredBodies = AnimController.Limbs.Select(l => l.body.FarseerBody).ToList();
ignoredBodies.Add(AnimController.Collider.FarseerBody);
var body = Submarine.PickBody(
attackLimb.SimPosition,
attackPos,
ignoredBodies,
Physics.CollisionCharacter | Physics.CollisionWall);
IDamageable attackTarget = null;
if (body != null)
{
attackPos = Submarine.LastPickedPosition;
if (body.UserData is Submarine sub)
{
body = Submarine.PickBody(
attackLimb.SimPosition - ((Submarine)body.UserData).SimPosition,
attackPos - ((Submarine)body.UserData).SimPosition,
ignoredBodies,
Physics.CollisionWall);
if (body != null)
{
attackPos = Submarine.LastPickedPosition + sub.SimPosition;
attackTarget = body.UserData as IDamageable;
}
}
else
{
if (body.UserData is IDamageable)
{
attackTarget = (IDamageable)body.UserData;
}
else if (body.UserData is Limb)
{
attackTarget = ((Limb)body.UserData).character;
}
}
}
attackLimb.UpdateAttack(deltaTime, attackPos, attackTarget, out AttackResult attackResult);
if (!attackLimb.attack.IsRunning)
{
attackCoolDown = 1.0f;
@@ -1394,22 +1476,23 @@ namespace Barotrauma
{
Limb selfLimb = AnimController.GetLimb(LimbType.Head);
if (selfLimb == null) { selfLimb = AnimController.GetLimb(LimbType.Torso); }
if (selfLimb == null) { selfLimb = AnimController.Limbs.FirstOrDefault(); }
if (selfLimb == null) { selfLimb = AnimController.MainLimb; }
return selfLimb;
}
public bool CanSeeTarget(ISpatialEntity target, Limb seeingLimb = null)
{
seeingLimb = seeingLimb ?? GetSeeingLimb();
seeingLimb ??= GetSeeingLimb();
if (seeingLimb == null) { return false; }
ISpatialEntity seeingEntity = AnimController.SimplePhysicsEnabled ? this : seeingLimb as ISpatialEntity;
// TODO: Could we just use the method below? If not, let's refactor it so that we can.
Vector2 diff = ConvertUnits.ToSimUnits(target.WorldPosition - seeingLimb.WorldPosition);
Vector2 diff = ConvertUnits.ToSimUnits(target.WorldPosition - seeingEntity.WorldPosition);
Body closestBody;
//both inside the same sub (or both outside)
//OR the we're inside, the other character outside
if (target.Submarine == Submarine || target.Submarine == null)
{
closestBody = Submarine.CheckVisibility(seeingLimb.SimPosition, seeingLimb.SimPosition + diff);
closestBody = Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff);
}
//we're outside, the other character inside
else if (Submarine == null)
@@ -1419,7 +1502,7 @@ namespace Barotrauma
//both inside different subs
else
{
closestBody = Submarine.CheckVisibility(seeingLimb.SimPosition, seeingLimb.SimPosition + diff);
closestBody = Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff);
if (!IsBlocking(closestBody))
{
closestBody = Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff);
@@ -1436,10 +1519,11 @@ namespace Barotrauma
}
else if (body.UserData is Item item && item != target)
{
// TODO: The door collider should be disabled, so this check is probably unnecessary.
var door = item.GetComponent<Door>();
if (door != null)
{
return !door.IsOpen;
return !door.IsOpen && !door.IsBroken;
}
}
return false;
@@ -1466,7 +1550,7 @@ namespace Barotrauma
Structure wall = closestBody.UserData as Structure;
Item item = closestBody.UserData as Item;
Door door = item?.GetComponent<Door>();
return (wall == null || !wall.CastShadow) && (door == null || door.IsOpen);
return (wall == null || !wall.CastShadow) && (door == null || door.IsOpen || door.IsBroken);
}
public bool HasItem(Item item, bool requireEquipped = false) => requireEquipped ? HasEquippedItem(item) : item.IsOwnedBy(this);
@@ -1601,8 +1685,8 @@ namespace Barotrauma
if (IsItemTakenBySomeoneElse(item)) { continue; }
float itemPriority = customPriorityFunction != null ? customPriorityFunction(item) : 1;
if (itemPriority <= 0) { continue; }
Item rootContainer = item.GetRootContainer();
Vector2 itemPos = (rootContainer ?? item).WorldPosition;
Entity rootInventoryOwner = item.GetRootInventoryOwner();
Vector2 itemPos = (rootInventoryOwner ?? item).WorldPosition;
float yDist = Math.Abs(WorldPosition.Y - itemPos.Y);
yDist = yDist > 100 ? yDist * 5 : 0;
float dist = Math.Abs(WorldPosition.X - itemPos.X) + yDist;
@@ -1620,13 +1704,16 @@ namespace Barotrauma
public bool IsItemTakenBySomeoneElse(Item item) => item.FindParentInventory(i => i.Owner != this && i.Owner is Character owner && !owner.IsDead && !owner.Removed) != null;
public bool CanInteractWith(Character c, float maxDist = 200.0f, bool checkVisibility = true)
public bool CanInteractWith(Character c, float maxDist = 200.0f, bool checkVisibility = true, bool skipDistanceCheck = false)
{
if (c == this || Removed || !c.Enabled || !c.CanBeSelected) return false;
if (!c.CharacterHealth.UseHealthWindow && !c.CanBeDragged && c.onCustomInteract == null) return false;
if (c == this || Removed || !c.Enabled || !c.CanBeSelected) { return false; }
if (!c.CharacterHealth.UseHealthWindow && !c.CanBeDragged && c.onCustomInteract == null) { return false; }
maxDist = ConvertUnits.ToSimUnits(maxDist);
if (Vector2.DistanceSquared(SimPosition, c.SimPosition) > maxDist * maxDist) return false;
if (!skipDistanceCheck)
{
maxDist = ConvertUnits.ToSimUnits(maxDist);
if (Vector2.DistanceSquared(SimPosition, c.SimPosition) > maxDist * maxDist) { return false; }
}
return checkVisibility ? CanSeeCharacter(c) : true;
}
@@ -2394,7 +2481,7 @@ namespace Barotrauma
}
}
private readonly float maxAIRange = 10000;
private readonly float maxAIRange = 20000;
private readonly float aiTargetChangeSpeed = 5;
private void UpdateSightRange(float deltaTime)
@@ -2628,40 +2715,60 @@ namespace Barotrauma
GameServer.Log(sb.ToString(), ServerLog.MessageType.Attack);
}
#endif
bool isNotClient = GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient;
TrySeverLimbJoints(limbHit, attack.SeverLimbsProbability);
// Don't allow beheading for monster attacks, because it happens too frequently (crawlers/tigerthreshers etc attacking each other -> they will most often target to the head)
TrySeverLimbJoints(limbHit, attack.SeverLimbsProbability, attackResult.Damage, allowBeheading: AIController == null || AIController is HumanAIController);
return attackResult;
}
public void TrySeverLimbJoints(Limb targetLimb, float severLimbsProbability)
public void TrySeverLimbJoints(Limb targetLimb, float severLimbsProbability, float damage, bool allowBeheading)
{
bool isNotClient = GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient;
if (isNotClient &&
IsDead && Rand.Range(0.0f, 1.0f) < severLimbsProbability)
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
#if DEBUG
if (targetLimb.character != this)
{
foreach (LimbJoint joint in AnimController.LimbJoints)
{
if (joint.CanBeSevered && (joint.LimbA == targetLimb || joint.LimbB == targetLimb))
{
#if CLIENT
CurrentHull?.AddDecal("blood", WorldPosition, Rand.Range(0.5f, 1.5f));
DebugConsole.ThrowError($"{Name} is attempting to sever joints of {targetLimb.character.Name}!");
return;
}
#endif
AnimController.SeverLimbJoint(joint);
if (joint.LimbA == targetLimb)
{
joint.LimbB.body.LinearVelocity += targetLimb.LinearVelocity * 0.5f;
}
else
{
joint.LimbA.body.LinearVelocity += targetLimb.LinearVelocity * 0.5f;
}
}
if (damage < targetLimb.Params.MinSeveranceDamage) { return; }
if (!IsDead)
{
if (!allowBeheading && targetLimb.type == LimbType.Head) { return; }
if (!targetLimb.CanBeSeveredAlive) { return; }
}
bool wasSevered = false;
float random = Rand.Value();
foreach (LimbJoint joint in AnimController.LimbJoints)
{
if (!joint.CanBeSevered) { continue; }
if (joint.LimbA != targetLimb && joint.LimbB != targetLimb) { continue; }
float probability = severLimbsProbability;
if (!IsDead)
{
probability *= joint.Params.SeveranceProbabilityModifier;
}
if (probability <= 0) { continue; }
if (random > probability) { continue; }
bool severed = AnimController.SeverLimbJoint(joint);
if (!wasSevered)
{
wasSevered = severed;
}
if (severed)
{
Limb otherLimb = joint.LimbA == targetLimb ? joint.LimbB : joint.LimbA;
otherLimb.body.ApplyLinearImpulse(targetLimb.LinearVelocity * targetLimb.Mass);
}
}
if (wasSevered)
{
if (targetLimb.character.AIController is EnemyAIController enemyAI)
{
enemyAI.ReevaluateAttacks();
}
ApplyStatusEffects(ActionType.OnSevered, 1.0f);
targetLimb.ApplyStatusEffects(ActionType.OnSevered, 1.0f);
}
}
@@ -2742,6 +2849,10 @@ namespace Barotrauma
AttackResult attackResult = hitLimb.AddDamage(simPos, afflictions, playSound);
CharacterHealth.ApplyDamage(hitLimb, attackResult);
ApplyStatusEffects(ActionType.OnDamaged, 1.0f);
if (attackResult.Damage > 0)
{
hitLimb.ApplyStatusEffects(ActionType.OnDamaged, 1.0f);
}
if (attacker != this)
{
OnAttacked?.Invoke(attacker, attackResult);
@@ -2800,8 +2911,10 @@ namespace Barotrauma
{
SelectedConstruction = null;
}
HealthUpdateInterval = 0.0f;
}
private readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
public void ApplyStatusEffects(ActionType actionType, float deltaTime)
{
foreach (StatusEffect statusEffect in statusEffects)
@@ -2810,30 +2923,61 @@ namespace Barotrauma
if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
var targets = new List<ISerializableEntity>();
targets.Clear();
statusEffect.GetNearbyTargets(WorldPosition, targets);
statusEffect.Apply(ActionType.OnActive, deltaTime, this, targets);
}
else
{
statusEffect.Apply(actionType, deltaTime, this, this);
if (statusEffect.targetLimbs != null)
{
foreach (var limbType in statusEffect.targetLimbs)
{
if (statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs))
{
// Target all matching limbs
foreach (var limb in AnimController.Limbs)
{
if (limb.IsSevered) { continue; }
if (limb.type == limbType)
{
statusEffect.Apply(actionType, deltaTime, this, limb);
}
}
}
else if (statusEffect.HasTargetType(StatusEffect.TargetType.Limb))
{
// Target just the first matching limb
Limb limb = AnimController.GetLimb(limbType);
statusEffect.Apply(actionType, deltaTime, this, limb);
}
}
}
}
}
if (actionType != ActionType.OnDamaged && actionType != ActionType.OnSevered)
{
// OnDamaged is called only for the limb that is hit.
AnimController.Limbs.ForEach(l => l.ApplyStatusEffects(actionType, deltaTime));
}
}
private void Implode(bool isNetworkMessage = false)
{
if (CharacterHealth.Unkillable) { return; }
if (CharacterHealth.Unkillable || IsDead) { return; }
if (!isNetworkMessage)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) return;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
}
Kill(CauseOfDeathType.Pressure, null, isNetworkMessage);
CharacterHealth.PressureAffliction.Strength = CharacterHealth.PressureAffliction.Prefab.MaxStrength;
CharacterHealth.SetAllDamage(200.0f, 0.0f, 0.0f);
BreakJoints();
CharacterHealth.ApplyAffliction(null, new Affliction(AfflictionPrefab.Pressure, AfflictionPrefab.Pressure.MaxStrength));
if (isNetworkMessage && GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && Vitality <= CharacterHealth.MinVitality) { Kill(CauseOfDeathType.Pressure, null, isNetworkMessage: true); }
if (IsDead)
{
BreakJoints();
}
}
public void BreakJoints()
@@ -2841,6 +2985,7 @@ namespace Barotrauma
Vector2 centerOfMass = AnimController.GetCenterOfMass();
foreach (Limb limb in AnimController.Limbs)
{
if (limb.IsSevered) { continue; }
limb.AddDamage(limb.SimPosition, 500.0f, 0.0f, 0.0f, false);
Vector2 diff = centerOfMass - limb.SimPosition;
@@ -2861,7 +3006,10 @@ namespace Barotrauma
foreach (var joint in AnimController.LimbJoints)
{
joint.LimitEnabled = false;
if (joint.revoluteJoint != null)
{
joint.revoluteJoint.LimitEnabled = false;
}
}
}
@@ -2928,9 +3076,12 @@ namespace Barotrauma
AnimController.ResetPullJoints();
foreach (RevoluteJoint joint in AnimController.LimbJoints)
foreach (var joint in AnimController.LimbJoints)
{
joint.MotorEnabled = false;
if (joint.revoluteJoint != null)
{
joint.revoluteJoint.MotorEnabled = false;
}
}
if (GameMain.GameSession != null)
@@ -2961,7 +3112,11 @@ namespace Barotrauma
foreach (LimbJoint joint in AnimController.LimbJoints)
{
joint.MotorEnabled = true;
var revoluteJoint = joint.revoluteJoint;
if (revoluteJoint != null)
{
revoluteJoint.MotorEnabled = true;
}
joint.Enabled = true;
joint.IsSevered = false;
}
@@ -3176,5 +3331,15 @@ namespace Barotrauma
}
return targetPos;
}
public bool IsCaptain => HasJob("captain");
public bool IsEngineer => HasJob("engineer");
public bool IsMechanic => HasJob("mechanic");
public bool IsMedic => HasJob("medicaldoctor");
public bool IsOfficer => HasJob("securityofficer");
public bool IsAsssitant => HasJob("assistant");
public bool IsWatchman => HasJob("watchman");
public bool HasJob(string identifier) => Info?.Job?.Prefab.Identifier == identifier;
}
}
@@ -4,7 +4,7 @@ using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using Barotrauma.IO;
using System.Linq;
using System.Xml.Linq;
@@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using Barotrauma.IO;
using System.Linq;
using System.Text;
using System.Xml.Linq;
@@ -97,11 +97,13 @@ namespace Barotrauma
private void ApplyDamage(float deltaTime, bool applyForce)
{
int limbCount = character.AnimController.Limbs.Count(l => !l.ignoreCollisions && !l.IsSevered);
foreach (Limb limb in character.AnimController.Limbs)
{
if (limb.IsSevered) { continue; }
float random = Rand.Value(Rand.RandSync.Server);
huskInfection.Clear();
huskInfection.Add(AfflictionPrefab.InternalDamage.Instantiate(random * 10 * deltaTime / character.AnimController.Limbs.Length));
huskInfection.Add(AfflictionPrefab.InternalDamage.Instantiate(random * 10 * deltaTime / limbCount));
character.LastDamageSource = null;
float force = applyForce ? random * 0.5f * limb.Mass : 0;
character.DamageLimb(limb.WorldPosition, limb, huskInfection, 0, false, force);
@@ -186,18 +188,20 @@ namespace Barotrauma
}
}
if (character.Inventory.Items.Length != husk.Inventory.Items.Length)
if (character.Inventory != null && husk.Inventory != null)
{
string errorMsg = "Failed to move items from the source character's inventory into a husk's inventory (inventory sizes don't match)";
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("AfflictionHusk.CreateAIHusk:InventoryMismatch", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
yield return CoroutineStatus.Success;
}
for (int i = 0; i < character.Inventory.Items.Length && i < husk.Inventory.Items.Length; i++)
{
if (character.Inventory.Items[i] == null) continue;
husk.Inventory.TryPutItem(character.Inventory.Items[i], i, true, false, null);
if (character.Inventory.Items.Length != husk.Inventory.Items.Length)
{
string errorMsg = "Failed to move items from the source character's inventory into a husk's inventory (inventory sizes don't match)";
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("AfflictionHusk.CreateAIHusk:InventoryMismatch", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
yield return CoroutineStatus.Success;
}
for (int i = 0; i < character.Inventory.Items.Length && i < husk.Inventory.Items.Length; i++)
{
if (character.Inventory.Items[i] == null) continue;
husk.Inventory.TryPutItem(character.Inventory.Items[i], i, true, false, null);
}
}
husk.SetStun(5);
@@ -255,20 +259,19 @@ namespace Barotrauma
Limb attachLimb = null;
if (matchingAffliction.AttachLimbId > -1)
{
attachLimb = ragdoll.Limbs.FirstOrDefault(l => l.Params.ID == matchingAffliction.AttachLimbId);
attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.Params.ID == matchingAffliction.AttachLimbId);
}
else if (matchingAffliction.AttachLimbName != null)
{
attachLimb = ragdoll.Limbs.FirstOrDefault(l => l.Name == matchingAffliction.AttachLimbName);
attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.Name == matchingAffliction.AttachLimbName);
}
else if (matchingAffliction.AttachLimbType != LimbType.None)
{
attachLimb = ragdoll.Limbs.FirstOrDefault(l => l.type == matchingAffliction.AttachLimbType);
attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.type == matchingAffliction.AttachLimbType);
}
if (attachLimb == null)
{
DebugConsole.Log("Attachment limb not defined in the affliction prefab or no matching limb could be found. Using the appendage definition as it is.");
attachLimb = ragdoll.Limbs.FirstOrDefault(l => l.Params.ID == jointParams.Limb1);
attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.Params.ID == jointParams.Limb1);
}
if (attachLimb != null)
{
@@ -286,10 +289,6 @@ namespace Barotrauma
ragdoll.AddJoint(jointParams);
appendage.Add(huskAppendage);
}
else
{
DebugConsole.ThrowError("Attachment limb not found!");
}
}
}
return appendage;
@@ -189,6 +189,7 @@ namespace Barotrauma
}
public static AfflictionPrefab InternalDamage;
public static AfflictionPrefab ImpactDamage;
public static AfflictionPrefab Bleeding;
public static AfflictionPrefab Burn;
public static AfflictionPrefab OxygenLow;
@@ -291,6 +292,7 @@ namespace Barotrauma
{
CPRSettings.Unload();
InternalDamage = null;
ImpactDamage = null;
Bleeding = null;
Burn = null;
OxygenLow = null;
@@ -437,6 +439,9 @@ namespace Barotrauma
case "internaldamage":
InternalDamage = prefab;
break;
case "blunttrauma":
ImpactDamage = prefab;
break;
case "bleeding":
Bleeding = prefab;
break;
@@ -456,6 +461,8 @@ namespace Barotrauma
Stun = prefab;
break;
}
if (ImpactDamage == null) { ImpactDamage = InternalDamage; }
if (prefab != null)
{
Prefabs.Add(prefab, isOverride);
@@ -1,6 +1,6 @@
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.IO;
using Barotrauma.IO;
using System.Linq;
using Barotrauma.Extensions;
using System.Xml.Linq;
@@ -1,4 +1,5 @@
using Microsoft.Xna.Framework;
using System;
namespace Barotrauma
{
@@ -19,7 +20,7 @@ namespace Barotrauma
{
foreach (Affliction affliction in afflictions)
{
if (!affliction.Prefab.IsBuff || affliction == this || affliction.MultiplierSource != this) continue;
if (!affliction.Prefab.IsBuff || affliction == this || affliction.MultiplierSource != this) { continue; }
affliction.MultiplierSource = null;
affliction.StrengthDiminishMultiplier = 1f;
}
@@ -28,9 +29,9 @@ namespace Barotrauma
{
foreach (Affliction affliction in afflictions)
{
if (!affliction.Prefab.IsBuff || affliction == this || affliction.MultiplierSource == this) continue;
if (!affliction.Prefab.IsBuff || affliction == this) { continue; }
float multiplier = GetDiminishMultiplier();
if (affliction.StrengthDiminishMultiplier < multiplier) continue;
if (affliction.StrengthDiminishMultiplier < multiplier && affliction.MultiplierSource != this) { continue; }
affliction.MultiplierSource = this;
affliction.StrengthDiminishMultiplier = multiplier;
@@ -40,14 +41,15 @@ namespace Barotrauma
private float GetDiminishMultiplier()
{
if (Strength < Prefab.ActivationThreshold) return 1.0f;
if (Strength < Prefab.ActivationThreshold) { return 1.0f; }
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
if (currentEffect == null) return 1.0f;
if (currentEffect == null) { return 1.0f; }
return MathHelper.Lerp(
float multiplier = MathHelper.Lerp(
currentEffect.MinBuffMultiplier,
currentEffect.MaxBuffMultiplier,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
return 1.0f / Math.Max(multiplier, 0.001f);
}
}
}
@@ -127,7 +127,7 @@ namespace Barotrauma
public bool IsUnconscious
{
get { return Vitality <= 0.0f; }
get { return Vitality <= 0.0f || Character.IsDead; }
}
public float PressureKillDelay { get; private set; } = 5.0f;
@@ -252,8 +252,8 @@ namespace Barotrauma
: afflictions.Where(limbHealthFilter).Union(limbHealths.SelectMany(lh => lh.Afflictions.Where(limbHealthFilter)));
}
private LimbHealth GetMatchingLimbHealth(Limb limb) => limbHealths[limb.HealthIndex];
private LimbHealth GetMatchingLimbHealth(Affliction affliction) => GetMatchingLimbHealth(Character.AnimController.GetLimb(affliction.Prefab.IndicatorLimb));
private LimbHealth GetMatchingLimbHealth(Limb limb) => limb == null ? null : limbHealths[limb.HealthIndex];
private LimbHealth GetMatchingLimbHealth(Affliction affliction) => GetMatchingLimbHealth(Character.AnimController.GetLimb(affliction.Prefab.IndicatorLimb, excludeSevered: false));
/// <summary>
/// Returns the limb afflictions and non-limbspecific afflictions that are set to be displayed on this limb.
@@ -349,13 +349,16 @@ namespace Barotrauma
/// Most monsters for example don't have separate healths for different limbs, essentially meaning that every affliction is applied to every limb.</param>
public float GetAfflictionStrength(string afflictionType, Limb limb, bool requireLimbSpecific)
{
if (requireLimbSpecific && limbHealths.Count == 1) return 0.0f;
if (requireLimbSpecific && limbHealths.Count == 1) { return 0.0f; }
float strength = 0.0f;
foreach (Affliction affliction in limbHealths[limb.HealthIndex].Afflictions)
{
if (affliction.Strength < affliction.Prefab.ActivationThreshold) continue;
if (affliction.Prefab.AfflictionType == afflictionType) strength += affliction.Strength;
if (affliction.Strength < affliction.Prefab.ActivationThreshold) { continue; }
if (affliction.Prefab.AfflictionType == afflictionType)
{
strength += affliction.Strength;
}
}
return strength;
}
@@ -365,17 +368,23 @@ namespace Barotrauma
float strength = 0.0f;
foreach (Affliction affliction in afflictions)
{
if (affliction.Strength < affliction.Prefab.ActivationThreshold) continue;
if (affliction.Prefab.AfflictionType == afflictionType) strength += affliction.Strength;
if (affliction.Strength < affliction.Prefab.ActivationThreshold) { continue; }
if (affliction.Prefab.AfflictionType == afflictionType)
{
strength += affliction.Strength;
}
}
if (!allowLimbAfflictions) return strength;
if (!allowLimbAfflictions) { return strength; }
foreach (LimbHealth limbHealth in limbHealths)
{
foreach (Affliction affliction in limbHealth.Afflictions)
{
if (affliction.Strength < affliction.Prefab.ActivationThreshold) continue;
if (affliction.Prefab.AfflictionType == afflictionType) strength += affliction.Strength;
if (affliction.Strength < affliction.Prefab.ActivationThreshold) { continue; }
if (affliction.Prefab.AfflictionType == afflictionType)
{
strength += affliction.Strength;
}
}
}
@@ -506,6 +515,34 @@ namespace Barotrauma
if (Vitality <= MinVitality) { Kill(); }
}
public float GetLimbDamage(Limb limb, string afflictionType = null)
{
float damageStrength;
if (limb.IsSevered)
{
return 1;
}
else
{
// Instead of using the limbhealth count here, I think it's best to define the max vitality per limb roughly with a constant value.
// Therefore with e.g. 80 health, the max damage per limb would be 20.
// Having at least 20 damage on both legs would cause maximum limping.
float max = MaxVitality / 4;
if (string.IsNullOrEmpty(afflictionType))
{
float damage = GetAfflictionStrength("damage", limb, true);
float bleeding = GetAfflictionStrength("bleeding", limb, true);
float burn = GetAfflictionStrength("burn", limb, true);
damageStrength = Math.Min(damage + bleeding + burn, max);
}
else
{
damageStrength = Math.Min(GetAfflictionStrength("damage", limb, true), max);
}
return damageStrength / max;
}
}
public void RemoveAllAfflictions()
{
foreach (LimbHealth limbHealth in limbHealths)
@@ -523,7 +560,7 @@ namespace Barotrauma
private void AddLimbAffliction(Limb limb, Affliction newAffliction)
{
if (!newAffliction.Prefab.LimbSpecific || limb == null) return;
if (!newAffliction.Prefab.LimbSpecific || limb == null) { return; }
if (limb.HealthIndex < 0 || limb.HealthIndex >= limbHealths.Count)
{
DebugConsole.ThrowError("Limb health index out of bounds. Character\"" + Character.Name +
@@ -535,8 +572,8 @@ namespace Barotrauma
private void AddLimbAffliction(LimbHealth limbHealth, Affliction newAffliction)
{
if (!DoesBleed && newAffliction is AfflictionBleeding) return;
if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) return;
if (!DoesBleed && newAffliction is AfflictionBleeding) { return; }
if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) { return; }
foreach (Affliction affliction in limbHealth.Afflictions)
{
@@ -545,7 +582,10 @@ namespace Barotrauma
affliction.Strength = Math.Min(affliction.Prefab.MaxStrength, affliction.Strength + (newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(affliction.Prefab.Identifier))));
affliction.Source = newAffliction.Source;
CalculateVitality();
if (Vitality <= MinVitality) Kill();
if (Vitality <= MinVitality)
{
Kill();
}
return;
}
}
@@ -560,7 +600,10 @@ namespace Barotrauma
Character.HealthUpdateInterval = 0.0f;
CalculateVitality();
if (Vitality <= MinVitality) Kill();
if (Vitality <= MinVitality)
{
Kill();
}
#if CLIENT
selectedLimbIndex = -1;
#endif
@@ -894,10 +937,7 @@ namespace Barotrauma
partial void RemoveProjSpecific();
/// <summary>
/// Automatically filters out buffs.
/// </summary>
public static IEnumerable<Affliction> SortAfflictionsBySeverity(IEnumerable<Affliction> afflictions) =>
afflictions.Where(a => !a.Prefab.IsBuff).OrderByDescending(a => a.DamagePerSecond).ThenByDescending(a => a.Strength);
public static IEnumerable<Affliction> SortAfflictionsBySeverity(IEnumerable<Affliction> afflictions, bool excludeBuffs = true) =>
afflictions.Where(a => !excludeBuffs || !a.Prefab.IsBuff).OrderByDescending(a => a.DamagePerSecond).ThenByDescending(a => a.Strength);
}
}
@@ -64,7 +64,7 @@ namespace Barotrauma
public readonly Dictionary<int, List<string>> ItemIdentifiers = new Dictionary<int, List<string>>();
public readonly Dictionary<int, Dictionary<string, bool>> ShowItemPreview = new Dictionary<int, Dictionary<string, bool>>();
public readonly List<SkillPrefab> Skills = new List<SkillPrefab>();
public readonly List<AutonomousObjective> AutomaticOrders = new List<AutonomousObjective>();
public readonly List<AutonomousObjective> AutonomousObjective = new List<AutonomousObjective>();
public readonly List<string> AppropriateOrders = new List<string>();
[Serialize("1,1,1,1", false)]
@@ -163,6 +163,7 @@ namespace Barotrauma
}
public Sprite Icon;
public Sprite IconSmall;
public string FilePath { get; private set; }
public XElement Element { get; private set; }
@@ -198,7 +199,7 @@ namespace Barotrauma
}
break;
case "autonomousobjectives":
subElement.Elements().ForEach(order => AutomaticOrders.Add(new AutonomousObjective(order)));
subElement.Elements().ForEach(order => AutonomousObjective.Add(new AutonomousObjective(order)));
break;
case "appropriateobjectives":
case "appropriateorders":
@@ -207,6 +208,9 @@ namespace Barotrauma
case "jobicon":
Icon = new Sprite(subElement.FirstElement());
break;
case "jobiconsmall":
IconSmall = new Sprite(subElement.FirstElement());
break;
}
}
@@ -19,8 +19,8 @@ namespace Barotrauma
None, LeftHand, RightHand, LeftArm, RightArm, LeftForearm, RightForearm,
LeftLeg, RightLeg, LeftFoot, RightFoot, Head, Torso, Tail, Legs, RightThigh, LeftThigh, Waist, Jaw
};
partial class LimbJoint : RevoluteJoint
partial class LimbJoint
{
public bool IsSevered;
public bool CanBeSevered => Params.CanBeSevered;
@@ -30,27 +30,135 @@ namespace Barotrauma
public float Scale => Params.Scale * ragdoll.RagdollParams.JointScale;
public LimbJoint(Limb limbA, Limb limbB, JointParams jointParams, Ragdoll ragdoll) : this(limbA, limbB, Vector2.One, Vector2.One)
public readonly RevoluteJoint revoluteJoint;
public readonly WeldJoint weldJoint;
public Joint Joint => revoluteJoint ?? weldJoint as Joint;
public bool Enabled
{
get => Joint.Enabled;
set => Joint.Enabled = value;
}
public Body BodyA => Joint.BodyA;
public Body BodyB => Joint.BodyB;
public Vector2 WorldAnchorA
{
get => Joint.WorldAnchorA;
set => Joint.WorldAnchorA = value;
}
public Vector2 WorldAnchorB
{
get => Joint.WorldAnchorB;
set => Joint.WorldAnchorB = value;
}
public Vector2 LocalAnchorA
{
get => revoluteJoint != null ? revoluteJoint.LocalAnchorA : weldJoint.LocalAnchorA;
set
{
if (weldJoint != null)
{
weldJoint.LocalAnchorA = value;
}
else
{
revoluteJoint.LocalAnchorA = value;
}
}
}
public Vector2 LocalAnchorB
{
get => revoluteJoint != null ? revoluteJoint.LocalAnchorB : weldJoint.LocalAnchorB;
set
{
if (weldJoint != null)
{
weldJoint.LocalAnchorB = value;
}
else
{
revoluteJoint.LocalAnchorB = value;
}
}
}
public bool LimitEnabled
{
get => revoluteJoint != null ? revoluteJoint.LimitEnabled : false;
set
{
if (revoluteJoint != null)
{
revoluteJoint.LimitEnabled = value;
}
}
}
public float LowerLimit
{
get => revoluteJoint != null ? revoluteJoint.LowerLimit : 0;
set
{
if (revoluteJoint != null)
{
revoluteJoint.LowerLimit = value;
}
}
}
public float UpperLimit
{
get => revoluteJoint != null ? revoluteJoint.UpperLimit : 0;
set
{
if (revoluteJoint != null)
{
revoluteJoint.UpperLimit = value;
}
}
}
public float JointAngle => revoluteJoint != null ? revoluteJoint.JointAngle : weldJoint.ReferenceAngle;
public LimbJoint(Limb limbA, Limb limbB, JointParams jointParams, Ragdoll ragdoll) : this(limbA, limbB, Vector2.One, Vector2.One, jointParams.WeldJoint)
{
Params = jointParams;
this.ragdoll = ragdoll;
LoadParams();
}
public LimbJoint(Limb limbA, Limb limbB, Vector2 anchor1, Vector2 anchor2)
: base(limbA.body.FarseerBody, limbB.body.FarseerBody, anchor1, anchor2)
public LimbJoint(Limb limbA, Limb limbB, Vector2 anchor1, Vector2 anchor2, bool weld = false)
{
CollideConnected = false;
MotorEnabled = true;
MaxMotorTorque = 0.25f;
if (weld)
{
weldJoint = new WeldJoint(limbA.body.FarseerBody, limbB.body.FarseerBody, anchor1, anchor2);
}
else
{
revoluteJoint = new RevoluteJoint(limbA.body.FarseerBody, limbB.body.FarseerBody, anchor1, anchor2)
{
MotorEnabled = true,
MaxMotorTorque = 0.25f
};
}
Joint.CollideConnected = false;
LimbA = limbA;
LimbB = limbB;
}
public void LoadParams()
{
MaxMotorTorque = Params.Stiffness;
LimitEnabled = Params.LimitEnabled;
if (revoluteJoint != null)
{
revoluteJoint.MaxMotorTorque = Params.Stiffness;
revoluteJoint.LimitEnabled = Params.LimitEnabled;
}
if (float.IsNaN(Params.LowerLimit))
{
Params.LowerLimit = 0;
@@ -61,17 +169,33 @@ namespace Barotrauma
}
if (ragdoll.IsFlipped)
{
LocalAnchorA = ConvertUnits.ToSimUnits(new Vector2(-Params.Limb1Anchor.X, Params.Limb1Anchor.Y) * Scale);
LocalAnchorB = ConvertUnits.ToSimUnits(new Vector2(-Params.Limb2Anchor.X, Params.Limb2Anchor.Y) * Scale);
UpperLimit = MathHelper.ToRadians(-Params.LowerLimit);
LowerLimit = MathHelper.ToRadians(-Params.UpperLimit);
if (weldJoint != null)
{
weldJoint.LocalAnchorA = ConvertUnits.ToSimUnits(new Vector2(-Params.Limb1Anchor.X, Params.Limb1Anchor.Y) * Scale);
weldJoint.LocalAnchorB = ConvertUnits.ToSimUnits(new Vector2(-Params.Limb2Anchor.X, Params.Limb2Anchor.Y) * Scale);
}
else
{
revoluteJoint.LocalAnchorA = ConvertUnits.ToSimUnits(new Vector2(-Params.Limb1Anchor.X, Params.Limb1Anchor.Y) * Scale);
revoluteJoint.LocalAnchorB = ConvertUnits.ToSimUnits(new Vector2(-Params.Limb2Anchor.X, Params.Limb2Anchor.Y) * Scale);
revoluteJoint.UpperLimit = MathHelper.ToRadians(-Params.LowerLimit);
revoluteJoint.LowerLimit = MathHelper.ToRadians(-Params.UpperLimit);
}
}
else
{
LocalAnchorA = ConvertUnits.ToSimUnits(Params.Limb1Anchor * Scale);
LocalAnchorB = ConvertUnits.ToSimUnits(Params.Limb2Anchor * Scale);
UpperLimit = MathHelper.ToRadians(Params.UpperLimit);
LowerLimit = MathHelper.ToRadians(Params.LowerLimit);
if (weldJoint != null)
{
weldJoint.LocalAnchorA = ConvertUnits.ToSimUnits(Params.Limb1Anchor * Scale);
weldJoint.LocalAnchorB = ConvertUnits.ToSimUnits(Params.Limb2Anchor * Scale);
}
else
{
revoluteJoint.LocalAnchorA = ConvertUnits.ToSimUnits(Params.Limb1Anchor * Scale);
revoluteJoint.LocalAnchorB = ConvertUnits.ToSimUnits(Params.Limb2Anchor * Scale);
revoluteJoint.UpperLimit = MathHelper.ToRadians(Params.UpperLimit);
revoluteJoint.LowerLimit = MathHelper.ToRadians(Params.LowerLimit);
}
}
}
}
@@ -166,10 +290,20 @@ namespace Barotrauma
if (isSevered)
{
ragdoll.SubtractMass(this);
if (type == LimbType.Head)
{
character.Kill(CauseOfDeathType.Unknown, null);
}
}
else
{
severedFadeOutTimer = 0.0f;
}
if (!isSevered) severedFadeOutTimer = 0.0f;
#if CLIENT
if (isSevered) damageOverlayStrength = 100.0f;
if (isSevered)
{
damageOverlayStrength = 100.0f;
}
#endif
}
}
@@ -366,14 +500,42 @@ namespace Barotrauma
public string Name => Params.Name;
// Exposed for status effects
public bool IsDead => character.IsDead;
public bool CanBeSeveredAlive
{
get
{
if (character.IsHumanoid) { return false; }
if (this == character.AnimController.MainLimb) { return false; }
if (character.AnimController.CanWalk)
{
switch (type)
{
case LimbType.LeftFoot:
case LimbType.RightFoot:
case LimbType.LeftLeg:
case LimbType.RightLeg:
case LimbType.LeftThigh:
case LimbType.RightThigh:
case LimbType.Legs:
case LimbType.Waist:
return false;
}
}
return true;
}
}
public Dictionary<string, SerializableProperty> SerializableProperties
{
get;
private set;
}
private readonly List<StatusEffect> statusEffects = new List<StatusEffect>();
public Limb(Ragdoll ragdoll, Character character, LimbParams limbParams)
{
this.ragdoll = ragdoll;
@@ -436,6 +598,9 @@ namespace Barotrauma
case "damagemodifier":
DamageModifiers.Add(new DamageModifier(subElement, character.Name));
break;
case "statuseffect":
statusEffects.Add(StatusEffect.Load(subElement, Name));
break;
}
}
@@ -521,11 +686,12 @@ namespace Barotrauma
afflictionsCopy.Add(newAffliction);
}
}
AddDamageProjSpecific(afflictionsCopy, playSound, appliedDamageModifiers);
return new AttackResult(afflictionsCopy, this, appliedDamageModifiers);
var result = new AttackResult(afflictionsCopy, this, appliedDamageModifiers);
AddDamageProjSpecific(playSound, result);
return result;
}
partial void AddDamageProjSpecific(IEnumerable<Affliction> afflictions, bool playSound, IEnumerable<DamageModifier> appliedDamageModifiers);
partial void AddDamageProjSpecific(bool playSound, AttackResult result);
public bool SectorHit(Vector2 armorSector, Vector2 simPosition)
{
@@ -582,7 +748,8 @@ namespace Barotrauma
public bool UpdateAttack(float deltaTime, Vector2 attackSimPos, IDamageable damageTarget, out AttackResult attackResult, float distance = -1, Limb targetLimb = null)
{
attackResult = default(AttackResult);
float dist = distance > -1 ? distance : ConvertUnits.ToDisplayUnits(Vector2.Distance(SimPosition, attackSimPos));
Vector2 simPos = ragdoll.SimplePhysicsEnabled ? character.SimPosition : SimPosition;
float dist = distance > -1 ? distance : ConvertUnits.ToDisplayUnits(Vector2.Distance(simPos, attackSimPos));
bool wasRunning = attack.IsRunning;
attack.UpdateAttackTimer(deltaTime);
@@ -595,7 +762,7 @@ namespace Barotrauma
case HitDetection.Distance:
if (dist < attack.DamageRange)
{
structureBody = Submarine.PickBody(SimPosition, attackSimPos, collisionCategory: Physics.CollisionWall | Physics.CollisionLevel, allowInsideFixture: true);
structureBody = Submarine.PickBody(simPos, attackSimPos, collisionCategory: Physics.CollisionWall | Physics.CollisionLevel, allowInsideFixture: true);
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.
@@ -689,6 +856,7 @@ namespace Barotrauma
{
if (limbIndex < 0 || limbIndex >= character.AnimController.Limbs.Length) { continue; }
Limb limb = character.AnimController.Limbs[limbIndex];
if (limb.IsSevered) { continue; }
diff = attackSimPos - limb.SimPosition;
if (diff == Vector2.Zero) { continue; }
limb.body.ApplyTorque(limb.Mass * character.AnimController.Dir * attack.Torque * limb.Params.AttackForceMultiplier);
@@ -811,6 +979,30 @@ namespace Barotrauma
}
}
private readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
public void ApplyStatusEffects(ActionType actionType, float deltaTime)
{
foreach (StatusEffect statusEffect in statusEffects)
{
if (statusEffect.type != actionType) { continue; }
if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
targets.Clear();
statusEffect.GetNearbyTargets(WorldPosition, targets);
statusEffect.Apply(ActionType.OnActive, deltaTime, character, targets);
}
else
{
if (statusEffect.HasTargetType(StatusEffect.TargetType.Character))
{
statusEffect.Apply(actionType, deltaTime, character, character, WorldPosition);
}
statusEffect.Apply(actionType, deltaTime, character, this, WorldPosition);
}
}
}
public void Remove()
{
body?.Remove();
@@ -1,7 +1,7 @@
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using Barotrauma.IO;
using System;
using System.Linq;
using System.Xml.Linq;
@@ -72,6 +72,12 @@ namespace Barotrauma
[Editable, Serialize(true, true, description: "Should the character be flipped depending on which direction it faces. Should usually be enabled on all characters that have distinctive upper and lower sides.")]
public bool Flip { get; set; }
[Serialize(1f, true, description: "Reduces continuous flipping when the character abruptly changes direction."), Editable]
public float FlipCooldown { get; set; }
[Serialize(0.5f, true, description: "How much it takes before the character flips. The timer starts when the character starts to move in the different direction."), Editable]
public float FlipDelay { get; set; }
[Serialize(10.0f, true, description: "How much force is used to move the head to the correct position."), Editable(MinValueFloat = 0, MaxValueFloat = 100)]
public float HeadMoveForce { get; set; }
@@ -146,9 +152,18 @@ namespace Barotrauma
[Editable, Serialize(true, true, description: "Should the character be flipped depending on which direction it faces. Should usually be enabled on all characters that have distinctive upper and lower sides.")]
public bool Flip { get; set; }
[Serialize(1f, true, description: "Reduces continuous flipping when the character abruptly changes direction."), Editable]
public float FlipCooldown { get; set; }
[Serialize(0.5f, true, description: "How much it takes before the character flips. The timer starts when the character starts to move in the different direction."), Editable]
public float FlipDelay { get; set; }
[Editable, Serialize(true, true, description: "If enabled, the character will simply be mirrored horizontally when it wants to turn around. If disabled, it will rotate itself to face the other direction.")]
public bool Mirror { get; set; }
[Editable, Serialize(true, true, description: "Disabling this will make mirroring instantaneous.")]
public bool MirrorLerp { get; set; }
[Serialize(5f, true), Editable]
public float WaveAmplitude { get; set; }
@@ -205,7 +220,6 @@ namespace Barotrauma
interface IFishAnimation
{
bool Flip { get; set; }
string FootAngles { get; set; }
Dictionary<int, float> FootAnglesInRadians { get; set; }
float TailAngle { get; set; }
@@ -214,5 +228,8 @@ namespace Barotrauma
float TorsoTorque { get; set; }
float TailTorque { get; set; }
float FootTorque { get; set; }
bool Flip { get; set; }
float FlipCooldown { get; set; }
float FlipDelay { get; set; }
}
}
@@ -25,10 +25,10 @@ namespace Barotrauma
[Serialize("", true, description: "If defined, different species of the same group are considered like the characters of the same species by the AI."), Editable]
public string Group { get; private set; }
[Serialize(false, true), Editable]
[Serialize(false, true), Editable(ReadOnly = true)]
public bool Humanoid { get; private set; }
[Serialize(false, true), Editable]
[Serialize(false, true), Editable(ReadOnly = true)]
public bool HasInfo { get; private set; }
[Serialize(false, true), Editable]
@@ -43,13 +43,13 @@ namespace Barotrauma
[Serialize(false, true, description: "Can the creature live without water or does it die on dry land?"), Editable]
public bool NeedsWater { get; set; }
[Serialize(false, true), Editable]
[Serialize(false, false), Editable]
public bool CanSpeak { get; set; }
[Serialize(100f, true, description: "How much noise the character makes when moving?"), Editable(minValue: 0f, maxValue: 1000f)]
[Serialize(100f, true, description: "How much noise the character makes when moving?"), Editable(minValue: 0f, maxValue: 100000f)]
public float Noise { get; set; }
[Serialize(100f, true, description: "How visible the character is?"), Editable(minValue: 0f, maxValue: 1000f)]
[Serialize(100f, true, description: "How visible the character is?"), Editable(minValue: 0f, maxValue: 100000f)]
public float Visibility { get; set; }
[Serialize("blood", true), Editable]
@@ -70,6 +70,9 @@ namespace Barotrauma
[Serialize(false, true), Editable]
public bool HideInSonar { get; set; }
[Serialize(0f, true), Editable]
public float SonarDisruption { get; set; }
public readonly string File;
public readonly List<SubParam> SubParams = new List<SubParam>();
@@ -474,8 +477,8 @@ namespace Barotrauma
[Serialize(true, true, description: "Enforce aggressive behavior if the creature is spawned as a target of a monster mission."), Editable()]
public bool EnforceAggressiveBehaviorForMissions { get; private set; }
[Serialize(false, true, description: "Should the character target or ignore walls when it's inside the submarine. Doesn't have any effect if no target priority for walls is defined."), Editable()]
public bool TargetInnerWalls { get; private set; }
[Serialize(true, true, description: "Should the character target or ignore walls when it's outside the submarine. Doesn't have any effect if no target priority for walls is defined."), Editable()]
public bool TargetOuterWalls { get; private set; }
[Serialize(false, true, description: "If enabled, the character chooses randomly from the available attacks. The priority is used as a weight for weighted random."), Editable()]
public bool RandomAttack { get; private set; }
@@ -1,8 +1,12 @@
using System.IO;
using System.Collections.Generic;
using System.Xml;
using System.Collections.Generic;
using System.Xml.Linq;
using Microsoft.Xna.Framework;
#if DEBUG
using System.IO;
using System.Xml;
#else
using Barotrauma.IO;
#endif
namespace Barotrauma
{
@@ -75,7 +79,7 @@ namespace Barotrauma
Folder = Path.GetDirectoryName(FullPath);
}
public virtual bool Save(string fileNameWithoutExtension = null, XmlWriterSettings settings = null)
public virtual bool Save(string fileNameWithoutExtension = null, System.Xml.XmlWriterSettings settings = null)
{
if (!Directory.Exists(Folder))
{
@@ -85,7 +89,7 @@ namespace Barotrauma
Serialize();
if (settings == null)
{
settings = new XmlWriterSettings
settings = new System.Xml.XmlWriterSettings
{
Indent = true,
OmitXmlDeclaration = true,
@@ -3,7 +3,7 @@ using System;
using System.Collections.Generic;
using System.Xml.Linq;
using System.Linq;
using System.IO;
using Barotrauma.IO;
using System.Xml;
using Barotrauma.Extensions;
#if CLIENT
@@ -470,6 +470,12 @@ namespace Barotrauma
[Serialize(true, true), Editable]
public bool CanBeSevered { get; set; }
[Serialize(0f, true, description:"Default 0 (Can't be severed when the creature is alive). Modifies the severance probability (defined per item/attack) when the character is alive. Currently only affects non-humanoid ragdolls. Also note that if CanBeSevered is false, this property doesn't have any effect."), Editable(MinValueFloat = 0, MaxValueFloat = 10, ValueStep = 0.1f, DecimalCount = 2)]
public float SeveranceProbabilityModifier { get; set; }
[Serialize("gore", true), Editable]
public string BreakSound { get; set; }
[Serialize(true, true), Editable]
public bool LimitEnabled { get; set; }
@@ -491,6 +497,9 @@ namespace Barotrauma
[Serialize(1f, true, description: "CAUTION: Not fully implemented. Only use for limb joints that connect non-animated limbs!"), Editable]
public float Scale { get; set; }
[Serialize(false, false), Editable(ReadOnly = true)]
public bool WeldJoint { get; set; }
public JointParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) { }
}
@@ -605,7 +614,11 @@ namespace Barotrauma
[Serialize(1f, true), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = 10)]
public float AttackForceMultiplier { get; set; }
[Serialize(1f, true, description:"How much damage must be done by the attack in order to be able to cut off the limb. Note that it's evaluated after the damage modifiers."), Editable(DecimalCount = 0, MinValueFloat = 0, MaxValueFloat = 1000)]
public float MinSeveranceDamage { get; set; }
// Non-editable ->
// TODO: make read-only
[Serialize(0, true)]
public int HealthIndex { get; set; }
@@ -936,7 +949,7 @@ namespace Barotrauma
{
public override string Name => "Light Texture";
[Serialize("", true), Editable]
[Serialize("Content/Lights/pointlight_bright.png", true), Editable]
public string Texture { get; private set; }
[Serialize("0.5, 0.5", true), Editable(DecimalCount = 2)]
@@ -128,7 +128,7 @@ namespace Barotrauma
if (Current == null)
{
DebugConsole.NewMessage("Now skill settings found in the selected content packages. Using default values.");
DebugConsole.NewMessage("No skill settings found in the selected content packages. Using default values.");
Current = new SkillSettings(null);
}
}