(61d00a474) v0.9.7.1

This commit is contained in:
Regalis
2020-03-04 13:04:10 +01:00
parent 3c50efa5c9
commit 3c09ebe02f
5086 changed files with 786063 additions and 295871 deletions
@@ -0,0 +1,149 @@
using Microsoft.Xna.Framework;
using System.Collections.Generic;
namespace Barotrauma
{
public enum AIState { Idle, Attack, Escape, Eat, Flee, Avoid, Aggressive, PassiveAggressive }
abstract partial class AIController : ISteerable
{
public bool Enabled;
public readonly Character Character;
private AIState state;
protected void ResetAITarget()
{
_lastAiTarget = null;
_selectedAiTarget = null;
}
// Update only when the value changes, not when it keeps the same.
protected AITarget _lastAiTarget;
// Updated each time the value is updated (also when the value is the same).
protected AITarget _previousAiTarget;
protected AITarget _selectedAiTarget;
public AITarget SelectedAiTarget
{
get { return _selectedAiTarget; }
protected set
{
_previousAiTarget = _selectedAiTarget;
_selectedAiTarget = value;
if (_selectedAiTarget != _previousAiTarget)
{
if (_previousAiTarget != null)
{
_lastAiTarget = _previousAiTarget;
}
OnTargetChanged(_previousAiTarget, _selectedAiTarget);
}
}
}
protected SteeringManager steeringManager;
public SteeringManager SteeringManager
{
get { return steeringManager; }
}
public Vector2 Steering
{
get { return Character.AnimController.TargetMovement; }
set { Character.AnimController.TargetMovement = value; }
}
public Vector2 SimPosition
{
get { return Character.SimPosition; }
}
public Vector2 WorldPosition
{
get { return Character.WorldPosition; }
}
public Vector2 Velocity
{
get { return Character.AnimController.Collider.LinearVelocity; }
}
public virtual bool CanEnterSubmarine
{
get { return true; }
}
public virtual bool CanFlip
{
get { return true; }
}
public virtual AIObjectiveManager ObjectiveManager
{
get { return null; }
}
public AIState State
{
get { return state; }
set
{
if (state == value) { return; }
PreviousState = state;
OnStateChanged(state, value);
state = value;
}
}
public AIState PreviousState { get; protected set; }
private IEnumerable<Hull> visibleHulls;
private float hullVisibilityTimer;
const float hullVisibilityInterval = 0.5f;
public IEnumerable<Hull> VisibleHulls
{
get
{
if (visibleHulls == null)
{
visibleHulls = Character.GetVisibleHulls();
}
return visibleHulls;
}
private set
{
visibleHulls = value;
}
}
public AIController (Character c)
{
Character = c;
hullVisibilityTimer = Rand.Range(0f, hullVisibilityTimer);
Enabled = true;
}
public virtual void OnAttacked(Character attacker, AttackResult attackResult) { }
public virtual void SelectTarget(AITarget target) { }
public virtual void Update(float deltaTime)
{
if (hullVisibilityTimer > 0)
{
hullVisibilityTimer--;
}
else
{
hullVisibilityTimer = hullVisibilityInterval;
VisibleHulls = Character.GetVisibleHulls();
}
}
protected virtual void OnStateChanged(AIState from, AIState to) { }
protected virtual void OnTargetChanged(AITarget previousTarget, AITarget newTarget) { }
}
}
@@ -0,0 +1,246 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma
{
partial class AITarget
{
public static List<AITarget> List = new List<AITarget>();
private Entity entity;
public Entity Entity
{
get
{
if (entity != null && entity.Removed) { return null; }
return entity;
}
}
private float soundRange;
private float sightRange;
/// <summary>
/// How long does it take for the ai target to fade out if not kept alive.
/// </summary>
public float FadeOutTime { get; private set; } = 2;
public bool Static { get; private set; }
public bool StaticSound { get; private set; }
public bool StaticSight { get; private set; }
public float SoundRange
{
get { return soundRange; }
set
{
if (float.IsNaN(value))
{
DebugConsole.ThrowError("Attempted to set the SoundRange of an AITarget to NaN.\n" + Environment.StackTrace);
return;
}
soundRange = MathHelper.Clamp(value, MinSoundRange, MaxSoundRange);
}
}
public float SightRange
{
get { return sightRange; }
set
{
if (float.IsNaN(value))
{
DebugConsole.ThrowError("Attempted to set the SightRange of an AITarget to NaN.\n" + Environment.StackTrace);
return;
}
sightRange = MathHelper.Clamp(value, MinSightRange, MaxSightRange);
}
}
private float sectorRad = MathHelper.TwoPi;
public float SectorDegrees
{
get { return MathHelper.ToDegrees(sectorRad); }
set { sectorRad = MathHelper.ToRadians(value); }
}
private Vector2 sectorDir;
public Vector2 SectorDir
{
get { return sectorDir; }
set
{
if (!MathUtils.IsValid(value))
{
string errorMsg = "Invalid AITarget sector direction (" + value + ")\n" + Environment.StackTrace;
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("AITarget.SectorDir:" + entity?.ToString(), GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
return;
}
sectorDir = value;
}
}
public float SonarDisruption
{
get;
set;
}
public string SonarLabel;
public string SonarIconIdentifier;
public bool Enabled => SoundRange > 0 || SightRange > 0;
public float MinSoundRange, MinSightRange;
public float MaxSoundRange = 100000, MaxSightRange = 100000;
public TargetType Type { get; private set; }
public enum TargetType
{
Any,
HumanOnly,
EnemyOnly
}
public Vector2 WorldPosition
{
get
{
if (entity == null || entity.Removed)
{
#if DEBUG
DebugConsole.ThrowError("Attempted to access a removed AITarget\n" + Environment.StackTrace);
#endif
GameAnalyticsManager.AddErrorEventOnce("AITarget.WorldPosition:EntityRemoved",
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Attempted to access a removed AITarget\n" + Environment.StackTrace);
return Vector2.Zero;
}
return entity.WorldPosition;
}
}
public Vector2 SimPosition
{
get
{
if (entity == null || entity.Removed)
{
#if DEBUG
DebugConsole.ThrowError("Attempted to access a removed AITarget\n" + Environment.StackTrace);
#endif
GameAnalyticsManager.AddErrorEventOnce("AITarget.WorldPosition:EntityRemoved",
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Attempted to access a removed AITarget\n" + Environment.StackTrace);
return Vector2.Zero;
}
return entity.SimPosition;
}
}
public void Reset()
{
if (Static)
{
SightRange = MaxSightRange;
SoundRange = MaxSoundRange;
}
else
{
// Non-static ai targets must be kept alive by a custom logic (e.g. item components)
SightRange = StaticSight ? MaxSightRange : MinSightRange;
SoundRange = StaticSound ? MaxSoundRange : MinSoundRange;
}
}
public AITarget(Entity e, XElement element) : this(e)
{
SightRange = element.GetAttributeFloat("sightrange", 0.0f);
SoundRange = element.GetAttributeFloat("soundrange", 0.0f);
MinSightRange = element.GetAttributeFloat("minsightrange", 0f);
MinSoundRange = element.GetAttributeFloat("minsoundrange", 0f);
MaxSightRange = element.GetAttributeFloat("maxsightrange", SightRange);
MaxSoundRange = element.GetAttributeFloat("maxsoundrange", SoundRange);
FadeOutTime = element.GetAttributeFloat("fadeouttime", FadeOutTime);
Static = element.GetAttributeBool("static", Static);
StaticSight = element.GetAttributeBool("staticsight", StaticSight);
StaticSound = element.GetAttributeBool("staticsound", StaticSound);
if (Static)
{
StaticSound = true;
StaticSight = true;
}
SonarDisruption = element.GetAttributeFloat("sonardisruption", 0.0f);
SonarLabel = element.GetAttributeString("sonarlabel", "");
SonarIconIdentifier = element.GetAttributeString("sonaricon", "");
string typeString = element.GetAttributeString("type", "Any");
if (Enum.TryParse(typeString, out TargetType t))
{
Type = t;
}
Reset();
}
public AITarget(Entity e)
{
entity = e;
List.Add(this);
}
public void Update(float deltaTime)
{
if (Enabled && !Static && FadeOutTime > 0)
{
// The aitarget goes silent/invisible if the components don't keep it active
if (!StaticSight)
{
DecreaseSightRange(deltaTime);
}
if (!StaticSound)
{
DecreaseSoundRange(deltaTime);
}
}
}
public void IncreaseSoundRange(float deltaTime, float speed = 1)
{
SoundRange += speed * deltaTime * (MaxSoundRange / FadeOutTime);
}
public void IncreaseSightRange(float deltaTime, float speed = 1)
{
SightRange += speed * deltaTime * (MaxSightRange / FadeOutTime);
}
public void DecreaseSoundRange(float deltaTime, float speed = 1)
{
SoundRange -= speed * deltaTime * (MaxSoundRange / FadeOutTime);
}
public void DecreaseSightRange(float deltaTime, float speed = 1)
{
SightRange -= speed * deltaTime * (MaxSightRange / FadeOutTime);
}
public bool IsWithinSector(Vector2 worldPosition)
{
if (sectorRad >= MathHelper.TwoPi) return true;
Vector2 diff = worldPosition - WorldPosition;
return MathUtils.GetShortestAngle(MathUtils.VectorToAngle(diff), MathUtils.VectorToAngle(sectorDir)) <= sectorRad * 0.5f;
}
public void Remove()
{
List.Remove(this);
entity = null;
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,28 @@
using Microsoft.Xna.Framework;
namespace Barotrauma
{
interface ISteerable
{
Vector2 Steering
{
get;
set;
}
Vector2 Velocity
{
get;
}
Vector2 SimPosition
{
get;
}
Vector2 WorldPosition
{
get;
}
}
}
@@ -0,0 +1,617 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
using Barotrauma.Extensions;
using FarseerPhysics;
namespace Barotrauma
{
class IndoorsSteeringManager : SteeringManager
{
private PathFinder pathFinder;
private SteeringPath currentPath;
private bool canOpenDoors, canBreakDoors;
private Character character;
private Vector2 currentTarget;
private float findPathTimer;
private float buttonPressCooldown;
const float ButtonPressInterval = 0.5f;
public SteeringPath CurrentPath
{
get { return currentPath; }
}
public PathFinder PathFinder
{
get { return pathFinder; }
}
public Vector2 CurrentTarget
{
get { return currentTarget; }
}
public bool IsPathDirty
{
get;
private set;
}
/// <summary>
/// Returns true if the current or the next node is in ladders.
/// </summary>
public bool InLadders =>
currentPath != null &&
currentPath.CurrentNode != null && (currentPath.CurrentNode.Ladders != null ||
(currentPath.NextNode != null && currentPath.NextNode.Ladders != null));
/// <summary>
/// Returns true if any node in the path is in stairs
/// </summary>
public bool PathHasStairs => currentPath != null && currentPath.Nodes.Any(n => n.Stairs != null);
public bool IsNextNodeLadder => GetNextLadder() != null;
public bool IsNextLadderSameAsCurrent
{
get
{
if (currentPath == null) { return false; }
if (currentPath.CurrentNode == null) { return false; }
if (currentPath.NextNode == null) { return false; }
var currentLadder = currentPath.CurrentNode.Ladders;
if (currentLadder == null) { return false; }
var nextLadder = GetNextLadder();
return nextLadder != null && nextLadder == currentLadder;
}
}
public IndoorsSteeringManager(ISteerable host, bool canOpenDoors, bool canBreakDoors) : base(host)
{
pathFinder = new PathFinder(WayPoint.WayPointList.FindAll(wp => wp.SpawnType == SpawnType.Path), indoorsSteering: true);
pathFinder.GetNodePenalty = GetNodePenalty;
this.canOpenDoors = canOpenDoors;
this.canBreakDoors = canBreakDoors;
character = (host as AIController).Character;
findPathTimer = Rand.Range(0.0f, 1.0f);
}
public override void Update(float speed)
{
base.Update(speed);
buttonPressCooldown -= 1.0f / 60.0f;
findPathTimer -= 1.0f / 60.0f;
}
public void SetPath(SteeringPath path)
{
currentPath = path;
if (path.Nodes.Any()) currentTarget = path.Nodes[path.Nodes.Count - 1].SimPosition;
findPathTimer = 1.0f;
IsPathDirty = false;
}
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);
}
/// <summary>
/// Seeks the ladder from the current and the next two nodes.
/// </summary>
public Ladder GetNextLadder()
{
if (currentPath == null) { return null; }
if (currentPath.NextNode == null) { return null; }
if (currentPath.NextNode.Ladders != null)
{
return currentPath.NextNode.Ladders;
}
else
{
int index = currentPath.CurrentIndex + 2;
if (currentPath.Nodes.Count > index)
{
var node = currentPath.Nodes[index];
if (node == null) { return null; }
return node.Ladders;
}
return null;
}
}
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);
//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)
{
IsPathDirty = true;
if (findPathTimer > 0.0f) { return Vector2.Zero; }
currentTarget = target;
Vector2 currentPos = host.SimPosition;
if (character != null && character.Submarine == null)
{
var targetHull = Hull.FindHull(ConvertUnits.ToDisplayUnits(target), null, false);
if (targetHull != null && targetHull.Submarine != null)
{
currentPos -= targetHull.Submarine.SimPosition;
}
}
pathFinder.InsideSubmarine = character.Submarine != null;
var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", startNodeFilter, endNodeFilter, nodeFilter);
bool useNewPath = currentPath == null || needsNewPath || currentPath.Finished;
if (!useNewPath && currentPath != null && currentPath.CurrentNode != null && newPath.Nodes.Any() && !newPath.Unreachable)
{
// It's possible that the current path was calculated from a start point that is no longer valid.
// Therefore, let's accept also paths with a greater cost than the current, if the current node is much farther than the new start node.
useNewPath = newPath.Cost < currentPath.Cost ||
Vector2.DistanceSquared(character.WorldPosition, currentPath.CurrentNode.WorldPosition) > Math.Pow(Vector2.Distance(character.WorldPosition, newPath.Nodes.First().WorldPosition) * 3, 2);
}
if (useNewPath)
{
currentPath = newPath;
}
float priority = MathHelper.Lerp(3, 1, character.Params.PathFinderPriority);
findPathTimer = priority * Rand.Range(1.0f, 1.2f);
IsPathDirty = false;
return DiffToCurrentNode();
}
Vector2 diff = DiffToCurrentNode();
var collider = character.AnimController.Collider;
//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)
{
diff.Y = 0.0f;
}
//if (diff.LengthSquared() < 0.001f) { return -host.Steering; }
if (diff == Vector2.Zero) { return Vector2.Zero; }
return Vector2.Normalize(diff) * weight;
}
protected override Vector2 DoSteeringSeek(Vector2 target, float weight) => CalculateSteeringSeek(target, weight, null, null, null);
private Vector2 DiffToCurrentNode()
{
if (currentPath == null || currentPath.Unreachable) return Vector2.Zero;
if (currentPath.Finished)
{
Vector2 pos2 = host.SimPosition;
if (character != null && character.Submarine == null &&
CurrentPath.Nodes.Count > 0 && CurrentPath.Nodes.Last().Submarine != null)
{
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)
{
if (character.Submarine == null)
{
pos -= CurrentPath.CurrentNode.Submarine.SimPosition;
}
else if (character.Submarine != currentPath.CurrentNode.Submarine)
{
pos -= ConvertUnits.ToSimUnits(currentPath.CurrentNode.Submarine.Position - character.Submarine.Position);
}
}
}
bool isDiving = character.AnimController.InWater && character.AnimController.HeadInWater;
//only humanoids can climb ladders
if (!isDiving && character.AnimController is HumanoidAnimController && IsNextLadderSameAsCurrent)
{
if (character.SelectedConstruction != currentPath.CurrentNode.Ladders.Item &&
currentPath.CurrentNode.Ladders.Item.IsInsideTrigger(character.WorldPosition))
{
currentPath.CurrentNode.Ladders.Item.TryInteract(character, false, true);
}
}
var collider = character.AnimController.Collider;
if (character.IsClimbing && !isDiving)
{
Vector2 diff = currentPath.CurrentNode.SimPosition - pos;
Ladder nextLadder = GetNextLadder();
bool nextLadderSameAsCurrent = IsNextLadderSameAsCurrent;
if (nextLadderSameAsCurrent)
{
//climbing ladders -> don't move horizontally
diff.X = 0.0f;
}
//at the same height as the waypoint
if (Math.Abs(collider.SimPosition.Y - currentPath.CurrentNode.SimPosition.Y) < (collider.height / 2 + collider.radius) * 1.25f)
{
float heightFromFloor = character.AnimController.GetColliderBottom().Y - character.AnimController.FloorY;
if (heightFromFloor <= 0.0f)
{
diff.Y = Math.Max(diff.Y, 1.0f);
}
// 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)
{
character.AnimController.Anim = AnimController.Animation.None;
character.SelectedConstruction = null;
}
else if (!nextLadderSameAsCurrent)
{
// Try to change the ladder (hatches between two submarines)
if (character.SelectedConstruction != nextLadder.Item && nextLadder.Item.IsInsideTrigger(character.WorldPosition))
{
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();
}
}
else if (nextLadder != null)
{
//if the current node is below the character and the next one is above (or vice versa)
//and both are on ladders, we can skip directly to the next one
//e.g. no point in going down to reach the starting point of a path when we could go directly to the one above
if (Math.Sign(currentPath.CurrentNode.WorldPosition.Y - character.WorldPosition.Y) != Math.Sign(currentPath.NextNode.WorldPosition.Y - character.WorldPosition.Y))
{
currentPath.SkipToNextNode();
}
}
return diff;
}
else if (character.AnimController.InWater)
{
// If the character is underwater, we don't need the ladders anymore
if (character.IsClimbing && isDiving)
{
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)
{
verticalDistance *= 2;
}
float distance = horizontalDistance + verticalDistance;
if (ConvertUnits.ToSimUnits(distance) < targetDistance)
{
currentPath.SkipToNextNode();
}
}
else if (!IsNextLadderSameAsCurrent)
{
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.
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;
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)
{
currentPath.SkipToNextNode();
}
}
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.IsStuck) { return false; }
if (!canOpenDoors || character.LockHands) { return false; }
if (door.HasIntegratedButtons)
{
return door.CanBeOpenedWithoutTools(character);
}
else
{
return door.Item.GetConnectedComponents<Controller>(true).Any(b => b.HasAccess(character) && (buttonFilter == null || buttonFilter(b)));
}
}
private void CheckDoorsInPath()
{
for (int i = 0; i < 2; i++)
{
WayPoint currentWaypoint = null;
WayPoint nextWaypoint = null;
Door door = null;
bool shouldBeOpen = false;
if (currentPath.Nodes.Count == 1)
{
door = currentPath.Nodes.First().ConnectedDoor;
shouldBeOpen = door != null;
}
else
{
if (i == 0)
{
currentWaypoint = currentPath.CurrentNode;
nextWaypoint = currentPath.NextNode;
}
else
{
currentWaypoint = currentPath.PrevNode;
nextWaypoint = currentPath.CurrentNode;
}
if (currentWaypoint?.ConnectedDoor == null) { continue; }
if (nextWaypoint == null)
{
//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;
}
else
{
door = currentWaypoint.ConnectedGap.ConnectedDoor;
if (door.LinkedGap.IsHorizontal)
{
int dir = Math.Sign(nextWaypoint.WorldPosition.X - door.Item.WorldPosition.X);
shouldBeOpen = (door.Item.WorldPosition.X - character.WorldPosition.X) * dir > -50.0f;
}
else
{
int dir = Math.Sign(nextWaypoint.WorldPosition.Y - door.Item.WorldPosition.Y);
shouldBeOpen = (door.Item.WorldPosition.Y - character.WorldPosition.Y) * dir > -80.0f;
}
}
}
if (door == null) { return; }
//toggle the door if it's the previous node and open, or if it's current node and closed
if (door.IsOpen != shouldBeOpen)
{
Controller closestButton = null;
float closestDist = 0;
bool canAccess = CanAccessDoor(door, button =>
{
if (currentWaypoint == null) { return true; }
// Check that the button is on the right side of the door.
if (door.LinkedGap.IsHorizontal)
{
int dir = Math.Sign(nextWaypoint.WorldPosition.X - door.Item.WorldPosition.X);
if (button.Item.WorldPosition.X * dir > door.Item.WorldPosition.X * dir) { return false; }
}
else
{
int dir = Math.Sign(nextWaypoint.WorldPosition.Y - door.Item.WorldPosition.Y);
if (button.Item.WorldPosition.Y * dir > door.Item.WorldPosition.Y * dir) { return false; }
}
float distance = Vector2.DistanceSquared(button.Item.WorldPosition, character.WorldPosition);
if (closestButton == null || distance < closestDist)
{
closestButton = button;
closestDist = distance;
}
return true;
});
if (canAccess)
{
if (door.HasIntegratedButtons)
{
door.Item.TryInteract(character, false, true);
buttonPressCooldown = ButtonPressInterval;
break;
}
else if (closestButton != null)
{
if (Vector2.DistanceSquared(closestButton.Item.WorldPosition, character.WorldPosition) < MathUtils.Pow(closestButton.Item.InteractDistance * 2, 2))
{
closestButton.Item.TryInteract(character, false, true);
buttonPressCooldown = ButtonPressInterval;
break;
}
else
{
// Can't reach the button closest to the character.
// It's possible that we could reach another buttons.
// If this becomes an issue, we could go through them here and check if any of them are reachable
// (would have to cache a collection of buttons instead of a single reference in the CanAccess filter method above)
var body = Submarine.PickBody(character.SimPosition, character.GetRelativeSimPosition(closestButton.Item), collisionCategory: Physics.CollisionWall | Physics.CollisionLevel);
if (body != null)
{
if (body.UserData is Item item)
{
var d = item.GetComponent<Door>();
if (d == null || d.IsOpen) { return; }
}
// The button is on the wrong side of the door or a wall
currentPath.Unreachable = true;
}
return;
}
}
}
else if (shouldBeOpen)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Pathfinding error: Cannot access the door", Color.Yellow);
#endif
currentPath.Unreachable = true;
return;
}
}
}
}
private float? GetNodePenalty(PathNode node, PathNode nextNode)
{
if (character == null) { return 0.0f; }
if (nextNode.Waypoint.isObstructed) { return null; }
float penalty = 0.0f;
if (nextNode.Waypoint.ConnectedGap != null && nextNode.Waypoint.ConnectedGap.Open < 0.9f)
{
var door = nextNode.Waypoint.ConnectedDoor;
if (door == null)
{
penalty = 100.0f;
}
else
{
if (!CanAccessDoor(door, button =>
{
// Ignore buttons that are on the wrong side of the door
if (door.IsHorizontal)
{
if (Math.Sign(button.Item.WorldPosition.Y - door.Item.WorldPosition.Y) != Math.Sign(character.WorldPosition.Y - door.Item.WorldPosition.Y))
{
return false;
}
}
else
{
if (Math.Sign(button.Item.WorldPosition.X - door.Item.WorldPosition.X) != Math.Sign(character.WorldPosition.X - door.Item.WorldPosition.X))
{
return false;
}
}
return true;
}))
{
return null;
}
}
}
//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
{
return null;
}
}
if (node.Waypoint != null && node.Waypoint.CurrentHull != null)
{
var hull = node.Waypoint.CurrentHull;
if (hull.FireSources.Count > 0)
{
foreach (FireSource fs in hull.FireSources)
{
penalty += fs.Size.X * 10.0f;
}
}
if (character.NeedsAir && hull.WaterVolume / hull.Rect.Width > 100.0f)
{
penalty += 500.0f;
}
if (character.PressureProtection < 10.0f && hull.WaterVolume > hull.Volume)
{
penalty += 1000.0f;
}
}
return penalty;
}
public void Wander(float deltaTime, float wallAvoidDistance = 150, bool stayStillInTightSpace = true)
{
//steer away from edges of the hull
bool wander = false;
bool inWater = character.AnimController.InWater;
var currentHull = character.CurrentHull;
if (currentHull != null && !inWater)
{
float roomWidth = currentHull.Rect.Width;
if (stayStillInTightSpace && roomWidth < wallAvoidDistance * 4)
{
Reset();
}
else
{
float leftDist = character.Position.X - currentHull.Rect.X;
float rightDist = currentHull.Rect.Right - character.Position.X;
if (leftDist < wallAvoidDistance && rightDist < wallAvoidDistance)
{
if (Math.Abs(rightDist - leftDist) > wallAvoidDistance / 2)
{
SteeringManual(deltaTime, Vector2.UnitX * Math.Sign(rightDist - leftDist));
return;
}
else if (stayStillInTightSpace)
{
Reset();
return;
}
}
if (leftDist < wallAvoidDistance)
{
float speed = (wallAvoidDistance - leftDist) / wallAvoidDistance;
SteeringManual(deltaTime, Vector2.UnitX * MathHelper.Clamp(speed, 0.25f, 1));
WanderAngle = 0.0f;
}
else if (rightDist < wallAvoidDistance)
{
float speed = (wallAvoidDistance - rightDist) / wallAvoidDistance;
SteeringManual(deltaTime, -Vector2.UnitX * MathHelper.Clamp(speed, 0.25f, 1));
WanderAngle = MathHelper.Pi;
}
else
{
wander = true;
}
}
}
else
{
wander = true;
}
if (wander)
{
SteeringWander();
SteeringAvoid(deltaTime, lookAheadDistance: ConvertUnits.ToSimUnits(wallAvoidDistance), 5);
}
if (!inWater)
{
//reset vertical steering to prevent dropping down from platforms etc
ResetY();
}
}
}
}
@@ -0,0 +1,307 @@
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;
namespace Barotrauma
{
class LatchOntoAI
{
const float RaycastInterval = 5.0f;
private float raycastTimer;
private Body attachTargetBody;
private Vector2 attachSurfaceNormal;
private Submarine attachTargetSubmarine;
private bool attachToSub;
private bool attachToWalls;
private float minDeattachSpeed = 3.0f, maxDeattachSpeed = 10.0f;
private float damageOnDetach = 0.0f, detachStun = 0.0f;
private float deattachTimer;
private Vector2 wallAttachPos;
private float attachCooldown;
private Limb attachLimb;
private Vector2 localAttachPos;
private float attachLimbRotation;
private float jointDir;
private List<WeldJoint> attachJoints = new List<WeldJoint>();
public List<WeldJoint> AttachJoints
{
get { return attachJoints; }
}
public Vector2? WallAttachPos
{
get;
private set;
}
public bool IsAttached
{
get { return attachJoints.Count > 0; }
}
public bool IsAttachedToSub => IsAttached && (attachTargetBody?.UserData is Submarine || attachTargetBody?.UserData is Entity entity && entity.Submarine != null);
public LatchOntoAI(XElement element, EnemyAIController enemyAI)
{
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);
detachStun = element.GetAttributeFloat("detachstun", 0.0f);
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))
{
attachLimb = enemyAI.Character.AnimController.GetLimb(attachLimbType);
}
if (attachLimb == null) attachLimb = enemyAI.Character.AnimController.MainLimb;
enemyAI.Character.OnDeath += OnCharacterDeath;
}
public void SetAttachTarget(Body attachTarget, Submarine attachTargetSub, Vector2 attachPos, Vector2 attachSurfaceNormal)
{
attachTargetBody = attachTarget;
attachTargetSubmarine = attachTargetSub;
this.attachSurfaceNormal = attachSurfaceNormal;
wallAttachPos = attachPos;
}
public void Update(EnemyAIController enemyAI, float deltaTime)
{
Character character = enemyAI.Character;
if (character.Submarine != null)
{
DeattachFromBody();
WallAttachPos = null;
return;
}
if (attachJoints.Count > 0)
{
if (Math.Sign(attachLimb.Dir) != Math.Sign(jointDir))
{
attachJoints[0].LocalAnchorA =
new Vector2(-attachJoints[0].LocalAnchorA.X, attachJoints[0].LocalAnchorA.Y);
attachJoints[0].ReferenceAngle = -attachJoints[0].ReferenceAngle;
jointDir = attachLimb.Dir;
}
for (int i = 0; i < attachJoints.Count; i++)
{
//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)
{
DebugConsole.ThrowError("Limb body of the character \"" + character.Name + "\" is very far from the attach joint anchor -> deattach");
DeattachFromBody();
return;
}
}
}
attachCooldown -= deltaTime;
deattachTimer -= deltaTime;
Vector2 transformedAttachPos = wallAttachPos;
if (character.Submarine == null && attachTargetSubmarine != null)
{
transformedAttachPos += ConvertUnits.ToSimUnits(attachTargetSubmarine.Position);
}
if (transformedAttachPos != Vector2.Zero)
{
WallAttachPos = transformedAttachPos;
}
switch (enemyAI.State)
{
case AIState.Idle:
if (attachToWalls && character.Submarine == null && Level.Loaded != null)
{
if (!IsAttached)
{
raycastTimer -= deltaTime;
//check if there are any walls nearby the character could attach to
if (raycastTimer < 0.0f)
{
wallAttachPos = Vector2.Zero;
var cells = Level.Loaded.GetCells(character.WorldPosition, 1);
if (cells.Count > 0)
{
float closestDist = float.PositiveInfinity;
foreach (Voronoi2.VoronoiCell cell in cells)
{
foreach (Voronoi2.GraphEdge edge in cell.Edges)
{
if (MathUtils.GetLineIntersection(edge.Point1, edge.Point2, character.WorldPosition, cell.Center, out Vector2 intersection))
{
attachSurfaceNormal = edge.GetNormal(cell);
attachTargetBody = cell.Body;
Vector2 potentialAttachPos = ConvertUnits.ToSimUnits(intersection);
float distSqr = Vector2.DistanceSquared(character.SimPosition, wallAttachPos);
if (distSqr < closestDist)
{
wallAttachPos = potentialAttachPos;
closestDist = distSqr;
}
break;
}
}
}
}
raycastTimer = RaycastInterval;
}
}
}
else
{
wallAttachPos = Vector2.Zero;
}
if (wallAttachPos == Vector2.Zero)
{
DeattachFromBody();
}
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)
{
//close enough to a wall -> attach
AttachToBody(character.AnimController.Collider, attachLimb, attachTargetBody, wallAttachPos);
enemyAI.SteeringManager.Reset();
}
else
{
//move closer to the wall
DeattachFromBody();
enemyAI.SteeringManager.SteeringAvoid(deltaTime, 1.0f, 0.1f);
enemyAI.SteeringManager.SteeringSeek(wallAttachPos);
}
}
break;
case AIState.Attack:
if (enemyAI.AttackingLimb != 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 (Vector2.DistanceSquared(ConvertUnits.ToDisplayUnits(transformedAttachPos), enemyAI.AttackingLimb.WorldPosition) < enemyAI.AttackingLimb.attack.DamageRange * enemyAI.AttackingLimb.attack.DamageRange)
{
AttachToBody(character.AnimController.Collider, attachLimb, attachTargetBody, transformedAttachPos);
}
}
}
}
break;
default:
WallAttachPos = null;
DeattachFromBody();
break;
}
if (IsAttached && attachTargetBody != null && deattachTimer < 0.0f)
{
Entity entity = attachTargetBody.UserData as Entity;
Submarine attachedSub = entity is Submarine sub ? sub : entity?.Submarine;
if (attachedSub != null)
{
float velocity = attachedSub.Velocity == Vector2.Zero ? 0.0f : attachedSub.Velocity.Length();
float velocityFactor = (maxDeattachSpeed - minDeattachSpeed <= 0.0f) ?
Math.Sign(Math.Abs(velocity) - minDeattachSpeed) :
(Math.Abs(velocity) - minDeattachSpeed) / (maxDeattachSpeed - minDeattachSpeed);
if (Rand.Range(0.0f, 1.0f) < velocityFactor)
{
DeattachFromBody();
character.AddDamage(character.WorldPosition, new List<Affliction>() { AfflictionPrefab.InternalDamage.Instantiate(damageOnDetach) }, detachStun, true);
attachCooldown = 5.0f;
}
}
deattachTimer = 5.0f;
}
}
private void AttachToBody(PhysicsBody collider, Limb attachLimb, Body targetBody, Vector2 attachPos)
{
//already attached to something
if (attachJoints.Count > 0)
{
//already attached to the target body, no need to do anything
if (attachJoints[0].BodyB == targetBody) return;
DeattachFromBody();
}
jointDir = attachLimb.Dir;
Vector2 transformedLocalAttachPos = localAttachPos * attachLimb.character.AnimController.RagdollParams.LimbScale;
if (jointDir < 0.0f) transformedLocalAttachPos.X = -transformedLocalAttachPos.X;
//transformedLocalAttachPos = Vector2.Transform(transformedLocalAttachPos, Matrix.CreateRotationZ(attachLimb.Rotation));
float angle = MathUtils.VectorToAngle(-attachSurfaceNormal) - MathHelper.PiOver2 + attachLimbRotation * attachLimb.Dir;
attachLimb.body.SetTransform(attachPos + attachSurfaceNormal * transformedLocalAttachPos.Length(), angle);
var limbJoint = new WeldJoint(attachLimb.body.FarseerBody, targetBody,
transformedLocalAttachPos, targetBody.GetLocalPoint(attachPos), false)
{
FrequencyHz = 10.0f,
DampingRatio = 0.5f,
KinematicBodyB = true,
CollideConnected = false,
};
GameMain.World.Add(limbJoint);
attachJoints.Add(limbJoint);
// Limb scale is already taken into account when creating the collider.
Vector2 colliderFront = collider.GetLocalFront();
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)
{
FrequencyHz = 10.0f,
DampingRatio = 0.5f,
KinematicBodyB = true,
CollideConnected = false,
//Length = 0.1f
};
GameMain.World.Add(colliderJoint);
attachJoints.Add(colliderJoint);
}
public void DeattachFromBody()
{
foreach (Joint joint in attachJoints)
{
GameMain.World.Remove(joint);
}
attachJoints.Clear();
}
private void OnCharacterDeath(Character character, CauseOfDeath causeOfDeath)
{
DeattachFromBody();
character.OnDeath -= OnCharacterDeath;
}
}
}
@@ -0,0 +1,441 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
class NPCConversation
{
const int MaxPreviousConversations = 20;
private class ConversationCollection
{
public readonly string Identifier;
public readonly Dictionary<string, List<NPCConversation>> Conversations;
public ConversationCollection(string identifier)
{
Identifier = identifier;
Conversations = new Dictionary<string, List<NPCConversation>>();
}
public void Add(string language, string filePath, XElement subElement)
{
if (!Conversations.ContainsKey(language))
{
Conversations.Add(language, new List<NPCConversation>());
}
Conversations[language].Add(new NPCConversation(subElement, filePath));
}
public void RemoveByFile(string filePath)
{
List<string> keysToRemove = new List<string>();
foreach (var kpv in Conversations)
{
kpv.Value.RemoveAll(c => c.FilePath == filePath);
if (kpv.Value.Count == 0) { keysToRemove.Add(kpv.Key); }
}
foreach (var key in keysToRemove)
{
Conversations.Remove(key);
}
}
}
private static Dictionary<string, ConversationCollection> allConversations = new Dictionary<string, ConversationCollection>();
public readonly string FilePath;
public readonly string Line;
public readonly List<JobPrefab> AllowedJobs;
public readonly List<string> Flags;
//The line can only be selected when eventmanager intensity is between these values
//null = no restriction
public float? maxIntensity, minIntensity;
public readonly List<NPCConversation> Responses;
private readonly int speakerIndex;
private readonly List<string> allowedSpeakerTags;
public static void LoadAll(IEnumerable<ContentFile> files)
{
foreach (var file in files)
{
if (Path.GetExtension(file.Path) == ".csv") continue; // .csv files are not supported
LoadFromFile(file);
}
}
public static void LoadFromFile(ContentFile file)
{
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
if (doc == null) { return; }
string language = doc.Root.GetAttributeString("Language", "English");
string identifier = doc.Root.GetAttributeString("identifier", null);
if (string.IsNullOrWhiteSpace(identifier))
{
DebugConsole.ThrowError($"Conversations file '{file.Path}' has no identifier!");
return;
}
foreach (XElement subElement in doc.Root.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "conversation":
if (!allConversations.ContainsKey(identifier))
{
allConversations.Add(identifier, new ConversationCollection(identifier));
}
allConversations[identifier].Add(language, file.Path, subElement);
break;
case "personalitytrait":
new NPCPersonalityTrait(subElement, file.Path);
break;
}
}
}
public static void RemoveByFile(string filePath)
{
List<string> keysToRemove = new List<string>();
foreach (var kpv in allConversations)
{
kpv.Value.RemoveByFile(filePath);
if (!kpv.Value.Conversations.Any())
{
keysToRemove.Add(kpv.Key);
}
}
foreach (string key in keysToRemove)
{
allConversations.Remove(key);
}
NPCPersonalityTrait.List.RemoveAll(npt => npt.FilePath == filePath);
}
public NPCConversation(XElement element, string filePath)
{
FilePath = filePath;
Line = element.GetAttributeString("line", "");
speakerIndex = element.GetAttributeInt("speaker", 0);
AllowedJobs = new List<JobPrefab>();
string allowedJobsStr = element.GetAttributeString("allowedjobs", "");
foreach (string allowedJobIdentifier in allowedJobsStr.Split(','))
{
string key = allowedJobIdentifier.ToLowerInvariant();
if (JobPrefab.Prefabs.ContainsKey(key))
{
AllowedJobs.Add(JobPrefab.Prefabs[key]);
}
}
Flags = new List<string>(element.GetAttributeStringArray("flags", new string[0]));
allowedSpeakerTags = new List<string>();
string allowedSpeakerTagsStr = element.GetAttributeString("speakertags", "");
foreach (string tag in allowedSpeakerTagsStr.Split(','))
{
if (string.IsNullOrEmpty(tag)) continue;
allowedSpeakerTags.Add(tag.Trim().ToLowerInvariant());
}
if (element.Attribute("minintensity") != null) minIntensity = element.GetAttributeFloat("minintensity", 0.0f);
if (element.Attribute("maxintensity") != null) maxIntensity = element.GetAttributeFloat("maxintensity", 1.0f);
Responses = new List<NPCConversation>();
foreach (XElement subElement in element.Elements())
{
Responses.Add(new NPCConversation(subElement, filePath));
}
}
private static List<string> GetCurrentFlags(Character speaker)
{
var currentFlags = new List<string>();
if (Submarine.MainSub != null && Submarine.MainSub.AtDamageDepth) currentFlags.Add("SubmarineDeep");
if (GameMain.GameSession != null && Timing.TotalTime < GameMain.GameSession.RoundStartTime + 30.0f) currentFlags.Add("Initial");
if (speaker != null)
{
if (speaker.AnimController.InWater) currentFlags.Add("Underwater");
currentFlags.Add(speaker.CurrentHull == null ? "Outside" : "Inside");
if (Character.Controlled != null)
{
if (Character.Controlled.CharacterHealth.GetAffliction("psychosis") != null)
{
currentFlags.Add(speaker != Character.Controlled ? "Psychosis" : "PsychosisSelf");
}
}
var afflictions = speaker.CharacterHealth.GetAllAfflictions();
foreach (Affliction affliction in afflictions)
{
var currentEffect = affliction.Prefab.GetActiveEffect(affliction.Strength);
if (currentEffect != null && !string.IsNullOrEmpty(currentEffect.DialogFlag) && !currentFlags.Contains(currentEffect.DialogFlag))
{
currentFlags.Add(currentEffect.DialogFlag);
}
}
}
return currentFlags;
}
private static List<NPCConversation> previousConversations = new List<NPCConversation>();
public static List<Pair<Character, string>> CreateRandom(List<Character> availableSpeakers)
{
Dictionary<int, Character> assignedSpeakers = new Dictionary<int, Character>();
List<Pair<Character, string>> lines = new List<Pair<Character, string>>();
CreateConversation(availableSpeakers, assignedSpeakers, null, lines,
availableConversations: allConversations.Values.SelectMany(cc => cc.Conversations.Where(kpv => kpv.Key == TextManager.Language).SelectMany(kpv => kpv.Value)).ToList());
return lines;
}
public static List<Pair<Character, string>> CreateRandom(List<Character> availableSpeakers, List<string> requiredFlags)
{
Dictionary<int, Character> assignedSpeakers = new Dictionary<int, Character>();
List<Pair<Character, string>> lines = new List<Pair<Character, string>>();
var availableConversations = allConversations.Values.SelectMany(cc => cc.Conversations.SelectMany(
kpv => kpv.Value.Where(conversation => kpv.Key == TextManager.Language && requiredFlags.All(f => conversation.Flags.Contains(f))))).ToList();
if (availableConversations.Count > 0)
{
CreateConversation(availableSpeakers, assignedSpeakers, null, lines, availableConversations: availableConversations, ignoreFlags: true);
}
return lines;
}
private static void CreateConversation(
List<Character> availableSpeakers,
Dictionary<int, Character> assignedSpeakers,
NPCConversation baseConversation,
List<Pair<Character, string>> lineList,
List<NPCConversation> availableConversations,
bool ignoreFlags = false)
{
List<NPCConversation> conversations = baseConversation == null ? availableConversations : baseConversation.Responses;
if (conversations.Count == 0) return;
int conversationIndex = Rand.Int(conversations.Count);
NPCConversation selectedConversation = conversations[conversationIndex];
if (string.IsNullOrEmpty(selectedConversation.Line)) return;
Character speaker = null;
//speaker already assigned for this line
if (assignedSpeakers.ContainsKey(selectedConversation.speakerIndex))
{
//check if the character has all required flags to say the line
var characterFlags = GetCurrentFlags(assignedSpeakers[selectedConversation.speakerIndex]);
if (selectedConversation.Flags.All(flag => characterFlags.Contains(flag)))
{
speaker = assignedSpeakers[selectedConversation.speakerIndex];
}
}
if (speaker == null)
{
var allowedSpeakers = new List<Character>();
List<NPCConversation> potentialLines = new List<NPCConversation>(conversations);
//remove lines that are not appropriate for the intensity of the current situation
if (GameMain.GameSession?.EventManager != null)
{
potentialLines.RemoveAll(l =>
(l.minIntensity.HasValue && GameMain.GameSession.EventManager.CurrentIntensity < l.minIntensity) ||
(l.maxIntensity.HasValue && GameMain.GameSession.EventManager.CurrentIntensity > l.maxIntensity));
}
while (potentialLines.Count > 0)
{
//select a random line and attempt to find a speaker for it
// and if no valid speaker is found, choose another random line
selectedConversation = GetRandomConversation(potentialLines, baseConversation == null);
if (selectedConversation == null || string.IsNullOrEmpty(selectedConversation.Line)) return;
//speaker already assigned for this line
if (assignedSpeakers.ContainsKey(selectedConversation.speakerIndex))
{
speaker = assignedSpeakers[selectedConversation.speakerIndex];
break;
}
foreach (Character potentialSpeaker in availableSpeakers)
{
//check if the character has an appropriate job to say the line
if ((potentialSpeaker.Info?.Job != null && potentialSpeaker.Info.Job.Prefab.OnlyJobSpecificDialog) ||
selectedConversation.AllowedJobs.Count > 0)
{
if (!selectedConversation.AllowedJobs.Contains(potentialSpeaker.Info?.Job.Prefab)) continue;
}
//check if the character has all required flags to say the line
if (!ignoreFlags)
{
var characterFlags = GetCurrentFlags(potentialSpeaker);
if (!selectedConversation.Flags.All(flag => characterFlags.Contains(flag))) continue;
}
//check if the character has an appropriate personality
if (selectedConversation.allowedSpeakerTags.Count > 0)
{
if (potentialSpeaker.Info?.PersonalityTrait == null) continue;
if (!selectedConversation.allowedSpeakerTags.Any(t => potentialSpeaker.Info.PersonalityTrait.AllowedDialogTags.Any(t2 => t2 == t))) continue;
}
else
{
if (potentialSpeaker.Info?.PersonalityTrait != null &&
!potentialSpeaker.Info.PersonalityTrait.AllowedDialogTags.Contains("none"))
{
continue;
}
}
allowedSpeakers.Add(potentialSpeaker);
}
if (allowedSpeakers.Count == 0)
{
potentialLines.Remove(selectedConversation);
}
else
{
break;
}
}
if (allowedSpeakers.Count == 0) return;
speaker = allowedSpeakers[Rand.Int(allowedSpeakers.Count)];
availableSpeakers.Remove(speaker);
assignedSpeakers.Add(selectedConversation.speakerIndex, speaker);
}
if (baseConversation == null)
{
previousConversations.Insert(0, selectedConversation);
if (previousConversations.Count > MaxPreviousConversations) previousConversations.RemoveAt(MaxPreviousConversations);
}
lineList.Add(new Pair<Character, string>(speaker, selectedConversation.Line));
CreateConversation(availableSpeakers, assignedSpeakers, selectedConversation, lineList, availableConversations);
}
private static NPCConversation GetRandomConversation(List<NPCConversation> conversations, bool avoidPreviouslyUsed)
{
if (!avoidPreviouslyUsed)
{
return conversations.Count == 0 ? null : conversations[Rand.Int(conversations.Count)];
}
List<float> probabilities = new List<float>();
foreach (NPCConversation conversation in conversations)
{
probabilities.Add(GetConversationProbability(conversation));
}
return ToolBox.SelectWeightedRandom(conversations, probabilities, Rand.RandSync.Unsynced);
}
private static float GetConversationProbability(NPCConversation conversation)
{
int index = previousConversations.IndexOf(conversation);
if (index < 0) return 10.0f;
return 1.0f - 1.0f / (index + 1);
}
#if DEBUG
public static void WriteToCSV()
{
System.Text.StringBuilder sb = new System.Text.StringBuilder();
foreach (string key in allConversations.Keys)
{
foreach (string lang in allConversations[key].Conversations.Keys)
{
if (lang != TextManager.Language) { continue; }
foreach (var current in allConversations[key].Conversations[lang])
{
WriteConversation(sb, current, 0);
WriteSubConversations(sb, current.Responses, 1);
WriteEmptyRow(sb);
}
}
}
StreamWriter file = new StreamWriter(@"NPCConversations.csv");
file.WriteLine(sb.ToString());
file.Close();
}
private static void WriteConversation(System.Text.StringBuilder sb, NPCConversation conv, int depthIndex)
{
sb.Append(conv.speakerIndex); // Speaker index
sb.Append('*');
sb.Append(depthIndex); // Depth index
sb.Append('*');
sb.Append(conv.Line); // Original
sb.Append('*');
// Translated
sb.Append('*');
sb.Append(string.Join(",", conv.Flags)); // Flags
sb.Append('*');
for (int i = 0; i < conv.AllowedJobs.Count; i++) // Jobs
{
sb.Append(conv.AllowedJobs[i].Identifier);
if (i < conv.AllowedJobs.Count - 1)
{
sb.Append(",");
}
}
sb.Append('*');
sb.Append(string.Join(",", conv.allowedSpeakerTags)); // Traits
sb.Append('*');
sb.Append(conv.minIntensity); // Minimum intensity
sb.Append('*');
sb.Append(conv.maxIntensity); // Maximum intensity
sb.Append('*');
// Comments
sb.AppendLine();
}
private static void WriteSubConversations(System.Text.StringBuilder sb, List<NPCConversation> responses, int depthIndex)
{
for (int i = 0; i < responses.Count; i++)
{
WriteConversation(sb, responses[i], depthIndex);
if (responses[i].Responses != null && responses[i].Responses.Count > 0)
{
WriteSubConversations(sb, responses[i].Responses, depthIndex + 1);
}
}
}
private static void WriteEmptyRow(System.Text.StringBuilder sb)
{
for (int i = 0; i < 8; i++)
{
sb.Append('*');
}
sb.AppendLine();
}
#endif
}
}
@@ -0,0 +1,377 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Barotrauma.Extensions;
namespace Barotrauma
{
abstract class AIObjective
{
public virtual float Devotion => AIObjectiveManager.baseDevotion;
public abstract string DebugTag { get; }
public virtual bool ForceRun => false;
public virtual bool IgnoreUnsafeHulls => false;
public virtual bool AbandonWhenCannotCompleteSubjectives => true;
public virtual bool AllowSubObjectiveSorting => false;
/// <summary>
/// Can there be multiple objective instaces of the same type? Currently multiple instances allowed only for main objectives and the subobjectives of objetive loops.
/// In theory, there could be multiple subobjectives of same type for concurrent objectives, but that would make things more complex -> potential issues
/// </summary>
public virtual bool AllowMultipleInstances => false;
/// <summary>
/// Run the main objective with all subobjectives concurrently?
/// If false, the main objective will continue only when all the subobjectives have been removed (done).
/// </summary>
public virtual bool ConcurrentObjectives => false;
public virtual bool KeepDivingGearOn => false;
public virtual bool UnequipItems => false;
protected readonly List<AIObjective> subObjectives = new List<AIObjective>();
private float _cumulatedDevotion;
protected float CumulatedDevotion
{
get { return _cumulatedDevotion; }
set { _cumulatedDevotion = MathHelper.Clamp(value, 0, MaxDevotion); }
}
protected virtual float MaxDevotion => 10;
/// <summary>
/// Final priority value after all calculations.
/// </summary>
public float Priority { get; set; }
public float PriorityModifier { get; private set; } = 1;
public readonly Character character;
public readonly AIObjectiveManager objectiveManager;
public string Option { get; private set; }
private bool _abandon;
public bool Abandon
{
get { return _abandon; }
set
{
_abandon = value;
if (_abandon)
{
OnAbandon();
}
}
}
public virtual bool CanBeCompleted => !Abandon;
/// <summary>
/// When true, the objective is never completed, unless CanBeCompleted returns false.
/// </summary>
public virtual bool IsLoop { get; set; }
public IEnumerable<AIObjective> SubObjectives => subObjectives;
public AIObjective CurrentSubObjective => subObjectives.FirstOrDefault();
private readonly List<AIObjective> all = new List<AIObjective>();
public IEnumerable<AIObjective> GetSubObjectivesRecursive(bool includingSelf = false)
{
all.Clear();
if (includingSelf)
{
all.Add(this);
}
foreach (var subObjective in subObjectives)
{
all.AddRange(subObjective.GetSubObjectivesRecursive(true));
}
return all;
}
public event Action Completed;
public event Action Abandoned;
public event Action Selected;
public event Action Deselected;
protected HumanAIController HumanAIController => character.AIController as HumanAIController;
protected IndoorsSteeringManager PathSteering => HumanAIController.PathSteering;
protected SteeringManager SteeringManager => HumanAIController.SteeringManager;
public AIObjective GetActiveObjective()
{
var subObjective = CurrentSubObjective;
return subObjective == null ? this : subObjective.GetActiveObjective();
}
public AIObjective(Character character, AIObjectiveManager objectiveManager, float priorityModifier, string option = null)
{
this.objectiveManager = objectiveManager;
this.character = character;
Option = option ?? string.Empty;
PriorityModifier = priorityModifier;
}
/// <summary>
/// Makes the character act according to the objective, or according to any subobjectives that need to be completed before this one
/// </summary>
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++)
{
subObjectives[i].TryComplete(deltaTime);
if (!ConcurrentObjectives) { return; }
}
Act(deltaTime);
}
// TODO: check turret aioperate
public void AddSubObjective(AIObjective objective, bool addFirst = false)
{
var type = objective.GetType();
subObjectives.RemoveAll(o => o.GetType() == type);
if (addFirst)
{
subObjectives.Insert(0, objective);
}
else
{
subObjectives.Add(objective);
}
}
/// <summary>
/// This method allows multiple subobjectives of same type. Use with caution.
/// </summary>
public void AddSubObjectiveInQueue(AIObjective objective)
{
if (!subObjectives.Contains(objective))
{
subObjectives.Add(objective);
}
}
public void RemoveSubObjective<T>(ref T objective) where T : AIObjective
{
if (objective != null)
{
if (subObjectives.Contains(objective))
{
subObjectives.Remove(objective);
}
objective = null;
}
}
public void SortSubObjectives()
{
if (!AllowSubObjectiveSorting) { return; }
if (subObjectives.None()) { return; }
subObjectives.ForEach(so => so.GetPriority());
subObjectives.Sort((x, y) => y.Priority.CompareTo(x.Priority));
if (ConcurrentObjectives)
{
subObjectives.ForEach(so => so.SortSubObjectives());
}
else
{
subObjectives.First().SortSubObjectives();
}
}
/// <summary>
/// Call this only when the priority needs to be recalculated. Use the cached Priority property when you don't need to recalculate.
/// </summary>
public virtual float GetPriority()
{
Priority = CumulatedDevotion * PriorityModifier;
return Priority;
}
private void UpdateDevotion(float deltaTime)
{
var currentObjective = objectiveManager.CurrentObjective;
if (currentObjective != null && (currentObjective == this || currentObjective.subObjectives.Any(so => so == this)))
{
CumulatedDevotion += Devotion * PriorityModifier * deltaTime;
}
}
public virtual bool IsDuplicate<T>(T otherObjective) where T : AIObjective => otherObjective.Option == Option;
public virtual void Update(float deltaTime)
{
if (objectiveManager.CurrentOrder == this)
{
Priority = AIObjectiveManager.OrderPriority;
}
else if (objectiveManager.WaitTimer <= 0)
{
UpdateDevotion(deltaTime);
}
subObjectives.ForEach(so => so.Update(deltaTime));
}
/// <summary>
/// Checks if the subobjectives in the given collection are removed from the subobjectives. And if so, removes it also from the dictionary.
/// </summary>
protected void SyncRemovedObjectives<T1, T2>(Dictionary<T1, T2> dictionary, IEnumerable<T1> collection) where T2 : AIObjective
{
foreach (T1 key in collection)
{
if (dictionary.TryGetValue(key, out T2 objective))
{
if (!subObjectives.Contains(objective))
{
dictionary.Remove(key);
}
}
}
}
/// <summary>
/// Checks if the objective already is created and added in subobjectives. If not, creates it.
/// Handles objectives that cannot be completed. If the objective has been removed form the subobjectives, a null value is assigned to the reference.
/// Returns true if the objective was created and successfully added.
/// </summary>
protected bool TryAddSubObjective<T>(ref T objective, Func<T> constructor, Action onCompleted = null, Action onAbandon = null) where T : AIObjective
{
if (objective != null)
{
// Sub objective already found, no need to do anything if it remains in the subobjectives
// If the sub objective is removed -> it's either completed or impossible to complete.
if (!subObjectives.Contains(objective))
{
objective = null;
}
return false;
}
else
{
objective = constructor();
if (!subObjectives.Contains(objective))
{
if (objective.AllowMultipleInstances)
{
subObjectives.Add(objective);
}
else
{
AddSubObjective(objective);
}
if (onCompleted != null)
{
objective.Completed += onCompleted;
}
if (onAbandon != null)
{
objective.Abandoned += onAbandon;
}
return true;
}
#if DEBUG
DebugConsole.ThrowError("Attempted to add a duplicate subobjective!\n" + Environment.StackTrace);
#endif
return false;
}
}
public virtual void OnSelected()
{
Reset();
Selected?.Invoke();
}
public virtual void OnDeselected()
{
CumulatedDevotion = 0;
Deselected?.Invoke();
}
protected virtual void OnCompleted()
{
Completed?.Invoke();
}
protected virtual void OnAbandon()
{
Abandoned?.Invoke();
}
public virtual void Reset()
{
isCompleted = false;
hasBeenChecked = false;
_abandon = false;
CumulatedDevotion = 0;
}
protected abstract void Act(float deltaTime);
private bool isCompleted;
private bool hasBeenChecked;
public bool IsCompleted
{
get
{
if (!hasBeenChecked)
{
CheckState();
}
return isCompleted;
}
protected set
{
isCompleted = value;
}
}
protected abstract bool Check();
private bool CheckState()
{
hasBeenChecked = true;
CheckSubObjectives();
if (subObjectives.None())
{
if (Check())
{
isCompleted = true;
OnCompleted();
}
}
return isCompleted;
}
private void CheckSubObjectives()
{
for (int i = 0; i < subObjectives.Count; i++)
{
var subObjective = subObjectives[i];
subObjective.CheckState();
if (subObjective.IsCompleted)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Removing SUBobjective {subObjective.DebugTag} of {DebugTag}, because it is completed.", Color.LightGreen);
#endif
subObjectives.Remove(subObjective);
}
else if (!subObjective.CanBeCompleted)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Removing SUBobjective {subObjective.DebugTag} of {DebugTag}, because it cannot be completed.", Color.Red);
#endif
subObjectives.Remove(subObjective);
if (AbandonWhenCannotCompleteSubjectives)
{
Abandon = true;
}
}
}
}
}
}
@@ -0,0 +1,83 @@
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
class AIObjectiveChargeBatteries : AIObjectiveLoop<PowerContainer>
{
public override string DebugTag => "charge batteries";
public override bool UnequipItems => true;
private IEnumerable<PowerContainer> batteryList;
public AIObjectiveChargeBatteries(Character character, AIObjectiveManager objectiveManager, string option, float priorityModifier)
: base(character, objectiveManager, priorityModifier, option) { }
protected override bool Filter(PowerContainer battery)
{
if (battery == null) { return false; }
var item = battery.Item;
if (item.Submarine == null) { return false; }
if (item.CurrentHull == null) { return false; }
if (item.Submarine.TeamID != character.TeamID) { return false; }
if (item.ConditionPercentage <= 0) { return false; }
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(item, true)) { return false; }
if (Character.CharacterList.Any(c => c.CurrentHull == item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; }
if (IsReady(battery)) { return false; }
return true;
}
protected override float TargetEvaluation()
{
if (Option == "charge")
{
return Targets.Max(t => MathHelper.Lerp(100, 0, Math.Abs(PowerContainer.aiRechargeTargetRatio - t.RechargeRatio)));
}
else
{
return Targets.Max(t => MathHelper.Lerp(0, 100, t.RechargeRatio));
}
}
protected override IEnumerable<PowerContainer> GetList()
{
if (batteryList == null)
{
if (character == null || character.Submarine == null)
{
return new PowerContainer[0];
}
batteryList = character.Submarine.GetItems(true).Select(i => i.GetComponent<PowerContainer>()).Where(b => b != null);
}
return batteryList;
}
private bool IsReady(PowerContainer battery)
{
if (battery.HasBeenTuned && character.CurrentOrder == null) { return true; }
if (Option == "charge")
{
return battery.RechargeRatio >= PowerContainer.aiRechargeTargetRatio;
}
else
{
return battery.RechargeRatio <= 0;
}
}
protected override AIObjective ObjectiveConstructor(PowerContainer battery) =>
new AIObjectiveOperateItem(battery, character, objectiveManager, Option, false, priorityModifier: PriorityModifier)
{
IsLoop = false,
Override = character.CurrentOrder != null,
completionCondition = () => IsReady(battery)
};
protected override void OnObjectiveCompleted(AIObjective objective, PowerContainer target)
=> HumanAIController.RemoveTargets<AIObjectiveChargeBatteries, PowerContainer>(character, target);
}
}
@@ -0,0 +1,683 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
class AIObjectiveCombat : AIObjective
{
public override string DebugTag => "combat";
public override bool KeepDivingGearOn => true;
public override bool IgnoreUnsafeHulls => true;
private readonly CombatMode initialMode;
private float seekWeaponsTimer;
const float seekWeaponsInterval = 1;
private float ignoreWeaponTimer;
const float ignoredWeaponsClearTime = 10;
const float coolDown = 10.0f;
// Won't take the offensive with weapons that have lower priority than this
const float goodWeaponPriority = 30;
public Character Enemy { get; private set; }
public bool HoldPosition { get; set; }
private Item _weapon;
private Item Weapon
{
get { return _weapon; }
set
{
_weapon = value;
_weaponComponent = null;
RemoveSubObjective(ref seekAmmunition);
}
}
private ItemComponent _weaponComponent;
private ItemComponent WeaponComponent
{
get
{
if (Weapon == null) { return null; }
if (_weaponComponent == null)
{
_weaponComponent =
Weapon.GetComponent<RangedWeapon>() as ItemComponent ??
Weapon.GetComponent<MeleeWeapon>() as ItemComponent ??
Weapon.GetComponent<RepairTool>() as ItemComponent;
}
return _weaponComponent;
}
}
public override bool ConcurrentObjectives => true;
public override bool AbandonWhenCannotCompleteSubjectives => false;
private readonly AIObjectiveFindSafety findSafety;
private readonly HashSet<ItemComponent> weapons = new HashSet<ItemComponent>();
private readonly HashSet<Item> ignoredWeapons = new HashSet<Item>();
private AIObjectiveContainItem seekAmmunition;
private AIObjectiveGoTo retreatObjective;
private AIObjectiveGoTo followTargetObjective;
private Hull retreatTarget;
private float coolDownTimer;
private IEnumerable<FarseerPhysics.Dynamics.Body> myBodies;
private float aimTimer;
public enum CombatMode
{
Defensive,
Offensive,
Retreat
}
public CombatMode Mode { get; private set; }
public AIObjectiveCombat(Character character, Character enemy, CombatMode mode, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
Enemy = enemy;
coolDownTimer = coolDown;
findSafety = objectiveManager.GetObjective<AIObjectiveFindSafety>();
if (findSafety != null)
{
findSafety.Priority = 0;
HumanAIController.UnreachableHulls.Clear();
}
Mode = mode;
initialMode = Mode;
if (Enemy == null)
{
Mode = CombatMode.Retreat;
}
}
public override float GetPriority()
{
Priority = (Enemy != null && (Enemy.Removed || Enemy.IsDead)) ? 0 : Math.Min(100 * PriorityModifier, 100);
return Priority;
}
public override void Update(float deltaTime)
{
base.Update(deltaTime);
ignoreWeaponTimer -= deltaTime;
seekWeaponsTimer -= deltaTime;
if (ignoreWeaponTimer < 0)
{
ignoredWeapons.Clear();
ignoreWeaponTimer = ignoredWeaponsClearTime;
}
}
protected override bool Check()
{
bool completed = (Enemy != null && (Enemy.Removed || Enemy.IsDead)) || (initialMode != CombatMode.Offensive && coolDownTimer <= 0);
if (completed)
{
if (objectiveManager.CurrentOrder == this && Enemy != null && Enemy.IsDead)
{
character.Speak(TextManager.Get("DialogTargetDown"), null, 3.0f, "targetdown", 30.0f);
}
if (Weapon != null)
{
Unequip();
}
}
return completed;
}
protected override void Act(float deltaTime)
{
if (initialMode != CombatMode.Offensive)
{
coolDownTimer -= deltaTime;
}
if (seekAmmunition == null)
{
if (Mode != CombatMode.Retreat && TryArm() && Enemy != null && !Enemy.Removed)
{
OperateWeapon(deltaTime);
}
if (!HoldPosition && seekAmmunition == null)
{
Move();
}
}
}
private void Move()
{
switch (Mode)
{
case CombatMode.Offensive:
Engage();
break;
case CombatMode.Defensive:
case CombatMode.Retreat:
Retreat();
break;
default:
throw new NotImplementedException();
}
}
private bool IsLoaded(ItemComponent weapon) => weapon.HasRequiredContainedItems(character, addMessage: false);
private bool TryArm()
{
if (character.LockHands || Enemy == null)
{
Weapon = null;
return false;
}
if (seekWeaponsTimer < 0)
{
seekWeaponsTimer = seekWeaponsInterval;
// First go through all weapons and try to reload without seeking ammunition
var allWeapons = GetAllWeapons().ToList();
while (allWeapons.Any())
{
Weapon = GetWeapon(allWeapons, out _weaponComponent);
if (Weapon == null)
{
// No weapons
break;
}
if (!character.Inventory.Items.Contains(Weapon) || WeaponComponent == null)
{
// Not in the inventory anymore or cannot find the weapon component
allWeapons.Remove(WeaponComponent);
Weapon = null;
continue;
}
if (initialMode == CombatMode.Offensive)
{
// In the offensive mode, let's ignore weapons that cannot be used in the offensive mode
if (WeaponComponent.CombatPriority < goodWeaponPriority)
{
allWeapons.Remove(WeaponComponent);
Weapon = null;
continue;
}
}
if (IsLoaded(WeaponComponent))
{
// All good, the weapon is loaded
break;
}
if (Reload(seekAmmo: false))
{
// All good, reloading successful
break;
}
else
{
// No ammo.
allWeapons.Remove(WeaponComponent);
Weapon = null;
}
}
if (Weapon == null)
{
// No weapon found with the conditions above. Try again, now let's try to seek ammunition too
Weapon = GetWeapon(out _weaponComponent);
if (Weapon != null)
{
if (!CheckWeapon(seekAmmo: true))
{
if (seekAmmunition != null)
{
// No loaded weapon, but we are trying to seek ammunition.
return false;
}
else
{
Weapon = null;
}
}
}
}
}
else
{
if (!CheckWeapon(seekAmmo: false))
{
Weapon = null;
}
}
if (Weapon == null)
{
Mode = CombatMode.Retreat;
}
else
{
Mode = WeaponComponent.CombatPriority >= goodWeaponPriority ? initialMode : CombatMode.Defensive;
}
return Weapon != null;
bool CheckWeapon(bool seekAmmo)
{
if (!character.Inventory.Items.Contains(Weapon) || WeaponComponent == null)
{
// Not in the inventory anymore or cannot find the weapon component
return false;
}
if (!IsLoaded(WeaponComponent))
{
// Try reloading (and seek ammo)
if (!Reload(seekAmmo))
{
return false;
}
}
return true;
};
}
private void OperateWeapon(float deltaTime)
{
switch (Mode)
{
case CombatMode.Offensive:
case CombatMode.Defensive:
if (Equip())
{
Attack(deltaTime);
}
break;
case CombatMode.Retreat:
break;
default:
throw new NotImplementedException();
}
}
private Item GetWeapon(out ItemComponent weaponComponent)
{
GetAllWeapons();
return GetWeapon(weapons, out weaponComponent);
}
private Item GetWeapon(IEnumerable<ItemComponent> weaponList, out ItemComponent weaponComponent)
{
weaponComponent = weaponList.OrderByDescending(w => CalculateWeaponPriority(w)).FirstOrDefault();
if (weaponComponent == null) { return null; }
if (weaponComponent.CombatPriority < 1) { return null; }
return weaponComponent.Item;
}
private float CalculateWeaponPriority(ItemComponent weapon)
{
float priority = weapon.CombatPriority;
// Halve the priority for weapons that don't have proper ammunition loaded.
if (!weapon.HasRequiredContainedItems(character, addMessage: false))
{
priority /= 2;
}
return priority;
}
private HashSet<ItemComponent> GetAllWeapons()
{
weapons.Clear();
foreach (var item in character.Inventory.Items)
{
if (item == null) { continue; }
if (ignoredWeapons.Contains(item)) { continue; }
SeekWeapons(item, weapons);
if (item.OwnInventory != null)
{
item.OwnInventory.Items.ForEach(i => SeekWeapons(i, weapons));
}
}
return weapons;
}
private void SeekWeapons(Item item, ICollection<ItemComponent> weaponList)
{
if (item == null) { return; }
foreach (var component in item.Components)
{
if (component is RangedWeapon rw)
{
weaponList.Add(rw);
}
else if (component is MeleeWeapon mw)
{
weaponList.Add(mw);
}
else
{
var effects = component.statusEffectLists;
if (effects != null)
{
foreach (var statusEffects in effects.Values)
{
foreach (var statusEffect in statusEffects)
{
if (statusEffect.Afflictions.Any())
{
weaponList.Add(component);
}
}
}
}
}
}
}
private void Unequip()
{
if (!character.LockHands && character.SelectedItems.Contains(Weapon))
{
if (!Weapon.AllowedSlots.Contains(InvSlotType.Any) || !character.Inventory.TryPutItem(Weapon, character, new List<InvSlotType>() { InvSlotType.Any }))
{
Weapon.Drop(character);
}
}
}
private bool Equip()
{
if (character.LockHands) { return false; }
if (!WeaponComponent.HasRequiredContainedItems(character, addMessage: false))
{
return false;
}
if (!character.HasEquippedItem(Weapon))
{
Weapon.TryInteract(character, forceSelectKey: true);
var slots = Weapon.AllowedSlots.FindAll(s => s == InvSlotType.LeftHand || s == InvSlotType.RightHand || s == (InvSlotType.LeftHand | InvSlotType.RightHand));
if (character.Inventory.TryPutItem(Weapon, character, slots))
{
aimTimer = Rand.Range(0.5f, 1f);
}
else
{
Weapon = null;
Mode = CombatMode.Retreat;
return false;
}
}
return true;
}
private void Retreat()
{
RemoveSubObjective(ref followTargetObjective);
RemoveSubObjective(ref seekAmmunition);
if (retreatObjective != null && retreatObjective.Target != retreatTarget)
{
retreatObjective = null;
}
if (retreatTarget == null || (retreatObjective != null && !retreatObjective.CanBeCompleted))
{
retreatTarget = findSafety.FindBestHull(HumanAIController.VisibleHulls);
}
if (retreatTarget != null && character.CurrentHull != retreatTarget)
{
TryAddSubObjective(ref retreatObjective, () => new AIObjectiveGoTo(retreatTarget, character, objectiveManager, false, true),
onAbandon: () =>
{
if (Enemy != null && HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull))
{
// If in the same room with an enemy -> don't try to escape because we'd want to fight it
SteeringManager.Reset();
RemoveSubObjective(ref retreatObjective);
}
else
{
// else abandon and fall back to find safety mode
Abandon = true;
}
},
onCompleted: () => RemoveSubObjective(ref retreatObjective));
}
}
private void Engage()
{
if (character.LockHands || Enemy == null)
{
Mode = CombatMode.Retreat;
SteeringManager.Reset();
return;
}
retreatTarget = null;
RemoveSubObjective(ref retreatObjective);
RemoveSubObjective(ref seekAmmunition);
if (followTargetObjective != null && followTargetObjective.Target != Enemy)
{
followTargetObjective = null;
}
TryAddSubObjective(ref followTargetObjective,
constructor: () => new AIObjectiveGoTo(Enemy, character, objectiveManager, repeat: true, getDivingGearIfNeeded: true)
{
IgnoreIfTargetDead = true,
DialogueIdentifier = "dialogcannotreachtarget",
TargetName = Enemy.DisplayName
},
onAbandon: () =>
{
Mode = CombatMode.Defensive;
SteeringManager.Reset();
RemoveSubObjective(ref followTargetObjective);
});
if (followTargetObjective != null)
{
followTargetObjective.CloseEnough =
WeaponComponent is RangedWeapon ? 1000 :
WeaponComponent is MeleeWeapon mw ? mw.Range :
WeaponComponent is RepairTool rt ? rt.Range : 50;
}
}
/// <summary>
/// Seeks for more ammunition. Creates a new subobjective.
/// </summary>
private void SeekAmmunition(string[] ammunitionIdentifiers)
{
retreatTarget = null;
RemoveSubObjective(ref retreatObjective);
RemoveSubObjective(ref followTargetObjective);
TryAddSubObjective(ref seekAmmunition,
constructor: () => new AIObjectiveContainItem(character, ammunitionIdentifiers, Weapon.GetComponent<ItemContainer>(), objectiveManager)
{
targetItemCount = Weapon.GetComponent<ItemContainer>().Capacity,
checkInventory = false
},
onCompleted: () => RemoveSubObjective(ref seekAmmunition),
onAbandon: () =>
{
SteeringManager.Reset();
RemoveSubObjective(ref seekAmmunition);
ignoredWeapons.Add(Weapon);
Weapon = null;
});
}
/// <summary>
/// Reloads the ammunition found in the inventory.
/// If seekAmmo is true, tries to get find the ammo elsewhere.
/// </summary>
private bool Reload(bool seekAmmo)
{
if (WeaponComponent == null) { return false; }
if (!WeaponComponent.requiredItems.ContainsKey(RelatedItem.RelationType.Contained)) { return false; }
var containedItems = Weapon.ContainedItems;
// Drop empty ammo
foreach (Item containedItem in containedItems)
{
if (containedItem == null) { continue; }
if (containedItem.Condition <= 0)
{
containedItem.Drop(character);
}
}
RelatedItem item = null;
Item ammunition = null;
string[] ammunitionIdentifiers = null;
foreach (RelatedItem requiredItem in WeaponComponent.requiredItems[RelatedItem.RelationType.Contained])
{
ammunition = containedItems.FirstOrDefault(it => it.Condition > 0 && requiredItem.MatchesItem(it));
if (ammunition != null)
{
// Ammunition still remaining
return true;
}
item = requiredItem;
ammunitionIdentifiers = requiredItem.Identifiers;
}
// No ammo
if (ammunition == null)
{
if (ammunitionIdentifiers != null)
{
// Try reload ammunition from inventory
ammunition = character.Inventory.FindItem(i => ammunitionIdentifiers.Any(id => id == i.Prefab.Identifier || i.HasTag(id)) && i.Condition > 0, true);
if (ammunition != null)
{
var container = Weapon.GetComponent<ItemContainer>();
if (container.Item.ParentInventory == character.Inventory)
{
if (!container.Inventory.CanBePut(ammunition))
{
return false;
}
character.Inventory.RemoveItem(ammunition);
if (!container.Inventory.TryPutItem(ammunition, null))
{
ammunition.Drop(character);
}
}
else
{
container.Combine(ammunition, character);
}
}
}
}
if (WeaponComponent.HasRequiredContainedItems(character, addMessage: false))
{
return true;
}
else if (ammunition == null && !HoldPosition && initialMode == CombatMode.Offensive && seekAmmo && ammunitionIdentifiers != null)
{
SeekAmmunition(ammunitionIdentifiers);
}
return false;
}
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)
{
bool isOperatingButtons = false;
if (SteeringManager == PathSteering)
{
var door = PathSteering.CurrentPath?.CurrentNode?.ConnectedDoor;
if (door != null && !door.IsOpen)
{
isOperatingButtons = door.HasIntegratedButtons || door.Item.GetConnectedComponents<Controller>(true).Any();
}
}
if (!isOperatingButtons)
{
character.SetInput(InputType.Aim, false, true);
}
}
bool isFacing = character.AnimController.Dir > 0 && Enemy.WorldPosition.X > character.WorldPosition.X || character.AnimController.Dir < 0 && Enemy.WorldPosition.X < character.WorldPosition.X;
if (!isFacing)
{
aimTimer = Rand.Range(1f, 1.5f);
}
if (aimTimer > 0)
{
aimTimer -= deltaTime;
return;
}
if (WeaponComponent is MeleeWeapon meleeWeapon)
{
if (squaredDistance <= meleeWeapon.Range * meleeWeapon.Range)
{
character.SetInput(InputType.Shoot, false, true);
Weapon.Use(deltaTime, character);
}
}
else
{
if (WeaponComponent is RepairTool repairTool)
{
if (squaredDistance > repairTool.Range * repairTool.Range) { return; }
}
if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.Position - Weapon.Position) < MathHelper.PiOver4)
{
if (myBodies == null)
{
myBodies = character.AnimController.Limbs.Select(l => l.body.FarseerBody);
}
var collisionCategories = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel;
var pickedBody = Submarine.PickBody(Weapon.SimPosition, Enemy.SimPosition, myBodies, collisionCategories);
if (pickedBody != null)
{
Character target = null;
if (pickedBody.UserData is Character c)
{
target = c;
}
else if (pickedBody.UserData is Limb limb)
{
target = limb.character;
}
if (target != null && (target == Enemy || !HumanAIController.IsFriendly(target)))
{
character.SetInput(InputType.Shoot, false, true);
Weapon.Use(deltaTime, character);
float reloadTime = 0;
if (WeaponComponent is RangedWeapon rangedWeapon)
{
reloadTime = rangedWeapon.Reload;
}
if (WeaponComponent is MeleeWeapon mw)
{
reloadTime = mw.Reload;
}
aimTimer = reloadTime * Rand.Range(1f, 1.5f);
}
}
}
}
}
//private float CalculateEnemyStrength()
//{
// float enemyStrength = 0;
// AttackContext currentContext = character.GetAttackContext();
// foreach (Limb limb in Enemy.AnimController.Limbs)
// {
// if (limb.attack == null) continue;
// if (!limb.attack.IsValidContext(currentContext)) { continue; }
// if (!limb.attack.IsValidTarget(AttackTarget.Character)) { continue; }
// enemyStrength += limb.attack.GetTotalDamage(false);
// }
// return enemyStrength;
//}
}
}
@@ -0,0 +1,181 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
class AIObjectiveContainItem: AIObjective
{
public override string DebugTag => "contain item";
public Func<Item, float> GetItemPriority;
public int targetItemCount = 1;
public string[] ignoredContainerIdentifiers;
public bool checkInventory = true;
//can either be a tag or an identifier
public readonly string[] itemIdentifiers;
public readonly ItemContainer container;
public readonly Item item;
private AIObjectiveGetItem getItemObjective;
private AIObjectiveGoTo goToObjective;
private readonly HashSet<Item> containedItems = new HashSet<Item>();
public bool AllowToFindDivingGear { get; set; } = true;
public float ConditionLevel { get; set; }
public bool Equip { get; set; }
public bool RemoveEmpty { get; set; } = true;
public AIObjectiveContainItem(Character character, Item item, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
this.container = container;
this.item = item;
}
public AIObjectiveContainItem(Character character, string itemIdentifier, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: this(character, new string[] { itemIdentifier }, container, objectiveManager, priorityModifier) { }
public AIObjectiveContainItem(Character character, string[] itemIdentifiers, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
this.itemIdentifiers = itemIdentifiers;
for (int i = 0; i < itemIdentifiers.Length; i++)
{
itemIdentifiers[i] = itemIdentifiers[i].ToLowerInvariant();
}
this.container = container;
}
protected override bool Check()
{
if (IsCompleted) { return true; }
if (item != null)
{
return container.Inventory.Items.Contains(item);
}
else
{
int containedItemCount = 0;
foreach (Item i in container.Inventory.Items)
{
if (i != null && CheckItem(i))
{
containedItemCount++;
}
}
return containedItemCount >= targetItemCount;
}
}
private bool CheckItem(Item i) => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id)) && i.ConditionPercentage > ConditionLevel;
protected override void Act(float deltaTime)
{
if (container == null)
{
Abandon = true;
return;
}
Item itemToContain = item ?? character.Inventory.FindItem(i => CheckItem(i) && i.Container != container.Item, recursive: true);
if (itemToContain != null)
{
if (character.CanInteractWith(container.Item, out _, checkLinked: false))
{
if (RemoveEmpty)
{
foreach (var emptyItem in container.Inventory.Items)
{
if (emptyItem == null) { continue; }
if (emptyItem.Condition <= 0)
{
emptyItem.Drop(character);
}
}
}
// Contain the item
if (itemToContain.ParentInventory == character.Inventory)
{
if (!container.Inventory.CanBePut(itemToContain))
{
Abandon = true;
}
else
{
character.Inventory.RemoveItem(itemToContain);
if (container.Inventory.TryPutItem(itemToContain, null))
{
IsCompleted = true;
}
else
{
itemToContain.Drop(character);
Abandon = true;
}
}
}
else
{
if (container.Combine(itemToContain, character))
{
IsCompleted = true;
}
else
{
Abandon = true;
}
}
}
else
{
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(container.Item, character, objectiveManager, getDivingGearIfNeeded: AllowToFindDivingGear)
{
DialogueIdentifier = "dialogcannotreachtarget",
TargetName = container.Item.Name
},
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref goToObjective));
}
}
else
{
// No matching items in the inventory, try to get an item
TryAddSubObjective(ref getItemObjective, () =>
new AIObjectiveGetItem(character, itemIdentifiers, objectiveManager, equip: Equip, checkInventory: checkInventory)
{
GetItemPriority = GetItemPriority,
ignoredContainerIdentifiers = ignoredContainerIdentifiers,
ignoredItems = containedItems,
AllowToFindDivingGear = this.AllowToFindDivingGear
}, onAbandon: () =>
{
Abandon = true;
}, onCompleted: () =>
{
if (getItemObjective.TargetItem != null)
{
containedItems.Add(getItemObjective.TargetItem);
}
else
{
if (container.Inventory.FindItem(i => CheckItem(i), recursive: false) != null)
{
IsCompleted = true;
}
else
{
Abandon = true;
}
}
RemoveSubObjective(ref getItemObjective);
});
}
}
}
}
@@ -0,0 +1,134 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
namespace Barotrauma
{
class AIObjectiveDecontainItem : AIObjective
{
public override string DebugTag => "decontain item";
public Func<Item, float> GetItemPriority;
//can either be a tag or an identifier
private readonly string[] itemIdentifiers;
private readonly ItemContainer sourceContainer;
private ItemContainer targetContainer;
private readonly Item targetItem;
private AIObjectiveGoTo goToObjective;
private AIObjectiveContainItem containObjective;
public bool Equip { get; set; }
/// <summary>
/// If true drops the item when containing the item fails.
/// In both cases abandons the objective.
/// Note that has no effect if the target container was not defined (always drops) -> completes when the item is dropped.
/// </summary>
public bool DropIfFailsToContain { get; set; } = true;
public AIObjectiveDecontainItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, ItemContainer sourceContainer = null, ItemContainer targetContainer = null, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
this.targetItem = targetItem;
this.sourceContainer = sourceContainer;
this.targetContainer = targetContainer;
}
public AIObjectiveDecontainItem(Character character, string itemIdentifier, AIObjectiveManager objectiveManager, ItemContainer sourceContainer, ItemContainer targetContainer = null, float priorityModifier = 1)
: this(character, new string[] { itemIdentifier }, objectiveManager, sourceContainer, targetContainer, priorityModifier) { }
public AIObjectiveDecontainItem(Character character, string[] itemIdentifiers, AIObjectiveManager objectiveManager, ItemContainer sourceContainer, ItemContainer targetContainer = null, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
this.itemIdentifiers = itemIdentifiers;
for (int i = 0; i < itemIdentifiers.Length; i++)
{
itemIdentifiers[i] = itemIdentifiers[i].ToLowerInvariant();
}
this.sourceContainer = sourceContainer;
this.targetContainer = targetContainer;
}
protected override bool Check() => IsCompleted;
protected override void Act(float deltaTime)
{
Item itemToDecontain = targetItem ?? sourceContainer.Inventory.FindItem(i => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id)), recursive: false);
if (itemToDecontain == null)
{
Abandon = true;
return;
}
if (targetContainer == null)
{
if (sourceContainer == null)
{
Abandon = true;
return;
}
if (itemToDecontain.Container != sourceContainer.Item)
{
IsCompleted = true;
return;
}
}
else
{
if (targetContainer.Inventory.Items.Contains(itemToDecontain))
{
IsCompleted = true;
return;
}
}
if (goToObjective == null && !itemToDecontain.IsOwnedBy(character))
{
if (sourceContainer == null)
{
Abandon = true;
return;
}
if (!character.CanInteractWith(sourceContainer.Item, out _, checkLinked: false))
{
TryAddSubObjective(ref goToObjective,
constructor: () => new AIObjectiveGoTo(sourceContainer.Item, character, objectiveManager)
{
// If the container changes, the item is no longer where it was
abortCondition = () => itemToDecontain.Container != sourceContainer.Item,
DialogueIdentifier = "dialogcannotreachtarget",
TargetName = sourceContainer.Item.Name
},
onAbandon: () => Abandon = true);
return;
}
}
if (targetContainer != null)
{
TryAddSubObjective(ref containObjective,
constructor: () => new AIObjectiveContainItem(character, itemToDecontain, targetContainer, objectiveManager)
{
Equip = this.Equip,
RemoveEmpty = false,
GetItemPriority = this.GetItemPriority,
ignoredContainerIdentifiers = sourceContainer != null ? new string[] { sourceContainer.Item.Prefab.Identifier } : null
},
onCompleted: () => IsCompleted = true,
onAbandon: () =>
{
if (DropIfFailsToContain)
{
itemToDecontain.Drop(character);
}
Abandon = true;
});
}
else
{
itemToDecontain.Drop(character);
IsCompleted = true;
}
}
}
}
@@ -0,0 +1,146 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
class AIObjectiveExtinguishFire : AIObjective
{
public override string DebugTag => "extinguish fire";
public override bool ForceRun => true;
public override bool ConcurrentObjectives => true;
public override bool KeepDivingGearOn => true;
private readonly Hull targetHull;
private AIObjectiveGetItem getExtinguisherObjective;
private AIObjectiveGoTo gotoObjective;
private float useExtinquisherTimer;
public AIObjectiveExtinguishFire(Character character, Hull targetHull, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
this.targetHull = targetHull;
}
public override float GetPriority()
{
if (!objectiveManager.IsCurrentOrder<AIObjectiveExtinguishFires>()
&& Character.CharacterList.Any(c => c.CurrentHull == targetHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c)))
{
Priority = 0;
}
else
{
float yDist = Math.Abs(character.WorldPosition.Y - targetHull.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 3 : 0;
float dist = Math.Abs(character.WorldPosition.X - targetHull.WorldPosition.X) + yDist;
float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, dist));
if (targetHull == character.CurrentHull)
{
distanceFactor = 1;
}
float severity = AIObjectiveExtinguishFires.GetFireSeverity(targetHull);
float severityFactor = MathHelper.Lerp(0, 1, severity / 100);
float devotion = CumulatedDevotion / 100;
Priority = MathHelper.Lerp(0, 100, MathHelper.Clamp(devotion + (severityFactor * distanceFactor * PriorityModifier), 0, 1));
}
return Priority;
}
protected override bool Check() => targetHull.FireSources.None();
private float sinTime;
protected override void Act(float deltaTime)
{
var extinguisherItem = character.Inventory.FindItemByIdentifier("fireextinguisher") ?? character.Inventory.FindItemByTag("fireextinguisher");
if (extinguisherItem == null || extinguisherItem.Condition <= 0.0f || !character.HasEquippedItem(extinguisherItem))
{
TryAddSubObjective(ref getExtinguisherObjective, () =>
{
character.Speak(TextManager.Get("DialogFindExtinguisher"), null, 2.0f, "findextinguisher", 30.0f);
return new AIObjectiveGetItem(character, "fireextinguisher", objectiveManager, equip: true)
{
// If the item is inside an unsafe hull, decrease the priority
GetItemPriority = i => HumanAIController.UnsafeHulls.Contains(i.CurrentHull) ? 0.1f : 1
};
});
}
else
{
var extinguisher = extinguisherItem.GetComponent<RepairTool>();
if (extinguisher == null)
{
#if DEBUG
DebugConsole.ThrowError($"{character.Name}: AIObjectiveExtinguishFire failed - the item \"" + extinguisherItem + "\" has no RepairTool component but is tagged as an extinguisher");
#endif
Abandon = true;
return;
}
foreach (FireSource fs in targetHull.FireSources)
{
bool inRange = fs.IsInDamageRange(character, MathHelper.Clamp(fs.DamageRange * 1.5f, extinguisher.Range * 0.5f, extinguisher.Range));
bool move = !inRange || !HumanAIController.VisibleHulls.Contains(fs.Hull);
if (inRange || useExtinquisherTimer > 0.0f)
{
useExtinquisherTimer += deltaTime;
if (useExtinquisherTimer > 2.0f)
{
useExtinquisherTimer = 0.0f;
}
// Aim
character.CursorPosition = fs.Position;
Vector2 fromCharacterToFireSource = fs.WorldPosition - character.WorldPosition;
float dist = fromCharacterToFireSource.Length();
character.CursorPosition += VectorExtensions.Forward(extinguisherItem.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, dist / 2);
if (extinguisherItem.RequireAimToUse)
{
bool isOperatingButtons = false;
if (SteeringManager == PathSteering)
{
var door = PathSteering.CurrentPath?.CurrentNode?.ConnectedDoor;
if (door != null && !door.IsOpen)
{
isOperatingButtons = door.HasIntegratedButtons || door.Item.GetConnectedComponents<Controller>(true).Any();
}
}
if (!isOperatingButtons)
{
character.SetInput(InputType.Aim, false, true);
}
sinTime += deltaTime * 10;
}
character.SetInput(extinguisherItem.IsShootable ? InputType.Shoot : InputType.Use, false, true);
extinguisher.Use(deltaTime, character);
if (!targetHull.FireSources.Contains(fs))
{
character.Speak(TextManager.GetWithVariable("DialogPutOutFire", "[roomname]", targetHull.DisplayName, true), null, 0, "putoutfire", 10.0f);
}
if (!character.CanSeeTarget(fs))
{
move = true;
}
}
if (move)
{
//go to the first firesource
TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(fs, character, objectiveManager, closeEnough: extinguisher.Range / 2)
{
DialogueIdentifier = "dialogcannotreachfire",
TargetName = fs.Hull.DisplayName
},
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref gotoObjective));
}
else
{
character.AIController.SteeringManager.Reset();
}
break;
}
}
}
}
}
@@ -0,0 +1,39 @@
using System.Linq;
using System.Collections.Generic;
using Barotrauma.Extensions;
using System;
namespace Barotrauma
{
class AIObjectiveExtinguishFires : AIObjectiveLoop<Hull>
{
public override string DebugTag => "extinguish fires";
public override bool ForceRun => true;
public AIObjectiveExtinguishFires(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { }
protected override bool Filter(Hull hull) => IsValidTarget(hull, character);
protected override float TargetEvaluation() => objectiveManager.CurrentObjective == this ? 100 : Targets.Sum(t => GetFireSeverity(t));
public static float GetFireSeverity(Hull hull) => hull.FireSources.Sum(fs => fs.Size.X);
protected override IEnumerable<Hull> GetList() => Hull.hullList;
protected override AIObjective ObjectiveConstructor(Hull target)
=> new AIObjectiveExtinguishFire(character, target, objectiveManager, PriorityModifier);
protected override void OnObjectiveCompleted(AIObjective objective, Hull target)
=> HumanAIController.RemoveTargets<AIObjectiveExtinguishFires, Hull>(character, target);
public static bool IsValidTarget(Hull hull, Character character)
{
if (hull == null) { return false; }
if (hull.FireSources.None()) { return false; }
if (hull.Submarine == null) { return false; }
if (hull.Submarine.TeamID != character.TeamID) { return false; }
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(hull, true)) { return false; }
return true;
}
}
}
@@ -0,0 +1,47 @@
using Barotrauma.Items.Components;
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Barotrauma.Extensions;
namespace Barotrauma
{
class AIObjectiveFightIntruders : AIObjectiveLoop<Character>
{
public override string DebugTag => "fight intruders";
protected override float IgnoreListClearInterval => 30;
public override bool IgnoreUnsafeHulls => true;
public AIObjectiveFightIntruders(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier) { }
protected override bool Filter(Character target) => IsValidTarget(target, character);
protected override IEnumerable<Character> GetList() => Character.CharacterList;
protected override float TargetEvaluation()
{
// TODO: sorting criteria
return 100;
}
protected override AIObjective ObjectiveConstructor(Character target)
=> new AIObjectiveCombat(character, target, AIObjectiveCombat.CombatMode.Offensive, objectiveManager, PriorityModifier);
protected override void OnObjectiveCompleted(AIObjective objective, Character target)
=> HumanAIController.RemoveTargets<AIObjectiveFightIntruders, Character>(character, target);
public static bool IsValidTarget(Character target, Character character)
{
if (target == null || target.IsDead || target.Removed) { return false; }
if (target == character) { return false; }
if (HumanAIController.IsFriendly(character, target)) { return false; }
if (target.Submarine == null) { return false; }
if (target.Submarine.TeamID != character.TeamID) { return false; }
if (target.CurrentHull == null) { return false; }
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, true)) { return false; }
return true;
}
}
}
@@ -0,0 +1,130 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using Barotrauma.Extensions;
namespace Barotrauma
{
class AIObjectiveFindDivingGear : AIObjective
{
public override string DebugTag => $"find diving gear ({gearTag})";
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
private readonly string gearTag;
private readonly string fallbackTag;
private AIObjectiveGetItem getDivingGear;
private AIObjectiveContainItem getOxygen;
public static float lowOxygenThreshold = 10;
protected override bool Check() => HumanAIController.HasItem(character, gearTag, "oxygensource") || HumanAIController.HasItem(character, fallbackTag, "oxygensource");
public AIObjectiveFindDivingGear(Character character, bool needDivingSuit, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier)
{
gearTag = needDivingSuit ? "divingsuit" : "divingmask";
fallbackTag = needDivingSuit ? "divingsuit" : "diving";
}
protected override void Act(float deltaTime)
{
if (character.LockHands)
{
Abandon = true;
return;
}
var item = character.Inventory.FindItemByIdentifier(gearTag, true) ?? character.Inventory.FindItemByTag(gearTag, true);
if (item == null && fallbackTag != gearTag)
{
item = character.Inventory.FindItemByTag(fallbackTag, true);
}
if (item == null || !character.HasEquippedItem(item))
{
TryAddSubObjective(ref getDivingGear, () =>
{
if (item == null)
{
character.Speak(TextManager.Get("DialogGetDivingGear"), null, 0.0f, "getdivinggear", 30.0f);
}
return new AIObjectiveGetItem(character, gearTag, objectiveManager, equip: true) { AllowToFindDivingGear = false };
},
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref getDivingGear));
}
else
{
var containedItems = item.ContainedItems;
if (containedItems == null)
{
#if DEBUG
DebugConsole.ThrowError($"{character.Name}: AIObjectiveFindDivingGear failed - the item \"" + item + "\" has no proper inventory");
#endif
Abandon = true;
return;
}
// Drop empty tanks
foreach (Item containedItem in containedItems)
{
if (containedItem == null) { continue; }
if (containedItem.Condition <= 0.0f)
{
containedItem.Drop(character);
}
}
if (containedItems.None(it => it.HasTag("oxygensource") && it.Condition > lowOxygenThreshold))
{
var oxygenTank = character.Inventory.FindItemByTag("oxygensource", true);
if (oxygenTank != null)
{
var container = item.GetComponent<ItemContainer>();
if (container.Item.ParentInventory == character.Inventory)
{
if (!container.Inventory.CanBePut(oxygenTank))
{
Abandon = true;
}
character.Inventory.RemoveItem(oxygenTank);
if (!container.Inventory.TryPutItem(oxygenTank, null))
{
oxygenTank.Drop(character);
Abandon = true;
}
}
else
{
container.Combine(oxygenTank, character);
}
}
else
{
// Seek oxygen that has min 10% condition left
TryAddSubObjective(ref getOxygen, () =>
{
character.Speak(TextManager.Get("DialogGetOxygenTank"), null, 0, "getoxygentank", 30.0f);
return new AIObjectiveContainItem(character, new string[] { "oxygensource" }, item.GetComponent<ItemContainer>(), objectiveManager)
{
AllowToFindDivingGear = false,
ConditionLevel = lowOxygenThreshold
};
},
onAbandon: () =>
{
// Try to seek any oxygen sources
TryAddSubObjective(ref getOxygen, () =>
{
return new AIObjectiveContainItem(character, new string[] { "oxygensource" }, item.GetComponent<ItemContainer>(), objectiveManager)
{
AllowToFindDivingGear = false,
ConditionLevel = 0
};
},
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref getOxygen));
},
onCompleted: () => RemoveSubObjective(ref getOxygen));
}
}
}
}
}
}
@@ -0,0 +1,293 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
class AIObjectiveFindSafety : AIObjective
{
public override string DebugTag => "find safety";
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
public override bool IgnoreUnsafeHulls => true;
public override bool ConcurrentObjectives => true;
public override bool IsLoop { get => true; set => throw new System.Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace); }
// TODO: expose?
const float priorityIncrease = 100;
const float priorityDecrease = 10;
const float SearchHullInterval = 3.0f;
private float currenthullSafety;
private float searchHullTimer;
private AIObjectiveGoTo goToObjective;
private AIObjectiveFindDivingGear divingGearObjective;
public AIObjectiveFindSafety(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { }
protected override bool Check() => false;
public override bool CanBeCompleted => true;
private bool resetPriority;
public override float GetPriority() => Priority;
public override void Update(float deltaTime)
{
if (resetPriority)
{
Priority = 0;
resetPriority = false;
return;
}
if (character.CurrentHull == null)
{
currenthullSafety = 0;
Priority = objectiveManager.CurrentOrder is AIObjectiveGoTo ? 0 : 100;
return;
}
if (HumanAIController.NeedsDivingGear(character, character.CurrentHull, out _) && !HumanAIController.HasDivingGear(character))
{
Priority = 100;
}
currenthullSafety = HumanAIController.CurrentHullSafety;
if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD)
{
Priority -= priorityDecrease * deltaTime;
}
else
{
float dangerFactor = (100 - currenthullSafety) / 100;
Priority += dangerFactor * priorityIncrease * deltaTime;
}
Priority = MathHelper.Clamp(Priority, 0, 100);
if (divingGearObjective != null && !divingGearObjective.IsCompleted && divingGearObjective.CanBeCompleted)
{
// Boost the priority while seeking the diving gear
Priority = Math.Max(Priority, Math.Min(AIObjectiveManager.OrderPriority + 20, 100));
}
}
private Hull currentSafeHull;
private Hull previousSafeHull;
protected override void Act(float deltaTime)
{
var currentHull = character.CurrentHull;
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);
});
}
else if (divingGearObjective == null || !divingGearObjective.CanBeCompleted)
{
if (currenthullSafety < HumanAIController.HULL_SAFETY_THRESHOLD)
{
searchHullTimer = Math.Min(1, searchHullTimer);
}
if (searchHullTimer > 0.0f)
{
searchHullTimer -= deltaTime;
}
else
{
searchHullTimer = SearchHullInterval * Rand.Range(0.9f, 1.1f);
previousSafeHull = currentSafeHull;
currentSafeHull = FindBestHull();
if (currentSafeHull == null)
{
currentSafeHull = previousSafeHull;
}
if (currentSafeHull != null && currentSafeHull != currentHull)
{
if (goToObjective?.Target != currentSafeHull)
{
RemoveSubObjective(ref goToObjective);
}
TryAddSubObjective(ref goToObjective,
constructor: () => new AIObjectiveGoTo(currentSafeHull, character, objectiveManager, getDivingGearIfNeeded: true)
{
AllowGoingOutside = HumanAIController.HasDivingSuit(character, conditionPercentage: 50)
},
onCompleted: () =>
{
if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD ||
HumanAIController.NeedsDivingGear(character, currentHull, out bool needsSuit) && (needsSuit ? HumanAIController.HasDivingSuit(character) : HumanAIController.HasDivingMask(character)))
{
resetPriority = true;
searchHullTimer = Math.Min(1, searchHullTimer);
}
RemoveSubObjective(ref goToObjective);
// If diving gear objective failed, let's reset it here.
RemoveSubObjective(ref divingGearObjective);
},
onAbandon: () =>
{
// Don't ignore any hulls if outside, because apparently it happens that we can't find a path, in which case we just want to try again.
// If we ignore the hull, it might be the only airlock in the target sub, which ignores the whole sub.
if (currentHull != null && goToObjective != null)
{
if (goToObjective.Target is Hull hull)
{
HumanAIController.UnreachableHulls.Add(hull);
}
}
RemoveSubObjective(ref goToObjective);
});
}
else
{
RemoveSubObjective(ref goToObjective);
}
}
if (subObjectives.Any(so => so.CanBeCompleted)) { return; }
if (currentHull != null)
{
//goto objective doesn't exist (a safe hull not found, or a path to a safe hull not found)
// -> attempt to manually steer away from hazards
Vector2 escapeVel = Vector2.Zero;
// TODO: optimize
foreach (FireSource fireSource in HumanAIController.VisibleHulls.SelectMany(h => h.FireSources))
{
Vector2 dir = character.Position - fireSource.Position;
float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(fireSource.Position, character.Position), 0.1f, 10.0f);
escapeVel += new Vector2(Math.Sign(dir.X) * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier);
}
foreach (Character enemy in Character.CharacterList)
{
if (!HumanAIController.IsActive(enemy) || HumanAIController.IsFriendly(enemy)) { continue; }
if (HumanAIController.VisibleHulls.Contains(enemy.CurrentHull))
{
Vector2 dir = character.Position - enemy.Position;
float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(enemy.Position, character.Position), 0.1f, 10.0f);
escapeVel += new Vector2(Math.Sign(dir.X) * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier);
}
}
if (escapeVel != Vector2.Zero)
{
float left = currentHull.Rect.X + 50;
float right = currentHull.Rect.Right - 50;
//only move if we haven't reached the edge of the room
if (escapeVel.X < 0 && character.Position.X > left || escapeVel.X > 0 && character.Position.X < right)
{
character.AIController.SteeringManager.SteeringManual(deltaTime, escapeVel);
}
else
{
character.AnimController.TargetDir = escapeVel.X < 0.0f ? Direction.Right : Direction.Left;
character.AIController.SteeringManager.Reset();
}
return;
}
}
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
}
}
public Hull FindBestHull(IEnumerable<Hull> ignoredHulls = null, bool allowChangingTheSubmarine = true)
{
Hull bestHull = null;
float bestValue = 0;
foreach (Hull hull in Hull.hullList)
{
if (hull.Submarine == null) { continue; }
if (!allowChangingTheSubmarine && hull.Submarine != character.Submarine) { continue; }
if (ignoredHulls != null && ignoredHulls.Contains(hull)) { continue; }
if (HumanAIController.UnreachableHulls.Contains(hull)) { continue; }
float hullSafety = 0;
if (character.CurrentHull != null && character.Submarine != null)
{
// Inside
if (!character.Submarine.IsConnectedTo(hull.Submarine)) { continue; }
hullSafety = HumanAIController.GetHullSafety(hull, hull.GetConnectedHulls(true, 1), character);
float yDist = Math.Abs(character.WorldPosition.Y - hull.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 3 : 0;
float dist = Math.Abs(character.WorldPosition.X - hull.WorldPosition.X) + yDist;
float distanceFactor = MathHelper.Lerp(1, 0.9f, MathUtils.InverseLerp(0, 10000, dist));
hullSafety *= distanceFactor;
//skip the hull if the safety is already less than the best hull
//(no need to do the expensive pathfinding if we already know we're not going to choose this hull)
if (hullSafety < bestValue) { continue; }
// Don't allow to go outside if not already outside.
var path = character.CurrentHull != null ?
PathSteering.PathFinder.FindPath(character.SimPosition, hull.SimPosition, nodeFilter: node => node.Waypoint.CurrentHull != null) :
PathSteering.PathFinder.FindPath(character.SimPosition, hull.SimPosition);
if (path.Unreachable && character.CurrentHull != null)
{
HumanAIController.UnreachableHulls.Add(hull);
continue;
}
// Each unsafe node reduces the hull safety value.
// Ignore the current hull, because otherwise we couldn't find a path out.
int unsafeNodes = path.Nodes.Count(n => n.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(n.CurrentHull));
hullSafety /= 1 + unsafeNodes;
// If the target is not inside a friendly submarine, considerably reduce the hull safety.
if (!character.Submarine.IsEntityFoundOnThisSub(hull, true))
{
hullSafety /= 10;
}
}
else
{
// Outside
if (hull.RoomName != null && hull.RoomName.Contains("airlock", StringComparison.OrdinalIgnoreCase))
{
hullSafety = 100;
}
else
{
// TODO: could also target gaps that get us inside?
foreach (Item item in Item.ItemList)
{
if (item.CurrentHull != hull && item.HasTag("airlock"))
{
hullSafety = 100;
break;
}
}
}
// TODO: could we get a closest door to the outside and target the flowing hull if no airlock is found?
// Huge preference for closer targets
float distance = Vector2.DistanceSquared(character.WorldPosition, hull.WorldPosition);
float distanceFactor = MathHelper.Lerp(1, 0.2f, MathUtils.InverseLerp(0, MathUtils.Pow(100000, 2), distance));
hullSafety *= distanceFactor;
// If the target is not inside a friendly submarine, considerably reduce the hull safety.
if (hull.Submarine.TeamID != character.TeamID && hull.Submarine.TeamID != Character.TeamType.FriendlyNPC)
{
hullSafety /= 10;
}
}
if (hullSafety > bestValue)
{
bestHull = hull;
bestValue = hullSafety;
}
}
return bestHull;
}
}
}
@@ -0,0 +1,148 @@
using Barotrauma.Items.Components;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
class AIObjectiveFixLeak : AIObjective
{
public override string DebugTag => "fix leak";
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
public Gap Leak { get; private set; }
private AIObjectiveGetItem getWeldingTool;
private AIObjectiveContainItem refuelObjective;
private AIObjectiveGoTo gotoObjective;
private AIObjectiveOperateItem operateObjective;
public AIObjectiveFixLeak(Gap leak, Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base (character, objectiveManager, priorityModifier)
{
Leak = leak;
}
protected override bool Check() => Leak.Open <= 0 || Leak.Removed;
public override float GetPriority()
{
if (Leak.Removed || Leak.Open <= 0)
{
Priority = 0;
}
else
{
float xDist = Math.Abs(character.WorldPosition.X - Leak.WorldPosition.X);
float yDist = Math.Abs(character.WorldPosition.Y - Leak.WorldPosition.Y);
// Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally).
// If the target is close, ignore the distance factor alltogether so that we keep fixing the leaks that are nearby.
float distanceFactor = xDist < 200 && yDist < 100 ? 1 : MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, xDist + yDist * 3.0f));
float severity = AIObjectiveFixLeaks.GetLeakSeverity(Leak) / 100;
float max = Math.Min((AIObjectiveManager.OrderPriority - 1), 90);
float devotion = CumulatedDevotion / 100;
Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1));
}
return Priority;
}
protected override void Act(float deltaTime)
{
var weldingTool = character.Inventory.FindItemByTag("weldingequipment", true);
if (weldingTool == null)
{
TryAddSubObjective(ref getWeldingTool, () => new AIObjectiveGetItem(character, "weldingequipment", objectiveManager, true),
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref getWeldingTool));
return;
}
else
{
var containedItems = weldingTool.ContainedItems;
if (containedItems == null)
{
#if DEBUG
DebugConsole.ThrowError($"{character.Name}: AIObjectiveFixLeak failed - the item \"" + weldingTool + "\" has no proper inventory");
#endif
Abandon = true;
return;
}
// Drop empty tanks
foreach (Item containedItem in containedItems)
{
if (containedItem == null) { continue; }
if (containedItem.Condition <= 0.0f)
{
containedItem.Drop(character);
}
}
if (containedItems.None(i => i.HasTag("weldingfuel") && i.Condition > 0.0f))
{
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfuel", weldingTool.GetComponent<ItemContainer>(), objectiveManager),
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref refuelObjective));
return;
}
}
if (subObjectives.Any()) { return; }
var repairTool = weldingTool.GetComponent<RepairTool>();
if (repairTool == null)
{
#if DEBUG
DebugConsole.ThrowError($"{character.Name}: AIObjectiveFixLeak failed - the item \"" + weldingTool + "\" has no RepairTool component but is tagged as a welding tool");
#endif
Abandon = true;
return;
}
Vector2 toLeak = Leak.WorldPosition - character.WorldPosition;
// TODO: use the collider size/reach?
if (!character.AnimController.InWater && Math.Abs(toLeak.X) < 100 && toLeak.Y < 0.0f && toLeak.Y > -150)
{
HumanAIController.AnimController.Crouching = true;
}
float reach = repairTool.Range + ConvertUnits.ToDisplayUnits(((HumanoidAnimController)character.AnimController).ArmLength);
bool canOperate = toLeak.LengthSquared() < reach * reach;
if (canOperate)
{
TryAddSubObjective(ref operateObjective, () => new AIObjectiveOperateItem(repairTool, character, objectiveManager, option: "", requireEquip: true, operateTarget: Leak),
onAbandon: () => Abandon = true,
onCompleted: () =>
{
if (Check()) { IsCompleted = true; }
else
{
// Failed to operate. Probably too far.
Abandon = true;
}
});
}
else
{
TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(Leak, character, objectiveManager)
{
AllowGoingOutside = !Leak.IsRoomToRoom && objectiveManager.IsCurrentOrder<AIObjectiveFixLeaks>() && HumanAIController.HasDivingSuit(character, conditionPercentage: 50),
CloseEnough = reach,
DialogueIdentifier = Leak.FlowTargetHull != null ? "dialogcannotreachleak" : null,
TargetName = Leak.FlowTargetHull?.DisplayName
},
onAbandon: () =>
{
if (Check()) { IsCompleted = true; }
else if ((Leak.WorldPosition - character.WorldPosition).LengthSquared() > reach * reach * 2)
{
// Too far
Abandon = true;
}
else
{
// We are close, try again.
RemoveSubObjective(ref gotoObjective);
}
},
onCompleted: () => RemoveSubObjective(ref gotoObjective));
}
}
}
}
@@ -0,0 +1,78 @@
using Microsoft.Xna.Framework;
using System;
using System.Linq;
using Barotrauma.Extensions;
using System.Collections.Generic;
namespace Barotrauma
{
class AIObjectiveFixLeaks : AIObjectiveLoop<Gap>
{
public override string DebugTag => "fix leaks";
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
public AIObjectiveFixLeaks(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { }
protected override bool Filter(Gap gap) => IsValidTarget(gap, character);
public static float GetLeakSeverity(Gap leak)
{
if (leak == null) { return 0; }
float sizeFactor = MathHelper.Lerp(1, 10, MathUtils.InverseLerp(0, 200, leak.Size));
float severity = sizeFactor * leak.Open;
if (!leak.IsRoomToRoom)
{
severity *= 10;
// If there is a leak in the outer walls, the severity cannot be lower than 10, no matter how small the leak
return MathHelper.Clamp(severity, 10, 100);
}
else
{
return MathHelper.Min(severity, 100);
}
}
protected override float TargetEvaluation()
{
int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveFixLeaks>(), onlyBots: true);
int totalLeaks = Targets.Count();
if (totalLeaks == 0) { return 0; }
int secondaryLeaks = Targets.Count(l => l.IsRoomToRoom);
int leaks = totalLeaks - secondaryLeaks;
bool anyFixers = otherFixers > 0;
if (objectiveManager.CurrentOrder == this)
{
float ratio = anyFixers ? totalLeaks / (float)otherFixers : 1;
return Targets.Sum(t => GetLeakSeverity(t)) * ratio;
}
else
{
float ratio = leaks == 0 ? 1 : anyFixers ? leaks / otherFixers : 1;
if (anyFixers && (ratio <= 1 || otherFixers > 5 || otherFixers / (float)HumanAIController.CountCrew(onlyBots: true) > 0.75f))
{
// Enough fixers
return 0;
}
return Targets.Sum(t => GetLeakSeverity(t)) * ratio;
}
}
protected override IEnumerable<Gap> GetList() => Gap.GapList;
protected override AIObjective ObjectiveConstructor(Gap gap)
=> new AIObjectiveFixLeak(gap, character, objectiveManager, PriorityModifier);
protected override void OnObjectiveCompleted(AIObjective objective, Gap target)
=> HumanAIController.RemoveTargets<AIObjectiveFixLeaks, Gap>(character, target);
public static bool IsValidTarget(Gap gap, Character character)
{
if (gap == null) { return false; }
if (gap.ConnectedWall == null || gap.ConnectedDoor != null || gap.Open <= 0 || gap.linkedTo.All(l => l == null)) { return false; }
if (gap.Submarine == null) { return false; }
if (gap.Submarine.TeamID != character.TeamID) { return false; }
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(gap, true)) { return false; }
return true;
}
}
}
@@ -0,0 +1,296 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
class AIObjectiveGetItem : AIObjective
{
public override string DebugTag => "get item";
private readonly bool equip;
public HashSet<Item> ignoredItems = new HashSet<Item>();
public Func<Item, float> GetItemPriority;
public Func<Item, bool> ItemFilter;
public float TargetCondition { get; set; } = 1;
//can be either tags or identifiers
private string[] itemIdentifiers;
public IEnumerable<string> Identifiers => itemIdentifiers;
private Item targetItem, moveToTarget, rootContainer;
private bool isDoneSeeking;
public Item TargetItem => targetItem;
private int currSearchIndex;
public string[] ignoredContainerIdentifiers;
private AIObjectiveGoTo goToObjective;
private float currItemPriority;
private bool checkInventory;
public bool AllowToFindDivingGear { get; set; } = true;
public AIObjectiveGetItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, bool equip = true, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
currSearchIndex = -1;
this.equip = equip;
this.targetItem = targetItem;
}
public AIObjectiveGetItem(Character character, string itemIdentifier, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1)
: this(character, new string[] { itemIdentifier }, objectiveManager, equip, checkInventory, priorityModifier) { }
public AIObjectiveGetItem(Character character, string[] itemIdentifiers, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
currSearchIndex = -1;
this.equip = equip;
this.itemIdentifiers = itemIdentifiers;
for (int i = 0; i < itemIdentifiers.Length; i++)
{
itemIdentifiers[i] = itemIdentifiers[i].ToLowerInvariant();
}
this.checkInventory = checkInventory;
}
private bool CheckInventory()
{
if (itemIdentifiers == null) { return false; }
var item = character.Inventory.FindItem(i => CheckItem(i), recursive: true);
if (item != null)
{
targetItem = item;
rootContainer = item.GetRootContainer();
moveToTarget = rootContainer ?? item;
}
return item != null;
}
protected override void Act(float deltaTime)
{
if (character.LockHands)
{
Abandon = true;
return;
}
if (itemIdentifiers != null && !isDoneSeeking)
{
if (checkInventory)
{
if (CheckInventory())
{
isDoneSeeking = true;
}
}
if (!isDoneSeeking)
{
FindTargetItem();
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
return;
}
}
if (targetItem == null || targetItem.Removed)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Target null or removed. Aborting.", Color.Red);
#endif
Abandon = true;
return;
}
if (character.IsItemTakenBySomeoneElse(targetItem))
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Found an item, but it's already equipped by someone else.", Color.Yellow);
#endif
// Try again
Reset();
return;
}
if (character.CanInteractWith(targetItem, out _, checkLinked: false))
{
var pickable = targetItem.GetComponent<Pickable>();
if (pickable == null)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Target not pickable. Aborting.", Color.Yellow);
#endif
Abandon = true;
return;
}
if (equip)
{
int targetSlot = -1;
//check if all the slots required by the item are free
foreach (InvSlotType slots in pickable.AllowedSlots)
{
if (slots.HasFlag(InvSlotType.Any)) { continue; }
for (int i = 0; i < character.Inventory.Items.Length; i++)
{
//slot not needed by the item, continue
if (!slots.HasFlag(character.Inventory.SlotTypes[i])) { continue; }
targetSlot = i;
//slot free, continue
var otherItem = character.Inventory.Items[i];
if (otherItem == null) { continue; }
//try to move the existing item to LimbSlot.Any and continue if successful
if (otherItem.AllowedSlots.Contains(InvSlotType.Any) &&
character.Inventory.TryPutItem(otherItem, character, new List<InvSlotType>() { InvSlotType.Any }))
{
continue;
}
//if everything else fails, simply drop the existing item
otherItem.Drop(character);
}
}
if (character.Inventory.TryPutItem(targetItem, targetSlot, false, false, character))
{
targetItem.Equip(character);
IsCompleted = true;
}
else
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Failed to equip/move the item '{targetItem.Name}' into the character inventory. Aborting.", Color.Red);
#endif
Abandon = true;
}
}
else
{
if (character.Inventory.TryPutItem(targetItem, null, new List<InvSlotType>() { InvSlotType.Any }))
{
IsCompleted = true;
}
else
{
Abandon = true;
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Failed to equip/move the item '{targetItem.Name}' into the character inventory. Aborting.", Color.Red);
#endif
}
}
}
else
{
TryAddSubObjective(ref goToObjective,
constructor: () =>
{
return new AIObjectiveGoTo(moveToTarget, character, objectiveManager, repeat: false, getDivingGearIfNeeded: AllowToFindDivingGear)
{
// 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,
DialogueIdentifier = "dialogcannotreachtarget",
TargetName = moveToTarget.Name
};
},
onAbandon: () =>
{
ignoredItems.Add(targetItem);
Reset();
},
onCompleted: () => RemoveSubObjective(ref goToObjective));
}
}
private void FindTargetItem()
{
if (itemIdentifiers == null)
{
if (targetItem == null)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot find the item, because neither identifiers nor item was defined.", Color.Red);
#endif
Abandon = true;
}
return;
}
for (int i = 0; i < 10 && currSearchIndex < Item.ItemList.Count - 1; i++)
{
currSearchIndex++;
var item = Item.ItemList[currSearchIndex];
if (item.Submarine == null) { continue; }
if (item.CurrentHull == null) { continue; }
if (item.Submarine.TeamID != character.TeamID) { continue; }
if (!CheckItem(item)) { continue; }
if (ignoredContainerIdentifiers != null && item.Container != null)
{
if (ignoredContainerIdentifiers.Contains(item.ContainerIdentifier)) { continue; }
}
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(item, true)) { continue; }
if (character.IsItemTakenBySomeoneElse(item)) { continue; }
float itemPriority = 1;
if (GetItemPriority != null)
{
itemPriority = GetItemPriority(item);
}
Item rootContainer = item.GetRootContainer();
Vector2 itemPos = (rootContainer ?? item).WorldPosition;
float yDist = Math.Abs(character.WorldPosition.Y - itemPos.Y);
yDist = yDist > 100 ? yDist * 5 : 0;
float dist = Math.Abs(character.WorldPosition.X - itemPos.X) + yDist;
float distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, 10000, dist));
itemPriority *= distanceFactor;
itemPriority *= item.Condition / item.MaxCondition;
//ignore if the item has a lower priority than the currently selected one
if (itemPriority < currItemPriority) { continue; }
currItemPriority = itemPriority;
targetItem = item;
moveToTarget = rootContainer ?? item;
this.rootContainer = rootContainer;
}
if (currSearchIndex >= Item.ItemList.Count - 1)
{
isDoneSeeking = true;
if (targetItem == null)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot find the item with the following identifier(s): {string.Join(", ", itemIdentifiers)}", Color.Yellow);
#endif
Abandon = true;
}
}
}
protected override bool Check()
{
if (IsCompleted) { return true; }
if (targetItem != null)
{
return character.HasItem(targetItem, equip);
}
else if (itemIdentifiers != null)
{
var matchingItem = character.Inventory.FindItem(i => CheckItem(i), recursive: true);
if (matchingItem != null)
{
return !equip || character.HasEquippedItem(matchingItem);
}
return false;
}
return false;
}
private bool CheckItem(Item item)
{
if (ignoredItems.Contains(item)) { return false; };
if (item.Condition < TargetCondition) { return false; }
if (ItemFilter != null && !ItemFilter(item)) { return false; }
return itemIdentifiers.Any(id => id == item.Prefab.Identifier || item.HasTag(id));
}
public override void Reset()
{
base.Reset();
RemoveSubObjective(ref goToObjective);
targetItem = null;
moveToTarget = null;
rootContainer = null;
isDoneSeeking = false;
currSearchIndex = 0;
}
}
}
@@ -0,0 +1,347 @@
using Microsoft.Xna.Framework;
using System;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
class AIObjectiveGoTo : AIObjective
{
public override string DebugTag => "go to";
private AIObjectiveFindDivingGear findDivingGear;
private readonly bool repeat;
//how long until the path to the target is declared unreachable
private float waitUntilPathUnreachable;
private bool getDivingGearIfNeeded;
/// <summary>
/// Doesn't allow the objective to complete if this condition is false
/// </summary>
public Func<bool> requiredCondition;
/// <summary>
/// Aborts the objective when this condition is true
/// </summary>
public Func<bool> abortCondition;
public Func<PathNode, bool> endNodeFilter;
public bool followControlledCharacter;
public bool mimic;
private float _closeEnough = 50;
/// <summary>
/// Display units
/// </summary>
public float CloseEnough
{
get { return _closeEnough; }
set
{
_closeEnough = Math.Max(_closeEnough, value);
}
}
public bool IgnoreIfTargetDead { get; set; }
public bool AllowGoingOutside { get; set; }
public override bool AbandonWhenCannotCompleteSubjectives => !repeat;
public string DialogueIdentifier { get; set; }
public string TargetName { get; set; }
public ISpatialEntity Target { get; private set; }
public override float GetPriority()
{
if (followControlledCharacter && Character.Controlled == null)
{
Priority = 0;
}
if (Target is Entity e && e.Removed)
{
Priority = 0;
}
if (IgnoreIfTargetDead && Target is Character character && character.IsDead)
{
Priority = 0;
}
return objectiveManager.CurrentOrder == this ? AIObjectiveManager.OrderPriority : Priority;
}
public AIObjectiveGoTo(ISpatialEntity target, Character character, AIObjectiveManager objectiveManager, bool repeat = false, bool getDivingGearIfNeeded = true, float priorityModifier = 1, float closeEnough = 0)
: base (character, objectiveManager, priorityModifier)
{
this.Target = target;
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);
}
}
private void SpeakCannotReach()
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot reach the target: {Target.ToString()}", Color.Yellow);
#endif
if (objectiveManager.CurrentOrder != null && DialogueIdentifier != null)
{
string msg = TargetName == null ? TextManager.Get(DialogueIdentifier, true) : TextManager.GetWithVariable(DialogueIdentifier, "[name]", TargetName, true);
if (msg != null)
{
character.Speak(msg, identifier: DialogueIdentifier, minDurationBetweenSimilar: 20.0f);
}
}
}
protected override void Act(float deltaTime)
{
if (followControlledCharacter)
{
if (Character.Controlled == null)
{
Abandon = true;
SteeringManager.Reset();
return;
}
Target = Character.Controlled;
}
if (Target == character)
{
// Wait
character.AIController.SteeringManager.Reset();
return;
}
waitUntilPathUnreachable -= deltaTime;
if (!character.IsClimbing)
{
character.SelectedConstruction = null;
}
if (Target is Entity e)
{
if (e.Removed)
{
Abandon = true;
SteeringManager.Reset();
return;
}
else
{
character.AIController.SelectTarget(e.AiTarget);
}
}
Hull targetHull = GetTargetHull();
if (!followControlledCharacter)
{
// Abandon if going through unsafe paths. Note ignores unsafe nodes when following an order or when the objective is set to ignore unsafe hulls.
bool containsUnsafeNodes = HumanAIController.CurrentOrder == null && !HumanAIController.ObjectiveManager.CurrentObjective.IgnoreUnsafeHulls
&& PathSteering != null && PathSteering.CurrentPath != null
&& PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull));
if (containsUnsafeNodes || HumanAIController.UnreachableHulls.Contains(targetHull))
{
Abandon = true;
}
}
bool insideSteering = SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.IsPathDirty;
bool isInside = character.CurrentHull != null;
bool targetIsOutside = (Target != null && targetHull == null) || (insideSteering && PathSteering.CurrentPath.HasOutdoorsNodes);
if (isInside && targetIsOutside && !AllowGoingOutside)
{
Abandon = true;
}
else if (waitUntilPathUnreachable < 0)
{
if (SteeringManager == PathSteering && PathSteering.CurrentPath != null && PathSteering.CurrentPath.Unreachable && !PathSteering.IsPathDirty)
{
if (repeat)
{
SpeakCannotReach();
SteeringManager.Reset();
}
else
{
Abandon = true;
}
}
}
if (Abandon)
{
SpeakCannotReach();
SteeringManager.Reset();
}
else
{
if (getDivingGearIfNeeded && !character.LockHands)
{
Character followTarget = Target as Character;
bool needsDivingSuit = targetIsOutside;
bool needsDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(character, targetHull, out needsDivingSuit);
if (!needsDivingGear && mimic)
{
if (HumanAIController.HasDivingSuit(followTarget))
{
needsDivingGear = true;
needsDivingSuit = true;
}
else if (HumanAIController.HasDivingMask(followTarget))
{
needsDivingGear = true;
}
}
bool needsEquipment = false;
if (needsDivingSuit)
{
needsEquipment = !HumanAIController.HasDivingSuit(character, AIObjectiveFindDivingGear.lowOxygenThreshold);
}
else if (needsDivingGear)
{
needsEquipment = !HumanAIController.HasDivingGear(character, AIObjectiveFindDivingGear.lowOxygenThreshold);
}
if (needsEquipment)
{
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager),
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref findDivingGear));
return;
}
}
if (repeat && IsCloseEnough)
{
OnCompleted();
return;
}
if (SteeringManager == PathSteering)
{
Func<PathNode, bool> nodeFilter = null;
if (isInside && !AllowGoingOutside)
{
nodeFilter = node => node.Waypoint.CurrentHull != null;
}
PathSteering.SteeringSeek(character.GetRelativeSimPosition(Target), 1, n =>
{
if (n.Waypoint.isObstructed) { return false; }
return (n.Waypoint.CurrentHull == null) == (character.CurrentHull == null);
}, endNodeFilter, nodeFilter);
if (!isInside && (PathSteering.CurrentPath == null || PathSteering.IsPathDirty || PathSteering.CurrentPath.Unreachable))
{
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(Target.WorldPosition - character.WorldPosition));
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: 5, weight: 15);
}
}
else
{
SteeringManager.SteeringSeek(character.GetRelativeSimPosition(Target), 10);
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: 5, weight: 15);
}
}
}
private Hull GetTargetHull()
{
if (Target is Hull h)
{
return h;
}
else if (Target is Item i)
{
return i.CurrentHull;
}
else if (Target is Character c)
{
return c.CurrentHull;
}
else if (Target is Gap g)
{
return g.FlowTargetHull;
}
else if (Target is WayPoint wp)
{
return wp.CurrentHull;
}
else if (Target is FireSource fs)
{
return fs.Hull;
}
return null;
}
private bool IsCloseEnough
{
get
{
if (character.IsClimbing && SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.CurrentPath.Finished)
{
// Still in ladders and the path is not finished -> don't release
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;
}
}
protected override bool Check()
{
if (IsCompleted) { return true; }
// First check the distance
// Then the custom condition
// And finally check if can interact (heaviest)
if (Target == null)
{
Abandon = true;
return false;
}
if (abortCondition != null && abortCondition())
{
Abandon = true;
return false;
}
if (repeat)
{
return false;
}
else
{
if (IsCloseEnough)
{
if (requiredCondition == null || requiredCondition())
{
if (Target is Item item)
{
if (character.CanInteractWith(item, out _, checkLinked: false)) { IsCompleted = true; }
}
else if (Target is Character targetCharacter)
{
if (character.CanInteractWith(targetCharacter, CloseEnough)) { IsCompleted = true; }
}
else
{
IsCompleted = true;
}
}
}
}
return IsCompleted;
}
private void StopMovement()
{
character.AIController.SteeringManager.Reset();
if (Target != null)
{
character.AnimController.TargetDir = Target.WorldPosition.X > character.WorldPosition.X ? Direction.Right : Direction.Left;
}
}
protected override void OnCompleted()
{
StopMovement();
HumanAIController.FaceTarget(Target);
base.OnCompleted();
}
}
}
@@ -0,0 +1,269 @@
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
class AIObjectiveIdle : AIObjective
{
public override string DebugTag => "idle";
public override bool UnequipItems => true;
private readonly float newTargetIntervalMin = 10;
private readonly float newTargetIntervalMax = 20;
private readonly float standStillMin = 2;
private readonly float standStillMax = 10;
private readonly float walkDurationMin = 5;
private readonly float walkDurationMax = 10;
private Hull currentTarget;
private float newTargetTimer;
private bool searchingNewHull;
private float standStillTimer;
private float walkDuration;
private readonly List<Hull> targetHulls = new List<Hull>(20);
private readonly List<float> hullWeights = new List<float>(20);
public AIObjectiveIdle(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier)
{
standStillTimer = Rand.Range(-10.0f, 10.0f);
walkDuration = Rand.Range(0.0f, 10.0f);
}
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); }
private float randomTimer;
private float randomUpdateInterval = 5;
public float Random { get; private set; }
public void CalculatePriority()
{
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));
}
public override float GetPriority() => Priority;
public override void Update(float deltaTime)
{
if (objectiveManager.CurrentObjective == this)
{
if (randomTimer > 0)
{
randomTimer -= deltaTime;
}
else
{
CalculatePriority();
}
}
}
protected override void Act(float deltaTime)
{
if (PathSteering == null) { return; }
//don't keep dragging others when idling
if (character.SelectedCharacter != null)
{
character.DeselectCharacter();
}
if (!character.IsClimbing)
{
character.SelectedConstruction = null;
}
bool currentTargetIsInvalid = currentTarget == null || IsForbidden(currentTarget) ||
(PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull)));
if (currentTargetIsInvalid || currentTarget == null && HumanAIController.VisibleHulls.Any(h => IsForbidden(h)))
{
//don't reset to zero, otherwise the character will keep calling FindTargetHulls
//almost constantly when there's a small number of potential hulls to move to
newTargetTimer = Math.Min(newTargetTimer, 0.5f);
//standStillTimer = 0.0f;
}
else if (character.IsClimbing)
{
if (currentTarget == null)
{
newTargetTimer = 0;
}
else if (Math.Abs(character.AnimController.TargetMovement.Y) > 0.9f)
{
// Don't allow new targets when climbing straight up or down
newTargetTimer = Math.Max(newTargetIntervalMin, newTargetTimer);
}
}
else if (character.AnimController.InWater)
{
if (currentTarget == null)
{
newTargetTimer = Math.Min(newTargetTimer, 0.5f);
}
}
if (newTargetTimer <= 0.0f)
{
if (!searchingNewHull)
{
//find all available hulls first
FindTargetHulls();
searchingNewHull = true;
return;
}
else if (targetHulls.Count > 0)
{
//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 =>
{
if (node.Waypoint.CurrentHull == null) { return false; }
// Check that there is no unsafe or forbidden hulls on the way to the target
if (node.Waypoint.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(node.Waypoint.CurrentHull)) { return false; }
if (isCurrentHullAllowed && IsForbidden(node.Waypoint.CurrentHull)) { return false; }
return true;
});
if (path.Unreachable)
{
//can't go to this room, remove it from the list and try another room next frame
int index = targetHulls.IndexOf(currentTarget);
targetHulls.RemoveAt(index);
hullWeights.RemoveAt(index);
PathSteering.Reset();
currentTarget = null;
return;
}
searchingNewHull = false;
}
else
{
// Couldn't find a target for some reason -> reset
newTargetTimer = Math.Max(newTargetIntervalMin, newTargetTimer);
searchingNewHull = false;
}
if (currentTarget != null)
{
character.AIController.SelectTarget(currentTarget.AiTarget);
string errorMsg = null;
#if DEBUG
bool isRoomNameFound = currentTarget.DisplayName != null;
errorMsg = "(Character " + character.Name + " idling, target " + (isRoomNameFound ? currentTarget.DisplayName : currentTarget.ToString()) + ")";
#endif
var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, errorMsgStr: errorMsg, nodeFilter: node => node.Waypoint.CurrentHull != null);
PathSteering.SetPath(path);
}
newTargetTimer = currentTarget != null && character.AnimController.InWater ? newTargetIntervalMin : Rand.Range(newTargetIntervalMin, newTargetIntervalMax);
}
newTargetTimer -= deltaTime;
//wander randomly
// - if reached the end of the path
// - if the target is unreachable
// - if the path requires going outside
if (!character.IsClimbing)
{
if (SteeringManager != PathSteering || (PathSteering.CurrentPath != null &&
(PathSteering.CurrentPath.Finished || PathSteering.CurrentPath.Unreachable || PathSteering.CurrentPath.HasOutdoorsNodes)))
{
Wander(deltaTime);
return;
}
}
if (currentTarget != null)
{
if (SteeringManager == PathSteering)
{
PathSteering.SteeringSeek(character.GetRelativeSimPosition(currentTarget), weight: 1, nodeFilter: node => node.Waypoint.CurrentHull != null);
}
else
{
character.AIController.SteeringManager.SteeringSeek(character.GetRelativeSimPosition(currentTarget));
}
}
else
{
Wander(deltaTime);
}
}
public void Wander(float deltaTime)
{
if (character.IsClimbing) { return; }
if (!character.AnimController.InWater)
{
standStillTimer -= deltaTime;
if (standStillTimer > 0.0f)
{
walkDuration = Rand.Range(walkDurationMin, walkDurationMax);
PathSteering.Reset();
return;
}
if (standStillTimer < -walkDuration)
{
standStillTimer = Rand.Range(standStillMin, standStillMax);
}
}
PathSteering.Wander(deltaTime);
}
private void FindTargetHulls()
{
targetHulls.Clear();
hullWeights.Clear();
foreach (var hull in Hull.hullList)
{
if (HumanAIController.UnsafeHulls.Contains(hull)) { continue; }
if (hull.Submarine == null) { continue; }
if (hull.Submarine.TeamID != character.TeamID) { continue; }
// If the character is inside, only take connected hulls into account.
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(hull, true)) { continue; }
if (IsForbidden(hull)) { continue; }
// Ignore hulls that are too low to stand inside
if (character.AnimController is HumanoidAnimController animController)
{
if (hull.CeilingHeight < ConvertUnits.ToDisplayUnits(animController.HeadPosition.Value))
{
continue;
}
}
if (!targetHulls.Contains(hull))
{
targetHulls.Add(hull);
float weight = hull.Volume;
// Prefer rooms that are closer. Avoid rooms that are not in the same level.
float yDist = Math.Abs(character.WorldPosition.Y - hull.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 5 : 0;
float dist = Math.Abs(character.WorldPosition.X - hull.WorldPosition.X) + yDist;
float distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, 2500, dist));
weight *= distanceFactor;
hullWeights.Add(weight);
}
}
}
public static bool IsForbidden(Hull hull)
{
if (hull == null) { return true; }
string hullName = hull.RoomName;
if (hullName == null) { return false; }
return hullName.Contains("ballast", StringComparison.OrdinalIgnoreCase) || hullName.Contains("airlock", StringComparison.OrdinalIgnoreCase);
}
}
}
@@ -0,0 +1,220 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
abstract class AIObjectiveLoop<T> : AIObjective
{
public HashSet<T> Targets { get; private set; } = new HashSet<T>();
public Dictionary<T, AIObjective> Objectives { get; private set; } = new Dictionary<T, AIObjective>();
protected HashSet<T> ignoreList = new HashSet<T>();
private float ignoreListTimer;
protected float targetUpdateTimer;
private float syncTimer;
private readonly float syncTime = 1;
// By default, doesn't clear the list automatically
protected virtual float IgnoreListClearInterval => 0;
public HashSet<T> ReportedTargets { get; private set; } = new HashSet<T>();
public bool AddTarget(T target)
{
if (character.IsDead) { return false; }
if (ReportedTargets.Contains(target))
{
return false;
}
if (Filter(target))
{
ReportedTargets.Add(target);
return true;
}
return false;
}
public AIObjectiveLoop(Character character, AIObjectiveManager objectiveManager, float priorityModifier, string option = null)
: base(character, objectiveManager, priorityModifier, option) { }
protected override void Act(float deltaTime) { }
protected override bool Check() => false;
public override bool CanBeCompleted => true;
public override bool AbandonWhenCannotCompleteSubjectives => false;
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 void Update(float deltaTime)
{
base.Update(deltaTime);
if (IgnoreListClearInterval > 0)
{
if (ignoreListTimer > IgnoreListClearInterval)
{
Reset();
}
else
{
ignoreListTimer += deltaTime;
}
}
if (targetUpdateTimer < 0)
{
UpdateTargets();
}
else
{
targetUpdateTimer -= deltaTime;
}
if (syncTimer < 0)
{
syncTimer = syncTime * Rand.Range(0.9f, 1.1f);
// Sync objectives, subobjectives and targets
foreach (var objective in Objectives)
{
var target = objective.Key;
if (!Targets.Contains(target))
{
subObjectives.Remove(objective.Value);
}
}
SyncRemovedObjectives(Objectives, GetList());
}
else
{
syncTimer -= deltaTime;
}
if (Objectives.None() && Targets.Any(t => !ignoreList.Contains(t)))
{
CreateObjectives();
}
}
// the timer is set between 1 and 10 seconds, depending on the priority modifier and a random +-25%
private float SetTargetUpdateTimer() => targetUpdateTimer = 1 / MathHelper.Clamp(PriorityModifier * Rand.Range(0.75f, 1.25f), 0.1f, 1);
public override void Reset()
{
base.Reset();
ignoreList.Clear();
ignoreListTimer = 0;
UpdateTargets();
}
public override float GetPriority()
{
if (character.LockHands || character.Submarine == null || Targets.None())
{
Priority = 0;
}
else
{
// Allow the target value to be more than 100.
float targetValue = TargetEvaluation();
if (InverseTargetEvaluation)
{
targetValue = 100 - targetValue;
}
var currentSubObjective = CurrentSubObjective;
if (currentSubObjective != null && currentSubObjective.Priority > targetValue)
{
// If the priority is higher than the target value, let's just use it.
// The priority calculation is more precise, but it takes into account things like distances,
// so it's better not to use it if it's lower than the rougher targetValue.
targetValue = Priority;
}
// If the target value is less than 1% of the max value, let's just treat it as zero.
if (targetValue < 1)
{
Priority = 0;
}
else
{
if (objectiveManager.CurrentOrder == this)
{
Priority = AIObjectiveManager.OrderPriority;
}
else
{
float max = MathHelper.Min(AIObjectiveManager.OrderPriority - 1, 90);
float value = MathHelper.Clamp((CumulatedDevotion + (targetValue * PriorityModifier)) / 100, 0, 1);
Priority = MathHelper.Lerp(0, max, value);
}
}
}
return Priority;
}
protected void UpdateTargets()
{
SetTargetUpdateTimer();
Targets.Clear();
FindTargets();
CreateObjectives();
}
protected virtual void FindTargets()
{
foreach (T target in GetList())
{
// The bots always find targets when the objective is an order.
if (objectiveManager.CurrentOrder != this)
{
// Battery or pump states cannot currently be reported (not implemented) and therefore we must ignore them -> the bots always know if they require attention.
bool ignore = this is AIObjectiveChargeBatteries || this is AIObjectivePumpWater;
if (!ignore && !ReportedTargets.Contains(target)) { continue; }
}
if (!Filter(target)) { continue; }
if (!ignoreList.Contains(target))
{
Targets.Add(target);
}
}
}
protected virtual void CreateObjectives()
{
foreach (T target in Targets)
{
if (ignoreList.Contains(target)) { continue; }
if (!Objectives.TryGetValue(target, out AIObjective objective))
{
objective = ObjectiveConstructor(target);
Objectives.Add(target, objective);
if (!subObjectives.Contains(objective))
{
subObjectives.Add(objective);
}
objective.Completed += () =>
{
Objectives.Remove(target);
OnObjectiveCompleted(objective, target);
};
objective.Abandoned += () =>
{
Objectives.Remove(target);
ignoreList.Add(target);
targetUpdateTimer = 0;
};
}
}
}
protected abstract void OnObjectiveCompleted(AIObjective objective, T target);
/// <summary>
/// List of all possible items of the specified type. Used for filtering the removed objectives.
/// </summary>
protected abstract IEnumerable<T> GetList();
protected abstract float TargetEvaluation();
protected abstract AIObjective ObjectiveConstructor(T target);
protected abstract bool Filter(T target);
}
}
@@ -0,0 +1,340 @@
using Barotrauma.Items.Components;
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
class AIObjectiveManager
{
// TODO: expose
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 List<AIObjective> Objectives { get; private set; } = new List<AIObjective>();
private readonly Character character;
private float _waitTimer;
/// <summary>
/// When set above zero, the character will stand still doing nothing until the timer runs out. Does not affect orders, find safety or combat.
/// </summary>
public float WaitTimer
{
get { return _waitTimer; }
set
{
_waitTimer = IsAllowedToWait() ? value : 0;
}
}
public AIObjective CurrentOrder { get; private set; }
public AIObjective CurrentObjective { get; private set; }
public bool IsCurrentOrder<T>() where T : AIObjective => CurrentOrder is T;
public bool IsCurrentObjective<T>() where T : AIObjective => CurrentObjective is T;
public bool IsActiveObjective<T>() where T : AIObjective => GetActiveObjective() is T;
public AIObjective GetActiveObjective() => CurrentObjective?.GetActiveObjective();
/// <summary>
/// Returns the last active objective of the specific type.
/// </summary>
public T GetActiveObjective<T>() where T : AIObjective => CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).LastOrDefault(so => so is T) as T;
/// <summary>
/// Returns all active objectives of the specific type. Creates a new collection -> don't use too frequently.
/// </summary>
public IEnumerable<T> GetActiveObjectives<T>() where T : AIObjective => CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).Where(so => so is T).Select(so => so as T);
public bool HasActiveObjective<T>() where T : AIObjective => CurrentObjective is T || CurrentObjective != null && CurrentObjective.GetSubObjectivesRecursive().Any(so => so is T);
public AIObjectiveManager(Character character)
{
this.character = character;
CreateAutonomousObjectives();
}
public void AddObjective<T>(T objective) where T : AIObjective
{
if (objective == null)
{
#if DEBUG
DebugConsole.ThrowError("Attempted to add a null objective to AIObjectiveManager\n" + Environment.StackTrace);
#endif
return;
}
// Can't use the generic type, because it's possible that the user of this method uses the base type AIObjective.
// We need to get the highest type.
var type = objective.GetType();
if (objective.AllowMultipleInstances)
{
if (Objectives.FirstOrDefault(o => o.GetType() == type) is T existingObjective && existingObjective.IsDuplicate(objective))
{
Objectives.Remove(existingObjective);
}
}
else
{
Objectives.RemoveAll(o => o.GetType() == type);
}
Objectives.Add(objective);
}
public Dictionary<AIObjective, CoroutineHandle> DelayedObjectives { get; private set; } = new Dictionary<AIObjective, CoroutineHandle>();
public void CreateAutonomousObjectives()
{
foreach (var delayedObjective in DelayedObjectives)
{
CoroutineManager.StopCoroutines(delayedObjective.Value);
}
DelayedObjectives.Clear();
Objectives.Clear();
AddObjective(new AIObjectiveFindSafety(character, this));
AddObjective(new AIObjectiveIdle(character, this));
int objectiveCount = Objectives.Count;
foreach (var automaticOrder in character.Info.Job.Prefab.AutomaticOrders)
{
var orderPrefab = Order.GetPrefab(automaticOrder.identifier);
if (orderPrefab == null) { throw new Exception($"Could not find a matching prefab by the identifier: '{automaticOrder.identifier}'"); }
// TODO: Similar code is used in CrewManager:815-> DRY
var matchingItems = orderPrefab.ItemIdentifiers.Any() ?
Item.ItemList.FindAll(it => orderPrefab.ItemIdentifiers.Contains(it.Prefab.Identifier) || it.HasTag(orderPrefab.ItemIdentifiers)) :
Item.ItemList.FindAll(it => it.Components.Any(ic => ic.GetType() == orderPrefab.ItemComponentType));
matchingItems.RemoveAll(it => it.Submarine != character.Submarine);
var item = matchingItems.GetRandom();
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);
if (objective != null)
{
AddObjective(objective, delay: Rand.Value() / 2);
objectiveCount++;
}
}
_waitTimer = Math.Max(_waitTimer, Rand.Range(0.5f, 1f) * objectiveCount);
}
public void AddObjective<T>(T objective, float delay, Action callback = null) where T : AIObjective
{
if (objective == null)
{
#if DEBUG
DebugConsole.ThrowError($"{character.Name}: Attempted to add a null objective to AIObjectiveManager\n" + Environment.StackTrace);
#endif
return;
}
if (DelayedObjectives.TryGetValue(objective, out CoroutineHandle coroutine))
{
CoroutineManager.StopCoroutines(coroutine);
DelayedObjectives.Remove(objective);
}
coroutine = CoroutineManager.InvokeAfter(() =>
{
//round ended before the coroutine finished
if (GameMain.GameSession == null || Level.Loaded == null) { return; }
DelayedObjectives.Remove(objective);
AddObjective(objective);
callback?.Invoke();
}, delay);
DelayedObjectives.Add(objective, coroutine);
}
public T GetObjective<T>() where T : AIObjective => Objectives.FirstOrDefault(o => o is T) as T;
private AIObjective GetCurrentObjective()
{
var previousObjective = CurrentObjective;
var firstObjective = Objectives.FirstOrDefault();
if (CurrentOrder != null && firstObjective != null && CurrentOrder.Priority > firstObjective.Priority)
{
CurrentObjective = CurrentOrder;
}
else
{
CurrentObjective = firstObjective;
}
if (previousObjective != CurrentObjective)
{
previousObjective?.OnDeselected();
CurrentObjective?.OnSelected();
GetObjective<AIObjectiveIdle>().CalculatePriority();
}
return CurrentObjective;
}
public float GetCurrentPriority()
{
return CurrentObjective == null ? 0.0f : CurrentObjective.Priority;
}
public void UpdateObjectives(float deltaTime)
{
CurrentOrder?.Update(deltaTime);
if (WaitTimer > 0)
{
WaitTimer -= deltaTime;
return;
}
for (int i = 0; i < Objectives.Count; i++)
{
var objective = Objectives[i];
if (objective.IsCompleted)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Removing objective {objective.DebugTag}, because it is completed.", Color.LightGreen);
#endif
Objectives.Remove(objective);
}
else if (!objective.CanBeCompleted)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Removing objective {objective.DebugTag}, because it cannot be completed.", Color.Red);
#endif
Objectives.Remove(objective);
}
else if (objective != CurrentOrder)
{
objective.Update(deltaTime);
}
}
GetCurrentObjective();
}
public void SortObjectives()
{
if (Objectives.Any())
{
Objectives.ForEach(o => o.GetPriority());
Objectives.Sort((x, y) => y.Priority.CompareTo(x.Priority));
}
GetCurrentObjective()?.SortSubObjectives();
}
public void DoCurrentObjective(float deltaTime)
{
if (WaitTimer <= 0)
{
CurrentObjective?.TryComplete(deltaTime);
}
else
{
character.AIController.SteeringManager.Reset();
}
}
public void SetOrder(AIObjective objective)
{
CurrentOrder = objective;
}
public void SetOrder(Order order, string option, Character orderGiver)
{
CurrentOrder = CreateObjective(order, option, orderGiver);
if (CurrentOrder == null)
{
// Recreate objectives, because some of them may be removed, if impossible to complete (e.g. due to path finding)
CreateAutonomousObjectives();
}
else
{
CurrentOrder.Reset();
}
}
public AIObjective CreateObjective(Order order, string option, Character orderGiver, float priorityModifier = 1)
{
if (order == null) { return null; }
AIObjective newObjective;
switch (order.Identifier.ToLowerInvariant())
{
case "follow":
if (orderGiver == null) { return null; }
newObjective = new AIObjectiveGoTo(orderGiver, character, this, repeat: true, priorityModifier: priorityModifier)
{
CloseEnough = 100,
AllowGoingOutside = true,
IgnoreIfTargetDead = true,
followControlledCharacter = orderGiver == character,
mimic = true,
DialogueIdentifier = "dialogcannotreachplace"
};
break;
case "wait":
newObjective = new AIObjectiveGoTo(character, character, this, repeat: true, priorityModifier: priorityModifier)
{
AllowGoingOutside = character.CurrentHull == null
};
break;
case "fixleaks":
newObjective = new AIObjectiveFixLeaks(character, this, priorityModifier);
break;
case "chargebatteries":
newObjective = new AIObjectiveChargeBatteries(character, this, option, priorityModifier);
break;
case "rescue":
newObjective = new AIObjectiveRescueAll(character, this, priorityModifier);
break;
case "repairsystems":
newObjective = new AIObjectiveRepairItems(character, this, priorityModifier)
{
RequireAdequateSkills = option == "jobspecific"
};
break;
case "pumpwater":
newObjective = new AIObjectivePumpWater(character, this, option, priorityModifier: priorityModifier);
break;
case "extinguishfires":
newObjective = new AIObjectiveExtinguishFires(character, this, priorityModifier);
break;
case "fightintruders":
newObjective = new AIObjectiveFightIntruders(character, this, priorityModifier);
break;
case "steer":
var steering = (order?.TargetEntity as Item)?.GetComponent<Steering>();
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)
{
IsLoop = true,
// Don't override unless it's an order by a player
Override = orderGiver != null && orderGiver.IsPlayer
};
break;
default:
if (order.TargetItemComponent == null) { return null; }
newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option, requireEquip: false, useController: order.UseController, priorityModifier: priorityModifier)
{
IsLoop = true,
// Don't override unless it's an order by a player
Override = orderGiver != null && orderGiver.IsPlayer
};
break;
}
return newObjective;
}
private bool IsAllowedToWait()
{
if (CurrentOrder != null) { return false; }
if (CurrentObjective is AIObjectiveCombat || CurrentObjective is AIObjectiveFindSafety) { return false; }
if (character.AnimController.InWater) { return false; }
if (character.IsClimbing) { return false; }
if (character.AIController is HumanAIController humanAI)
{
if (humanAI.UnsafeHulls.Contains(character.CurrentHull)) { return false; }
}
if (AIObjectiveIdle.IsForbidden(character.CurrentHull)) { return false; }
return true;
}
}
}
@@ -0,0 +1,224 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
class AIObjectiveOperateItem : AIObjective
{
public override string DebugTag => "operate item";
public override bool UnequipItems => true;
private ItemComponent component, controller;
private Entity operateTarget;
private bool requireEquip;
private bool useController;
private AIObjectiveGoTo goToObjective;
private AIObjectiveGetItem getItemObjective;
public bool Override { get; set; } = true;
public override bool CanBeCompleted => base.CanBeCompleted && (!useController || controller != null);
public Entity OperateTarget => operateTarget;
public ItemComponent Component => component;
public ItemComponent GetTarget() => useController ? controller : component;
public Func<bool> completionCondition;
public override float GetPriority()
{
if (component.Item.ConditionPercentage <= 0)
{
Priority = 0;
}
else
{
if (objectiveManager.CurrentOrder == this)
{
Priority = AIObjectiveManager.OrderPriority;
}
if (component.Item.CurrentHull == null || component.Item.CurrentHull.FireSources.None() || IsOperatedByAnother(GetTarget()))
{
Priority = 0;
}
else if (Character.CharacterList.Any(c => c.CurrentHull == component.Item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c)))
{
Priority = 0;
}
else
{
float value = CumulatedDevotion + (AIObjectiveManager.OrderPriority * PriorityModifier);
float max = MathHelper.Min((AIObjectiveManager.OrderPriority - 1), 90);
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)
: base (character, objectiveManager, priorityModifier, option)
{
this.component = item ?? throw new System.ArgumentNullException("item", "Attempted to create an AIObjectiveOperateItem with a null target.");
this.requireEquip = requireEquip;
this.operateTarget = operateTarget;
this.useController = useController;
if (useController)
{
//try finding the controller with the simpler non-recursive method first
controller =
component.Item.GetConnectedComponents<Controller>().FirstOrDefault() ??
component.Item.GetConnectedComponents<Controller>(recursive: true).FirstOrDefault();
}
}
private bool IsOperatedByAnother(ItemComponent target)
{
foreach (var c in Character.CharacterList)
{
if (c == character) { continue; }
if (!HumanAIController.IsFriendly(c)) { continue; }
if (c.SelectedConstruction != target.Item) { continue; }
// If the other character is player, don't try to operate
if (c.IsRemotePlayer || Character.Controlled == c) { return true; }
if (c.AIController is HumanAIController humanAi)
{
// If the other character is ordered to operate the item, let him do it
if (humanAi.ObjectiveManager.IsCurrentOrder<AIObjectiveOperateItem>())
{
return true;
}
else
{
if (target is Steering)
{
// Steering is hard-coded -> cannot use the required skills collection defined in the xml
return character.GetSkillLevel("helm") <= c.GetSkillLevel("helm");
}
else
{
return target.DegreeOfSuccess(character) <= target.DegreeOfSuccess(c);
}
}
}
else
{
// Shouldn't go here, unless we allow non-humans to operate items
return false;
}
}
return false;
}
protected override void Act(float deltaTime)
{
if (character.LockHands)
{
Abandon = true;
return;
}
ItemComponent target = GetTarget();
if (useController && controller == null)
{
character.Speak(TextManager.GetWithVariable("DialogCantFindController", "[item]", component.Item.Name, true), null, 2.0f, "cantfindcontroller", 30.0f);
Abandon = true;
return;
}
// Don't allow to operate an item that someone with a better skills already operates, unless this is an order
if (objectiveManager.CurrentOrder != this && IsOperatedByAnother(target))
{
// Don't abandon
return;
}
if (target.CanBeSelected)
{
if (character.CanInteractWith(target.Item, out _, checkLinked: false))
{
HumanAIController.FaceTarget(target.Item);
if (character.SelectedConstruction != target.Item)
{
target.Item.TryInteract(character, false, true);
}
if (component.AIOperate(deltaTime, character, this))
{
IsCompleted = completionCondition == null || completionCondition();
}
}
else
{
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(target.Item, character, objectiveManager, closeEnough: 50)
{
DialogueIdentifier = "dialogcannotreachtarget",
TargetName = target.Item.Name
},
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref goToObjective));
}
}
else
{
if (component.Item.GetComponent<Pickable>() == null)
{
//controller/target can't be selected and the item cannot be picked -> objective can't be completed
Abandon = true;
return;
}
else if (!character.Inventory.Items.Contains(component.Item))
{
TryAddSubObjective(ref getItemObjective, () => new AIObjectiveGetItem(character, component.Item, objectiveManager, equip: true),
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref getItemObjective));
}
else
{
if (requireEquip && !character.HasEquippedItem(component.Item))
{
//the item has to be equipped before using it if it's holdable
var holdable = component.Item.GetComponent<Holdable>();
if (holdable == null)
{
#if DEBUG
DebugConsole.ThrowError($"{character.Name}: AIObjectiveOperateItem failed - equipping item " + component.Item + " is required but the item has no Holdable component");
#endif
return;
}
for (int i = 0; i < character.Inventory.Capacity; i++)
{
if (character.Inventory.SlotTypes[i] == InvSlotType.Any || !holdable.AllowedSlots.Any(s => s.HasFlag(character.Inventory.SlotTypes[i])))
{
continue;
}
//equip slot already taken
if (character.Inventory.Items[i] != null)
{
//try to put the item in an Any slot, and drop it if that fails
if (!character.Inventory.Items[i].AllowedSlots.Contains(InvSlotType.Any) ||
!character.Inventory.TryPutItem(character.Inventory.Items[i], character, new List<InvSlotType>() { InvSlotType.Any }))
{
character.Inventory.Items[i].Drop(character);
}
}
if (character.Inventory.TryPutItem(component.Item, i, true, false, character))
{
component.Item.Equip(character);
break;
}
}
return;
}
if (component.AIOperate(deltaTime, character, this))
{
IsCompleted = completionCondition == null || completionCondition();
}
}
}
}
protected override bool Check() => IsCompleted && !IsLoop;
}
}
@@ -0,0 +1,85 @@
using Barotrauma.Items.Components;
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Barotrauma.Extensions;
namespace Barotrauma
{
class AIObjectivePumpWater : AIObjectiveLoop<Pump>
{
public override string DebugTag => "pump water";
public override bool KeepDivingGearOn => true;
public override bool UnequipItems => true;
private IEnumerable<Pump> pumpList;
public AIObjectivePumpWater(Character character, AIObjectiveManager objectiveManager, string option, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier, option) { }
protected override void FindTargets()
{
if (Option == null) { return; }
base.FindTargets();
}
protected override bool Filter(Pump pump)
{
if (pump == null) { return false; }
if (pump.Item.HasTag("ballast")) { return false; }
if (pump.Item.Submarine == null) { return false; }
if (pump.Item.CurrentHull == null) { return false; }
if (pump.Item.Submarine.TeamID != character.TeamID) { return false; }
if (pump.Item.ConditionPercentage <= 0) { return false; }
if (pump.Item.CurrentHull.FireSources.Count > 0) { return false; }
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(pump.Item, true)) { return false; }
if (Character.CharacterList.Any(c => c.CurrentHull == pump.Item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; }
if (IsReady(pump)) { return false; }
return true;
}
protected override IEnumerable<Pump> GetList()
{
if (pumpList == null)
{
if (character == null || character.Submarine == null) { return new Pump[0]; }
pumpList = character.Submarine.GetItems(true).Select(i => i.GetComponent<Pump>()).Where(p => p != null);
}
return pumpList;
}
protected override float TargetEvaluation()
{
if (Option == "stoppumping")
{
return Targets.Max(t => MathHelper.Lerp(0, 100, Math.Abs(t.FlowPercentage / 100)));
}
else
{
return Targets.Max(t => MathHelper.Lerp(100, 0, Math.Abs(-t.FlowPercentage / 100)));
}
}
private bool IsReady(Pump pump)
{
if (Option == "stoppumping")
{
return !pump.IsActive || MathUtils.NearlyEqual(pump.FlowPercentage, 0);
}
else
{
return !pump.Item.InWater || pump.IsActive && pump.FlowPercentage <= -99.9f;
}
}
protected override AIObjective ObjectiveConstructor(Pump pump)
=> new AIObjectiveOperateItem(pump, character, objectiveManager, Option, false)
{
IsLoop = false,
completionCondition = () => IsReady(pump)
};
protected override void OnObjectiveCompleted(AIObjective objective, Pump target)
=> HumanAIController.RemoveTargets<AIObjectivePumpWater, Pump>(character, target);
}
}
@@ -0,0 +1,248 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
using Barotrauma.Extensions;
using FarseerPhysics;
namespace Barotrauma
{
class AIObjectiveRepairItem : AIObjective
{
public override string DebugTag => "repair item";
public override bool KeepDivingGearOn => true;
public Item Item { get; private set; }
private AIObjectiveGoTo goToObjective;
private AIObjectiveContainItem refuelObjective;
private float previousCondition = -1;
private RepairTool repairTool;
private bool IsRepairing => character.SelectedConstruction == Item && Item.GetComponent<Repairable>()?.CurrentFixer == character;
public AIObjectiveRepairItem(Character character, Item item, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier)
{
Item = item;
}
public override float GetPriority()
{
// TODO: priority list?
// Ignore items that are being repaired by someone else.
if (Item.Repairables.Any(r => r.CurrentFixer != null && r.CurrentFixer != character))
{
Priority = 0;
}
else
{
float yDist = Math.Abs(character.WorldPosition.Y - Item.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 5 : 0;
float dist = Math.Abs(character.WorldPosition.X - Item.WorldPosition.X) + yDist;
float distanceFactor = MathHelper.Lerp(1, 0.25f, MathUtils.InverseLerp(0, 5000, dist));
if (Item.CurrentHull == character.CurrentHull)
{
distanceFactor = 1;
}
float damagePriority = MathHelper.Lerp(1, 0, Item.Condition / Item.MaxCondition);
float successFactor = MathHelper.Lerp(0, 1, Item.Repairables.Average(r => r.DegreeOfSuccess(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));
}
return Priority;
}
protected override bool Check()
{
IsCompleted = Item.IsFullCondition;
if (IsCompleted && IsRepairing)
{
character?.Speak(TextManager.GetWithVariable("DialogItemRepaired", "[itemname]", Item.Name, true), null, 0.0f, "itemrepaired", 10.0f);
}
return IsCompleted;
}
protected override void Act(float deltaTime)
{
// Only continue when the get item sub objectives have been completed.
if (subObjectives.Any()) { return; }
foreach (Repairable repairable in Item.Repairables)
{
if (!repairable.HasRequiredItems(character, false))
{
//make sure we have all the items required to fix the target item
foreach (var kvp in repairable.requiredItems)
{
foreach (RelatedItem requiredItem in kvp.Value)
{
subObjectives.Add(new AIObjectiveGetItem(character, requiredItem.Identifiers, objectiveManager, true));
}
}
return;
}
}
if (repairTool == null)
{
FindRepairTool();
}
if (repairTool != null)
{
var containedItems = repairTool.Item.ContainedItems;
if (containedItems == null)
{
#if DEBUG
DebugConsole.ThrowError($"{character.Name}: AIObjectiveRepairItem failed - the item \"" + repairTool + "\" has no proper inventory");
#endif
Abandon = true;
return;
}
// Drop empty tanks
foreach (Item containedItem in containedItems)
{
if (containedItem == null) { continue; }
if (containedItem.Condition <= 0.0f)
{
containedItem.Drop(character);
}
}
RelatedItem item = null;
Item fuel = null;
foreach (RelatedItem requiredItem in repairTool.requiredItems[RelatedItem.RelationType.Contained])
{
item = requiredItem;
fuel = containedItems.FirstOrDefault(it => it.Condition > 0.0f && requiredItem.MatchesItem(it));
if (fuel != null) { break; }
}
if (fuel == null)
{
RemoveSubObjective(ref goToObjective);
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, item.Identifiers, repairTool.Item.GetComponent<ItemContainer>(), objectiveManager),
onCompleted: () => RemoveSubObjective(ref refuelObjective),
onAbandon: () => Abandon = true);
return;
}
}
if (character.CanInteractWith(Item, out _, checkLinked: false))
{
HumanAIController.FaceTarget(Item);
if (repairTool != null)
{
OperateRepairTool(deltaTime);
}
foreach (Repairable repairable in Item.Repairables)
{
if (repairable.CurrentFixer != null && repairable.CurrentFixer != character)
{
// Someone else is repairing the target. Abandon the objective if the other is better at this than us.
Abandon = repairable.DegreeOfSuccess(character) < repairable.DegreeOfSuccess(repairable.CurrentFixer);
}
if (!Abandon)
{
if (character.SelectedConstruction != Item)
{
if (!Item.TryInteract(character, true, true))
{
Abandon = true;
}
}
if (previousCondition == -1)
{
previousCondition = Item.Condition;
}
else if (Item.Condition < previousCondition)
{
// If the current condition is less than the previous condition, we can't complete the task, so let's abandon it. The item is probably deteriorating at a greater speed than we can repair it.
Abandon = true;
}
}
if (Abandon)
{
if (IsRepairing)
{
character.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, true), null, 0.0f, "cannotrepair", 10.0f);
}
repairable.StopRepairing(character);
}
else if (repairable.CurrentFixer != character)
{
repairable.StartRepairing(character, Repairable.FixActions.Repair);
}
break;
}
}
else
{
RemoveSubObjective(ref refuelObjective);
// If cannot reach the item, approach it.
TryAddSubObjective(ref goToObjective,
constructor: () =>
{
previousCondition = -1;
var objective = new AIObjectiveGoTo(Item, character, objectiveManager)
{
// Don't stop in ladders, because we can't interact with other items while holding the ladders.
endNodeFilter = node => node.Waypoint.Ladders == null
};
if (repairTool != null)
{
objective.CloseEnough = repairTool.Range * 0.75f;
}
return objective;
},
onAbandon: () =>
{
Abandon = true;
if (IsRepairing)
{
character.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, true), null, 0.0f, "cannotrepair", 10.0f);
}
});
}
}
private void FindRepairTool()
{
foreach (Repairable repairable in Item.Repairables)
{
foreach (var kvp in repairable.requiredItems)
{
foreach (RelatedItem requiredItem in kvp.Value)
{
foreach (var item in character.Inventory.Items)
{
if (requiredItem.MatchesItem(item))
{
repairTool = item.GetComponent<RepairTool>();
}
}
}
}
}
}
private void OperateRepairTool(float deltaTime)
{
character.CursorPosition = Item.Position;
if (repairTool.Item.RequireAimToUse)
{
character.SetInput(InputType.Aim, false, true);
}
Vector2 fromToolToTarget = Item.Position - repairTool.Item.Position;
if (fromToolToTarget.LengthSquared() < MathUtils.Pow(repairTool.Range / 2, 2))
{
// Too close -> steer away
character.AIController.SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(character.SimPosition - Item.SimPosition) / 2);
}
else
{
character.AIController.SteeringManager.Reset();
}
if (VectorExtensions.Angle(VectorExtensions.Forward(repairTool.Item.body.TransformedRotation), fromToolToTarget) < MathHelper.PiOver4)
{
repairTool.Use(deltaTime, character);
}
}
}
}
@@ -0,0 +1,123 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
namespace Barotrauma
{
class AIObjectiveRepairItems : AIObjectiveLoop<Item>
{
public override string DebugTag => "repair items";
/// <summary>
/// Should the character only attempt to fix items they have the skills to fix, or any damaged item
/// </summary>
public bool RequireAdequateSkills;
public override bool AllowMultipleInstances => true;
public override bool IsDuplicate<T>(T otherObjective) =>
(otherObjective as AIObjective) is AIObjectiveRepairItems repairObjective && repairObjective.RequireAdequateSkills == RequireAdequateSkills;
public AIObjectiveRepairItems(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { }
protected override void CreateObjectives()
{
foreach (var item in Targets)
{
foreach (Repairable repairable in item.Repairables)
{
if (!Objectives.TryGetValue(item, out AIObjective objective))
{
objective = ObjectiveConstructor(item);
Objectives.Add(item, objective);
if (!subObjectives.Contains(objective))
{
subObjectives.Add(objective);
}
objective.Completed += () =>
{
Objectives.Remove(item);
OnObjectiveCompleted(objective, item);
};
objective.Abandoned += () =>
{
Objectives.Remove(item);
ignoreList.Add(item);
targetUpdateTimer = 0;
};
}
break;
}
}
}
protected override bool Filter(Item item)
{
if (!IsValidTarget(item, character)) { return false; }
if (item.CurrentHull.FireSources.Count > 0) { return false; }
// Don't repair items in rooms that have enemies inside.
if (Character.CharacterList.Any(c => c.CurrentHull == item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; }
if (!Objectives.ContainsKey(item))
{
if (item != character.SelectedConstruction)
{
float condition = item.ConditionPercentage;
if (item.Repairables.All(r => condition >= r.AIRepairThreshold)) { return false; }
}
}
if (RequireAdequateSkills)
{
if (item.Repairables.Any(r => !r.HasRequiredSkills(character))) { return false; }
}
return true;
}
protected override float TargetEvaluation()
{
if (character.SelectedConstruction != null && Targets.Any(t => character.SelectedConstruction == t && t.ConditionPercentage < 100))
{
// Don't stop fixing until done
return 100;
}
int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveRepairItems>(), onlyBots: true);
int items = Targets.Count;
bool anyFixers = otherFixers > 0;
float ratio = anyFixers ? items / (float)otherFixers : 1;
if (objectiveManager.CurrentOrder == this)
{
return Targets.Sum(t => 100 - t.ConditionPercentage) * ratio;
}
else
{
if (anyFixers && (ratio <= 1 || otherFixers > 5 || otherFixers / (float)HumanAIController.CountCrew(onlyBots: true) > 0.75f))
{
// Enough fixers
return 0;
}
return Targets.Sum(t => 100 - t.ConditionPercentage) * ratio;
}
}
protected override IEnumerable<Item> GetList() => Item.ItemList;
protected override AIObjective ObjectiveConstructor(Item item)
=> new AIObjectiveRepairItem(character, item, objectiveManager, PriorityModifier);
protected override void OnObjectiveCompleted(AIObjective objective, Item target)
=> HumanAIController.RemoveTargets<AIObjectiveRepairItems, Item>(character, target);
public static bool IsValidTarget(Item item, Character character)
{
if (item == null) { return false; }
if (item.IsFullCondition) { return false; }
if (item.CurrentHull == null) { return false; }
if (item.Submarine == null) { return false; }
if (item.Submarine.TeamID != character.TeamID) { return false; }
if (item.Repairables.None()) { return false; }
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(item, true)) { return false; }
return true;
}
}
}
@@ -0,0 +1,303 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
class AIObjectiveRescue : AIObjective
{
public override string DebugTag => "rescue";
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
const float TreatmentDelay = 0.5f;
const float CloseEnoughToTreat = 100.0f;
private readonly Character targetCharacter;
private AIObjectiveGoTo goToObjective;
private AIObjectiveGetItem getItemObjective;
private float treatmentTimer;
private Hull safeHull;
public AIObjectiveRescue(Character character, Character targetCharacter, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
if (targetCharacter == null)
{
string errorMsg = $"{character.Name}: Attempted to create a Rescue objective with no target!\n" + Environment.StackTrace;
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("AIObjectiveRescue:ctor:targetnull", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
Abandon = true;
return;
}
this.targetCharacter = targetCharacter;
}
protected override void Act(float deltaTime)
{
if (character.LockHands || targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead)
{
Abandon = true;
return;
}
if (targetCharacter.SelectedBy != null && targetCharacter.SelectedBy != character)
{
var otherCharacter = character.SelectedBy;
if (otherCharacter != null)
{
// Someone else is rescuing/holding the target.
Abandon = otherCharacter.IsPlayer || character.GetSkillLevel("medical") < otherCharacter.GetSkillLevel("medical");
}
}
if (targetCharacter != character)
{
// Unconcious target is not in a safe place -> Move to a safe place first
if (targetCharacter.IsUnconscious && HumanAIController.GetHullSafety(targetCharacter.CurrentHull, targetCharacter) < HumanAIController.HULL_SAFETY_THRESHOLD)
{
if (character.SelectedCharacter != targetCharacter)
{
character.Speak(TextManager.GetWithVariables("DialogFoundUnconsciousTarget", new string[2] { "[targetname]", "[roomname]" },
new string[2] { targetCharacter.Name, targetCharacter.CurrentHull.DisplayName }, new bool[2] { false, true }),
null, 1.0f, "foundunconscioustarget" + targetCharacter.Name, 60.0f);
// Go to the target and select it
if (!character.CanInteractWith(targetCharacter))
{
RemoveSubObjective(ref goToObjective);
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager)
{
CloseEnough = CloseEnoughToTreat,
DialogueIdentifier = "dialogcannotreachpatient",
TargetName = targetCharacter.DisplayName
},
onCompleted: () => RemoveSubObjective(ref goToObjective),
onAbandon: () =>
{
RemoveSubObjective(ref goToObjective);
Abandon = true;
});
}
else
{
character.SelectCharacter(targetCharacter);
}
}
else
{
// Drag the character into safety
if (safeHull == null)
{
safeHull = objectiveManager.GetObjective<AIObjectiveFindSafety>().FindBestHull(HumanAIController.VisibleHulls);
}
if (character.CurrentHull != safeHull)
{
RemoveSubObjective(ref goToObjective);
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(safeHull, character, objectiveManager),
onCompleted: () => RemoveSubObjective(ref goToObjective),
onAbandon: () =>
{
RemoveSubObjective(ref goToObjective);
safeHull = character.CurrentHull;
});
}
}
}
}
if (subObjectives.Any()) { return; }
if (targetCharacter != character && !character.CanInteractWith(targetCharacter))
{
RemoveSubObjective(ref goToObjective);
// Go to the target and select it
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager)
{
CloseEnough = CloseEnoughToTreat,
DialogueIdentifier = "dialogcannotreachpatient",
TargetName = targetCharacter.DisplayName
},
onCompleted: () => RemoveSubObjective(ref goToObjective),
onAbandon: () =>
{
RemoveSubObjective(ref goToObjective);
Abandon = true;
});
}
else
{
// We can start applying treatment
if (character != targetCharacter && character.SelectedCharacter != targetCharacter)
{
character.Speak(TextManager.GetWithVariables("DialogFoundWoundedTarget", new string[2] { "[targetname]", "[roomname]" },
new string[2] { targetCharacter.Name, targetCharacter.CurrentHull.DisplayName }, new bool[2] { false, true }),
null, 1.0f, "foundwoundedtarget" + targetCharacter.Name, 60.0f);
character.SelectCharacter(targetCharacter);
}
GiveTreatment(deltaTime);
}
}
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 void GiveTreatment(float deltaTime)
{
if (!targetCharacter.IsPlayer)
{
// If the target is a bot, don't let it move
targetCharacter.AIController.SteeringManager.Reset();
}
if (treatmentTimer > 0.0f)
{
treatmentTimer -= deltaTime;
return;
}
treatmentTimer = TreatmentDelay;
//find which treatments are the most suitable to treat the character's current condition
targetCharacter.CharacterHealth.GetSuitableTreatments(currentTreatmentSuitabilities, normalize: false);
//check if we already have a suitable treatment for any of the afflictions
foreach (Affliction affliction in GetSortedAfflictions(targetCharacter))
{
foreach (KeyValuePair<string, float> treatmentSuitability in affliction.Prefab.TreatmentSuitability)
{
if (currentTreatmentSuitabilities.ContainsKey(treatmentSuitability.Key) && currentTreatmentSuitabilities[treatmentSuitability.Key] > 0.0f)
{
Item matchingItem = character.Inventory.FindItemByIdentifier(treatmentSuitability.Key, true);
if (matchingItem == null) { continue; }
ApplyTreatment(affliction, matchingItem);
//wait a bit longer after applying a treatment to wait for potential side-effects to manifest
treatmentTimer = TreatmentDelay * 4;
return;
}
}
}
float cprSuitability = targetCharacter.Oxygen < 0.0f ? -targetCharacter.Oxygen * 100.0f : 0.0f;
//didn't have any suitable treatments available, try to find some medical items
if (currentTreatmentSuitabilities.Any(s => s.Value > cprSuitability))
{
itemNameList.Clear();
suitableItemIdentifiers.Clear();
foreach (KeyValuePair<string, float> treatmentSuitability in currentTreatmentSuitabilities)
{
if (treatmentSuitability.Value <= cprSuitability) { continue; }
if (MapEntityPrefab.Find(null, treatmentSuitability.Key, showErrorMessages: false) is ItemPrefab itemPrefab)
{
if (!Item.ItemList.Any(it => it.prefab.Identifier == treatmentSuitability.Key)) { continue; }
suitableItemIdentifiers.Add(treatmentSuitability.Key);
//only list the first 4 items
if (itemNameList.Count < 4)
{
itemNameList.Add(itemPrefab.Name);
}
}
}
if (itemNameList.Count > 0)
{
string itemListStr = "";
if (itemNameList.Count == 1)
{
itemListStr = itemNameList[0];
}
else
{
itemListStr = string.Join(" or ", string.Join(", ", itemNameList.Take(itemNameList.Count - 1)), itemNameList.Last());
}
if (targetCharacter != character)
{
character.Speak(TextManager.GetWithVariables("DialogListRequiredTreatments", new string[2] { "[targetname]", "[treatmentlist]" },
new string[2] { targetCharacter.Name, itemListStr }, new bool[2] { false, true }),
null, 2.0f, "listrequiredtreatments" + targetCharacter.Name, 60.0f);
}
character.DeselectCharacter();
RemoveSubObjective(ref getItemObjective);
TryAddSubObjective(ref getItemObjective,
constructor: () => new AIObjectiveGetItem(character, suitableItemIdentifiers.ToArray(), objectiveManager, equip: true),
onCompleted: () => RemoveSubObjective(ref getItemObjective),
onAbandon: () => RemoveSubObjective(ref getItemObjective));
}
}
if (character != targetCharacter)
{
character.AnimController.Anim = AnimController.Animation.CPR;
}
}
private void ApplyTreatment(Affliction affliction, Item item)
{
var targetLimb = targetCharacter.CharacterHealth.GetAfflictionLimb(affliction);
bool remove = false;
foreach (ItemComponent ic in item.Components)
{
if (!ic.HasRequiredContainedItems(user: character, addMessage: false)) { continue; }
#if CLIENT
ic.PlaySound(ActionType.OnUse, character);
#endif
ic.WasUsed = true;
ic.ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb);
if (ic.DeleteOnUse)
{
remove = true;
}
}
if (remove)
{
Entity.Spawner?.AddToRemoveQueue(item);
}
}
protected override bool Check()
{
if (character.LockHands || targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead)
{
Abandon = true;
return false;
}
// Don't go into rooms that have enemies
if (Character.CharacterList.Any(c => c.CurrentHull == targetCharacter.CurrentHull && !HumanAIController.IsFriendly(character, c) && HumanAIController.IsActive(c)))
{
Abandon = true;
return false;
}
bool isCompleted = AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) >= AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager, character, targetCharacter);
if (isCompleted && targetCharacter != character)
{
character.Speak(TextManager.GetWithVariable("DialogTargetHealed", "[targetname]", targetCharacter.Name),
null, 1.0f, "targethealed" + targetCharacter.Name, 60.0f);
}
return isCompleted;
}
public override float GetPriority()
{
if (targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead)
{
Priority = 0;
}
else
{
// Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally)
float dist = Math.Abs(character.WorldPosition.X - targetCharacter.WorldPosition.X) + Math.Abs(character.WorldPosition.Y - targetCharacter.WorldPosition.Y) * 2.0f;
float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, dist));
if (targetCharacter.CurrentHull == character.CurrentHull)
{
distanceFactor = 1;
}
float vitalityFactor = 1 - AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) / 100;
float devotion = CumulatedDevotion / 100;
Priority = MathHelper.Lerp(0, 100, MathHelper.Clamp(devotion + (vitalityFactor * distanceFactor * PriorityModifier), 0, 1));
}
return Priority;
}
public static IEnumerable<Affliction> GetSortedAfflictions(Character character) => CharacterHealth.SortAfflictionsBySeverity(character.CharacterHealth.GetAllAfflictions());
}
}
@@ -0,0 +1,124 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
class AIObjectiveRescueAll : AIObjectiveLoop<Character>
{
public override string DebugTag => "rescue all";
public override bool ForceRun => true;
public override bool InverseTargetEvaluation => true;
private const float vitalityThreshold = 80;
private const float vitalityThresholdForOrders = 100;
public static float GetVitalityThreshold(AIObjectiveManager manager, Character character, Character target)
{
if (manager == null)
{
return vitalityThreshold;
}
else
{
return character == target || manager.CurrentOrder is AIObjectiveRescueAll ? vitalityThresholdForOrders : vitalityThreshold;
}
}
public AIObjectiveRescueAll(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier) { }
protected override bool Filter(Character target) => IsValidTarget(target, character);
protected override IEnumerable<Character> GetList() => Character.CharacterList;
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)
{
return Targets.Min(t => GetVitalityFactor(t)) / ratio;
}
else
{
float multiplier = 1;
if (anyRescuers)
{
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;
}
}
}
}
return Targets.Min(t => GetVitalityFactor(t)) / ratio * multiplier;
}
}
public static float GetVitalityFactor(Character character)
{
float vitality = character.HealthPercentage - character.Bleeding - character.Bloodloss + Math.Min(character.Oxygen, 0);
return Math.Clamp(vitality, 0, 100);
}
protected override AIObjective ObjectiveConstructor(Character target)
=> new AIObjectiveRescue(character, target, objectiveManager, PriorityModifier);
protected override void OnObjectiveCompleted(AIObjective objective, Character target)
=> HumanAIController.RemoveTargets<AIObjectiveRescueAll, Character>(character, target);
public static bool IsValidTarget(Character target, Character character)
{
if (target == null || target.IsDead || target.Removed) { return false; }
if (!HumanAIController.IsFriendly(character, target)) { return false; }
if (character.AIController is HumanAIController humanAI)
{
if (GetVitalityFactor(target) >= GetVitalityThreshold(humanAI.ObjectiveManager, character, target)) { return false; }
if (!humanAI.ObjectiveManager.IsCurrentOrder<AIObjectiveRescueAll>())
{
// Ignore unsafe hulls, unless ordered
if (humanAI.UnsafeHulls.Contains(target.CurrentHull))
{
return false;
}
}
}
else
{
if (GetVitalityFactor(target) >= vitalityThreshold) { return false; }
}
if (target.Submarine == null || character.Submarine == null) { return false; }
if (target.Submarine.TeamID != character.Submarine.TeamID) { return false; }
if (target.CurrentHull == null) { 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)
{
// Ignore all concious targets that are currently fighting, fleeing or treating characters
if (targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveCombat>() ||
targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveFindSafety>() ||
targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveRescue>())
{
return false;
}
}
// Don't go into rooms that have enemies
if (Character.CharacterList.Any(c => c.CurrentHull == target.CurrentHull && !HumanAIController.IsFriendly(character, c) && HumanAIController.IsActive(c))) { return false; }
return true;
}
}
}
@@ -0,0 +1,314 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Xml.Linq;
using System.Linq;
namespace Barotrauma
{
public enum OrderCategory
{
Emergency,
Movement,
Power,
Maintenance,
Operate,
Undefined
}
class Order
{
public static Dictionary<string, Order> Prefabs { get; private set; }
public static Dictionary<OrderCategory, Tuple<Sprite, Color>> OrderCategoryIcons { get; private set; }
public static List<Order> PrefabList { get; private set; }
public static Order GetPrefab(string identifier)
{
if (!Prefabs.TryGetValue(identifier, out Order order))
{
DebugConsole.ThrowError($"Cannot find an order with the identifier '{identifier}'!");
}
return order;
}
public Order Prefab
{
get;
private set;
}
public readonly string Name;
public readonly Sprite SymbolSprite;
public readonly Type ItemComponentType;
public readonly string[] ItemIdentifiers;
public readonly string Identifier;
private Color? color;
public Color Color
{
get
{
if (color.HasValue)
{
return color.Value;
}
else if (OrderCategoryIcons.TryGetValue(Category, out Tuple<Sprite, Color> sprite))
{
return sprite.Item2;
}
else
{
return Color.White;
}
}
private set
{
color = value;
}
}
//if true, the order is issued to all available characters
public bool TargetAllCharacters;
public readonly float FadeOutTime;
public Entity TargetEntity;
public ItemComponent TargetItemComponent;
public readonly bool UseController;
public Controller ConnectedController;
public Character OrderGiver;
public readonly OrderCategory Category;
//legacy support
public readonly string[] AppropriateJobs;
public readonly string[] Options;
public readonly string[] OptionNames;
public readonly Dictionary<string, Sprite> OptionSprites;
public readonly float Weight;
static Order()
{
Prefabs = new Dictionary<string, Order>();
OrderCategoryIcons = new Dictionary<OrderCategory, Tuple<Sprite, Color>>();
foreach (ContentFile file in GameMain.Instance.GetFilesOfType(ContentType.Orders))
{
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
if (doc == null) { continue; }
var mainElement = doc.Root;
bool allowOverriding = false;
if (doc.Root.IsOverride())
{
mainElement = doc.Root.FirstElement();
allowOverriding = true;
}
foreach (XElement sourceElement in mainElement.Elements())
{
var element = sourceElement.IsOverride() ? sourceElement.FirstElement() : sourceElement;
string name = element.Name.ToString();
if (name.Equals("order", StringComparison.OrdinalIgnoreCase))
{
string identifier = element.GetAttributeString("identifier", null);
if (string.IsNullOrWhiteSpace(identifier))
{
DebugConsole.ThrowError($"Error in file {file.Path}: The order element '{name}' does not have an identifier! All orders must have a unique identifier.");
continue;
}
if (Prefabs.TryGetValue(identifier, out Order duplicate))
{
if (allowOverriding || sourceElement.IsOverride())
{
DebugConsole.NewMessage($"Overriding an existing order '{identifier}' with another one defined in '{file.Path}'", Color.Yellow);
Prefabs.Remove(identifier);
}
else
{
DebugConsole.ThrowError($"Error in file {file.Path}: Duplicate element with the idenfitier '{identifier}' found in '{file.Path}'! All orders must have a unique identifier. Use <override></override> tags to override an order with the same identifier.");
continue;
}
}
var newOrder = new Order(element);
newOrder.Prefab = newOrder;
Prefabs.Add(identifier, newOrder);
}
else if (name.Equals("ordercategory", StringComparison.OrdinalIgnoreCase))
{
var category = (OrderCategory)Enum.Parse(typeof(OrderCategory), element.GetAttributeString("category", "undefined"), true);
if (OrderCategoryIcons.ContainsKey(category))
{
if (allowOverriding || sourceElement.IsOverride())
{
DebugConsole.NewMessage($"Overriding an existing icon for the '{category}' order category with another one defined in '{file}'", Color.Yellow);
OrderCategoryIcons.Remove(category);
}
else
{
DebugConsole.ThrowError($"Error in file {file}: Duplicate element for the '{category}' order category found in '{file}'! All order categories must be unique. Use <override></override> tags to override an order category.");
continue;
}
}
var spriteElement = element.GetChildElement("sprite");
if (spriteElement != null)
{
var sprite = new Sprite(spriteElement, lazyLoad: true);
var color = element.GetAttributeColor("color", Color.White);
OrderCategoryIcons.Add(category, new Tuple<Sprite, Color>(sprite, color));
}
}
}
}
PrefabList = new List<Order>(Prefabs.Values);
}
/// <summary>
/// Constructor for order prefabs
/// </summary>
private Order(XElement orderElement)
{
Identifier = orderElement.GetAttributeString("identifier", "");
Name = TextManager.Get("OrderName." + Identifier, true) ?? "Name not found";
string targetItemType = orderElement.GetAttributeString("targetitemtype", "");
if (!string.IsNullOrWhiteSpace(targetItemType))
{
try
{
ItemComponentType = Type.GetType("Barotrauma.Items.Components." + targetItemType, true, true);
}
catch (Exception e)
{
DebugConsole.ThrowError("Error in the order definitions: item component type " + targetItemType + " not found", e);
}
}
ItemIdentifiers = orderElement.GetAttributeStringArray("targetitemidentifiers", new string[0], trim: true, convertToLowerInvariant: true);
color = orderElement.GetAttributeColor("color");
FadeOutTime = orderElement.GetAttributeFloat("fadeouttime", 0.0f);
UseController = orderElement.GetAttributeBool("usecontroller", false);
TargetAllCharacters = orderElement.GetAttributeBool("targetallcharacters", false);
AppropriateJobs = orderElement.GetAttributeStringArray("appropriatejobs", new string[0]);
Options = orderElement.GetAttributeStringArray("options", new string[0]);
Category = (OrderCategory)Enum.Parse(typeof(OrderCategory), orderElement.GetAttributeString("category", "undefined"), true);
Weight = orderElement.GetAttributeFloat(0.0f, "weight");
string translatedOptionNames = TextManager.Get("OrderOptions." + Identifier, true);
if (translatedOptionNames == null)
{
OptionNames = orderElement.GetAttributeStringArray("optionnames", new string[0]);
}
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)
{
DebugConsole.ThrowError("Error in Order " + Name + " - the number of option names doesn't match the number of options.");
OptionNames = Options;
}
var spriteElement = orderElement.GetChildElement("sprite");
if (spriteElement != null)
{
SymbolSprite = new Sprite(spriteElement, lazyLoad: true);
}
OptionSprites = new Dictionary<string, Sprite>();
if (Options != null && Options.Length > 0)
{
var optionSpriteElements = orderElement.GetChildElement("optionsprites")?.GetChildElements("sprite");
if (optionSpriteElements != null && optionSpriteElements.Any())
{
for (int i = 0; i < Options.Length; i++)
{
if (i >= optionSpriteElements.Count()) { break; };
var sprite = new Sprite(optionSpriteElements.ElementAt(i), lazyLoad: true);
OptionSprites.Add(Options[i], sprite);
}
}
}
}
/// <summary>
/// Constructor for order instances
/// </summary>
public Order(Order prefab, Entity targetEntity, ItemComponent targetItem, Character orderGiver = null)
{
Prefab = prefab;
Name = prefab.Name;
Identifier = prefab.Identifier;
ItemComponentType = prefab.ItemComponentType;
Options = prefab.Options;
SymbolSprite = prefab.SymbolSprite;
Color = prefab.Color;
UseController = prefab.UseController;
TargetAllCharacters = prefab.TargetAllCharacters;
AppropriateJobs = prefab.AppropriateJobs;
FadeOutTime = prefab.FadeOutTime;
Weight = prefab.Weight;
Category = prefab.Category;
OrderGiver = orderGiver;
TargetEntity = targetEntity;
if (targetItem != null)
{
if (UseController)
{
//try finding the controller with the simpler non-recursive method first
ConnectedController =
targetItem.Item.GetConnectedComponents<Controller>().FirstOrDefault() ??
targetItem.Item.GetConnectedComponents<Controller>(recursive: true).FirstOrDefault();
}
TargetEntity = targetItem.Item;
TargetItemComponent = targetItem;
}
}
public bool HasAppropriateJob(Character character)
{
if (character.Info == null || character.Info.Job == null) { return false; }
if (character.Info.Job.Prefab.AppropriateOrders.Any(appropriateOrderId => Identifier == appropriateOrderId)) { return true; }
if (!JobPrefab.Prefabs.Any(jp => jp.AppropriateOrders.Contains(Identifier)) &&
(AppropriateJobs == null || AppropriateJobs.Length == 0))
{
return true;
}
for (int i = 0; i < AppropriateJobs.Length; i++)
{
if (character.Info.Job.Prefab.Identifier.Equals(AppropriateJobs[i], StringComparison.OrdinalIgnoreCase)) { return true; }
}
return false;
}
public string GetChatMessage(string targetCharacterName, string targetRoomName, bool givingOrderToSelf, string orderOption = "")
{
orderOption ??= "";
string messageTag = (givingOrderToSelf && !TargetAllCharacters ? "OrderDialogSelf." : "OrderDialog.") + Identifier;
if (!string.IsNullOrEmpty(orderOption)) { messageTag += "." + orderOption; }
if (targetCharacterName == null) { targetCharacterName = ""; }
if (targetRoomName == null) { targetRoomName = ""; }
string msg = TextManager.GetWithVariables(messageTag, new string[2] { "[name]", "[roomname]" }, new string[2] { targetCharacterName, targetRoomName }, new bool[2] { false, true }, true);
if (msg == null) { return ""; }
return msg;
}
}
}
@@ -0,0 +1,443 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
class PathNode
{
private WayPoint wayPoint;
private int wayPointID;
public int state;
public PathNode Parent;
private Vector2 position;
public float F,G,H;
public List<PathNode> connections;
public List<float> distances;
public WayPoint Waypoint
{
get { return wayPoint; }
}
public Vector2 Position
{
get { return position; }
}
public PathNode(WayPoint wayPoint)
{
this.wayPoint = wayPoint;
this.position = wayPoint.SimPosition;
wayPointID = wayPoint.ID;
connections = new List<PathNode>();
}
public static List<PathNode> GenerateNodes(List<WayPoint> wayPoints)
{
var nodes = new Dictionary<int, PathNode>();
foreach (WayPoint wayPoint in wayPoints)
{
if (wayPoint == null) continue;
if (nodes.ContainsKey(wayPoint.ID))
{
#if DEBUG
DebugConsole.ThrowError("Error in PathFinder.GenerateNodes (duplicate ID \"" + wayPoint.ID + "\")");
#endif
continue;
}
nodes.Add(wayPoint.ID, new PathNode(wayPoint));
}
foreach (KeyValuePair<int,PathNode> node in nodes)
{
foreach (MapEntity linked in node.Value.wayPoint.linkedTo)
{
PathNode connectedNode = null;
nodes.TryGetValue(linked.ID, out connectedNode);
if (connectedNode == null) continue;
node.Value.connections.Add(connectedNode);
}
}
var nodeList = nodes.Values.ToList();
nodeList.RemoveAll(n => n.connections.Count == 0);
foreach (PathNode node in nodeList)
{
node.distances = new List<float>();
for (int i = 0; i< node.connections.Count; i++)
{
node.distances.Add(Vector2.Distance(node.position, node.connections[i].position));
}
}
return nodeList;
}
}
class PathFinder
{
public delegate float? GetNodePenaltyHandler(PathNode node, PathNode prevNode);
public GetNodePenaltyHandler GetNodePenalty;
private List<PathNode> nodes;
public bool InsideSubmarine { get; set; }
public PathFinder(List<WayPoint> wayPoints, bool indoorsSteering = false)
{
nodes = PathNode.GenerateNodes(wayPoints.FindAll(w => w.Submarine != null == indoorsSteering));
foreach (WayPoint wp in wayPoints)
{
wp.linkedTo.CollectionChanged += WaypointLinksChanged;
}
InsideSubmarine = indoorsSteering;
}
void WaypointLinksChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (Submarine.Unloading) return;
var waypoints = sender as IEnumerable<MapEntity>;
foreach (MapEntity me in waypoints)
{
WayPoint wp = me as WayPoint;
if (me == null) continue;
var node = nodes.Find(n => n.Waypoint == wp);
if (node == null) return;
if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Remove)
{
for (int i = node.connections.Count - 1; i >= 0; i--)
{
//remove connection if the waypoint isn't connected anymore
if (wp.linkedTo.FirstOrDefault(l => l == node.connections[i].Waypoint) == null)
{
node.connections.RemoveAt(i);
node.distances.RemoveAt(i);
}
}
}
else if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
{
for (int i = 0; i < wp.linkedTo.Count; i++)
{
WayPoint connected = wp.linkedTo[i] as WayPoint;
if (connected == null) continue;
//already connected, continue
if (node.connections.Any(n => n.Waypoint == connected)) continue;
var matchingNode = nodes.Find(n => n.Waypoint == connected);
if (matchingNode == null)
{
#if DEBUG
DebugConsole.ThrowError("Waypoint connections were changed, no matching path node found in PathFinder");
#endif
return;
}
node.connections.Add(matchingNode);
node.distances.Add(Vector2.Distance(node.Position, matchingNode.Position));
}
}
}
}
public SteeringPath FindPath(Vector2 start, Vector2 end, Submarine hostSub = null, string errorMsgStr = null, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null)
{
float closestDist = 0.0f;
PathNode startNode = null;
foreach (PathNode node in nodes)
{
if (nodeFilter != null && !nodeFilter(node)) { continue; }
if (startNodeFilter != null && !startNodeFilter(node)) { continue; }
Vector2 nodePos = node.Position;
if (hostSub != null)
{
Vector2 diff = hostSub.SimPosition - node.Waypoint.Submarine.SimPosition;
nodePos -= diff;
}
float xDiff = Math.Abs(start.X - nodePos.X);
float yDiff = Math.Abs(start.Y - nodePos.Y);
if (yDiff > 1.0f && node.Waypoint.Ladders == null && node.Waypoint.Stairs == null)
{
yDiff += 10.0f;
}
float dist = xDiff + (InsideSubmarine ? yDiff * 10.0f : yDiff); //higher cost for vertical movement when inside the sub
//prefer nodes that are closer to the end position
dist += (Math.Abs(end.X - nodePos.X) + Math.Abs(end.Y - nodePos.Y)) / 2.0f;
//much higher cost to waypoints that are outside
if (node.Waypoint.CurrentHull == null && InsideSubmarine)
{
dist *= 10.0f;
}
if (dist < closestDist || startNode == null)
{
//if searching for a path inside the sub, make sure the waypoint is visible
if (InsideSubmarine)
{
var body = Submarine.PickBody(
start, nodePos, null,
Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionStairs);
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;
}
}
closestDist = dist;
startNode = node;
}
}
if (startNode == null)
{
#if DEBUG
DebugConsole.NewMessage("Pathfinding error, couldn't find a start node. "+ errorMsgStr, Color.DarkRed);
#endif
return new SteeringPath(true);
}
closestDist = 0.0f;
PathNode endNode = null;
foreach (PathNode node in nodes)
{
if (nodeFilter != null && !nodeFilter(node)) { continue; }
if (endNodeFilter != null && !endNodeFilter(node)) { continue; }
Vector2 nodePos = node.Position;
if (hostSub != null)
{
Vector2 diff = hostSub.SimPosition - node.Waypoint.Submarine.SimPosition;
nodePos -= diff;
}
float dist = Vector2.DistanceSquared(end, nodePos);
if (InsideSubmarine)
{
//much higher cost to waypoints that are outside
if (node.Waypoint.CurrentHull == null) dist *= 10.0f;
//avoid stopping at a doorway
if (node.Waypoint.ConnectedDoor != null) dist *= 10.0f;
}
if (dist < closestDist || endNode == null)
{
//if searching for a path inside the sub, make sure the waypoint is visible
if (InsideSubmarine)
{
var body = Submarine.PickBody(end, nodePos, null,
Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionStairs );
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;
}
}
closestDist = dist;
endNode = node;
}
}
if (endNode == null)
{
#if DEBUG
DebugConsole.NewMessage("Pathfinding error, couldn't find an end node. " + errorMsgStr, Color.DarkRed);
#endif
return new SteeringPath(true);
}
var path = FindPath(startNode, endNode, nodeFilter);
return path;
}
public SteeringPath FindPath(WayPoint start, WayPoint end)
{
PathNode startNode=null, endNode=null;
foreach (PathNode node in nodes)
{
if (node.Waypoint == start)
{
startNode = node;
if (endNode != null) break;
}
if (node.Waypoint == end)
{
endNode = node;
if (startNode != null) break;
}
}
if (startNode == null || endNode == null)
{
#if DEBUG
DebugConsole.NewMessage("Pathfinding error, couldn't find matching pathnodes to waypoints.", Color.DarkRed);
#endif
return new SteeringPath(true);
}
return FindPath(startNode, endNode);
}
private SteeringPath FindPath(PathNode start, PathNode end, Func<PathNode, bool> filter = null)
{
if (start == end)
{
var path1 = new SteeringPath();
path1.AddNode(start.Waypoint);
return path1;
}
foreach (PathNode node in nodes)
{
node.Parent = null;
node.state = 0;
node.F = 0.0f;
node.G = 0.0f;
node.H = 0.0f;
}
start.state = 1;
while (true)
{
PathNode currNode = null;
float dist = float.MaxValue;
foreach (PathNode node in nodes)
{
if (filter != null && !filter(node)) { continue; }
if (node.state != 1) { continue; }
if (node.F < dist)
{
dist = node.F;
currNode = node;
}
}
if (currNode == null || currNode == end) { break; }
currNode.state = 2;
for (int i = 0; i < currNode.connections.Count; i++)
{
PathNode nextNode = currNode.connections[i];
//a node that hasn't been searched yet
if (nextNode.state == 0)
{
nextNode.H = Vector2.Distance(nextNode.Position, end.Position);
float penalty = 0.0f;
if (GetNodePenalty != null)
{
float? nodePenalty = GetNodePenalty(currNode, nextNode);
if (nodePenalty == null)
{
nextNode.state = -1;
continue;
}
penalty = nodePenalty.Value;
}
nextNode.G = currNode.G + currNode.distances[i] + penalty;
nextNode.F = nextNode.G + nextNode.H;
nextNode.Parent = currNode;
nextNode.state = 1;
}
//node that has been searched
else if (nextNode.state == 1 || nextNode.state == -1)
{
float tempG = currNode.G + currNode.distances[i];
if (GetNodePenalty != null)
{
float? nodePenalty = GetNodePenalty(currNode, nextNode);
if (nodePenalty == null) { continue; }
tempG += nodePenalty.Value;
}
//only use if this new route is better than the
//route the node was a part of
if (tempG < nextNode.G)
{
nextNode.G = tempG;
nextNode.F = nextNode.G + nextNode.H;
nextNode.Parent = currNode;
nextNode.state = 1;
}
}
}
}
if (end.state == 0 || end.Parent == null)
{
#if DEBUG
DebugConsole.NewMessage("Path not found", Color.Yellow);
#endif
return new SteeringPath(true);
}
SteeringPath path = new SteeringPath();
List<WayPoint> finalPath = new List<WayPoint>();
PathNode pathNode = end;
while (pathNode != start && pathNode != null)
{
finalPath.Add(pathNode.Waypoint);
//(there was one bug report that seems to have been caused by this loop never terminating:
//couldn't reproduce or figure out what caused it, but here's a workaround that prevents the game from crashing in case it happens again)
//should be fixed now, was most likely caused by the parent fields of the nodes not being cleared before starting the pathfinding
if (finalPath.Count > nodes.Count)
{
#if DEBUG
DebugConsole.ThrowError("Pathfinding error: constructing final path failed");
#endif
return new SteeringPath(true);
}
path.Cost += pathNode.F;
pathNode = pathNode.Parent;
}
finalPath.Add(start.Waypoint);
finalPath.Reverse();
foreach (WayPoint wayPoint in finalPath)
{
path.AddNode(wayPoint);
}
return path;
}
}
}
@@ -0,0 +1,181 @@
using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
using System;
using FarseerPhysics;
using Barotrauma.Extensions;
namespace Barotrauma
{
class SteeringManager
{
protected const float CircleDistance = 2.5f;
protected const float CircleRadius = 0.3f;
protected const float RayCastInterval = 0.5f;
protected ISteerable host;
protected Vector2 steering;
private float lastRayCastTime;
private bool avoidRayCastHit;
public Vector2 AvoidDir { get; private set; }
public Vector2 AvoidRayCastHitPosition { get; private set; }
public Vector2 AvoidLookAheadPos { get; private set; }
private float wanderAngle;
public float WanderAngle
{
get { return wanderAngle; }
set { wanderAngle = value; }
}
public SteeringManager(ISteerable host)
{
this.host = host;
wanderAngle = Rand.Range(0.0f, MathHelper.TwoPi);
}
public void SteeringSeek(Vector2 targetSimPos, float weight = 1)
{
steering += DoSteeringSeek(targetSimPos, weight);
}
public void SteeringWander(float weight = 1)
{
steering += DoSteeringWander(weight);
}
public void SteeringAvoid(float deltaTime, float lookAheadDistance, float weight = 1)
{
steering += DoSteeringAvoid(deltaTime, lookAheadDistance, weight);
}
public void SteeringManual(float deltaTime, Vector2 velocity)
{
steering += velocity;
}
public void Reset()
{
steering = Vector2.Zero;
}
public void ResetX()
{
steering.X = 0.0f;
}
public void ResetY()
{
steering.Y = 0.0f;
}
public virtual void Update(float speed)
{
if (steering == Vector2.Zero || !MathUtils.IsValid(steering))
{
steering = Vector2.Zero;
host.Steering = Vector2.Zero;
return;
}
if (steering.LengthSquared() > speed * speed)
{
steering = Vector2.Normalize(steering) * Math.Abs(speed);
}
host.Steering = steering;
}
protected virtual Vector2 DoSteeringSeek(Vector2 target, float weight)
{
Vector2 targetVel = target - host.SimPosition;
if (targetVel.LengthSquared() < 0.00001f) return Vector2.Zero;
targetVel = Vector2.Normalize(targetVel) * weight;
Vector2 newSteering = targetVel - host.Steering;
if (newSteering == Vector2.Zero) return Vector2.Zero;
float steeringSpeed = (newSteering + host.Steering).Length();
if (steeringSpeed > Math.Abs(weight))
{
newSteering = Vector2.Normalize(newSteering) * Math.Abs(weight);
}
return newSteering;
}
protected virtual Vector2 DoSteeringWander(float weight)
{
Vector2 circleCenter = (host.Steering == Vector2.Zero) ? Vector2.UnitY : host.Steering;
circleCenter = Vector2.Normalize(circleCenter) * CircleDistance;
Vector2 displacement = new Vector2(
(float)Math.Cos(wanderAngle),
(float)Math.Sin(wanderAngle));
displacement = displacement * CircleRadius;
float angleChange = 1.5f;
wanderAngle += Rand.Range(0.0f, 1.0f) * angleChange - angleChange * 0.5f;
Vector2 newSteering = circleCenter + displacement;
float steeringSpeed = (newSteering + host.Steering).Length();
if (steeringSpeed > weight)
{
newSteering = Vector2.Normalize(newSteering) * weight;
}
return newSteering;
}
protected virtual Vector2 DoSteeringAvoid(float deltaTime, float lookAheadDistance, float weight, Vector2? heading = null)
{
if (steering == Vector2.Zero || host.Steering == Vector2.Zero)
{
return Vector2.Zero;
}
float maxDistance = lookAheadDistance;
if (Timing.TotalTime >= lastRayCastTime + RayCastInterval)
{
avoidRayCastHit = false;
AvoidLookAheadPos = host.SimPosition + Vector2.Normalize(host.Steering) * maxDistance;
lastRayCastTime = (float)Timing.TotalTime;
Body closestBody = Submarine.CheckVisibility(host.SimPosition, AvoidLookAheadPos);
if (closestBody != null)
{
avoidRayCastHit = true;
AvoidRayCastHitPosition = Submarine.LastPickedPosition;
AvoidDir = Submarine.LastPickedNormal;
//add a bit of randomness
AvoidDir = MathUtils.RotatePoint(AvoidDir, Rand.Range(-0.15f, 0.15f));
//wait a bit longer for the next raycast
lastRayCastTime += RayCastInterval;
}
}
if (AvoidDir.LengthSquared() < 0.0001f) { return Vector2.Zero; }
//if raycast hit nothing, lerp avoid dir to zero
if (!avoidRayCastHit)
{
AvoidDir -= Vector2.Normalize(AvoidDir) * deltaTime * 0.5f;
}
Vector2 diff = AvoidRayCastHitPosition - host.SimPosition;
float dist = diff.Length();
//> 0 when heading in the same direction as the obstacle, < 0 when away from it
float dot = MathHelper.Clamp(Vector2.Dot(diff / dist, host.Steering), 0.0f, 1.0f);
if (dot < 0) { return Vector2.Zero; }
return AvoidDir * dot * weight * MathHelper.Clamp(1.0f - dist / lookAheadDistance, 0.0f, 1.0f);
}
}
}
@@ -0,0 +1,104 @@
using Microsoft.Xna.Framework;
using System.Collections.Generic;
namespace Barotrauma
{
class SteeringPath
{
private List<WayPoint> nodes;
int currentIndex;
public bool Unreachable
{
get;
set;
}
public SteeringPath(bool unreachable = false)
{
nodes = new List<WayPoint>();
Unreachable = unreachable;
}
public void AddNode(WayPoint node)
{
if (node == null) return;
nodes.Add(node);
if (node.CurrentHull == null) HasOutdoorsNodes = true;
}
public bool HasOutdoorsNodes
{
get;
private set;
}
public int CurrentIndex
{
get { return currentIndex; }
}
public float Cost
{
get;
set;
}
public WayPoint PrevNode
{
get
{
if (currentIndex-1 < 0 || currentIndex-1 > nodes.Count - 1) return null;
return nodes[currentIndex-1];
}
}
public WayPoint CurrentNode
{
get
{
if (currentIndex < 0 || currentIndex > nodes.Count - 1) return null;
return nodes[currentIndex];
}
}
public List<WayPoint> Nodes
{
get { return nodes; }
}
public WayPoint NextNode
{
get
{
if (currentIndex+1 < 0 || currentIndex+1 > nodes.Count - 1) return null;
return nodes[currentIndex+1];
}
}
public bool Finished
{
get { return currentIndex >= nodes.Count; }
}
public void SkipToNextNode()
{
currentIndex++;
}
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++;
return CurrentNode;
}
public void ClearPath()
{
nodes.Clear();
}
}
}
@@ -0,0 +1,121 @@
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
namespace Barotrauma
{
class SwarmBehavior
{
private readonly float minDistFromClosest;
private readonly float maxDistFromCenter;
private readonly float cohesion;
public List<AICharacter> Members { get; private set; } = new List<AICharacter>();
public HashSet<AICharacter> ActiveMembers { get; private set; } = new HashSet<AICharacter>();
private EnemyAIController ai;
public bool IsActive { get; set; }
public bool IsEnoughMembers => ActiveMembers.Count > 1;
public SwarmBehavior(XElement element, EnemyAIController ai)
{
this.ai = ai;
minDistFromClosest = ConvertUnits.ToSimUnits(element.GetAttributeFloat("mindistfromclosest", 10.0f));
maxDistFromCenter = ConvertUnits.ToSimUnits(element.GetAttributeFloat("maxdistfromcenter", 1000.0f));
cohesion = element.GetAttributeFloat("cohesion", 1) / 10;
}
public static void CreateSwarm(IEnumerable<AICharacter> swarm)
{
var aiControllers = new List<EnemyAIController>();
foreach (AICharacter character in swarm)
{
if (character.AIController is EnemyAIController enemyAI && enemyAI.SwarmBehavior != null)
{
aiControllers.Add(enemyAI);
}
}
var filteredMembers = aiControllers.Select(m => m.Character as AICharacter).Where(m => m != null);
foreach (EnemyAIController ai in aiControllers)
{
ai.SwarmBehavior.Members = filteredMembers.ToList();
}
}
public void Refresh()
{
Members.RemoveAll(m => m.IsDead || m.Removed || m.AIController is EnemyAIController ai && ai.State == AIState.Flee);
foreach (var member in Members)
{
if (!member.AIController.Enabled && member.IsRemotePlayer || Character.Controlled == member || !((EnemyAIController)member.AIController).SwarmBehavior.IsActive)
{
ActiveMembers.Remove(member);
}
else
{
ActiveMembers.Add(member);
}
}
}
public void UpdateSteering(float deltaTime)
{
if (!IsActive) { return; }
if (!IsEnoughMembers) { return; }
//calculate the "center of mass" of the swarm and the distance to the closest character in the swarm
float closestDistSqr = float.MaxValue;
Vector2 center = Vector2.Zero;
AICharacter closest = null;
foreach (AICharacter member in Members)
{
center += member.SimPosition;
if (member == ai.Character) { continue; }
float distSqr = Vector2.DistanceSquared(member.SimPosition, ai.Character.SimPosition);
if (distSqr < closestDistSqr)
{
closestDistSqr = distSqr;
closest = member;
}
}
center /= Members.Count;
if (closest == null) { return; }
//steer away from the closest if too close
float closestDist = (float)Math.Sqrt(closestDistSqr);
if (closestDist < minDistFromClosest)
{
Vector2 diff = closest.SimPosition - ai.SimPosition;
if (diff.LengthSquared() < 0.0001f)
{
diff = Vector2.UnitX;
}
ai.SteeringManager.SteeringManual(deltaTime, -diff);
}
//steer closer to the center of mass if too far
else if (Vector2.DistanceSquared(center, ai.SimPosition) > maxDistFromCenter * maxDistFromCenter)
{
float distFromCenter = Vector2.Distance(center, ai.SimPosition);
ai.SteeringManager.SteeringSeek(center, (distFromCenter - maxDistFromCenter) / 10.0f);
}
//keep the characters moving in roughly the same direction
if (cohesion > 0.0f)
{
Vector2 avgVel = Vector2.Zero;
foreach (AICharacter member in Members)
{
avgVel += member.AnimController.TargetMovement;
}
avgVel /= Members.Count;
ai.SteeringManager.SteeringManual(deltaTime, avgVel * cohesion);
}
}
}
}