(61d00a474) v0.9.7.1
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+83
@@ -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);
|
||||
}
|
||||
}
|
||||
+683
@@ -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;
|
||||
//}
|
||||
}
|
||||
}
|
||||
+181
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+134
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+146
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+47
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+130
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+293
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+148
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+78
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+296
@@ -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);
|
||||
}
|
||||
}
|
||||
+340
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+224
@@ -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;
|
||||
}
|
||||
}
|
||||
+85
@@ -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);
|
||||
}
|
||||
}
|
||||
+248
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+123
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+303
@@ -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());
|
||||
}
|
||||
}
|
||||
+124
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user