38f1ddb...178a853: v0.8.9.1, removed content folder

This commit is contained in:
Joonas Rikkonen
2019-03-18 19:46:58 +02:00
parent 38f1ddb6fe
commit 6c0679c297
1054 changed files with 151673 additions and 144931 deletions
@@ -2,15 +2,17 @@
namespace Barotrauma
{
partial class AIController : ISteerable
abstract partial class AIController : ISteerable
{
public enum AIState { None, Attack, GoTo, Escape, Eat }
public enum AIState { Idle, Attack, GoTo, Escape, Eat }
public bool Enabled;
public readonly Character Character;
protected AIState state;
private AIState state;
protected AITarget selectedAiTarget;
protected SteeringManager steeringManager;
@@ -24,7 +26,7 @@ namespace Barotrauma
get { return Character.AnimController.TargetMovement; }
set { Character.AnimController.TargetMovement = value; }
}
public Vector2 SimPosition
{
get { return Character.SimPosition; }
@@ -40,10 +42,35 @@ namespace Barotrauma
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 AITarget SelectedAiTarget
{
get { return selectedAiTarget; }
}
public AIState State
{
get { return state; }
set { state = value; }
set
{
if (state == value) return;
OnStateChanged(state, value);
state = value;
}
}
public AIController (Character c)
@@ -53,13 +80,13 @@ namespace Barotrauma
Enabled = true;
}
public virtual void OnAttacked(Character attacker, float amount) { }
public virtual void OnAttacked(Character attacker, AttackResult attackResult) { }
public virtual void SelectTarget(AITarget target) { }
public virtual void Update(float deltaTime) { }
//protected Structure lastStructurePicked;
protected virtual void OnStateChanged(AIState from, AIState to) { }
}
}
@@ -1,6 +1,7 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma
{
@@ -20,15 +21,45 @@ namespace Barotrauma
public float SoundRange
{
get { return soundRange; }
set { soundRange = Math.Max(value, 0.0f); }
set { soundRange = Math.Max(value, MinSoundRange); }
}
public float SightRange
{
get { return sightRange; }
set { sightRange = Math.Max(value, 0.0f); }
set { sightRange = Math.Max(value, MinSightRange); }
}
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 string SonarLabel;
public bool Enabled = true;
public float MinSoundRange, MinSightRange;
public Vector2 WorldPosition
{
get
@@ -67,12 +98,27 @@ namespace Barotrauma
}
}
public AITarget(Entity e, XElement element) : this(e)
{
SightRange = MinSightRange = element.GetAttributeFloat("sightrange", 0.0f);
SoundRange = MinSoundRange = element.GetAttributeFloat("soundrange", 0.0f);
SonarLabel = element.GetAttributeString("sonarlabel", "");
}
public AITarget(Entity e)
{
Entity = e;
List.Add(this);
}
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);
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,7 @@
using Microsoft.Xna.Framework;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
namespace Barotrauma
{
@@ -11,10 +13,19 @@ namespace Barotrauma
private AIObjectiveManager objectiveManager;
private AITarget selectedAiTarget;
private float updateObjectiveTimer;
private bool shouldCrouch;
private float crouchRaycastTimer;
const float CrouchRaycastInterval = 1.0f;
private SteeringManager outsideSteering, insideSteering;
public override AIObjectiveManager ObjectiveManager
{
get { return objectiveManager; }
}
public Order CurrentOrder
{
get;
@@ -29,7 +40,8 @@ namespace Barotrauma
public HumanAIController(Character c) : base(c)
{
steeringManager = new IndoorsSteeringManager(this, true);
insideSteering = new IndoorsSteeringManager(this, true, false);
outsideSteering = new SteeringManager(this);
objectiveManager = new AIObjectiveManager(c);
objectiveManager.AddObjective(new AIObjectiveFindSafety(c));
@@ -44,12 +56,23 @@ namespace Barotrauma
public override void Update(float deltaTime)
{
if (DisableCrewAI || Character.IsUnconscious) return;
if (Character.Submarine != null || selectedAiTarget?.Entity?.Submarine != null)
{
if (steeringManager != insideSteering) insideSteering.Reset();
steeringManager = insideSteering;
}
else
{
if (steeringManager != outsideSteering) outsideSteering.Reset();
steeringManager = outsideSteering;
}
(Character.AnimController as HumanoidAnimController).Crouching = shouldCrouch;
CheckCrouching(deltaTime);
Character.ClearInputs();
//steeringManager = Character.AnimController.CurrentHull == null ? outdoorsSteeringManager : indoorsSteeringManager;
if (updateObjectiveTimer>0.0f)
if (updateObjectiveTimer > 0.0f)
{
updateObjectiveTimer -= deltaTime;
}
@@ -59,22 +82,23 @@ namespace Barotrauma
updateObjectiveTimer = UpdateObjectiveInterval;
}
if (Character.SpeechImpediment < 100.0f)
{
ReportProblems();
UpdateSpeaking();
}
objectiveManager.DoCurrentObjective(deltaTime);
float currObjectivePriority = objectiveManager.GetCurrentPriority(Character);
float moveSpeed = 1.0f;
float currObjectivePriority = objectiveManager.GetCurrentPriority();
if (currObjectivePriority > 30.0f)
{
moveSpeed *= Character.AnimController.InWater ? Character.AnimController.SwimSpeedMultiplier : Character.AnimController.RunSpeedMultiplier;
}
steeringManager.Update(moveSpeed);
bool run = currObjectivePriority > 30.0f;
steeringManager.Update(Character.AnimController.GetCurrentSpeed(run));
bool ignorePlatforms = Character.AnimController.TargetMovement.Y < -0.5f &&
(-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X));
var currPath = (steeringManager as IndoorsSteeringManager).CurrentPath;
var currPath = (steeringManager as IndoorsSteeringManager)?.CurrentPath;
if (currPath != null && currPath.CurrentNode != null)
{
if (currPath.CurrentNode.SimPosition.Y < Character.AnimController.GetColliderBottom().Y)
@@ -84,22 +108,34 @@ namespace Barotrauma
}
Character.AnimController.IgnorePlatforms = ignorePlatforms;
(Character.AnimController as HumanoidAnimController).Crouching = false;
if (!Character.AnimController.InWater)
{
Character.AnimController.TargetMovement = new Vector2(
Vector2 targetMovement = new Vector2(
Character.AnimController.TargetMovement.X,
MathHelper.Clamp(Character.AnimController.TargetMovement.Y, -1.0f, 1.0f)) * Character.SpeedMultiplier;
MathHelper.Clamp(Character.AnimController.TargetMovement.Y, -1.0f, 1.0f));
Character.SpeedMultiplier = 1.0f;
float maxSpeed = Character.GetCurrentMaxSpeed(run);
targetMovement.X = MathHelper.Clamp(targetMovement.X, -maxSpeed, maxSpeed);
targetMovement.Y = MathHelper.Clamp(targetMovement.Y, -maxSpeed, maxSpeed);
//apply speed multiplier if
// a. it's boosting the movement speed and the character is trying to move fast (= running)
// b. it's a debuff that decreases movement speed
if (run || Character.SpeedMultiplier <= 0.0f) targetMovement *= Character.SpeedMultiplier;
Character.SpeedMultiplier = 1.0f; // Reset, items will set the value before the next update
Character.AnimController.TargetMovement = targetMovement;
}
if (Character.SelectedConstruction != null && Character.SelectedConstruction.GetComponent<Items.Components.Ladder>()!=null)
if (Character.AnimController.Anim == AnimController.Animation.Climbing &&
Character.SelectedConstruction != null &&
Character.SelectedConstruction.GetComponent<Items.Components.Ladder>() != null)
{
if (currPath != null && currPath.CurrentNode != null && currPath.CurrentNode.Ladders != null)
{
Character.AnimController.TargetMovement = new Vector2( 0.0f, Math.Sign(Character.AnimController.TargetMovement.Y));
Character.AnimController.TargetMovement = new Vector2(0.0f, Math.Sign(Character.AnimController.TargetMovement.Y));
}
}
@@ -112,7 +148,7 @@ namespace Barotrauma
//-> take the suit off
if (canTakeOffSuit && (Character.Oxygen < 50.0f || objectiveManager.CurrentObjective is AIObjectiveIdle))
{
var divingSuit = Character.Inventory.FindItem("Diving Suit");
var divingSuit = Character.Inventory.FindItemByIdentifier("divingsuit") ?? Character.Inventory.FindItemByTag("divingsuit");
if (divingSuit != null) divingSuit.Drop(Character);
}
@@ -136,27 +172,104 @@ namespace Barotrauma
Character.AnimController.TargetDir = Character.AnimController.TargetMovement.X > 0.0f ? Direction.Right : Direction.Left;
}
}
public override void OnAttacked(Character attacker, float amount)
private void ReportProblems()
{
if (amount <= 0.0f) return;
if (GameMain.Client != null) return;
var enemy = attacker as Character;
if (enemy == null || enemy == Character) return;
Order newOrder = null;
if (Character.CurrentHull != null)
{
if (Character.CurrentHull.FireSources.Count > 0)
{
var orderPrefab = Order.PrefabList.Find(o => o.AITag == "reportfire");
newOrder = new Order(orderPrefab, Character.CurrentHull, null);
}
objectiveManager.AddObjective(new AIObjectiveCombat(Character, enemy));
if (Character.CurrentHull.ConnectedGaps.Any(g => !g.IsRoomToRoom && g.ConnectedDoor == null && g.Open > 0.0f))
{
var orderPrefab = Order.PrefabList.Find(o => o.AITag == "reportbreach");
newOrder = new Order(orderPrefab, Character.CurrentHull, null);
}
//the objective in the manager is not necessarily the same as the one we just instantiated,
//because the objective isn't added if there's already an identical objective in the manager
var combatObjective = objectiveManager.GetObjective<AIObjectiveCombat>();
combatObjective.MaxEnemyDamage = Math.Max(amount, combatObjective.MaxEnemyDamage);
foreach (Character c in Character.CharacterList)
{
if (c.CurrentHull == Character.CurrentHull && !c.IsDead &&
(c.AIController is EnemyAIController || c.TeamID != Character.TeamID))
{
var orderPrefab = Order.PrefabList.Find(o => o.AITag == "reportintruders");
newOrder = new Order(orderPrefab, Character.CurrentHull, null);
}
}
}
if (Character.CurrentHull != null && (Character.Bleeding > 1.0f || Character.Vitality < Character.MaxVitality * 0.1f))
{
var orderPrefab = Order.PrefabList.Find(o => o.AITag == "requestfirstaid");
newOrder = new Order(orderPrefab, Character.CurrentHull, null);
}
if (newOrder != null)
{
if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime))
{
Character.Speak(
newOrder.GetChatMessage("", Character.CurrentHull?.RoomName), ChatMessageType.Order);
if (GameMain.Server != null)
{
OrderChatMessage msg = new OrderChatMessage(newOrder, "", Character.CurrentHull, null, Character);
GameMain.Server.SendOrderChatMessage(msg);
}
}
}
}
public void SetOrder(Order order, string option)
private void UpdateSpeaking()
{
if (Character.Oxygen < 20.0f)
{
Character.Speak(TextManager.Get("DialogLowOxygen"), null, 0, "lowoxygen", 30.0f);
}
if (Character.Bleeding > 2.0f)
{
Character.Speak(TextManager.Get("DialogBleeding"), null, 0, "bleeding", 30.0f);
}
if (Character.PressureTimer > 50.0f && Character.CurrentHull != null)
{
Character.Speak(TextManager.Get("DialogPressure").Replace("[roomname]", Character.CurrentHull.RoomName), null, 0, "pressure", 30.0f);
}
}
public override void OnAttacked(Character attacker, AttackResult attackResult)
{
float totalDamage = attackResult.Damage;
if (totalDamage <= 0.0f || attacker == null) return;
if (attacker.AnimController.Anim == AnimController.Animation.CPR && attacker.SelectedCharacter == Character)
{
// Don't attack characters that damage you while doing cpr, because let's assume that they are helping you.
// Should not cancel any existing ai objectives (so that if the character attacked you and then helped, we still would want to retaliate).
return;
}
objectiveManager.AddObjective(new AIObjectiveCombat(Character, attacker), Rand.Range(0.5f, 1, Rand.RandSync.Unsynced), () =>
{
//the objective in the manager is not necessarily the same as the one we just instantiated,
//because the objective isn't added if there's already an identical objective in the manager
var combatObjective = objectiveManager.GetObjective<AIObjectiveCombat>();
combatObjective.MaxEnemyDamage = Math.Max(totalDamage, combatObjective.MaxEnemyDamage);
});
}
public void SetOrder(Order order, string option, Character orderGiver, bool speak = true)
{
CurrentOrderOption = option;
CurrentOrder = order;
objectiveManager.SetOrder(order, option);
objectiveManager.SetOrder(order, option, orderGiver);
if (speak && Character.SpeechImpediment < 100.0f) Character.Speak(TextManager.Get("DialogAffirmative"), null, 1.0f);
SetOrderProjSpecific(order);
}
@@ -166,5 +279,21 @@ namespace Barotrauma
{
selectedAiTarget = target;
}
private void CheckCrouching(float deltaTime)
{
crouchRaycastTimer -= deltaTime;
if (crouchRaycastTimer > 0.0f) return;
crouchRaycastTimer = CrouchRaycastInterval;
//start the raycast in front of the character in the direction it's heading to
Vector2 startPos = Character.SimPosition;
startPos.X += MathHelper.Clamp(Character.AnimController.TargetMovement.X, -1.0f, 1.0f);
//do a raycast upwards to find any walls
float minCeilingDist = Character.AnimController.Collider.height / 2 + Character.AnimController.Collider.radius + 0.1f;
shouldCrouch = Submarine.PickBody(startPos, startPos + Vector2.UnitY * minCeilingDist, null, Physics.CollisionWall) != null;
}
}
}
@@ -10,7 +10,7 @@ namespace Barotrauma
private PathFinder pathFinder;
private SteeringPath currentPath;
private bool canOpenDoors;
private bool canOpenDoors, canBreakDoors;
private Character character;
@@ -33,20 +33,27 @@ namespace Barotrauma
get { return currentTarget; }
}
public IndoorsSteeringManager(ISteerable host, bool canOpenDoors)
public bool IsPathDirty
{
get;
private set;
}
public IndoorsSteeringManager(ISteerable host, bool canOpenDoors, bool canBreakDoors)
: base(host)
{
pathFinder = new PathFinder(WayPoint.WayPointList.FindAll(wp => wp.SpawnType == SpawnType.Path), 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 = 1)
public override void Update(float speed)
{
base.Update(speed);
@@ -58,16 +65,18 @@ namespace Barotrauma
currentPath = path;
if (path.Nodes.Any()) currentTarget = path.Nodes[path.Nodes.Count - 1].SimPosition;
findPathTimer = 1.0f;
IsPathDirty = false;
}
protected override Vector2 DoSteeringSeek(Vector2 target, float speed = 1)
protected override Vector2 DoSteeringSeek(Vector2 target, float speed)
{
//find a new path if one hasn't been found yet or the target is different from the current target
if (currentPath == null || Vector2.Distance(target, currentTarget) > 1.0f || findPathTimer < -1.0f)
{
if (findPathTimer > 0.0f) return Vector2.Zero;
IsPathDirty = true;
if (findPathTimer > 0.0f) return Vector2.Zero;
currentTarget = target;
Vector2 pos = host.SimPosition;
if (character != null && character.Submarine == null)
@@ -75,15 +84,16 @@ namespace Barotrauma
var targetHull = Hull.FindHull(FarseerPhysics.ConvertUnits.ToDisplayUnits(target), null, false);
if (targetHull != null && targetHull.Submarine != null)
{
pos -= targetHull.SimPosition;
pos -= targetHull.Submarine.SimPosition;
}
}
currentPath = pathFinder.FindPath(pos, target);
currentPath = pathFinder.FindPath(pos, target, "(Character: " + character.Name + ")");
findPathTimer = Rand.Range(1.0f, 1.2f);
return DiffToCurrentNode();
IsPathDirty = false;
return DiffToCurrentNode();
}
Vector2 diff = DiffToCurrentNode();
@@ -116,8 +126,11 @@ namespace Barotrauma
}
return currentTarget - pos2;
}
if (canOpenDoors && !character.LockHands) CheckDoorsInPath();
if (canOpenDoors && !character.LockHands)
{
CheckDoorsInPath();
}
Vector2 pos = host.SimPosition;
@@ -134,10 +147,12 @@ namespace Barotrauma
pos -= FarseerPhysics.ConvertUnits.ToSimUnits(currentPath.CurrentNode.Submarine.Position-character.Submarine.Position);
}
}
}
if (currentPath.CurrentNode != null && currentPath.CurrentNode.Ladders != null)
//only humanoids can climb ladders
if (currentPath.CurrentNode != null &&
currentPath.CurrentNode.Ladders != null &&
character.AnimController is HumanoidAnimController)
{
if (character.SelectedConstruction != currentPath.CurrentNode.Ladders.Item &&
currentPath.CurrentNode.Ladders.Item.IsInsideTrigger(character.WorldPosition))
@@ -158,9 +173,6 @@ namespace Barotrauma
//at the same height as the waypoint
if (Math.Abs(collider.SimPosition.Y - currentPath.CurrentNode.SimPosition.Y) < (collider.height / 2 + collider.radius) * 1.25f)
{
//climbing ladders -> don't move horizontally
diff.X = 0.0f;
float heightFromFloor = character.AnimController.GetColliderBottom().Y - character.AnimController.FloorY;
if (heightFromFloor <= 0.0f)
{
@@ -179,10 +191,29 @@ namespace Barotrauma
currentPath.SkipToNextNode();
}
}
else
{
//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 (currentPath.CurrentNode.Ladders != null && currentPath.CurrentNode.Ladders == currentPath.NextNode?.Ladders &&
Math.Sign(currentPath.CurrentNode.WorldPosition.Y - character.WorldPosition.Y) != Math.Sign(currentPath.NextNode.WorldPosition.Y - character.WorldPosition.Y))
{
currentPath.SkipToNextNode();
}
}
character.AnimController.IgnorePlatforms = false;
return diff;
}
else if (character.AnimController.InWater)
{
if (Vector2.DistanceSquared(pos, currentPath.CurrentNode.SimPosition) < collider.radius * collider.radius)
{
currentPath.SkipToNextNode();
}
}
else
{
Vector2 colliderBottom = character.AnimController.GetColliderBottom();
@@ -203,42 +234,54 @@ namespace Barotrauma
{
for (int i = 0; i < 2; i++)
{
WayPoint node = null;
WayPoint nextNode = null;
if (i==0)
{
node = currentPath.CurrentNode;
nextNode = currentPath.NextNode;
}
else
{
node = currentPath.PrevNode;
nextNode = currentPath.CurrentNode;
}
if (node == null || node.ConnectedGap == null || node.ConnectedGap.ConnectedDoor == null) continue;
if (nextNode == null) continue;
var door = node.ConnectedGap.ConnectedDoor;
Door door = null;
bool shouldBeOpen = false;
if (door.LinkedGap.IsHorizontal)
if (currentPath.Nodes.Count == 1)
{
int currentDir = Math.Sign(nextNode.WorldPosition.X - door.Item.WorldPosition.X);
shouldBeOpen = (door.Item.WorldPosition.X - character.WorldPosition.X) * currentDir > -50.0f;
door = currentPath.Nodes.First().ConnectedDoor;
shouldBeOpen = door != null;
}
else
{
int currentDir = Math.Sign(nextNode.WorldPosition.Y - door.Item.WorldPosition.Y);
WayPoint node = null;
WayPoint nextNode = null;
if (i == 0)
{
node = currentPath.CurrentNode;
nextNode = currentPath.NextNode;
}
else
{
node = currentPath.PrevNode;
nextNode = currentPath.CurrentNode;
}
if (node?.ConnectedDoor == null) continue;
shouldBeOpen = (door.Item.WorldPosition.Y - character.WorldPosition.Y) * currentDir > -80.0f;
if (nextNode == 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 = node.ConnectedGap.ConnectedDoor;
if (door.LinkedGap.IsHorizontal)
{
int currentDir = Math.Sign(nextNode.WorldPosition.X - door.Item.WorldPosition.X);
shouldBeOpen = (door.Item.WorldPosition.X - character.WorldPosition.X) * currentDir > -50.0f;
}
else
{
int currentDir = Math.Sign(nextNode.WorldPosition.Y - door.Item.WorldPosition.Y);
shouldBeOpen = (door.Item.WorldPosition.Y - character.WorldPosition.Y) * currentDir > -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)
{
@@ -267,7 +310,7 @@ namespace Barotrauma
return;
}
closestButton.Item.TryInteract(character, false, true, true);
closestButton.Item.TryInteract(character, false, true, false);
break;
}
}
@@ -281,23 +324,37 @@ namespace Barotrauma
float penalty = 0.0f;
if (nextNode.Waypoint.ConnectedGap != null && nextNode.Waypoint.ConnectedGap.Open < 0.9f)
{
if (nextNode.Waypoint.ConnectedGap.ConnectedDoor == null)
if (nextNode.Waypoint.ConnectedDoor == null)
{
penalty = 100.0f;
}
//door closed and the character can't open doors -> node can't be traversed
if (!canOpenDoors || character.LockHands) return null;
var doorButtons = nextNode.Waypoint.ConnectedGap.ConnectedDoor.Item.GetConnectedComponents<Controller>();
if (!doorButtons.Any()) return null;
foreach (Controller button in doorButtons)
if (!canBreakDoors)
{
if (Math.Sign(button.Item.Position.X - nextNode.Waypoint.Position.X) !=
Math.Sign(node.Position.X - nextNode.Position.X)) continue;
//door closed and the character can't open doors -> node can't be traversed
if (!canOpenDoors || character.LockHands) return null;
if (!button.HasRequiredItems(character, false)) return null;
var doorButtons = nextNode.Waypoint.ConnectedDoor.Item.GetConnectedComponents<Controller>();
if (!doorButtons.Any()) return null;
foreach (Controller button in doorButtons)
{
if (Math.Sign(button.Item.Position.X - nextNode.Waypoint.Position.X) !=
Math.Sign(node.Position.X - nextNode.Position.X)) continue;
if (!button.HasRequiredItems(character, false)) 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;
}
}
@@ -0,0 +1,296 @@
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 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 AIController.AIState.Idle:
if (attachToWalls && character.Submarine == null && Level.Loaded != null)
{
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)
{
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;
wallAttachPos = ConvertUnits.ToSimUnits(intersection);
break;
}
}
if (WallAttachPos != Vector2.Zero) 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, character.AnimController.GetCurrentSpeed(false) * 0.1f);
enemyAI.SteeringManager.SteeringSeek(wallAttachPos, character.AnimController.GetCurrentSpeed(true));
}
}
break;
case AIController.AIState.Attack:
if (enemyAI.AttackingLimb != null)
{
if (attachToSub && 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.Range * enemyAI.AttackingLimb.attack.Range)
{
AttachToBody(character.AnimController.Collider, attachLimb, attachTargetBody, transformedAttachPos);
}
}
}
}
break;
default:
WallAttachPos = null;
DeattachFromBody();
break;
}
if (attachTargetBody != null && deattachTimer < 0.0f)
{
Entity entity = attachTargetBody.UserData as Entity;
Submarine attachedSub = entity is Submarine ? (Submarine)entity : 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.AddJoint(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.AddJoint(colliderJoint);
attachJoints.Add(colliderJoint);
}
public void DeattachFromBody()
{
foreach (Joint joint in attachJoints)
{
GameMain.World.RemoveJoint(joint);
}
attachJoints.Clear();
}
private void OnCharacterDeath(Character character, CauseOfDeath causeOfDeath)
{
DeattachFromBody();
character.OnDeath -= OnCharacterDeath;
}
}
}
@@ -0,0 +1,315 @@
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 static List<NPCConversation> list = new List<NPCConversation>();
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<string> filePaths)
{
//language, identifier, filepath
List<Tuple<string, string, string>> contentPackageFiles = new List<Tuple<string, string, string>>();
foreach (string filePath in filePaths)
{
XDocument doc = XMLExtensions.TryLoadXml(filePath);
if (doc == null || doc.Root == null) continue;
string language = doc.Root.GetAttributeString("Language", "English");
string identifier = doc.Root.GetAttributeString("Identifier", "unknown");
contentPackageFiles.Add(new Tuple<string, string, string>(language, identifier, filePath));
}
List<Tuple<string, string, string>> translationFiles = new List<Tuple<string, string, string>>();
foreach (string filePath in Directory.GetFiles(Path.Combine("Content", "NPCConversations")))
{
XDocument doc = XMLExtensions.TryLoadXml(filePath);
if (doc == null || doc.Root == null) continue;
string language = doc.Root.GetAttributeString("Language", "English");
string identifier = doc.Root.GetAttributeString("Identifier", "unknown");
translationFiles.Add(new Tuple<string, string, string>(language, identifier, filePath));
}
//get the languages and identifiers of the files
for (int i = 0; i < contentPackageFiles.Count; i++)
{
var contentPackageFile = contentPackageFiles[i];
//correct language, all good
if (contentPackageFile.Item1 == TextManager.Language) continue;
//attempt to find a translation file with the correct language and a matching identifier
//if it fails, we'll just use the original file with the incorrect language
var translation = translationFiles.Find(t => t.Item1 == TextManager.Language && t.Item2 == contentPackageFile.Item2);
if (translation != null) contentPackageFiles[i] = translation; //replace with the translation file
}
foreach (var file in contentPackageFiles)
{
Load(file.Item3);
}
}
private static void Load(string file)
{
XDocument doc = XMLExtensions.TryLoadXml(file);
if (doc == null || doc.Root == null) return;
string language = doc.Root.GetAttributeString("Language", "English");
if (language != TextManager.Language) return;
foreach (XElement subElement in doc.Root.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "conversation":
list.Add(new NPCConversation(subElement));
break;
case "personalitytrait":
new NPCPersonalityTrait(subElement);
break;
}
}
}
public NPCConversation(XElement element)
{
Line = element.GetAttributeString("line", "");
speakerIndex = element.GetAttributeInt("speaker", 0);
AllowedJobs = new List<JobPrefab>();
string allowedJobsStr = element.GetAttributeString("allowedjobs", "");
foreach (string allowedJobIdentifier in allowedJobsStr.Split(','))
{
var jobPrefab = JobPrefab.List.Find(jp => jp.Identifier.ToLowerInvariant() == allowedJobIdentifier.ToLowerInvariant());
if (jobPrefab != null) AllowedJobs.Add(jobPrefab);
}
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));
}
}
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: list);
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 = list.FindAll(conversation => requiredFlags.All(f => conversation.Flags.Contains(f)));
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 (selectedConversation.AllowedJobs.Count > 0 && !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);
}
}
}
@@ -38,7 +38,7 @@ namespace Barotrauma
/// </summary>
public void TryComplete(float deltaTime)
{
subObjectives.RemoveAll(s => s.IsCompleted() || !s.CanBeCompleted);
subObjectives.RemoveAll(s => s.IsCompleted() || !s.CanBeCompleted || ShouldInterruptSubObjective(s));
foreach (AIObjective objective in subObjectives)
{
@@ -66,6 +66,18 @@ namespace Barotrauma
return currentSubObjective;
}
public void SortSubObjectives(AIObjectiveManager objectiveManager)
{
if (!subObjectives.Any()) return;
subObjectives.Sort((x, y) => y.GetPriority(objectiveManager).CompareTo(x.GetPriority(objectiveManager)));
subObjectives[0].SortSubObjectives(objectiveManager);
}
protected virtual bool ShouldInterruptSubObjective(AIObjective subObjective)
{
return false;
}
protected abstract void Act(float deltaTime);
public abstract bool IsCompleted();
@@ -0,0 +1,66 @@
using Barotrauma.Items.Components;
using System.Collections.Generic;
namespace Barotrauma
{
class AIObjectiveChargeBatteries : AIObjective
{
private List<PowerContainer> availableBatteries;
private string orderOption;
public AIObjectiveChargeBatteries(Character character, string option)
: base(character, option)
{
orderOption = option;
availableBatteries = new List<PowerContainer>();
foreach (Item item in Item.ItemList)
{
if (item.Submarine == null) continue;
if (item.Prefab.Identifier != "battery" && !item.HasTag("battery")) continue;
var powerContainer = item.GetComponent<PowerContainer>();
availableBatteries.Add(powerContainer);
}
if (availableBatteries.Count == 0)
{
character?.Speak(TextManager.Get("DialogNoBatteries"), null, 4.0f, "nobatteries", 10.0f);
}
}
public override float GetPriority(AIObjectiveManager objectiveManager)
{
if (objectiveManager.CurrentOrder == this)
{
return AIObjectiveManager.OrderPriority;
}
return 1.0f;
}
public override bool IsCompleted()
{
return false;
}
public override bool IsDuplicate(AIObjective otherObjective)
{
return otherObjective is AIObjectiveChargeBatteries other && other.orderOption == orderOption;
}
protected override void Act(float deltaTime)
{
if (availableBatteries.Count == 0)
{
AddSubObjective(new AIObjectiveIdle(character));
return;
}
foreach (PowerContainer battery in availableBatteries)
{
AddSubObjective(new AIObjectiveOperateItem(battery, character, orderOption, false));
}
}
}
}
@@ -1,4 +1,5 @@
using Microsoft.Xna.Framework;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -17,21 +18,13 @@ namespace Barotrauma
private AIObjectiveFindSafety escapeObjective;
float coolDownTimer;
private AIObjectiveContainItem reloadWeaponObjective;
private readonly float enemyStrength;
private float coolDownTimer;
public AIObjectiveCombat(Character character, Character enemy)
: base(character, "")
public AIObjectiveCombat(Character character, Character enemy) : base(character, "")
{
this.enemy = enemy;
foreach (Limb limb in enemy.AnimController.Limbs)
{
if (limb.attack == null) continue;
enemyStrength += limb.attack.GetDamage(1.0f);
}
coolDownTimer = CoolDown;
}
@@ -39,26 +32,66 @@ namespace Barotrauma
{
coolDownTimer -= deltaTime;
var weapon = character.Inventory.FindItem("weapon");
var weapon = character.Inventory.FindItemByTag("weapon");
if (weapon == null)
{
Escape(deltaTime);
}
else
{
//TODO: make sure the weapon is ready to use (projectiles/batteries loaded)
if (!character.SelectedItems.Contains(weapon))
{
if (character.Inventory.TryPutItem(weapon, 3, false, false, character))
if (character.Inventory.TryPutItem(weapon, 3, true, false, character))
{
weapon.Equip(character);
}
else
{
//couldn't equip the item, escape
Escape(deltaTime);
return;
}
}
//make sure the weapon is loaded
var weaponComponent =
weapon.GetComponent<RangedWeapon>() as ItemComponent ??
weapon.GetComponent<MeleeWeapon>() as ItemComponent ??
weapon.GetComponent<RepairTool>() as ItemComponent;
if (weaponComponent != null && weaponComponent.requiredItems.ContainsKey(RelatedItem.RelationType.Contained))
{
Item[] containedItems = weapon.ContainedItems;
foreach (RelatedItem requiredItem in weaponComponent.requiredItems[RelatedItem.RelationType.Contained])
{
Item containedItem = Array.Find(containedItems, it => it != null && it.Condition > 0.0f && requiredItem.MatchesItem(it));
if (containedItem == null)
{
var newReloadWeaponObjective = new AIObjectiveContainItem(character, requiredItem.Identifiers, weapon.GetComponent<ItemContainer>());
if (!newReloadWeaponObjective.IsDuplicate(reloadWeaponObjective))
{
reloadWeaponObjective = newReloadWeaponObjective;
}
}
}
}
if (reloadWeaponObjective != null)
{
if (reloadWeaponObjective.IsCompleted())
{
reloadWeaponObjective = null;
}
else if (!reloadWeaponObjective.CanBeCompleted)
{
Escape(deltaTime);
}
else
{
reloadWeaponObjective.TryComplete(deltaTime);
}
return;
}
character.CursorPosition = enemy.Position;
character.SetInput(InputType.Aim, false, true);
@@ -116,7 +149,7 @@ namespace Barotrauma
//clamp the strength to the health of this character
//(it doesn't make a difference whether the enemy does 200 or 600 damage, it's one hit kill anyway)
float enemyDanger = Math.Min(Math.Max(enemyStrength, MaxEnemyDamage), character.Health) + enemy.Health / 10.0f;
float enemyDanger = Math.Min(Math.Max(CalculateEnemyStrength(), MaxEnemyDamage), character.Health) + enemy.Health / 10.0f;
EnemyAIController enemyAI = enemy.AIController as EnemyAIController;
if (enemyAI != null)
@@ -134,5 +167,19 @@ namespace Barotrauma
return objective.enemy == enemy;
}
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;
}
}
}
@@ -9,7 +9,8 @@ namespace Barotrauma
{
public int MinContainedAmount = 1;
private string[] itemNames;
//can either be a tag or an identifier
private string[] itemIdentifiers;
private ItemContainer container;
@@ -21,16 +22,21 @@ namespace Barotrauma
private AIObjectiveGetItem getItemObjective;
private AIObjectiveGoTo goToObjective;
public AIObjectiveContainItem(Character character, string itemName, ItemContainer container)
: this(character, new string[] { itemName }, container)
public AIObjectiveContainItem(Character character, string itemIdentifier, ItemContainer container)
: this(character, new string[] { itemIdentifier }, container)
{
}
public AIObjectiveContainItem(Character character, string[] itemNames, ItemContainer container)
public AIObjectiveContainItem(Character character, string[] itemIdentifiers, ItemContainer container)
: base (character, "")
{
this.itemNames = itemNames;
this.itemIdentifiers = itemIdentifiers;
for (int i = 0; i < itemIdentifiers.Length; i++)
{
itemIdentifiers[i] = itemIdentifiers[i].ToLowerInvariant();
}
this.container = container;
}
@@ -41,7 +47,7 @@ namespace Barotrauma
int containedItemCount = 0;
foreach (Item item in container.Inventory.Items)
{
if (item != null && itemNames.Any(name => item.Prefab.NameMatches(name) || item.HasTag(name))) containedItemCount++;
if (item != null && itemIdentifiers.Any(id => item.Prefab.Identifier == id || item.HasTag(id))) containedItemCount++;
}
return containedItemCount >= MinContainedAmount;
@@ -56,7 +62,7 @@ namespace Barotrauma
return goToObjective.CanBeCompleted;
}
return getItemObjective == null || !getItemObjective.CanBeCompleted;
return getItemObjective == null || getItemObjective.CanBeCompleted;
}
}
@@ -75,12 +81,20 @@ namespace Barotrauma
if (isCompleted) return;
//get the item that should be contained
var itemToContain = character.Inventory.FindItem(itemNames);
Item itemToContain = null;
foreach (string identifier in itemIdentifiers)
{
itemToContain = character.Inventory.FindItemByIdentifier(identifier) ?? character.Inventory.FindItemByTag(identifier);
if (itemToContain != null) break;
}
if (itemToContain == null)
{
getItemObjective = new AIObjectiveGetItem(character, itemNames);
getItemObjective.GetItemPriority = GetItemPriority;
getItemObjective.IgnoreContainedItems = IgnoreAlreadyContainedItems;
getItemObjective = new AIObjectiveGetItem(character, itemIdentifiers)
{
GetItemPriority = GetItemPriority,
IgnoreContainedItems = IgnoreAlreadyContainedItems
};
AddSubObjective(getItemObjective);
return;
}
@@ -116,11 +130,11 @@ namespace Barotrauma
AIObjectiveContainItem objective = otherObjective as AIObjectiveContainItem;
if (objective == null) return false;
if (objective.container != container) return false;
if (objective.itemNames.Length != itemNames.Length) return false;
if (objective.itemIdentifiers.Length != itemIdentifiers.Length) return false;
for (int i = 0; i < itemNames.Length; i++)
for (int i = 0; i < itemIdentifiers.Length; i++)
{
if (objective.itemNames[i] != itemNames[i]) return false;
if (objective.itemIdentifiers[i] != itemIdentifiers[i]) return false;
}
return true;
@@ -0,0 +1,106 @@
using Barotrauma.Items.Components;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
namespace Barotrauma
{
class AIObjectiveExtinguishFire : AIObjective
{
private Hull targetHull;
private AIObjectiveGetItem getExtinguisherObjective;
private AIObjectiveGoTo gotoObjective;
private float useExtinquisherTimer;
public AIObjectiveExtinguishFire(Character character, Hull targetHull) :
base(character, "")
{
this.targetHull = targetHull;
}
public override float GetPriority(AIObjectiveManager objectiveManager)
{
return targetHull.FireSources.Sum(fs => fs.Size.X * 0.1f);
}
public override bool IsCompleted()
{
return targetHull.FireSources.Count == 0;
}
public override bool IsDuplicate(AIObjective otherObjective)
{
var otherExtinguishFire = otherObjective as AIObjectiveExtinguishFire;
return otherExtinguishFire != null && otherExtinguishFire.targetHull == targetHull;
}
public override bool CanBeCompleted
{
get { return getExtinguisherObjective == null || getExtinguisherObjective.CanBeCompleted; }
}
protected override void Act(float deltaTime)
{
var extinguisherItem = character.Inventory.FindItemByIdentifier("extinguisher") ?? character.Inventory.FindItemByTag("extinguisher");
if (extinguisherItem == null || extinguisherItem.Condition <= 0.0f || !character.HasEquippedItem(extinguisherItem))
{
if (getExtinguisherObjective == null)
{
character.Speak(TextManager.Get("DialogFindExtinguisher"), null, 2.0f, "findextinguisher", 30.0f);
getExtinguisherObjective = new AIObjectiveGetItem(character, "extinguisher", true);
}
else
{
getExtinguisherObjective.TryComplete(deltaTime);
}
return;
}
var extinguisher = extinguisherItem.GetComponent<RepairTool>();
if (extinguisher == null)
{
DebugConsole.ThrowError("AIObjectiveExtinguishFire failed - the item \"" + extinguisherItem + "\" has no RepairTool component but is tagged as an extinguisher");
return;
}
foreach (FireSource fs in targetHull.FireSources.ToList())
{
if (fs.IsInDamageRange(character, MathHelper.Clamp(fs.DamageRange * 1.5f, extinguisher.Range * 0.5f, extinguisher.Range)) || useExtinquisherTimer > 0.0f)
{
useExtinquisherTimer += deltaTime;
if (useExtinquisherTimer > 2.0f) useExtinquisherTimer = 0.0f;
character.CursorPosition = fs.Position;
character.SetInput(InputType.Aim, false, true);
character.AIController.SteeringManager.Reset();
extinguisher.Use(deltaTime, character);
if (!targetHull.FireSources.Contains(fs))
{
character.Speak(TextManager.Get("DialogPutOutFire").Replace("[roomname]", targetHull.Name), null, 0, "putoutfire", 10.0f);
}
return;
}
}
foreach (FireSource fs in targetHull.FireSources)
{
//go to the first firesource
if (gotoObjective == null || !gotoObjective.CanBeCompleted || gotoObjective.IsCompleted())
{
gotoObjective = new AIObjectiveGoTo(ConvertUnits.ToSimUnits(fs.Position), character);
}
else
{
gotoObjective.TryComplete(deltaTime);
}
break;
}
}
}
}
@@ -0,0 +1,47 @@
using System.Linq;
namespace Barotrauma
{
class AIObjectiveExtinguishFires : AIObjective
{
public AIObjectiveExtinguishFires(Character character) :
base(character, "")
{
if (!Hull.hullList.Any(h => h.FireSources.Count > 0))
{
character?.Speak(TextManager.Get("DialogNoFire"), null, 3.0f, "nofire", 30.0f);
}
}
public override float GetPriority(AIObjectiveManager objectiveManager)
{
if (objectiveManager.CurrentObjective == this)
{
return AIObjectiveManager.OrderPriority;
}
return Hull.hullList.Count(h => h.FireSources.Count > 0) * 10;
}
public override bool IsCompleted()
{
return !Hull.hullList.Any(h => h.FireSources.Count > 0);
}
public override bool IsDuplicate(AIObjective otherObjective)
{
return otherObjective is AIObjectiveExtinguishFires;
}
protected override void Act(float deltaTime)
{
foreach (Hull hull in Hull.hullList)
{
if (hull.FireSources.Count > 0)
{
AddSubObjective(new AIObjectiveExtinguishFire(character, hull));
}
}
}
}
}
@@ -7,19 +7,19 @@ namespace Barotrauma
{
private AIObjective subObjective;
private string gearName;
private string gearTag;
public override bool IsCompleted()
{
for (int i = 0; i < character.Inventory.Items.Length; i++)
{
if (CharacterInventory.limbSlots[i] == InvSlotType.Any || character.Inventory.Items[i] == null) continue;
if (character.Inventory.Items[i].Prefab.NameMatches(gearName) || character.Inventory.Items[i].HasTag(gearName))
if (character.Inventory.SlotTypes[i] == InvSlotType.Any || character.Inventory.Items[i] == null) continue;
if (character.Inventory.Items[i].HasTag(gearTag))
{
var containedItems = character.Inventory.Items[i].ContainedItems;
if (containedItems == null) continue;
var oxygenTank = Array.Find(containedItems, it => (it.Prefab.NameMatches("Oxygen Tank") || it.HasTag("oxygensource")) && it.Condition > 0.0f);
var oxygenTank = Array.Find(containedItems, it => (it.Prefab.Identifier == "oxygentank" || it.HasTag("oxygensource")) && it.Condition > 0.0f);
if (oxygenTank != null) return true;
}
}
@@ -30,18 +30,19 @@ namespace Barotrauma
public AIObjectiveFindDivingGear(Character character, bool needDivingSuit)
: base(character, "")
{
gearName = needDivingSuit ? "Diving Suit" : "diving";
gearTag = needDivingSuit ? "divingsuit" : "diving";
}
protected override void Act(float deltaTime)
{
var item = character.Inventory.FindItem(gearName);
if (item == null)
var item = character.Inventory.FindItemByTag(gearTag);
if (item == null || !character.HasEquippedItem(item))
{
//get a diving mask/suit first
if (!(subObjective is AIObjectiveGetItem))
{
subObjective = new AIObjectiveGetItem(character, gearName, true);
character.Speak(TextManager.Get("DialogGetDivingGear"), null, 0.0f, "getdivinggear", 30.0f);
subObjective = new AIObjectiveGetItem(character, gearTag, true);
}
}
else
@@ -57,7 +58,7 @@ namespace Barotrauma
{
containedItem.Drop();
}
else if (containedItem.Prefab.NameMatches("Oxygen Tank") || containedItem.HasTag("oxygensource"))
else if (containedItem.Prefab.Identifier == "oxygentank" || containedItem.HasTag("oxygensource"))
{
//we've got an oxygen source inside the mask/suit, all good
return;
@@ -66,7 +67,8 @@ namespace Barotrauma
if (!(subObjective is AIObjectiveContainItem) || subObjective.IsCompleted())
{
subObjective = new AIObjectiveContainItem(character, new string[] { "Oxygen Tank", "oxygensource" }, item.GetComponent<ItemContainer>());
character.Speak(TextManager.Get("DialogGetOxygenTank"), null, 0, "getoxygentank", 30.0f);
subObjective = new AIObjectiveContainItem(character, new string[] { "oxygentank", "oxygensource" }, item.GetComponent<ItemContainer>());
}
}
@@ -55,7 +55,10 @@ namespace Barotrauma
var bestHull = FindBestHull();
if (bestHull != null)
{
goToObjective = new AIObjectiveGoTo(bestHull, character);
goToObjective = new AIObjectiveGoTo(bestHull, character)
{
AllowGoingOutside = true
};
}
searchHullTimer = SearchHullInterval;
@@ -63,15 +66,57 @@ namespace Barotrauma
if (goToObjective != null)
{
var pathSteering = character.AIController.SteeringManager as IndoorsSteeringManager;
if (pathSteering != null && pathSteering.CurrentPath != null &&
goToObjective.TryComplete(deltaTime);
if (character.AIController.SteeringManager is IndoorsSteeringManager pathSteering &&
pathSteering.CurrentPath != null &&
pathSteering.CurrentPath.Unreachable && !unreachable.Contains(goToObjective.Target))
{
unreachable.Add(goToObjective.Target as Hull);
goToObjective = 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
else if (currentHull != null)
{
Vector2 escapeVel = Vector2.Zero;
foreach (FireSource fireSource in currentHull.FireSources)
{
int dir = Math.Sign(character.Position.X - fireSource.Position.X);
float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(fireSource.Position, character.Position), 0.1f, 10.0f);
escapeVel += new Vector2(dir * distMultiplier, 0.0f);
}
foreach (Character enemy in Character.CharacterList)
{
if (enemy.CurrentHull == currentHull && !enemy.IsDead && !enemy.IsUnconscious &&
(enemy.AIController is EnemyAIController || enemy.TeamID != character.TeamID))
{
float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(enemy.Position, character.Position), 0.1f, 10.0f);
escapeVel += new Vector2(Math.Sign(character.Position.X - enemy.Position.X) * distMultiplier, 0.0f);
}
}
goToObjective.TryComplete(deltaTime);
if (escapeVel != Vector2.Zero)
{
//only move if we haven't reached the edge of the room
if ((escapeVel.X < 0 && character.Position.X > currentHull.Rect.X + 50) ||
(escapeVel.X > 0 && character.Position.X < currentHull.Rect.Right - 50))
{
character.AIController.SteeringManager.SteeringManual(deltaTime, escapeVel);
}
else
{
character.AnimController.TargetDir = escapeVel.X < 0.0f ? Direction.Right : Direction.Left;
character.AIController.SteeringManager.Reset();
}
}
else
{
character.AIController.SteeringManager.Reset();
}
}
}
@@ -137,26 +182,30 @@ namespace Barotrauma
return 150.0f - character.Oxygen;
}
if (character.AnimController.CurrentHull == null) return 5.0f;
currenthullSafety = GetHullSafety(character.AnimController.CurrentHull, character);
if (character.CurrentHull == null) return 5.0f;
currenthullSafety = GetHullSafety(character.CurrentHull, character);
priority = 100.0f - currenthullSafety;
var nearbyHulls = character.AnimController.CurrentHull.GetConnectedHulls(3);
//var nearbyHulls = character.CurrentHull.GetConnectedHulls(3);
foreach (Hull hull in nearbyHulls)
//increase priority slightly if there's a fire in the room
//(will increase more heavily if near the damage range of the fire)
if (character.CurrentHull.FireSources.Count > 0)
{
priority += 5.0f;
}
/*foreach (Hull hull in nearbyHulls)
{
foreach (FireSource fireSource in hull.FireSources)
{
//increase priority if almost within damage range of a fire
if (character.Position.X > fireSource.Position.X - fireSource.DamageRange * 2 &&
character.Position.X < fireSource.Position.X + fireSource.Size.X + fireSource.DamageRange * 2 &&
character.Position.Y > hull.Rect.Y - hull.Rect.Height &&
character.Position.Y < hull.Rect.Y)
//heavily increase priority if almost within damage range of a fire
if (fireSource.IsInDamageRange(character, fireSource.DamageRange * 1.25f))
{
priority += Math.Max(fireSource.Size.X, 50.0f);
}
}
}
}*/
if (NeedsDivingGear())
{
@@ -189,25 +238,36 @@ namespace Barotrauma
if (hull.OxygenPercentage < 30.0f) safety -= (30.0f - hull.OxygenPercentage) * 5.0f;
if (safety <= 0.0f) return 0.0f;
bool extinguishFires =
character.AIController.ObjectiveManager?.CurrentOrder is AIObjectiveExtinguishFires ||
character.AIController.ObjectiveManager?.CurrentOrder is AIObjectiveExtinguishFire;
float fireAmount = 0.0f;
var nearbyHulls = hull.GetConnectedHulls(3);
foreach (Hull hull2 in nearbyHulls)
{
foreach (FireSource fireSource in hull2.FireSources)
{
//increase priority if almost within damage range of a fire
if (character.Position.X > fireSource.Position.X - fireSource.DamageRange * 2 &&
character.Position.X < fireSource.Position.X + fireSource.Size.X + fireSource.DamageRange * 2 &&
character.Position.Y > hull2.Rect.Y - hull2.Rect.Height &&
character.Position.Y < hull2.Rect.Y)
//increase priority if near the damage range of a fire
//if extinguishing fires, the character can go closer the damage range
if (fireSource.IsInDamageRange(character, fireSource.DamageRange * (extinguishFires ? 1.25f : 5.0f)))
{
fireAmount += Math.Max(fireSource.Size.X, 50.0f);
fireAmount += Math.Max(fireSource.Size.X, AIObjectiveManager.OrderPriority + 1.0f);
}
}
}
safety -= fireAmount;
foreach (Character enemy in Character.CharacterList)
{
if (enemy.CurrentHull == hull && !enemy.IsDead && !enemy.IsUnconscious &&
(enemy.AIController is EnemyAIController || enemy.TeamID != character.TeamID))
{
safety -= 10.0f;
}
}
return MathHelper.Clamp(safety, 0.0f, 100.0f);
}
}
@@ -8,9 +8,7 @@ namespace Barotrauma
class AIObjectiveFixLeak : AIObjective
{
private readonly Gap leak;
private AIObjectiveGoTo gotoObjective;
public Gap Leak
{
get { return leak; }
@@ -47,11 +45,11 @@ namespace Barotrauma
protected override void Act(float deltaTime)
{
var weldingTool = character.Inventory.FindItem("Welding Tool");
var weldingTool = character.Inventory.FindItemByTag("weldingtool");
if (weldingTool == null)
{
AddSubObjective(new AIObjectiveGetItem(character, "Welding Tool", true));
AddSubObjective(new AIObjectiveGetItem(character, "weldingtool", true));
return;
}
else
@@ -59,11 +57,10 @@ namespace Barotrauma
var containedItems = weldingTool.ContainedItems;
if (containedItems == null) return;
var fuelTank = Array.Find(containedItems, i => i.Prefab.NameMatches("Welding Fuel Tank") && i.Condition > 0.0f);
var fuelTank = Array.Find(containedItems, i => i.HasTag("weldingfueltank") && i.Condition > 0.0f);
if (fuelTank == null)
{
AddSubObjective(new AIObjectiveContainItem(character, "Welding Fuel Tank", weldingTool.GetComponent<ItemContainer>()));
AddSubObjective(new AIObjectiveContainItem(character, "weldingfueltank", weldingTool.GetComponent<ItemContainer>()));
return;
}
}
@@ -73,7 +70,15 @@ namespace Barotrauma
Vector2 standPosition = GetStandPosition();
if (Vector2.DistanceSquared(character.WorldPosition, leak.WorldPosition) > 100.0f * 100.0f)
Vector2 gapDiff = leak.WorldPosition - character.WorldPosition;
if (!character.AnimController.InWater && character.AnimController is HumanoidAnimController &&
Math.Abs(gapDiff.X) < 100.0f && gapDiff.Y < 0.0f && gapDiff.Y > -150.0f)
{
((HumanoidAnimController)character.AnimController).Crouching = true;
}
if (Math.Abs(gapDiff.X) > 100.0f || Math.Abs(gapDiff.Y) > 150.0f)
{
var gotoObjective = new AIObjectiveGoTo(ConvertUnits.ToSimUnits(standPosition), character);
if (!gotoObjective.IsCompleted())
@@ -105,9 +105,20 @@ namespace Barotrauma
{
if (gap.ConnectedWall == null) continue;
if (gap.ConnectedDoor != null || gap.Open <= 0.0f) continue;
if (character.TeamID == 0)
{
if (gap.Submarine == null) continue;
}
else
{
//prevent characters from attempting to fix leaks in the enemy sub
//team 1 plays in sub 0, team 2 in sub 1
Submarine mySub = character.TeamID < 1 || character.TeamID > Submarine.MainSubs.Length ?
Submarine.MainSub : Submarine.MainSubs[character.TeamID - 1];
//TODO: prevent the AI characters from fixing leaks in the enemy sub in sub-vs-sub missions if/when multiplayer bots are implemented
if (gap.Submarine == null) continue;
if (gap.Submarine != mySub) continue;
}
float gapPriority = GetGapFixPriority(gap);
@@ -10,7 +10,8 @@ namespace Barotrauma
{
public Func<Item, float> GetItemPriority;
private string[] itemNames;
//can be either tags or identifiers
private string[] itemIdentifiers;
private Item targetItem, moveToTarget;
@@ -45,29 +46,65 @@ namespace Barotrauma
: base(character, "")
{
canBeCompleted = true;
currSearchIndex = -1;
this.equip = equip;
currSearchIndex = 0;
this.targetItem = targetItem;
}
public AIObjectiveGetItem(Character character, string itemName, bool equip = false)
: this(character, new string[] { itemName }, equip)
public AIObjectiveGetItem(Character character, string itemIdentifier, bool equip = false)
: this(character, new string[] { itemIdentifier }, equip)
{
}
public AIObjectiveGetItem(Character character, string[] itemNames, bool equip = false)
public AIObjectiveGetItem(Character character, string[] itemIdentifiers, bool equip = false)
: base(character, "")
{
canBeCompleted = true;
currSearchIndex = -1;
this.equip = equip;
this.itemIdentifiers = itemIdentifiers;
for (int i = 0; i < itemIdentifiers.Length; i++)
{
itemIdentifiers[i] = itemIdentifiers[i].ToLowerInvariant();
}
currSearchIndex = 0;
CheckInventory();
}
this.itemNames = itemNames;
private void CheckInventory()
{
if (itemIdentifiers == null)
{
return;
}
for (int i = 0; i < character.Inventory.Items.Length; i++)
{
if (character.Inventory.Items[i] == null || character.Inventory.Items[i].Condition <= 0.0f) continue;
if (itemIdentifiers.Any(id => character.Inventory.Items[i].Prefab.Identifier == id || character.Inventory.Items[i].HasTag(id)))
{
targetItem = character.Inventory.Items[i];
moveToTarget = targetItem;
currItemPriority = 100.0f;
break;
}
//check items inside items (tool inside a toolbox etc)
var containedItems = character.Inventory.Items[i].ContainedItems;
if (containedItems != null)
{
foreach (Item containedItem in containedItems)
{
if (containedItem == null || containedItem.Condition <= 0.0f) continue;
if (itemIdentifiers.Any(id => containedItem.Prefab.Identifier == id || containedItem.HasTag(id)))
{
targetItem = containedItem;
moveToTarget = character.Inventory.Items[i];
currItemPriority = 100.0f;
break;
}
}
}
}
}
protected override void Act(float deltaTime)
@@ -99,7 +136,7 @@ namespace Barotrauma
for (int i = 0; i < character.Inventory.Items.Length; i++)
{
//slot not needed by the item, continue
if (!slots.HasFlag(CharacterInventory.limbSlots[i])) continue;
if (!slots.HasFlag(character.Inventory.SlotTypes[i])) continue;
targetSlot = i;
@@ -117,7 +154,7 @@ namespace Barotrauma
targetItem.TryInteract(character, false, true);
if (targetSlot > -1 && character.Inventory.IsInLimbSlot(targetItem, InvSlotType.Any))
if (targetSlot > -1 && !character.HasEquippedItem(targetItem))
{
character.Inventory.TryPutItem(targetItem, targetSlot, false, false, character);
}
@@ -127,14 +164,15 @@ namespace Barotrauma
if (goToObjective == null || moveToTarget != goToObjective.Target)
{
//check if we're already looking for a diving gear
bool gettingDivingGear = (targetItem != null && targetItem.Prefab.NameMatches("Diving Gear") || targetItem.HasTag("diving")) ||
(itemNames != null && (itemNames.Contains("diving") || itemNames.Contains("Diving Gear")));
bool gettingDivingGear = (targetItem != null && targetItem.Prefab.Identifier == "divingsuit" || targetItem.HasTag("diving")) ||
(itemIdentifiers != null && (itemIdentifiers.Contains("diving") || itemIdentifiers.Contains("divingsuit")));
//don't attempt to get diving gear to reach the destination if the item we're trying to get is diving gear
goToObjective = new AIObjectiveGoTo(moveToTarget, character, false, !gettingDivingGear);
}
goToObjective.TryComplete(deltaTime);
if (!goToObjective.CanBeCompleted) targetItem = null;
}
}
@@ -144,7 +182,7 @@ namespace Barotrauma
/// </summary>
private void FindTargetItem()
{
if (itemNames == null)
if (itemIdentifiers == null)
{
if (targetItem == null) canBeCompleted = false;
return;
@@ -152,7 +190,7 @@ namespace Barotrauma
float currDist = moveToTarget == null ? 0.0f : Vector2.DistanceSquared(moveToTarget.Position, character.Position);
for (int i = 0; i < 10 && currSearchIndex < Item.ItemList.Count - 2; i++)
for (int i = 0; i < 10 && currSearchIndex < Item.ItemList.Count - 1; i++)
{
currSearchIndex++;
@@ -160,21 +198,19 @@ namespace Barotrauma
if (item.CurrentHull == null || item.Condition <= 0.0f) continue;
if (IgnoreContainedItems && item.Container != null) continue;
if (!itemNames.Any(name => item.Prefab.NameMatches(name) || item.HasTag(name))) continue;
if (!itemIdentifiers.Any(id => item.Prefab.Identifier == id || item.HasTag(id))) continue;
//if the item is inside a character's inventory, don't steal it unless the character is dead
if (item.ParentInventory is CharacterInventory)
{
Character owner = item.ParentInventory.Owner as Character;
if (owner != null && !owner.IsDead) continue;
if (item.ParentInventory.Owner is Character owner && !owner.IsDead) continue;
}
//if the item is inside an item, which is inside a character's inventory, don't steal it
Item rootContainer = item.GetRootContainer();
if (rootContainer != null && rootContainer.ParentInventory is CharacterInventory)
{
Character owner = rootContainer.ParentInventory.Owner as Character;
if (owner != null && !owner.IsDead) continue;
if (rootContainer.ParentInventory.Owner is Character owner && !owner.IsDead) continue;
}
float itemPriority = 0.0f;
@@ -194,10 +230,11 @@ namespace Barotrauma
targetItem = item;
moveToTarget = rootContainer ?? item;
}
//if searched through all the items and a target wasn't found, can't be completed
if (currSearchIndex >= Item.ItemList.Count && targetItem == null) canBeCompleted = false;
if (currSearchIndex >= Item.ItemList.Count - 1 && targetItem == null) canBeCompleted = false;
}
public override bool IsDuplicate(AIObjective otherObjective)
@@ -205,16 +242,16 @@ namespace Barotrauma
AIObjectiveGetItem getItem = otherObjective as AIObjectiveGetItem;
if (getItem == null) return false;
if (getItem.equip != equip) return false;
if (getItem.itemNames != null && itemNames != null)
if (getItem.itemIdentifiers != null && itemIdentifiers != null)
{
if (getItem.itemNames.Length != itemNames.Length) return false;
for (int i = 0; i < getItem.itemNames.Length; i++)
if (getItem.itemIdentifiers.Length != itemIdentifiers.Length) return false;
for (int i = 0; i < getItem.itemIdentifiers.Length; i++)
{
if (getItem.itemNames[i] != itemNames[i]) return false;
if (getItem.itemIdentifiers[i] != itemIdentifiers[i]) return false;
}
return true;
}
else if (getItem.itemNames == null && itemNames == null)
else if (getItem.itemIdentifiers == null && itemIdentifiers == null)
{
return getItem.targetItem == targetItem;
}
@@ -224,11 +261,11 @@ namespace Barotrauma
public override bool IsCompleted()
{
if (itemNames != null)
if (itemIdentifiers != null)
{
foreach (string itemName in itemNames)
foreach (string itemName in itemIdentifiers)
{
var matchingItem = character.Inventory.FindItem(itemName);
var matchingItem = character.Inventory.FindItemByTag(itemName) ?? character.Inventory.FindItemByIdentifier(itemName);
if (matchingItem != null && (!equip || character.HasEquippedItem(matchingItem))) return true;
}
return false;
@@ -7,8 +7,6 @@ namespace Barotrauma
{
class AIObjectiveGoTo : AIObjective
{
private Entity target;
private Vector2 targetPos;
private bool repeat;
@@ -18,8 +16,17 @@ namespace Barotrauma
private bool getDivingGearIfNeeded;
public float CloseEnough = 0.5f;
public bool IgnoreIfTargetDead;
public bool AllowGoingOutside = false;
public override float GetPriority(AIObjectiveManager objectiveManager)
{
if (Target != null && Target.Removed) return 0.0f;
if (IgnoreIfTargetDead && Target is Character character && character.IsDead) return 0.0f;
if (objectiveManager.CurrentOrder == this)
{
return AIObjectiveManager.OrderPriority;
@@ -32,25 +39,26 @@ namespace Barotrauma
{
get
{
if (Target != null && Target.Removed) return false;
if (repeat || waitUntilPathUnreachable > 0.0f) return true;
var pathSteering = character.AIController.SteeringManager as IndoorsSteeringManager;
//path doesn't exist (= hasn't been searched for yet), assume for now that the target is reachable
if (pathSteering.CurrentPath == null) return true;
if (pathSteering?.CurrentPath == null) return true;
return (!pathSteering.CurrentPath.Unreachable);
if (!AllowGoingOutside && pathSteering.CurrentPath.HasOutdoorsNodes) return false;
return !pathSteering.CurrentPath.Unreachable;
}
}
public Entity Target
{
get { return target; }
}
public Entity Target { get; private set; }
public AIObjectiveGoTo(Entity target, Character character, bool repeat = false, bool getDivingGearIfNeeded = true)
: base (character, "")
{
this.target = target;
this.Target = target;
this.repeat = repeat;
waitUntilPathUnreachable = 5.0f;
@@ -70,58 +78,65 @@ namespace Barotrauma
protected override void Act(float deltaTime)
{
if (target == character)
if (Target == character)
{
character.AIController.SteeringManager.Reset();
return;
}
waitUntilPathUnreachable -= deltaTime;
if (character.SelectedConstruction!=null && character.SelectedConstruction.GetComponent<Ladder>()==null)
if (character.SelectedConstruction != null && character.SelectedConstruction.GetComponent<Ladder>() == null)
{
character.SelectedConstruction = null;
}
if (target != null) character.AIController.SelectTarget(target.AiTarget);
if (Target != null) character.AIController.SelectTarget(Target.AiTarget);
Vector2 currTargetPos = Vector2.Zero;
if (target == null)
if (Target == null)
{
currTargetPos = targetPos;
}
else
{
currTargetPos = target.SimPosition;
currTargetPos = Target.SimPosition;
//if character is outside the sub and target isn't, transform the position
if (character.Submarine != null && target.Submarine == null)
//if character is inside the sub and target isn't, transform the position
if (character.Submarine != null && Target.Submarine == null)
{
currTargetPos -= character.Submarine.SimPosition;
}
}
if (Vector2.DistanceSquared(currTargetPos, character.SimPosition) < 0.5f * 0.5f)
if (Vector2.DistanceSquared(currTargetPos, character.SimPosition) < CloseEnough * CloseEnough)
{
character.AIController.SteeringManager.Reset();
character.AnimController.TargetDir = currTargetPos.X > character.SimPosition.X ? Direction.Right : Direction.Left;
}
else
{
character.AIController.SteeringManager.SteeringSeek(currTargetPos);
var indoorsSteering = character.AIController.SteeringManager as IndoorsSteeringManager;
if (indoorsSteering.CurrentPath == null || indoorsSteering.CurrentPath.Unreachable)
{
indoorsSteering.SteeringWander();
}
else if (getDivingGearIfNeeded && indoorsSteering.CurrentPath != null && indoorsSteering.CurrentPath.HasOutdoorsNodes)
float normalSpeed = character.AnimController.GetCurrentSpeed(false);
character.AIController.SteeringManager.SteeringSeek(currTargetPos, normalSpeed);
if (getDivingGearIfNeeded && Target?.Submarine == null && AllowGoingOutside)
{
AddSubObjective(new AIObjectiveFindDivingGear(character, true));
}
else if (character.AIController.SteeringManager is IndoorsSteeringManager indoorsSteering)
{
if (indoorsSteering.CurrentPath == null || indoorsSteering.CurrentPath.Unreachable)
{
indoorsSteering.SteeringWander(normalSpeed);
}
else if (AllowGoingOutside &&
getDivingGearIfNeeded &&
indoorsSteering.CurrentPath != null &&
indoorsSteering.CurrentPath.HasOutdoorsNodes)
{
AddSubObjective(new AIObjectiveFindDivingGear(character, true));
}
}
}
}
@@ -132,15 +147,18 @@ namespace Barotrauma
bool completed = false;
float allowedDistance = 0.5f;
var item = target as Item;
if (item != null)
if (Target is Item item)
{
allowedDistance = Math.Max(ConvertUnits.ToSimUnits(item.InteractDistance), allowedDistance);
if (item.IsInsideTrigger(character.WorldPosition)) completed = true;
}
else if (Target is Character targetCharacter)
{
if (character.CanInteractWith(targetCharacter)) completed = true;
}
completed = completed || Vector2.DistanceSquared(target != null ? target.SimPosition : targetPos, character.SimPosition) < allowedDistance * allowedDistance;
completed = completed || Vector2.DistanceSquared(Target != null ? Target.SimPosition : targetPos, character.SimPosition) < allowedDistance * allowedDistance;
if (completed) character.AIController.SteeringManager.Reset();
@@ -152,7 +170,7 @@ namespace Barotrauma
AIObjectiveGoTo objective = otherObjective as AIObjectiveGoTo;
if (objective == null) return false;
if (objective.target == target) return true;
if (objective.Target == Target) return true;
return (objective.targetPos == targetPos);
}
@@ -1,4 +1,6 @@
using Microsoft.Xna.Framework;
using Barotrauma.Items.Components;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -12,10 +14,15 @@ namespace Barotrauma
private AITarget currentTarget;
private float newTargetTimer;
private float standStillTimer;
private float walkDuration;
private AIObjectiveFindSafety findSafety;
public AIObjectiveIdle(Character character) : base(character, "")
{
standStillTimer = Rand.Range(-10.0f, 10.0f);
walkDuration = Rand.Range(0.0f, 10.0f);
}
public override bool IsCompleted()
@@ -33,6 +40,16 @@ namespace Barotrauma
var pathSteering = character.AIController.SteeringManager as IndoorsSteeringManager;
if (pathSteering == null) return;
//don't keep dragging others when idling
if (character.SelectedCharacter != null)
{
character.DeselectCharacter();
}
if (character.SelectedConstruction != null && character.SelectedConstruction.GetComponent<Ladder>() == null)
{
character.SelectedConstruction = null;
}
if (character.AnimController.InWater)
{
//attempt to find a safer place if in water
@@ -48,10 +65,13 @@ namespace Barotrauma
if (currentTarget != null)
{
Vector2 pos = character.SimPosition;
if (character != null && character.Submarine == null) pos -= Submarine.MainSub.SimPosition;
var path = pathSteering.PathFinder.FindPath(pos, currentTarget.SimPosition);
if (path.Cost > 200.0f && character.AnimController.CurrentHull!=null) return;
if (character != null && character.Submarine == null) { pos -= Submarine.MainSub.SimPosition; }
string errorMsg = "(Character " + character.Name + " idling, target "
+ ((currentTarget.Entity is Hull hull && hull.RoomName != null) ? hull.RoomName : currentTarget.Entity.ToString()) + ")";
var path = pathSteering.PathFinder.FindPath(pos, currentTarget.SimPosition, errorMsg);
if (path.Cost > 1000.0f && character.AnimController.CurrentHull!=null) return;
pathSteering.SetPath(path);
}
@@ -70,6 +90,19 @@ namespace Barotrauma
if (pathSteering == null || (pathSteering.CurrentPath != null &&
(pathSteering.CurrentPath.NextNode == null || pathSteering.CurrentPath.Unreachable || pathSteering.CurrentPath.HasOutdoorsNodes)))
{
standStillTimer -= deltaTime;
if (standStillTimer > 0.0f)
{
walkDuration = Rand.Range(1.0f, 5.0f);
pathSteering.Reset();
return;
}
if (standStillTimer < -walkDuration)
{
standStillTimer = Rand.Range(1.0f, 10.0f);
}
//steer away from edges of the hull
if (character.AnimController.CurrentHull != null)
{
@@ -102,7 +135,7 @@ namespace Barotrauma
}
}
character.AIController.SteeringManager.SteeringWander();
character.AIController.SteeringManager.SteeringWander(character.AnimController.GetCurrentSpeed(false));
//reset vertical steering to prevent dropping down from platforms etc
character.AIController.SteeringManager.ResetY();
@@ -115,20 +148,21 @@ namespace Barotrauma
currentTarget = null;
return;
}
character.AIController.SteeringManager.SteeringSeek(currentTarget.SimPosition, 2.0f);
character.AIController.SteeringManager.SteeringSeek(currentTarget.SimPosition, character.AnimController.GetCurrentSpeed(true));
}
private AITarget FindRandomTarget()
{
if (Rand.Int(5)==1)
//random chance of navigating back to the room where the character spawned
if (Rand.Int(5) == 1)
{
var idCard = character.Inventory.FindItem("ID Card");
if (idCard==null) return null;
var idCard = character.Inventory.FindItemByIdentifier("idcard");
if (idCard == null) return null;
foreach (WayPoint wp in WayPoint.WayPointList)
{
if (wp.SpawnType != SpawnType.Human || wp.CurrentHull==null) continue;
if (wp.SpawnType != SpawnType.Human || wp.CurrentHull == null) continue;
foreach (string tag in wp.IdCardTags)
{
if (idCard.HasTag(tag)) return wp.CurrentHull.AiTarget;
@@ -140,9 +174,31 @@ namespace Barotrauma
List<Hull> targetHulls = new List<Hull>(Hull.hullList);
//ignore all hulls with fires or water in them
targetHulls.RemoveAll(h => h.FireSources.Any() || h.WaterVolume / h.Volume > 0.1f);
if (!targetHulls.Any()) return null;
if (character.Submarine != null)
{
targetHulls.RemoveAll(h => h.Submarine != character.Submarine);
}
return targetHulls[Rand.Range(0, targetHulls.Count)].AiTarget;
//remove ballast hulls
foreach (Item item in Item.ItemList)
{
if (item.HasTag("ballast") && targetHulls.Contains(item.CurrentHull))
{
targetHulls.Remove(item.CurrentHull);
}
}
//ignore hulls that are too low to stand inside
if (character.AnimController is HumanoidAnimController animController)
{
float minHeight = ConvertUnits.ToDisplayUnits(animController.HeadPosition.Value);
targetHulls.RemoveAll(h => h.CeilingHeight < minHeight);
}
if (!targetHulls.Any()) return null;
//prefer larger hulls
var targetHull = ToolBox.SelectWeightedRandom(targetHulls, targetHulls.Select(h => h.Volume).ToList(), Rand.RandSync.Unsynced);
return targetHull?.AiTarget;
}
return null;
@@ -1,4 +1,6 @@
using System.Collections.Generic;
using Barotrauma.Items.Components;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
@@ -12,7 +14,12 @@ namespace Barotrauma
private Character character;
private AIObjective currentOrder;
/// <summary>
/// When set above zero, the character will stand still doing nothing until the timer runs out (assuming they don't a high priority order active)
/// </summary>
public float WaitTimer;
public AIObjective CurrentOrder
{
get { return currentOrder; }
@@ -38,6 +45,23 @@ namespace Barotrauma
objectives.Add(objective);
}
public Dictionary<AIObjective, CoroutineHandle> DelayedObjectives { get; private set; } = new Dictionary<AIObjective, CoroutineHandle>();
public void AddObjective(AIObjective objective, float delay, Action callback = null)
{
if (DelayedObjectives.TryGetValue(objective, out CoroutineHandle coroutine))
{
CoroutineManager.StopCoroutines(coroutine);
DelayedObjectives.Remove(objective);
}
coroutine = CoroutineManager.InvokeAfter(() =>
{
DelayedObjectives.Remove(objective);
AddObjective(objective);
callback?.Invoke();
}, delay);
DelayedObjectives.Add(objective, coroutine);
}
public T GetObjective<T>() where T : AIObjective
{
foreach (AIObjective objective in objectives)
@@ -47,15 +71,21 @@ namespace Barotrauma
return null;
}
public float GetCurrentPriority(Character character)
private AIObjective GetCurrentObjective()
{
if (CurrentOrder != null &&
(objectives.Count == 0 || currentOrder.GetPriority(this) > objectives[0].GetPriority(this)))
{
return CurrentOrder.GetPriority(this);
return CurrentOrder;
}
return objectives.Count == 0 ? 0.0f : objectives[0].GetPriority(this);
return objectives.Count == 0 ? null : objectives[0];
}
public float GetCurrentPriority()
{
var currentObjective = GetCurrentObjective();
return currentObjective == null ? 0.0f : currentObjective.GetPriority(this);
}
public void UpdateObjectives()
@@ -67,46 +97,77 @@ namespace Barotrauma
//sort objectives according to priority
objectives.Sort((x, y) => y.GetPriority(this).CompareTo(x.GetPriority(this)));
GetCurrentObjective()?.SortSubObjectives(this);
}
public void DoCurrentObjective(float deltaTime)
{
if (currentOrder != null && (!objectives.Any() || objectives[0].GetPriority(this) < currentOrder.GetPriority(this)))
CurrentObjective = GetCurrentObjective();
if (CurrentObjective == null || (CurrentObjective.GetPriority(this) < OrderPriority && WaitTimer > 0.0f))
{
CurrentObjective = currentOrder;
currentOrder.TryComplete(deltaTime);
WaitTimer -= deltaTime;
character.AIController.SteeringManager.Reset();
return;
}
if (!objectives.Any()) return;
objectives[0].TryComplete(deltaTime);
CurrentObjective = objectives[0];
CurrentObjective?.TryComplete(deltaTime);
}
public void SetOrder(AIObjective objective)
{
currentOrder = objective;
}
public void SetOrder(Order order, string option)
public void SetOrder(Order order, string option, Character orderGiver)
{
currentOrder = null;
if (order == null) return;
currentOrder = null;
switch (order.Name.ToLowerInvariant())
switch (order.AITag.ToLowerInvariant())
{
case "follow":
currentOrder = new AIObjectiveGoTo(Character.Controlled, character, true);
currentOrder = new AIObjectiveGoTo(orderGiver, character, true)
{
CloseEnough = 1.5f,
AllowGoingOutside = true,
IgnoreIfTargetDead = true
};
break;
case "wait":
currentOrder = new AIObjectiveGoTo(character, character, true);
currentOrder = new AIObjectiveGoTo(character, character, true)
{
AllowGoingOutside = true
};
break;
case "fixleaks":
case "fix leaks":
currentOrder = new AIObjectiveFixLeaks(character);
break;
case "chargebatteries":
currentOrder = new AIObjectiveChargeBatteries(character, option);
break;
case "rescue":
currentOrder = new AIObjectiveRescueAll(character);
break;
case "repairsystems":
currentOrder = new AIObjectiveRepairItems(character) { RequireAdequateSkills = option != "all" };
break;
case "pumpwater":
currentOrder = new AIObjectivePumpWater(character, option);
break;
case "extinguishfires":
currentOrder = new AIObjectiveExtinguishFires(character);
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;
currentOrder = new AIObjectiveOperateItem(order.TargetItemComponent, character, option, false, null, order.UseController);
break;
default:
if (order.TargetItem == null) return;
currentOrder = new AIObjectiveOperateItem(order.TargetItem, character, option, false, null, order.UseController);
if (order.TargetItemComponent == null) return;
currentOrder = new AIObjectiveOperateItem(order.TargetItemComponent, character, option, false, null, order.UseController);
break;
}
}
@@ -17,10 +17,18 @@ namespace Barotrauma
private bool requireEquip;
private bool useController;
private AIObjectiveGoTo gotoObjective;
public override bool CanBeCompleted
{
get
{
if (gotoObjective != null && !gotoObjective.CanBeCompleted) return false;
if (useController && controller == null) return false;
return canBeCompleted;
}
}
@@ -43,23 +51,30 @@ namespace Barotrauma
public AIObjectiveOperateItem(ItemComponent item, Character character, string option, bool requireEquip, Entity operateTarget = null, bool useController = false)
: base (character, option)
{
this.component = item;
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)
{
var controllers = item.Item.GetConnectedComponents<Controller>();
var controllers = component.Item.GetConnectedComponents<Controller>();
if (controllers.Any()) controller = controllers[0];
}
canBeCompleted = true;
}
protected override void Act(float deltaTime)
{
ItemComponent target = controller == null ? component : controller;
ItemComponent target = useController ? controller : component;
if (useController && controller == null)
{
character.Speak(TextManager.Get("DialogCantFindController").Replace("[item]", component.Item.Name), null, 2.0f, "cantfindcontroller", 30.0f);
return;
}
if (target.CanBeSelected)
{
@@ -75,11 +90,17 @@ namespace Barotrauma
return;
}
AddSubObjective(new AIObjectiveGoTo(target.Item, character));
AddSubObjective(gotoObjective = new AIObjectiveGoTo(target.Item, character));
}
else
{
if (!character.Inventory.Items.Contains(component.Item))
if (component.Item.GetComponent<Pickable>() == null)
{
//controller/target can't be selected and the item cannot be picked -> objective can't be completed
canBeCompleted = false;
return;
}
else if (!character.Inventory.Items.Contains(component.Item))
{
AddSubObjective(new AIObjectiveGetItem(character, component.Item, true));
}
@@ -95,10 +116,10 @@ namespace Barotrauma
return;
}
for (int i = 0; i < CharacterInventory.limbSlots.Length; i++)
for (int i = 0; i < character.Inventory.Capacity; i++)
{
if (CharacterInventory.limbSlots[i] == InvSlotType.Any ||
!holdable.AllowedSlots.Any(s => s.HasFlag(CharacterInventory.limbSlots[i])))
if (character.Inventory.SlotTypes[i] == InvSlotType.Any ||
!holdable.AllowedSlots.Any(s => s.HasFlag(character.Inventory.SlotTypes[i])))
{
continue;
}
@@ -0,0 +1,108 @@
using Barotrauma.Items.Components;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Barotrauma
{
class AIObjectivePumpWater : AIObjective
{
private const float FindPumpsInterval = 5.0f;
private string orderOption;
private List<Pump> pumps;
private float lastFindPumpsTime;
public AIObjectivePumpWater(Character character, string option)
: base(character, option)
{
orderOption = option;
}
public override float GetPriority(AIObjectiveManager objectiveManager)
{
if (Timing.TotalTime >= lastFindPumpsTime + FindPumpsInterval)
{
FindPumps();
}
if (objectiveManager.CurrentOrder == this && pumps.Count > 0)
{
return AIObjectiveManager.OrderPriority;
}
return 0.0f;
}
public override bool IsCompleted()
{
return false;
}
public override bool IsDuplicate(AIObjective otherObjective)
{
return otherObjective is AIObjectivePumpWater;
}
protected override void Act(float deltaTime)
{
if (Timing.TotalTime < lastFindPumpsTime + FindPumpsInterval)
{
return;
}
FindPumps();
}
private void FindPumps()
{
lastFindPumpsTime = (float)Timing.TotalTime;
pumps = new List<Pump>();
foreach (Item item in Item.ItemList)
{
//don't attempt to use pumps outside the sub
if (item.Submarine == null) { continue; }
var pump = item.GetComponent<Pump>();
if (pump == null) continue;
if (item.HasTag("ballast")) continue;
//if the pump is connected to an item with a steering component, it must be a ballast pump
//(This may not work correctly if the signals are passed through some fancy circuit or a wifi component,
//which is why sub creators are encouraged to tag the ballast pumps)
bool connectedToSteering = false;
foreach (Connection c in item.Connections)
{
if (c.IsPower) continue;
if (item.GetConnectedComponentsRecursive<Steering>(c).Count > 0)
{
connectedToSteering = true;
break;
}
}
if (connectedToSteering) continue;
if (orderOption.ToLowerInvariant() == "stop pumping")
{
if (!pump.IsActive || pump.FlowPercentage == 0.0f) continue;
}
else
{
if (!pump.Item.InWater) continue;
if (pump.IsActive && pump.FlowPercentage <= -90.0f) continue;
}
pumps.Add(pump);
}
foreach (Pump pump in pumps)
{
AddSubObjective(new AIObjectiveOperateItem(pump, character, orderOption, false));
}
}
}
}
@@ -0,0 +1,102 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
namespace Barotrauma
{
class AIObjectiveRepairItem : AIObjective
{
private Item item;
public AIObjectiveRepairItem(Character character, Item item)
: base(character, "")
{
this.item = item;
}
public override float GetPriority(AIObjectiveManager objectiveManager)
{
bool insufficientSkills = true;
bool repairablesFound = false;
foreach (Repairable repairable in item.Repairables)
{
if (item.Condition > repairable.ShowRepairUIThreshold) { continue; }
if (repairable.DegreeOfSuccess(character) >= 0.5f) { insufficientSkills = false; }
repairablesFound = true;
}
if (!repairablesFound) { return 0.0f; }
float priority = 100.0f - item.Condition;
//vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally)
float dist =
Math.Abs(character.WorldPosition.X - item.WorldPosition.X) +
Math.Abs(character.WorldPosition.Y - item.WorldPosition.Y) * 2.0f;
//heavily increase the priority if the item is already selected
//so characters don't keep switching between nearby damaged items
if (character.SelectedConstruction == item)
{
priority += 50.0f;
}
if (insufficientSkills)
{
return MathHelper.Lerp(0.0f, 50.0f, priority / 100.0f / Math.Max(dist / 100.0f, 1.0f));
}
else
{
return MathHelper.Lerp(50.0f, 100.0f, priority / 100.0f / Math.Max(dist / 100.0f, 1.0f));
}
}
public override bool IsCompleted()
{
foreach (Repairable repairable in item.Repairables)
{
if (item.Condition < Math.Max(repairable.ShowRepairUIThreshold, item.Prefab.Health * 0.98f)) return false;
}
character?.Speak(TextManager.Get("DialogItemRepaired").Replace("[itemname]", item.Name), null, 0.0f, "itemrepaired", 10.0f);
return true;
}
public override bool IsDuplicate(AIObjective otherObjective)
{
return otherObjective is AIObjectiveRepairItem repairObjective && repairObjective.item == item;
}
protected override void Act(float deltaTime)
{
foreach (Repairable repairable in item.Repairables)
{
//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)
{
if (!character.Inventory.Items.Any(it => it != null && requiredItem.MatchesItem(it)))
{
AddSubObjective(new AIObjectiveGetItem(character, requiredItem.Identifiers, true));
return;
}
}
}
}
if (character.CanInteractWith(item))
{
foreach (Repairable repairable in item.Repairables)
{
if (character.SelectedConstruction != item) { item.TryInteract(character, true, true); }
repairable.CurrentFixer = character;
break;
}
}
else
{
AddSubObjective(new AIObjectiveGoTo(item, character));
}
}
}
}
@@ -0,0 +1,68 @@
using Barotrauma.Items.Components;
using System;
using System.Collections.Generic;
using System.Text;
namespace Barotrauma
{
class AIObjectiveRepairItems : AIObjective
{
/// <summary>
/// Should the character only attempt to fix items they have the skills to fix, or any damaged item
/// </summary>
public bool RequireAdequateSkills;
public AIObjectiveRepairItems(Character character)
: base(character, "")
{
}
public override float GetPriority(AIObjectiveManager objectiveManager)
{
GetBrokenItems();
if (subObjectives.Count > 0 && objectiveManager.CurrentOrder == this)
{
return AIObjectiveManager.OrderPriority;
}
return 1.0f;
}
public override bool IsCompleted()
{
return false;
}
public override bool IsDuplicate(AIObjective otherObjective)
{
return otherObjective is AIObjectiveRepairItems repairItems && repairItems.RequireAdequateSkills == RequireAdequateSkills;
}
protected override void Act(float deltaTime)
{
GetBrokenItems();
}
private void GetBrokenItems()
{
foreach (Item item in Item.ItemList)
{
//ignore items that are in full condition
if (item.Condition >= 100.0f) continue;
foreach (Repairable repairable in item.Repairables)
{
//ignore ones that are already fixed
if (item.Condition > repairable.ShowRepairUIThreshold) continue;
if (RequireAdequateSkills)
{
if (!repairable.HasRequiredSkills(character)) { continue; }
}
AddSubObjective(new AIObjectiveRepairItem(character, item));
break;
}
}
}
}
}
@@ -1,17 +1,37 @@
using Microsoft.Xna.Framework;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace Barotrauma
{
/*class AIObjectiveRescue : AIObjective
class AIObjectiveRescue : AIObjective
{
const float TreatmentDelay = 0.5f;
private readonly Character targetCharacter;
private AIObjectiveGoTo goToObjective;
private float treatmentTimer;
public override bool CanBeCompleted
{
get
{
if (targetCharacter.Removed) return false;
if (goToObjective != null && !goToObjective.CanBeCompleted) return false;
return true;
}
}
public AIObjectiveRescue(Character character, Character targetCharacter)
: base (character, "")
: base(character, "")
{
Debug.Assert(character != targetCharacter);
this.targetCharacter = targetCharacter;
}
@@ -21,14 +41,189 @@ namespace Barotrauma
return rescueObjective != null && rescueObjective.targetCharacter == targetCharacter;
}
public override float GetPriority(Character character)
protected override void Act(float deltaTime)
{
if (targetCharacter.AnimController.CurrentHull == null) return 0.0f;
//target in water -> move to a dry place first
if (targetCharacter.AnimController.InWater)
{
if (character.SelectedCharacter != targetCharacter)
{
if (!character.CanInteractWith(targetCharacter))
{
AddSubObjective(goToObjective = new AIObjectiveGoTo(targetCharacter, character));
}
else
{
character.SelectCharacter(targetCharacter);
}
}
else
{
AddSubObjective(new AIObjectiveFindSafety(character));
}
return;
}
float distance = Vector2.DistanceSquared(character.WorldPosition, targetCharacter.WorldPosition);
//target not in water -> we can start applying treatment
if (!character.CanInteractWith(targetCharacter))
{
AddSubObjective(goToObjective = new AIObjectiveGoTo(targetCharacter, character));
}
else
{
if (character.SelectedCharacter == null)
{
character?.Speak(TextManager.Get("DialogFoundUnconsciousTarget")
.Replace("[targetname]", targetCharacter.Name).Replace("[roomname]", character.CurrentHull.RoomName),
null, 1.0f,
"foundunconscioustarget" + targetCharacter.Name, 60.0f);
}
return targetCharacter.IsDead ? 1000.0f / distance : 10000.0f / distance;
character.SelectCharacter(targetCharacter);
GiveTreatment(deltaTime);
}
}
}*/
protected override bool ShouldInterruptSubObjective(AIObjective subObjective)
{
if (subObjective is AIObjectiveFindSafety)
{
if (character.SelectedCharacter != targetCharacter) return true;
if (character.AnimController.InWater || targetCharacter.AnimController.InWater) return false;
foreach (Limb limb in targetCharacter.AnimController.Limbs)
{
if (!Submarine.RectContains(targetCharacter.CurrentHull.WorldRect, limb.WorldPosition)) return false;
}
return !character.AnimController.InWater && !targetCharacter.AnimController.InWater &&
AIObjectiveFindSafety.GetHullSafety(character.CurrentHull, character) > 50.0f;
}
return false;
}
private void GiveTreatment(float deltaTime)
{
if (treatmentTimer > 0.0f)
{
treatmentTimer -= deltaTime;
}
treatmentTimer = TreatmentDelay;
var allAfflictions = targetCharacter.CharacterHealth.GetAllAfflictions()
.Where(a => a.GetVitalityDecrease(targetCharacter.CharacterHealth) > 0)
.ToList();
allAfflictions.Sort((a1, a2) =>
{
return Math.Sign(a2.GetVitalityDecrease(targetCharacter.CharacterHealth) - a1.GetVitalityDecrease(targetCharacter.CharacterHealth));
});
//check if we already have a suitable treatment for any of the afflictions
foreach (Affliction affliction in allAfflictions)
{
foreach (KeyValuePair<string, float> treatmentSuitability in affliction.Prefab.TreatmentSuitability)
{
if (treatmentSuitability.Value > 0.0f)
{
Item matchingItem = character.Inventory.FindItemByIdentifier(treatmentSuitability.Key);
if (matchingItem == null) { continue; }
ApplyTreatment(affliction, matchingItem);
return;
}
}
}
//didn't have any suitable treatments available, try to find some medical items
HashSet<string> suitableItemIdentifiers = new HashSet<string>();
foreach (Affliction affliction in allAfflictions)
{
foreach (KeyValuePair<string, float> treatmentSuitability in affliction.Prefab.TreatmentSuitability)
{
if (treatmentSuitability.Value > 0.0f)
{
suitableItemIdentifiers.Add(treatmentSuitability.Key);
}
}
}
if (suitableItemIdentifiers.Count > 0)
{
List<string> itemNameList = new List<string>();
foreach (string itemIdentifier in suitableItemIdentifiers)
{
if (MapEntityPrefab.Find(null, itemIdentifier, showErrorMessages: false) is ItemPrefab itemPrefab)
{
itemNameList.Add(itemPrefab.Name);
}
//only list the first 4 items
if (itemNameList.Count >= 4) break;
}
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());
}
character?.Speak(TextManager.Get("DialogListRequiredTreatments")
.Replace("[targetname]", targetCharacter.Name)
.Replace("[treatmentlist]", itemListStr),
null, 2.0f, "listrequiredtreatments" + targetCharacter.Name, 60.0f);
}
character.DeselectCharacter();
AddSubObjective(new AIObjectiveGetItem(character, suitableItemIdentifiers.ToArray(), true));
}
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(addMessage: false)) continue;
#if CLIENT
ic.PlaySound(ActionType.OnUse, character.WorldPosition, character);
#endif
ic.WasUsed = true;
ic.ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb);
if (ic.DeleteOnUse) remove = true;
}
if (remove)
{
Entity.Spawner?.AddToRemoveQueue(item);
}
}
public override bool IsCompleted()
{
bool isCompleted = !targetCharacter.IsUnconscious || targetCharacter.IsDead;
if (isCompleted)
{
character?.Speak(TextManager.Get("DialogTargetHealed").Replace("[targetname]", targetCharacter.Name),
null, 1.0f, "targethealed" + targetCharacter.Name, 60.0f);
}
return isCompleted;
}
public override float GetPriority(AIObjectiveManager objectiveManager)
{
if (targetCharacter.AnimController.CurrentHull == null) return 0.0f;
float distance = Vector2.DistanceSquared(character.WorldPosition, targetCharacter.WorldPosition);
return targetCharacter.IsDead ? 1000.0f / distance : 10000.0f / distance;
}
}
}
@@ -3,7 +3,7 @@ using System.Linq;
namespace Barotrauma
{
/*class AIObjectiveRescueAll : AIObjective
class AIObjectiveRescueAll : AIObjective
{
private List<Character> rescueTargets;
@@ -18,13 +18,19 @@ namespace Barotrauma
return true;
}
public override float GetPriority(Character character)
public override float GetPriority(AIObjectiveManager objectiveManager)
{
GetRescueTargets();
if (!rescueTargets.Any()) { return 0.0f; }
if (objectiveManager.CurrentObjective == this)
{
return AIObjectiveManager.OrderPriority;
}
//if there are targets to rescue, the priority is slightly less
//than the priority of explicit orders given to the character
return rescueTargets.Any() ? AIObjectiveManager.OrderPriority - 5.0f : 0.0f;
return AIObjectiveManager.OrderPriority - 5.0f;
}
private void GetRescueTargets()
@@ -32,9 +38,7 @@ namespace Barotrauma
rescueTargets = Character.CharacterList.FindAll(c =>
c.AIController is HumanAIController &&
c != character &&
(c.IsDead || c.IsUnconscious) &&
c.AnimController.CurrentHull != null &&
AIObjectiveFindSafety.GetHullSafety(c.AnimController.CurrentHull, c) < 50.0f);
c.IsUnconscious);
}
protected override void Act(float deltaTime)
@@ -44,5 +48,11 @@ namespace Barotrauma
AddSubObjective(new AIObjectiveRescue(character, target));
}
}
}*/
public override bool IsCompleted()
{
return false;
}
}
}
@@ -12,6 +12,12 @@ namespace Barotrauma
private static string ConfigFile = Path.Combine("Content", "Orders.xml");
public static List<Order> PrefabList;
public Order Prefab
{
get;
private set;
}
public readonly string Name;
public readonly string DoingText;
@@ -19,15 +25,25 @@ namespace Barotrauma
public readonly Sprite SymbolSprite;
public readonly Type ItemComponentType;
public readonly string ItemName;
public readonly string[] ItemIdentifiers;
public readonly string AITag;
public readonly Color Color;
//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 ItemComponent TargetItem;
public Controller ConnectedController;
public readonly string[] AppropriateJobs;
public readonly string[] Options;
public readonly string[] OptionNames;
static Order()
{
@@ -39,98 +55,125 @@ namespace Barotrauma
foreach (XElement orderElement in doc.Root.Elements())
{
if (orderElement.Name.ToString().ToLowerInvariant() != "order") continue;
PrefabList.Add(new Order(orderElement));
var newOrder = new Order(orderElement);
newOrder.Prefab = newOrder;
PrefabList.Add(newOrder);
}
//PrefabList.Add(new Order("Follow", "Following"));
//PrefabList.Add(new Order("Dismiss", "Dismissed"));
//PrefabList.Add(new Order("Wait", "Wait"));
//PrefabList.Add(new Order("Operate Reactor", "Operating reactor", typeof(Reactor), new string[] {"Power up", "Shutdown"}));
//PrefabList.Add(new Order("Operate Railgun", "Operating railgun", typeof(Turret), new string[] { "Fire at will", "Hold fire" }));
}
private Order(XElement orderElement)
{
Name = orderElement.GetAttributeString("name", "Name not found");
DoingText = orderElement.GetAttributeString("doingtext", "");
AITag = orderElement.GetAttributeString("aitag", "");
Name = TextManager.Get("OrderName." + AITag, true) ?? orderElement.GetAttributeString("name", "Name not found");
DoingText = TextManager.Get("OrderNameDoing." + AITag, true) ?? orderElement.GetAttributeString("doingtext", "");
string targetItemName = orderElement.GetAttributeString("targetitemtype", "");
if (!string.IsNullOrWhiteSpace(targetItemName))
string targetItemType = orderElement.GetAttributeString("targetitemtype", "");
if (!string.IsNullOrWhiteSpace(targetItemType))
{
try
{
ItemComponentType = Type.GetType("Barotrauma.Items.Components." + targetItemName, true, true);
ItemComponentType = Type.GetType("Barotrauma.Items.Components." + targetItemType, true, true);
}
catch (Exception e)
{
DebugConsole.ThrowError("Error in " + ConfigFile + ", item component type " + targetItemName + " not found", e);
DebugConsole.ThrowError("Error in " + ConfigFile + ", item component type " + targetItemType + " not found", e);
}
}
ItemName = orderElement.GetAttributeString("targetitemname", "");
Color = new Color(orderElement.GetAttributeVector4("color", new Vector4(1.0f, 1.0f, 1.0f, 1.0f)));
ItemIdentifiers = orderElement.GetAttributeStringArray("targetitemidentifiers", new string[0], trim: true, convertToLowerInvariant: true);
Color = orderElement.GetAttributeColor("color", Color.White);
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]);
string optionStr = orderElement.GetAttributeString("options", "");
if (string.IsNullOrWhiteSpace(optionStr))
string translatedOptionNames = TextManager.Get("OrderOptions." + AITag, true);
if (translatedOptionNames == null)
{
Options = new string[0];
OptionNames = orderElement.GetAttributeStringArray("optionnames", new string[0]);
}
else
{
Options = optionStr.Split(',');
for (int i = 0; i<Options.Length; i++)
string[] splitOptionNames = translatedOptionNames.Split(',');
OptionNames = new string[Options.Length];
for (int i = 0; i < Options.Length && i < splitOptionNames.Length; i++)
{
Options[i] = Options[i].Trim();
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;
}
foreach (XElement subElement in orderElement.Elements())
{
if (subElement.Name.ToString().ToLowerInvariant() != "sprite") continue;
SymbolSprite = new Sprite(subElement);
break;
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "sprite":
SymbolSprite = new Sprite(subElement);
break;
}
}
}
private Order(string name, string doingText, Type itemComponentType, string[] parameters = null)
public Order(Order prefab, Entity targetEntity, ItemComponent targetItem)
{
Name = name;
DoingText = doingText;
ItemComponentType = itemComponentType;
Options = parameters == null ? new string[0] : parameters;
}
Prefab = prefab;
public Order(Order prefab, ItemComponent targetItem)
{
Name = prefab.Name;
AITag = prefab.AITag;
DoingText = prefab.DoingText;
ItemComponentType = prefab.ItemComponentType;
Options = prefab.Options;
SymbolSprite = prefab.SymbolSprite;
Color = prefab.Color;
UseController = prefab.UseController;
TargetAllCharacters = prefab.TargetAllCharacters;
AppropriateJobs = prefab.AppropriateJobs;
FadeOutTime = prefab.FadeOutTime;
TargetItem = targetItem;
TargetEntity = targetEntity;
if (targetItem != null)
{
if (UseController)
{
var controllers = targetItem.Item.GetConnectedComponents<Controller>();
if (controllers.Count > 0) ConnectedController = controllers[0];
}
TargetEntity = targetItem.Item;
TargetItemComponent = targetItem;
}
}
public bool HasAppropriateJob(Character character)
{
if (AppropriateJobs == null || AppropriateJobs.Length == 0) return true;
if (character.Info == null || character.Info.Job == null) return false;
for (int i = 0; i < AppropriateJobs.Length; i++)
{
if (character.Info.Job.Prefab.Identifier.ToLowerInvariant() == AppropriateJobs[i].ToLowerInvariant()) return true;
}
return false;
}
private Order(string name, string doingText, string[] parameters = null)
:this (name,doingText, null, parameters)
public string GetChatMessage(string targetCharacterName, string targetRoomName, string orderOption = "")
{
orderOption = orderOption ?? "";
string messageTag = "OrderDialog." + AITag;
if (!string.IsNullOrEmpty(orderOption)) messageTag += "." + orderOption;
string msg = TextManager.Get(messageTag, true);
if (msg == null) return "";
if (targetCharacterName == null) targetCharacterName = "";
if (targetRoomName == null) targetRoomName = "";
return msg.Replace("[name]", targetCharacterName).Replace("[roomname]", targetRoomName);
}
}
@@ -1,4 +1,5 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -154,24 +155,29 @@ namespace Barotrauma
}
}
public SteeringPath FindPath(Vector2 start, Vector2 end)
{
System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
sw.Start();
public SteeringPath FindPath(Vector2 start, Vector2 end, string errorMsgStr)
{
float closestDist = 0.0f;
PathNode startNode = null;
foreach (PathNode node in nodes)
{
Vector2 nodePos = node.Position;
float dist = System.Math.Abs(start.X - nodePos.X) +
System.Math.Abs(start.Y - nodePos.Y) * 10.0f; //higher cost for vertical movement
float xDiff = System.Math.Abs(start.X - nodePos.X);
float yDiff = System.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 += Vector2.Distance(end, nodePos) / 10.0f;
if (dist<closestDist || startNode==null)
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) dist *= 10.0f;
if (dist < closestDist || startNode == null)
{
//if searching for a path inside the sub, make sure the waypoint is visible
if (insideSubmarine)
@@ -182,9 +188,9 @@ namespace Barotrauma
if (body != null)
{
if (body.UserData is Submarine) continue;
if (body.UserData is Structure && !((Structure)body.UserData).IsPlatform) continue;
if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) continue;
}
}
@@ -195,7 +201,7 @@ namespace Barotrauma
if (startNode == null)
{
DebugConsole.NewMessage("Pathfinding error, couldn't find a start node", Color.DarkRed);
DebugConsole.NewMessage("Pathfinding error, couldn't find a start node. "+ errorMsgStr, Color.DarkRed);
return new SteeringPath();
}
@@ -207,6 +213,10 @@ namespace Barotrauma
Vector2 nodePos = node.Position;
float dist = Vector2.Distance(end, nodePos);
//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
@@ -223,16 +233,13 @@ namespace Barotrauma
if (endNode == null)
{
DebugConsole.NewMessage("Pathfinding error, couldn't find an end node", Color.DarkRed);
DebugConsole.NewMessage("Pathfinding error, couldn't find an end node. "+ errorMsgStr, Color.DarkRed);
return new SteeringPath();
}
var path = FindPath(startNode,endNode);
sw.Stop();
System.Diagnostics.Debug.WriteLine("findpath: " + sw.ElapsedMilliseconds+" ms");
return path;
}
@@ -253,10 +260,9 @@ namespace Barotrauma
}
}
if (startNode == null || endNode == null)
{
DebugConsole.NewMessage("Pathfinding error, couldn't find matching pathnodes to waypoints", Color.DarkRed);
DebugConsole.NewMessage("Pathfinding error, couldn't find matching pathnodes to waypoints.", Color.DarkRed);
return new SteeringPath();
}
@@ -15,9 +15,7 @@ namespace Barotrauma
private Vector2 steering;
//the steering amount when avoiding obstacles
//(needs a separate variable because it's only updated when a raycast is done to detect any nearby obstacles)
private Vector2 avoidSteering;
private Vector2? avoidObstaclePos;
private float rayCastTimer;
private float wanderAngle;
@@ -35,19 +33,19 @@ namespace Barotrauma
wanderAngle = Rand.Range(0.0f, MathHelper.TwoPi);
}
public void SteeringSeek(Vector2 targetSimPos, float speed = 1.0f)
public void SteeringSeek(Vector2 targetSimPos, float speed)
{
steering += DoSteeringSeek(targetSimPos, speed);
}
public void SteeringWander(float speed = 1.0f)
public void SteeringWander(float speed)
{
steering += DoSteeringWander(speed);
}
public void SteeringAvoid(float deltaTime, float speed)
public void SteeringAvoid(float deltaTime, float lookAheadDistance, float speed)
{
steering += DoSteeringAvoid(deltaTime, speed);
steering += DoSteeringAvoid(deltaTime, lookAheadDistance, speed);
}
public void SteeringManual(float deltaTime, Vector2 velocity)
@@ -70,7 +68,7 @@ namespace Barotrauma
steering.Y = 0.0f;
}
public virtual void Update(float speed = 1.0f)
public virtual void Update(float speed)
{
if (steering == Vector2.Zero || !MathUtils.IsValid(steering))
{
@@ -88,7 +86,7 @@ namespace Barotrauma
host.Steering = steering;
}
protected virtual Vector2 DoSteeringSeek(Vector2 target, float speed = 1.0f)
protected virtual Vector2 DoSteeringSeek(Vector2 target, float speed)
{
Vector2 targetVel = target - host.SimPosition;
@@ -108,9 +106,9 @@ namespace Barotrauma
return newSteering;
}
protected virtual Vector2 DoSteeringWander(float speed = 1.0f)
protected virtual Vector2 DoSteeringWander(float speed)
{
Vector2 circleCenter = (host.Steering == Vector2.Zero) ? new Vector2(speed, 0.0f) : host.Steering;
Vector2 circleCenter = (host.Steering == Vector2.Zero) ? Rand.Vector(speed) : host.Steering;
circleCenter = Vector2.Normalize(circleCenter) * CircleDistance;
Vector2 displacement = new Vector2(
@@ -132,21 +130,19 @@ namespace Barotrauma
return newSteering;
}
protected virtual Vector2 DoSteeringAvoid(float deltaTime, float speed = 1.0f)
protected virtual Vector2 DoSteeringAvoid(float deltaTime, float lookAheadDistance, float speed)
{
if (steering == Vector2.Zero || host.Steering == Vector2.Zero) return Vector2.Zero;
float maxDistance = 2.0f;
Vector2 ahead = host.SimPosition + Vector2.Normalize(host.Steering) * maxDistance;
float maxDistance = lookAheadDistance;
if (rayCastTimer <= 0.0f)
{
Vector2 ahead = host.SimPosition + Vector2.Normalize(host.Steering) * maxDistance;
rayCastTimer = RayCastInterval;
Body closestBody = Submarine.CheckVisibility(host.SimPosition, ahead);
if (closestBody == null)
{
avoidSteering = Vector2.Zero;
avoidObstaclePos = null;
return Vector2.Zero;
}
else
@@ -163,19 +159,19 @@ namespace Barotrauma
obstaclePosition.X = closestStructure.SimPosition.X;
}
avoidSteering = Vector2.Normalize(Submarine.LastPickedPosition - obstaclePosition);
avoidObstaclePos = obstaclePosition;
//avoidSteering = Vector2.Normalize(Submarine.LastPickedPosition - obstaclePosition);
}
else if (closestBody.UserData is Item item)
/*else if (closestBody.UserData is Item)
{
avoidSteering = Vector2.Normalize(Submarine.LastPickedPosition - item.SimPosition);
}
}*/
else
{
avoidSteering = Vector2.Normalize(host.SimPosition - Submarine.LastPickedPosition);
avoidObstaclePos = Submarine.LastPickedPosition;
//avoidSteering = Vector2.Normalize(host.SimPosition - Submarine.LastPickedPosition);
}
//failed to normalize (the obstacle to avoid is at the same position as the character?)
// -> move to a random direction
if (!MathUtils.IsValid(avoidSteering)) avoidSteering = Rand.Vector(1.0f);
}
}
@@ -184,7 +180,14 @@ namespace Barotrauma
rayCastTimer -= deltaTime;
}
return avoidSteering * speed;
if (!avoidObstaclePos.HasValue) return Vector2.Zero;
Vector2 diff = avoidObstaclePos.Value - host.SimPosition;
float dist = diff.Length();
if (dist > maxDistance) return Vector2.Zero;
return -diff * (1.0f - dist / maxDistance) * speed;
}
}