(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; }