409d4d9...aeafa16 (merge human-ai)

This commit is contained in:
Joonas Rikkonen
2019-03-18 22:52:17 +02:00
parent 80ab27df22
commit 3301bed442
88 changed files with 2334 additions and 1657 deletions
@@ -356,7 +356,6 @@ namespace Barotrauma
private void UpdateIdle(float deltaTime)
{
float speed = Character.AnimController.GetCurrentSpeed(false);
if (Character.Submarine == null && SimPosition.Y < ConvertUnits.ToSimUnits(Character.CharacterHealth.CrushDepth * 0.75f))
{
//steer straight up if very deep
@@ -370,17 +369,17 @@ namespace Barotrauma
{
Vector2 targetSimPos = Character.Submarine == null ? ConvertUnits.ToSimUnits(SelectedAiTarget.WorldPosition) : SelectedAiTarget.SimPosition;
steeringManager.SteeringAvoid(deltaTime, colliderSize * 3.0f, speed);
steeringManager.SteeringSeek(targetSimPos, speed);
steeringManager.SteeringAvoid(deltaTime, colliderSize * 3.0f);
steeringManager.SteeringSeek(targetSimPos);
}
else
{
//wander around randomly
if (Character.Submarine == null)
{
steeringManager.SteeringAvoid(deltaTime, colliderSize * 5.0f, speed);
steeringManager.SteeringAvoid(deltaTime, colliderSize * 5.0f);
}
steeringManager.SteeringWander(speed / 2);
steeringManager.SteeringWander(0.5f);
}
}
@@ -398,12 +397,11 @@ namespace Barotrauma
Vector2 escapeDir = Vector2.Normalize(SimPosition - SelectedAiTarget.SimPosition);
if (!MathUtils.IsValid(escapeDir)) escapeDir = Vector2.UnitY;
float speed = Character.AnimController.GetCurrentSpeed(useMaxSpeed: true);
SteeringManager.SteeringManual(deltaTime, escapeDir * speed);
SteeringManager.SteeringWander(speed);
SteeringManager.SteeringManual(deltaTime, escapeDir);
SteeringManager.SteeringWander();
if (Character.CurrentHull == null)
{
SteeringManager.SteeringAvoid(deltaTime, colliderSize * 3.0f, speed);
SteeringManager.SteeringAvoid(deltaTime, colliderSize * 3.0f);
}
}
@@ -419,8 +417,6 @@ namespace Barotrauma
return;
}
float speed = Character.AnimController.GetCurrentSpeed(true);
selectedTargetMemory.Priority -= deltaTime * 0.1f;
Vector2 attackSimPosition = Character.Submarine == null ? ConvertUnits.ToSimUnits(SelectedAiTarget.WorldPosition) : SelectedAiTarget.SimPosition;
@@ -447,9 +443,12 @@ namespace Barotrauma
if (wallTarget != null)
{
attackSimPosition = ConvertUnits.ToSimUnits(wallTarget.Position);
if (Character.Submarine == null && SelectedAiTarget.Entity?.Submarine != null) attackSimPosition += ConvertUnits.ToSimUnits(SelectedAiTarget.Entity.Submarine.Position);
if (Character.Submarine == null && SelectedAiTarget.Entity?.Submarine != null)
{
attackSimPosition += ConvertUnits.ToSimUnits(SelectedAiTarget.Entity.Submarine.Position);
}
}
else if (SelectedAiTarget.Entity is Character)
else if (SelectedAiTarget.Entity is Character c)
{
//target the closest limb if the target is a character
float closestDist = Vector2.DistanceSquared(SelectedAiTarget.SimPosition, SimPosition) * 10.0f;
@@ -503,11 +502,11 @@ namespace Barotrauma
Character.AnimController.ReleaseStuckLimbs();
if (steeringManager is IndoorsSteeringManager)
{
steeringManager.SteeringManual(deltaTime, targetPos - Character.WorldPosition);
steeringManager.SteeringManual(deltaTime, Vector2.Normalize(targetPos - Character.WorldPosition));
}
else
{
steeringManager.SteeringSeek(ConvertUnits.ToSimUnits(targetPos), speed);
steeringManager.SteeringSeek(ConvertUnits.ToSimUnits(targetPos));
}
return;
}
@@ -518,11 +517,12 @@ namespace Barotrauma
//steer through the door manually if it's open or broken
if (door?.LinkedGap?.FlowTargetHull != null && !door.LinkedGap.IsRoomToRoom && (door.IsOpen || door.Item.Condition <= 0.0f))
{
var velocity = Vector2.Normalize(door.LinkedGap.FlowTargetHull.WorldPosition - Character.WorldPosition);
if (door.LinkedGap.IsHorizontal)
{
if (Character.WorldPosition.Y < door.Item.WorldRect.Y && Character.WorldPosition.Y > door.Item.WorldRect.Y - door.Item.Rect.Height)
{
var velocity = Vector2.UnitX * (door.LinkedGap.FlowTargetHull.WorldPosition.X - Character.WorldPosition.X) * speed;
velocity.Y = 0;
steeringManager.SteeringManual(deltaTime, velocity);
return;
}
@@ -531,7 +531,7 @@ namespace Barotrauma
{
if (Character.WorldPosition.X < door.Item.WorldRect.X && Character.WorldPosition.X > door.Item.WorldRect.Right)
{
var velocity = Vector2.UnitY * (door.LinkedGap.FlowTargetHull.WorldPosition.Y - Character.WorldPosition.Y) * speed;
velocity.X = 0;
steeringManager.SteeringManual(deltaTime, velocity);
return;
}
@@ -638,10 +638,10 @@ namespace Barotrauma
Vector2 targetingVector = Vector2.Normalize(steeringVector) * attackingLimb.attack.Range;
// Offset the position a bit so that we don't overshoot the movement.
Vector2 steerPos = attackSimPosition + targetingVector;
steeringManager.SteeringSeek(steerPos, speed);
steeringManager.SteeringSeek(steerPos, 10);
if (Character.CurrentHull == null)
{
SteeringManager.SteeringAvoid(deltaTime, colliderSize * 1.5f, speed);
SteeringManager.SteeringAvoid(deltaTime, colliderSize * 1.5f);
}
if (steeringManager is IndoorsSteeringManager indoorsSteering)
@@ -656,7 +656,7 @@ namespace Barotrauma
}
else if (indoorsSteering.CurrentPath.Finished)
{
steeringManager.SteeringManual(deltaTime, steeringVector);
steeringManager.SteeringManual(deltaTime, Vector2.Normalize(steeringVector));
}
else if (indoorsSteering.CurrentPath.CurrentNode?.ConnectedDoor != null)
{
@@ -800,7 +800,7 @@ namespace Barotrauma
if (attacker == null || attacker.AiTarget == null) return;
AITargetMemory targetMemory = FindTargetMemory(attacker.AiTarget);
targetMemory.Priority += GetRelativeDamage(attackResult.Damage, Character.Vitality) * aggressionhurt; ;
targetMemory.Priority += GetRelativeDamage(attackResult.Damage, Character.Vitality) * aggressionhurt;
}
// 10 dmg, 100 health -> 0.1
@@ -838,7 +838,7 @@ namespace Barotrauma
steeringManager.SteeringManual(deltaTime, attackDir * (1.0f - (dist / 500.0f)));
}
steeringManager.SteeringAvoid(deltaTime, colliderSize * 3.0f, Character.AnimController.GetCurrentSpeed(false));
steeringManager.SteeringAvoid(deltaTime, colliderSize * 3.0f);
}
#endregion
@@ -870,12 +870,12 @@ namespace Barotrauma
if (limbDist < 2.0f)
{
Character.SelectCharacter(SelectedAiTarget.Entity as Character);
steeringManager.SteeringManual(deltaTime, limbDiff);
steeringManager.SteeringManual(deltaTime, Vector2.Normalize(limbDiff));
Character.AnimController.Collider.ApplyForce(limbDiff * mouthLimb.Mass * 50.0f, mouthPos);
}
else
{
steeringManager.SteeringSeek(attackSimPosition - (mouthPos - SimPosition), Character.AnimController.GetCurrentSpeed(useMaxSpeed: true));
steeringManager.SteeringSeek(attackSimPosition - (mouthPos - SimPosition));
}
}
@@ -1,6 +1,7 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
@@ -19,8 +20,16 @@ namespace Barotrauma
private float crouchRaycastTimer;
const float CrouchRaycastInterval = 1.0f;
public const float HULL_SAFETY_THRESHOLD = 50;
// TODO: update the list when someone gives a report
public HashSet<Hull> UnsafeHulls { get; private set; } = new HashSet<Hull>();
private SteeringManager outsideSteering, insideSteering;
public IndoorsSteeringManager PathSteering => insideSteering as IndoorsSteeringManager;
public HumanoidAnimController AnimController => Character.AnimController as HumanoidAnimController;
public override AIObjectiveManager ObjectiveManager
{
get { return objectiveManager; }
@@ -68,17 +77,18 @@ namespace Barotrauma
steeringManager = outsideSteering;
}
(Character.AnimController as HumanoidAnimController).Crouching = shouldCrouch;
AnimController.Crouching = shouldCrouch;
CheckCrouching(deltaTime);
Character.ClearInputs();
objectiveManager.UpdateObjectives(deltaTime);
if (updateObjectiveTimer > 0.0f)
{
updateObjectiveTimer -= deltaTime;
}
else
{
objectiveManager.UpdateObjectives();
objectiveManager.SortObjectives();
updateObjectiveTimer = UpdateObjectiveInterval;
}
@@ -89,69 +99,107 @@ namespace Barotrauma
}
objectiveManager.DoCurrentObjective(deltaTime);
float currObjectivePriority = objectiveManager.GetCurrentPriority();
bool run = currObjectivePriority > 30.0f;
steeringManager.Update(Character.AnimController.GetCurrentSpeed(run));
bool run = objectiveManager.GetCurrentPriority() > AIObjectiveManager.OrderPriority;
if (ObjectiveManager.CurrentObjective is AIObjectiveGoTo goTo && goTo.Target != null)
{
if (Vector2.DistanceSquared(Character.SimPosition, goTo.Target.SimPosition) > 3 * 3)
{
run = true;
}
}
if (!run)
{
run = objectiveManager.CurrentObjective.ForceRun;
}
if (run)
{
run = !AnimController.Crouching && !AnimController.IsMovingBackwards;
}
float currentSpeed = Character.AnimController.GetCurrentSpeed(run);
steeringManager.Update(currentSpeed);
bool ignorePlatforms = Character.AnimController.TargetMovement.Y < -0.5f &&
(-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X));
var currPath = (steeringManager as IndoorsSteeringManager)?.CurrentPath;
if (currPath != null && currPath.CurrentNode != null)
if (steeringManager == insideSteering)
{
if (currPath.CurrentNode.SimPosition.Y < Character.AnimController.GetColliderBottom().Y)
var currPath = PathSteering.CurrentPath;
if (currPath != null && currPath.CurrentNode != null)
{
ignorePlatforms = true;
if (currPath.CurrentNode.SimPosition.Y < Character.AnimController.GetColliderBottom().Y)
{
ignorePlatforms = true;
}
}
}
Character.AnimController.IgnorePlatforms = ignorePlatforms;
if (Character.IsClimbing)
{
Character.AnimController.TargetMovement = new Vector2(0.0f, Math.Sign(Character.AnimController.TargetMovement.Y));
}
Vector2 targetMovement = AnimController.TargetMovement;
if (!Character.AnimController.InWater)
{
Vector2 targetMovement = new Vector2(
targetMovement = new Vector2(
Character.AnimController.TargetMovement.X,
MathHelper.Clamp(Character.AnimController.TargetMovement.Y, -1.0f, 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
float speedMultiplier = Character.SpeedMultiplier;
if (run || speedMultiplier <= 0.0f) targetMovement *= speedMultiplier;
Character.ResetSpeedMultiplier(); // Reset, items will set the value before the next update
Character.AnimController.TargetMovement = targetMovement;
}
if (Character.AnimController.Anim == AnimController.Animation.Climbing &&
Character.SelectedConstruction != null &&
Character.SelectedConstruction.GetComponent<Items.Components.Ladder>() != null)
float maxSpeed = Character.ApplyTemporarySpeedLimits(currentSpeed);
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
float speedMultiplier = Character.SpeedMultiplier;
if (run || speedMultiplier <= 0.0f) targetMovement *= speedMultiplier;
Character.ResetSpeedMultiplier(); // Reset, items will set the value before the next update
Character.AnimController.TargetMovement = targetMovement;
if (!NeedsDivingGear(Character.CurrentHull))
{
if (currPath != null && currPath.CurrentNode != null && currPath.CurrentNode.Ladders != null)
bool oxygenLow = Character.OxygenAvailable < CharacterHealth.LowOxygenThreshold;
bool highPressure = Character.CurrentHull == null || Character.CurrentHull.LethalPressure > 0 && Character.PressureProtection <= 0;
bool shouldKeepTheGearOn = objectiveManager.CurrentObjective.KeepDivingGearOn;
bool removeDivingSuit = (oxygenLow && !highPressure) || (!shouldKeepTheGearOn && Character.CurrentHull.WaterPercentage < 1 && !Character.IsClimbing && steeringManager == insideSteering && !PathSteering.InStairs);
if (removeDivingSuit)
{
Character.AnimController.TargetMovement = new Vector2(0.0f, Math.Sign(Character.AnimController.TargetMovement.Y));
var divingSuit = Character.Inventory.FindItemByIdentifier("divingsuit") ?? Character.Inventory.FindItemByTag("divingsuit");
if (divingSuit != null)
{
// TODO: take the item where it was taken from?
divingSuit.Drop(Character);
}
}
bool takeMaskOff = oxygenLow || (!shouldKeepTheGearOn && Character.CurrentHull.WaterPercentage < 20);
if (takeMaskOff)
{
var mask = Character.Inventory.FindItemByIdentifier("divingmask");
if (mask != null)
{
// Try to put the mask in an Any slot, and drop it if that fails
if (!mask.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(mask, Character, new List<InvSlotType>() { InvSlotType.Any }))
{
mask.Drop();
}
}
}
}
//suit can be taken off if there character is inside a hull and there's air in the room
bool canTakeOffSuit = Character.AnimController.CurrentHull != null &&
Character.AnimController.CurrentHull.OxygenPercentage > 30.0f &&
Character.AnimController.CurrentHull.WaterVolume < Character.AnimController.CurrentHull.Volume * 0.3f;
//the suit can be taken off and the character is running out of oxygen (couldn't find a tank for the suit?) or idling
//-> take the suit off
if (canTakeOffSuit && (Character.Oxygen < 50.0f || objectiveManager.CurrentObjective is AIObjectiveIdle))
if (!(ObjectiveManager.CurrentOrder is AIObjectiveExtinguishFires) && !(ObjectiveManager.CurrentObjective is AIObjectiveExtinguishFire))
{
var divingSuit = Character.Inventory.FindItemByIdentifier("divingsuit") ?? Character.Inventory.FindItemByTag("divingsuit");
if (divingSuit != null) divingSuit.Drop(Character);
var extinguisherItem = Character.Inventory.FindItemByIdentifier("extinguisher") ?? Character.Inventory.FindItemByTag("extinguisher");
if (extinguisherItem != null && Character.HasEquippedItem(extinguisherItem))
{
// TODO: take the item where it was taken from?
extinguisherItem.Drop();
}
}
if (Character.IsKeyDown(InputType.Aim))
@@ -173,14 +221,61 @@ namespace Barotrauma
{
Character.AnimController.TargetDir = Character.AnimController.TargetMovement.X > 0.0f ? Direction.Right : Direction.Left;
}
if (Character.CurrentHull != null)
{
PropagateHullSafety(Character, Character.CurrentHull);
}
}
protected void ReportProblems()
{
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);
}
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);
}
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, givingOrderToSelf: false), ChatMessageType.Order);
}
}
}
private void ReportProblems()
{
if (GameMain.Client != null) return;
partial void ReportProblems();
private void UpdateSpeaking()
{
if (Character.Oxygen < 20.0f)
@@ -204,11 +299,26 @@ namespace Barotrauma
float totalDamage = attackResult.Damage;
if (totalDamage <= 0.0f || attacker == null) return;
if (attacker.AnimController.Anim == AnimController.Animation.CPR && attacker.SelectedCharacter == Character)
if (attacker.SpeciesName == Character.SpeciesName)
{
// 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;
if (!attacker.IsRemotePlayer && Character.Controlled != attacker && attacker.AIController != null && attacker.AIController.Enabled)
{
// Don't react to damage done by friendly ai, because we know that it's accidental
return;
}
if (attacker.AnimController.Anim == Barotrauma.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;
}
float currentVitality = Character.CharacterHealth.Vitality;
float dmgPercentage = totalDamage / currentVitality * 100;
if (dmgPercentage < currentVitality / 10)
{
// Don't react to a minor amount of (accidental) dmg done by friendly characters
return;
}
}
objectiveManager.AddObjective(new AIObjectiveCombat(Character, attacker), Rand.Range(0.5f, 1, Rand.RandSync.Unsynced), () =>
@@ -251,5 +361,87 @@ namespace Barotrauma
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;
}
public static bool NeedsDivingGear(Hull hull) => hull == null || hull.OxygenPercentage < 50 || hull.WaterPercentage > 50;
/// <summary>
/// Check whether the character has a diving suit in usable condition plus some oxygen.
/// </summary>
public static bool HasDivingSuit(Character character) => HasItem(character, "divingsuit", "oxygensource");
/// <summary>
/// Check whether the character has a diving mask in usable condition plus some oxygen.
/// </summary>
public static bool HasDivingGear(Character character) => HasItem(character, "diving", "oxygensource");
public static bool HasItem(Character character, string tag, string containedTag, float conditionPercentage = 0)
{
var item = character.Inventory.FindItemByTag(tag);
return item != null &&
item.ConditionPercentage > conditionPercentage &&
character.HasEquippedItem(item) &&
(containedTag == null ||
(item.ContainedItems != null &&
item.ContainedItems.Any(i => i.HasTag(containedTag) && i.ConditionPercentage > conditionPercentage)));
}
/// <summary>
/// Updates the hull safety for all ai characters in the team.
/// </summary>
public static void PropagateHullSafety(Character character, Hull hull)
{
foreach (var c in Character.CharacterList)
{
if (c.TeamID == character.TeamID)
{
if (c.AIController is HumanAIController humanAi)
{
humanAi.RefreshHullSafety(hull);
}
}
}
}
private void RefreshHullSafety(Hull hull)
{
if (GetHullSafety(hull) > HULL_SAFETY_THRESHOLD)
{
UnsafeHulls.Remove(hull);
}
else
{
UnsafeHulls.Add(hull);
}
}
public float GetHullSafety(Hull hull)
{
if (hull == null) { return 0; }
bool ignoreFire = ObjectiveManager.CurrentObjective is AIObjectiveExtinguishFire || ObjectiveManager.CurrentOrder is AIObjectiveExtinguishFires;
bool ignoreWater = HasDivingSuit(Character);
bool ignoreOxygen = ignoreWater || HasDivingGear(Character);
return GetHullSafety(hull, Character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies: false);
}
public static float GetHullSafety(Hull hull, Character character, bool ignoreWater = false, bool ignoreOxygen = false, bool ignoreFire = false, bool ignoreEnemies = false)
{
if (hull == null) { return 0; }
if (hull.LethalPressure > 0 && character.PressureProtection <= 0) { return 0; }
float oxygenFactor = ignoreOxygen ? 1 : MathHelper.Lerp(0.25f, 1, hull.OxygenPercentage / 100);
float waterFactor = ignoreWater ? 1 : MathHelper.Lerp(1, 0.25f, hull.WaterPercentage / 100);
if (!character.NeedsAir)
{
oxygenFactor = 1;
waterFactor = 1;
}
// Even the smallest fire reduces the safety by 50%
float fire = hull.FireSources.Count * 0.5f + hull.FireSources.Sum(fs => fs.DamageRange) / hull.Size.X;
float fireFactor = ignoreFire ? 1 : MathHelper.Lerp(1, 0, MathHelper.Clamp(fire, 0, 1));
int enemyCount = Character.CharacterList.Count(e => e.CurrentHull == hull && !e.IsDead && !e.IsUnconscious && (e.AIController is EnemyAIController || e.TeamID != character.TeamID));
// The hull safety decreases 90% per enemy up to 100% (TODO: test smaller percentages)
float enemyFactor = ignoreEnemies ? 1 : MathHelper.Lerp(1, 0, MathHelper.Clamp(enemyCount * 0.9f, 0, 1));
float safety = oxygenFactor * waterFactor * fireFactor * enemyFactor;
return MathHelper.Clamp(safety * 100, 0, 100);
}
}
}
@@ -4,7 +4,6 @@ namespace Barotrauma
{
interface ISteerable
{
Vector2 Steering
{
get;
@@ -25,8 +24,5 @@ namespace Barotrauma
{
get;
}
}
}
@@ -43,8 +43,13 @@ namespace Barotrauma
private set;
}
public IndoorsSteeringManager(ISteerable host, bool canOpenDoors, bool canBreakDoors)
: base(host)
public bool InLadders => currentPath != null && currentPath.CurrentNode != null && currentPath.CurrentNode.Ladders != null;
private bool IsNextNodeLadder => currentPath.NextNode != null && currentPath.CurrentNode.Ladders != null;
private bool IsNextLadderSameAsCurrent => IsNextNodeLadder && currentPath.CurrentNode.Ladders == currentPath.NextNode.Ladders;
public bool InStairs => currentPath != null && currentPath.CurrentNode != null && currentPath.CurrentNode.Stairs != null;
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;
@@ -73,7 +78,7 @@ namespace Barotrauma
IsPathDirty = false;
}
protected override Vector2 DoSteeringSeek(Vector2 target, float speed)
protected override Vector2 DoSteeringSeek(Vector2 target, float weight)
{
//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)
@@ -105,16 +110,14 @@ namespace Barotrauma
var collider = character.AnimController.Collider;
//if not in water and the waypoint is between the top and bottom of the collider, no need to move vertically
if (!character.AnimController.InWater &&
character.AnimController.Anim != AnimController.Animation.Climbing &&
diff.Y < collider.height / 2 + collider.radius)
if (!character.AnimController.InWater && !character.IsClimbing && diff.Y < collider.height / 2 + collider.radius)
{
diff.Y = 0.0f;
}
if (diff.LengthSquared() < 0.001f) return -host.Steering;
return Vector2.Normalize(diff) * speed;
return Vector2.Normalize(diff) * weight;
}
private Vector2 DiffToCurrentNode()
@@ -155,9 +158,7 @@ namespace Barotrauma
}
//only humanoids can climb ladders
if (currentPath.CurrentNode != null &&
currentPath.CurrentNode.Ladders != null &&
character.AnimController is HumanoidAnimController)
if (character.AnimController is HumanoidAnimController && InLadders && IsNextLadderSameAsCurrent)
{
if (character.SelectedConstruction != currentPath.CurrentNode.Ladders.Item &&
currentPath.CurrentNode.Ladders.Item.IsInsideTrigger(character.WorldPosition))
@@ -167,13 +168,15 @@ namespace Barotrauma
}
var collider = character.AnimController.Collider;
if (character.AnimController.Anim == AnimController.Animation.Climbing)
if (character.IsClimbing)
{
Vector2 diff = currentPath.CurrentNode.SimPosition - pos;
//climbing ladders -> don't move horizontally
diff.X = 0.0f;
if (IsNextLadderSameAsCurrent)
{
//climbing ladders -> don't move horizontally
diff.X = 0.0f;
}
//at the same height as the waypoint
if (Math.Abs(collider.SimPosition.Y - currentPath.CurrentNode.SimPosition.Y) < (collider.height / 2 + collider.radius) * 1.25f)
@@ -183,27 +186,23 @@ namespace Barotrauma
{
diff.Y = Math.Max(diff.Y, 1.0f);
}
//we can safely skip to the next waypoint if the character is at a safe height above the floor,
//or if the next waypoint in the path is also on ladders
if ((heightFromFloor > 0.0f && heightFromFloor < collider.height * 1.5f) ||
(currentPath.NextNode != null && currentPath.NextNode.Ladders != null))
bool aboveFloor = heightFromFloor > 0.0f && heightFromFloor < collider.height * 1.5f;
if (aboveFloor || IsNextNodeLadder)
{
if (currentPath.NextNode != null && currentPath.NextNode.Ladders == null)
if (!IsNextLadderSameAsCurrent)
{
character.AnimController.Anim = AnimController.Animation.None;
}
currentPath.SkipToNextNode();
}
}
else
else if (IsNextLadderSameAsCurrent)
{
//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))
if (Math.Sign(currentPath.CurrentNode.WorldPosition.Y - character.WorldPosition.Y) != Math.Sign(currentPath.NextNode.WorldPosition.Y - character.WorldPosition.Y))
{
currentPath.SkipToNextNode();
}
@@ -222,9 +221,11 @@ namespace Barotrauma
else
{
Vector2 colliderBottom = character.AnimController.GetColliderBottom();
if (Math.Abs(collider.SimPosition.X - currentPath.CurrentNode.SimPosition.X) < collider.radius * 2 &&
currentPath.CurrentNode.SimPosition.Y > colliderBottom.Y &&
currentPath.CurrentNode.SimPosition.Y < colliderBottom.Y + 1.5f)
float horizontalDistance = Math.Abs(collider.SimPosition.X - currentPath.CurrentNode.SimPosition.X);
bool isAboveVerticalPosition = currentPath.CurrentNode.SimPosition.Y > colliderBottom.Y;
bool isNotTooHigh = currentPath.CurrentNode.SimPosition.Y < colliderBottom.Y + 1.5f;
if (horizontalDistance < collider.radius * 2 && isAboveVerticalPosition && isNotTooHigh)
{
currentPath.SkipToNextNode();
}
@@ -180,8 +180,8 @@ namespace Barotrauma
{
//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));
enemyAI.SteeringManager.SteeringAvoid(deltaTime, 1.0f, 0.1f);
enemyAI.SteeringManager.SteeringSeek(wallAttachPos);
}
}
break;
@@ -1,20 +1,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Barotrauma.Extensions;
namespace Barotrauma
{
abstract class AIObjective
{
protected readonly List<AIObjective> subObjectives;
public virtual float Devotion => AIObjectiveManager.baseDevotion;
public abstract string DebugTag { get; }
public virtual bool ForceRun => false;
public virtual bool KeepDivingGearOn => false;
protected readonly List<AIObjective> subObjectives = new List<AIObjective>();
protected float priority;
protected readonly Character character;
protected string option;
protected bool abandon;
public virtual bool CanBeCompleted
{
get { return true; }
}
public virtual bool CanBeCompleted => !abandon && subObjectives.All(so => so.CanBeCompleted);
public IEnumerable<AIObjective> SubObjectives => subObjectives;
public AIObjective CurrentSubObjective { get; private set; }
protected HumanAIController HumanAIController => character.AIController as HumanAIController;
protected IndoorsSteeringManager PathSteering => HumanAIController.PathSteering;
public string Option
{
@@ -23,7 +34,6 @@ namespace Barotrauma
public AIObjective(Character character, string option)
{
subObjectives = new List<AIObjective>();
this.character = character;
this.option = option;
@@ -38,7 +48,36 @@ namespace Barotrauma
/// </summary>
public void TryComplete(float deltaTime)
{
subObjectives.RemoveAll(s => s.IsCompleted() || !s.CanBeCompleted || ShouldInterruptSubObjective(s));
for (int i = 0; i < subObjectives.Count; i++)
{
var subObjective = subObjectives[i];
if (subObjective.IsCompleted())
{
#if DEBUG
DebugConsole.NewMessage($"Removing subobjective {subObjective.DebugTag} of {DebugTag}, because it is completed.");
#endif
subObjectives.Remove(subObjective);
}
else if (!subObjective.CanBeCompleted)
{
#if DEBUG
DebugConsole.NewMessage($"Removing subobjective {subObjective.DebugTag} of {DebugTag}, because it cannot be completed.");
#endif
subObjectives.Remove(subObjective);
}
else if (subObjective.ShouldInterruptSubObjective(subObjective))
{
#if DEBUG
DebugConsole.NewMessage($"Removing subobjective {subObjective.DebugTag} of {DebugTag}, because it is interrupted.");
#endif
subObjectives.Remove(subObjective);
}
}
if (!subObjectives.Contains(CurrentSubObjective))
{
CurrentSubObjective = null;
}
foreach (AIObjective objective in subObjectives)
{
@@ -56,32 +95,53 @@ namespace Barotrauma
subObjectives.Add(objective);
}
public AIObjective GetCurrentSubObjective()
{
AIObjective currentSubObjective = this;
while (currentSubObjective.subObjectives.Count > 0)
{
currentSubObjective = subObjectives[0];
}
return currentSubObjective;
}
public void SortSubObjectives(AIObjectiveManager objectiveManager)
{
if (!subObjectives.Any()) return;
if (subObjectives.None()) { return; }
subObjectives.Sort((x, y) => y.GetPriority(objectiveManager).CompareTo(x.GetPriority(objectiveManager)));
subObjectives[0].SortSubObjectives(objectiveManager);
CurrentSubObjective = SubObjectives.First();
CurrentSubObjective.SortSubObjectives(objectiveManager);
}
protected virtual bool ShouldInterruptSubObjective(AIObjective subObjective)
public virtual float GetPriority(AIObjectiveManager objectiveManager) => priority;
public virtual void Update(AIObjectiveManager objectiveManager, float deltaTime)
{
return false;
var subObjective = objectiveManager.CurrentObjective?.CurrentSubObjective;
if (objectiveManager.CurrentOrder == this)
{
priority = AIObjectiveManager.OrderPriority;
}
else if (objectiveManager.CurrentObjective == this || subObjective == this)
{
priority += Devotion * deltaTime;
}
priority = MathHelper.Clamp(priority, 0, 100);
subObjectives.ForEach(so => so.Update(objectiveManager, deltaTime));
}
/// <summary>
/// Checks if the subobjectives in the given collection are removed from the subobjectives. And if so, removes it also from the dictionary.
/// </summary>
protected void SyncRemovedObjectives<T1, T2>(Dictionary<T1, T2> dictionary, IEnumerable<T1> collection) where T2 : AIObjective
{
foreach (T1 key in collection)
{
if (dictionary.TryGetValue(key, out T2 objective))
{
if (!subObjectives.Contains(objective))
{
dictionary.Remove(key);
}
}
}
}
protected virtual bool ShouldInterruptSubObjective(AIObjective subObjective) => false;
protected abstract void Act(float deltaTime);
public abstract bool IsCompleted();
public abstract float GetPriority(AIObjectiveManager objectiveManager);
public abstract bool IsDuplicate(AIObjective otherObjective);
}
}
@@ -1,66 +1,58 @@
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
class AIObjectiveChargeBatteries : AIObjective
class AIObjectiveChargeBatteries : AIObjectiveLoop<PowerContainer>
{
private List<PowerContainer> availableBatteries;
public override string DebugTag => "charge batteries";
private readonly IEnumerable<PowerContainer> batteryList;
private string orderOption;
public AIObjectiveChargeBatteries(Character character, string option)
: base(character, option)
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;
batteryList = Item.ItemList.Select(i => i.GetComponent<PowerContainer>()).Where(b => b != null);
}
public override bool IsDuplicate(AIObjective otherObjective)
{
return otherObjective is AIObjectiveChargeBatteries other && other.orderOption == orderOption;
return otherObjective is AIObjectiveChargeBatteries other && other.Option == Option;
}
protected override void Act(float deltaTime)
protected override void FindTargets()
{
if (availableBatteries.Count == 0)
foreach (Item item in Item.ItemList)
{
AddSubObjective(new AIObjectiveIdle(character));
return;
if (item.Prefab.Identifier != "battery" && !item.HasTag("battery")) { continue; }
if (item.Submarine == null) { continue; }
if (item.Submarine.TeamID != character.TeamID) { continue; }
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(item, true)) { continue; }
var battery = item.GetComponent<PowerContainer>();
if (battery != null)
{
if (!ignoreList.Contains(battery))
{
if (!targets.Contains(battery))
{
targets.Add(battery);
}
}
}
}
foreach (PowerContainer battery in availableBatteries)
if (targets.None())
{
AddSubObjective(new AIObjectiveOperateItem(battery, character, orderOption, false));
character.Speak(TextManager.Get("DialogNoBatteries"), null, 4.0f, "nobatteries", 10.0f);
}
else
{
targets.Sort((x, y) => x.ChargePercentage.CompareTo(y.ChargePercentage));
}
}
protected override bool Filter(PowerContainer battery) => true;
protected override float Average(PowerContainer battery) => 100 - battery.ChargePercentage;
protected override IEnumerable<PowerContainer> GetList() => batteryList;
protected override AIObjective ObjectiveConstructor(PowerContainer battery) => new AIObjectiveOperateItem(battery, character, Option, false);
}
}
@@ -8,6 +8,10 @@ namespace Barotrauma
{
class AIObjectiveCombat : AIObjective
{
public override string DebugTag => "combat";
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
const float CoolDown = 10.0f;
//the largest amount of damage the enemy has inflicted on this character
@@ -117,6 +121,7 @@ namespace Barotrauma
private void Escape(float deltaTime)
{
// TODO: just let the find safety run?
if (escapeObjective == null)
{
escapeObjective = new AIObjectiveFindSafety(character);
@@ -7,6 +7,8 @@ namespace Barotrauma
{
class AIObjectiveContainItem: AIObjective
{
public override string DebugTag => "contain item";
public int MinContainedAmount = 1;
//can either be a tag or an identifier
@@ -6,6 +6,8 @@ namespace Barotrauma
{
class AIObjectiveDecontainItem : AIObjective
{
public override string DebugTag => "decontain item";
//can either be a tag or an identifier
private string[] itemIdentifiers;
@@ -8,6 +8,10 @@ namespace Barotrauma
{
class AIObjectiveExtinguishFire : AIObjective
{
public override string DebugTag => "extinguish fire";
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
private Hull targetHull;
private AIObjectiveGetItem getExtinguisherObjective;
@@ -16,15 +20,19 @@ namespace Barotrauma
private float useExtinquisherTimer;
public AIObjectiveExtinguishFire(Character character, Hull targetHull) :
base(character, "")
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);
if (gotoObjective != null && !gotoObjective.CanBeCompleted) { return 0; }
// Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally)
float dist = Math.Abs(character.WorldPosition.X - targetHull.WorldPosition.X) + Math.Abs(character.WorldPosition.Y - targetHull.WorldPosition.Y) * 2.0f;
float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 10000, dist));
float severityFactor = MathHelper.Lerp(0, 1, MathHelper.Clamp(targetHull.FireSources.Sum(fs => fs.Size.X) / targetHull.Size.X, 0, 1));
return MathHelper.Clamp(priority * severityFactor * distanceFactor, 0, 100);
}
public override bool IsCompleted()
@@ -40,7 +48,7 @@ namespace Barotrauma
public override bool CanBeCompleted
{
get { return getExtinguisherObjective == null || getExtinguisherObjective.CanBeCompleted; }
get { return getExtinguisherObjective == null || getExtinguisherObjective.IsCompleted() || getExtinguisherObjective.CanBeCompleted; }
}
protected override void Act(float deltaTime)
@@ -68,9 +76,11 @@ namespace Barotrauma
return;
}
foreach (FireSource fs in targetHull.FireSources.ToList())
foreach (FireSource fs in targetHull.FireSources)
{
if (fs.IsInDamageRange(character, MathHelper.Clamp(fs.DamageRange * 1.5f, extinguisher.Range * 0.5f, extinguisher.Range)) || useExtinquisherTimer > 0.0f)
// TODO: check if in the same room?
bool inRange = fs.IsInDamageRange(character, MathHelper.Clamp(fs.DamageRange * 1.5f, extinguisher.Range * 0.5f, extinguisher.Range));
if (!character.IsClimbing && (inRange || useExtinquisherTimer > 0.0f))
{
useExtinquisherTimer += deltaTime;
if (useExtinquisherTimer > 2.0f) useExtinquisherTimer = 0.0f;
@@ -86,20 +96,19 @@ namespace Barotrauma
}
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);
//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;
}
break;
}
}
}
@@ -1,32 +1,34 @@
using System.Linq;
using System.Collections.Generic;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
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 string DebugTag => "extinguish fires";
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
private Dictionary<Hull, AIObjectiveExtinguishFire> extinguishObjectives = new Dictionary<Hull, AIObjectiveExtinguishFire>();
public AIObjectiveExtinguishFires(Character character) : base(character, "") { }
public override float GetPriority(AIObjectiveManager objectiveManager)
{
if (objectiveManager.CurrentObjective == this)
if (character.Submarine == null) { return 0; }
int fireCount = character.Submarine.GetHulls(true).Sum(h => h.FireSources.Count);
if (objectiveManager.CurrentOrder == this && fireCount > 0)
{
return AIObjectiveManager.OrderPriority;
}
return Hull.hullList.Count(h => h.FireSources.Count > 0) * 10;
return MathHelper.Clamp(fireCount * 20, 0, 100);
}
public override bool IsCompleted()
{
return !Hull.hullList.Any(h => h.FireSources.Count > 0);
}
public override bool IsCompleted() => false;
public override bool CanBeCompleted => true;
public override bool IsDuplicate(AIObjective otherObjective)
{
@@ -35,13 +37,26 @@ namespace Barotrauma
protected override void Act(float deltaTime)
{
SyncRemovedObjectives(extinguishObjectives, Hull.hullList);
if (character.Submarine == null) { return; }
foreach (Hull hull in Hull.hullList)
{
if (hull.FireSources.Count > 0)
if (hull.FireSources.None()) { continue; }
if (hull.Submarine == null) { continue; }
if (hull.Submarine.TeamID != character.TeamID) { continue; }
// If the character is inside, only take connected hulls into account.
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(hull, true)) { continue; }
if (!extinguishObjectives.TryGetValue(hull, out AIObjectiveExtinguishFire objective))
{
AddSubObjective(new AIObjectiveExtinguishFire(character, hull));
objective = new AIObjectiveExtinguishFire(character, hull);
extinguishObjectives.Add(hull, objective);
AddSubObjective(objective);
}
}
if (extinguishObjectives.None())
{
character?.Speak(TextManager.Get("DialogNoFire"), null, 3.0f, "nofire", 30.0f);
}
}
}
}
@@ -6,6 +6,9 @@ namespace Barotrauma
{
class AIObjectiveFindDivingGear : AIObjective
{
public override string DebugTag => "find diving gear";
public override bool ForceRun => true;
private AIObjective subObjective;
private string gearTag;
@@ -28,6 +31,8 @@ namespace Barotrauma
return false;
}
public override bool CanBeCompleted => subObjective == null || subObjective.CanBeCompleted;
public AIObjectiveFindDivingGear(Character character, bool needDivingSuit)
: base(character, "")
{
@@ -2,100 +2,127 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Items.Components;
namespace Barotrauma
{
class AIObjectiveFindSafety : AIObjective
{
public override string DebugTag => "find safety";
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
// TODO: expose?
const float priorityIncrease = 25;
const float priorityDecrease = 10;
const float SearchHullInterval = 3.0f;
const float MinSafety = 50.0f;
private AIObjectiveGoTo goToObjective;
private List<Hull> unreachable;
private List<Hull> unreachable = new List<Hull>();
private float currenthullSafety;
private float searchHullTimer;
private AIObjectiveGoTo goToObjective;
private AIObjective divingGearObjective;
public float? OverrideCurrentHullSafety;
public AIObjectiveFindSafety(Character character)
: base(character, "")
{
unreachable = new List<Hull>();
}
public AIObjectiveFindSafety(Character character) : base(character, "") { }
public override bool IsCompleted()
{
return false;
}
public override bool IsCompleted() => false;
public override bool CanBeCompleted => true;
protected override void Act(float deltaTime)
{
var currentHull = character.AnimController.CurrentHull;
currenthullSafety = OverrideCurrentHullSafety == null ?
GetHullSafety(currentHull, character) : (float)OverrideCurrentHullSafety;
if (NeedsDivingGear())
var currentHull = character.AnimController.CurrentHull;
if (HumanAIController.NeedsDivingGear(currentHull))
{
if (!FindDivingGear(deltaTime)) return;
bool needsDivingSuit = currentHull == null || currentHull.WaterPercentage > 90;
bool hasEquipment = needsDivingSuit ? HumanAIController.HasDivingSuit(character) : HumanAIController.HasDivingGear(character);
if ((divingGearObjective == null || !divingGearObjective.CanBeCompleted) && !hasEquipment)
{
// If the previous objective cannot be completed, create a new and try again.
divingGearObjective = new AIObjectiveFindDivingGear(character, needsDivingSuit);
}
}
if (divingGearObjective != null)
{
divingGearObjective.TryComplete(deltaTime);
if (divingGearObjective.IsCompleted())
{
divingGearObjective = null;
priority = 0;
}
else if (divingGearObjective.CanBeCompleted)
{
// If diving gear objective is active, wait for it to complete.
return;
}
}
if (searchHullTimer > 0.0f)
{
searchHullTimer -= deltaTime;
}
else
else if (currenthullSafety < HumanAIController.HULL_SAFETY_THRESHOLD)
{
var bestHull = FindBestHull();
if (bestHull != null)
if (bestHull != null && bestHull != currentHull)
{
goToObjective = new AIObjectiveGoTo(bestHull, character)
if (goToObjective != null)
{
AllowGoingOutside = true
};
if (goToObjective.Target != bestHull)
{
goToObjective = new AIObjectiveGoTo(bestHull, character)
{
AllowGoingOutside = true
};
}
}
else
{
goToObjective = new AIObjectiveGoTo(bestHull, character)
{
AllowGoingOutside = true
};
}
}
searchHullTimer = SearchHullInterval;
}
if (goToObjective != null)
{
goToObjective.TryComplete(deltaTime);
if (character.AIController.SteeringManager is IndoorsSteeringManager pathSteering &&
pathSteering.CurrentPath != null &&
pathSteering.CurrentPath.Unreachable && !unreachable.Contains(goToObjective.Target))
if (!goToObjective.CanBeCompleted)
{
unreachable.Add(goToObjective.Target as Hull);
if (!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)
{
//goto objective doesn't exist (a safe hull not found, or a path to a safe hull not found)
// -> attempt to manually steer away from hazards
Vector2 escapeVel = Vector2.Zero;
foreach (FireSource fireSource in currentHull.FireSources)
{
int dir = Math.Sign(character.Position.X - fireSource.Position.X);
Vector2 dir = character.Position - fireSource.Position;
float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(fireSource.Position, character.Position), 0.1f, 10.0f);
escapeVel += new Vector2(dir * distMultiplier, 0.0f);
escapeVel += new Vector2(Math.Sign(dir.X) * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier);
}
foreach (Character enemy in Character.CharacterList)
foreach (Character enemy in Character.CharacterList)
{
if (enemy.CurrentHull == currentHull && !enemy.IsDead && !enemy.IsUnconscious &&
(enemy.AIController is EnemyAIController || enemy.TeamID != character.TeamID))
{
Vector2 dir = character.Position - enemy.Position;
float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(enemy.Position, character.Position), 0.1f, 10.0f);
escapeVel += new Vector2(Math.Sign(character.Position.X - enemy.Position.X) * distMultiplier, 0.0f);
escapeVel += new Vector2(Math.Sign(dir.X) * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier);
}
}
@@ -120,40 +147,70 @@ namespace Barotrauma
}
}
private bool FindDivingGear(float deltaTime)
{
if (divingGearObjective == null)
{
divingGearObjective = new AIObjectiveFindDivingGear(character, false);
}
if (divingGearObjective.IsCompleted()) return true;
divingGearObjective.TryComplete(deltaTime);
return divingGearObjective.IsCompleted();
}
private Hull FindBestHull()
{
Hull bestHull = null;
Hull bestHull = character.CurrentHull;
float bestValue = currenthullSafety;
foreach (Hull hull in Hull.hullList)
{
if (hull == character.AnimController.CurrentHull || unreachable.Contains(hull)) continue;
if (unreachable.Contains(hull)) { continue; }
if (hull.Submarine == null) { continue; }
float hullSafety = 0;
if (character.Submarine == null)
{
// Outside
if (hull.RoomName?.ToLowerInvariant() == "airlock")
{
hullSafety = 100;
}
else
{
// TODO: could also target gaps that get us inside?
foreach (Item item in Item.ItemList)
{
if (item.CurrentHull == hull && item.HasTag("airlock"))
{
hullSafety = 100;
break;
}
}
}
float hullValue = GetHullSafety(hull, character);
//slight preference over hulls that are closer
hullValue -= (float)Math.Sqrt(Math.Abs(character.Position.X - hull.Position.X)) * 0.1f;
hullValue -= (float)Math.Sqrt(Math.Abs(character.Position.Y - hull.Position.Y)) * 0.2f;
if (bestHull == null || hullValue > bestValue)
// Huge preference for closer targets
float distanceFactor = MathHelper.Lerp(1, 0.2f, MathUtils.InverseLerp(0, 100000, Vector2.Distance(character.WorldPosition, hull.WorldPosition)));
hullSafety *= distanceFactor;
// If the target is not inside a friendly submarine, considerably reduce the hull safety.
if (hull.Submarine.TeamID != character.TeamID && hull.Submarine.TeamID != Character.TeamType.FriendlyNPC)
{
hullSafety /= 10;
}
}
else
{
// Inside
// Not connected.
if (!character.Submarine.GetConnectedSubs().Contains(hull.Submarine)) { continue; }
hullSafety = HumanAIController.GetHullSafety(hull, character);
var path = PathSteering.PathFinder.FindPath(character.SimPosition, hull.SimPosition);
int unsafeNodes = path.Nodes.Count(n => n.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(n.CurrentHull));
// Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally)
float dist = Math.Abs(character.WorldPosition.X - hull.WorldPosition.X) + Math.Abs(character.WorldPosition.Y - hull.WorldPosition.Y) * 2.0f;
float distanceFactor = MathHelper.Lerp(1, 0.9f, MathUtils.InverseLerp(0, 10000, dist));
hullSafety *= distanceFactor;
// Each unsafe node reduces the hull safety value.
hullSafety /= 1 + unsafeNodes;
// If the target is not inside a friendly submarine, considerably reduce the hull safety.
if (!character.Submarine.IsEntityFoundOnThisSub(hull, true))
{
hullSafety /= 10;
}
}
if (hullSafety > bestValue)
{
bestHull = hull;
bestValue = hullValue;
bestValue = hullSafety;
}
}
return bestHull;
}
@@ -162,113 +219,30 @@ namespace Barotrauma
return (otherObjective is AIObjectiveFindSafety);
}
private bool NeedsDivingGear()
public override void Update(AIObjectiveManager objectiveManager, float deltaTime)
{
var currentHull = character.AnimController.CurrentHull;
if (currentHull == null) return true;
//there's lots of water in the room -> get a suit
if (currentHull.WaterVolume / currentHull.Volume > 0.5f) return true;
if (currentHull.OxygenPercentage < 30.0f) return true;
return false;
}
public override float GetPriority(AIObjectiveManager objectiveManager)
{
if (character.Oxygen < 80.0f)
if (character.CurrentHull == null)
{
return 150.0f - character.Oxygen;
currenthullSafety = 0;
priority = 5;
return;
}
if (character.CurrentHull == null) return 5.0f;
currenthullSafety = GetHullSafety(character.CurrentHull, character);
priority = 100.0f - currenthullSafety;
//var nearbyHulls = character.CurrentHull.GetConnectedHulls(3);
//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)
if (character.OxygenAvailable < CharacterHealth.LowOxygenThreshold) { priority = 100; }
currenthullSafety = OverrideCurrentHullSafety ?? HumanAIController.GetHullSafety(character.CurrentHull);
if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD)
{
priority += 5.0f;
}
/*foreach (Hull hull in nearbyHulls)
{
foreach (FireSource fireSource in hull.FireSources)
{
//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())
{
if (divingGearObjective != null && !divingGearObjective.IsCompleted()) priority += 20.0f;
}
return priority;
}
public static float GetHullSafety(Hull hull, Character character)
{
if (hull == null) return 0.0f;
float safety = 100.0f;
float waterPercentage = (hull.WaterVolume / hull.Volume) * 100.0f;
if (hull.LethalPressure > 0.0f && character.PressureProtection <= 0.0f)
{
safety -= 100.0f;
}
else if (character.OxygenAvailable <= 0.0f)
{
safety -= waterPercentage;
priority -= priorityDecrease * deltaTime;
}
else
{
safety -= waterPercentage * 0.1f;
float dangerFactor = (100 - currenthullSafety) / 100;
priority += dangerFactor * priorityIncrease * deltaTime;
}
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)
priority = MathHelper.Clamp(priority, 0, 100);
if (divingGearObjective != null && !divingGearObjective.IsCompleted() && divingGearObjective.CanBeCompleted)
{
foreach (FireSource fireSource in hull2.FireSources)
{
//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, AIObjectiveManager.OrderPriority + 1.0f);
}
}
priority = Math.Max(priority, AIObjectiveManager.OrderPriority + 10);
}
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,17 +8,22 @@ namespace Barotrauma
{
class AIObjectiveFixLeak : AIObjective
{
public override string DebugTag => "fix leak";
public override bool KeepDivingGearOn => true;
private readonly Gap leak;
private bool pathUnreachable;
private AIObjectiveFindDivingGear findDivingGear;
private AIObjectiveGoTo gotoObjective;
private AIObjectiveOperateItem operateObjective;
public Gap Leak
{
get { return leak; }
}
public AIObjectiveFixLeak(Gap leak, Character character)
: base (character, "")
public AIObjectiveFixLeak(Gap leak, Character character) : base (character, "")
{
this.leak = leak;
}
@@ -28,10 +33,11 @@ namespace Barotrauma
return leak.Open <= 0.0f || leak.Removed || pathUnreachable;
}
public override bool CanBeCompleted => !abandon && base.CanBeCompleted;
public override float GetPriority(AIObjectiveManager objectiveManager)
{
if (leak.Open == 0.0f) { return 0.0f; }
if (pathUnreachable) { return 0.0f; }
float leakSize = (leak.IsHorizontal ? leak.Rect.Height : leak.Rect.Width) * Math.Max(leak.Open, 0.1f);
@@ -49,6 +55,20 @@ namespace Barotrauma
protected override void Act(float deltaTime)
{
if (!leak.IsRoomToRoom)
{
if (findDivingGear == null)
{
findDivingGear = new AIObjectiveFindDivingGear(character, true);
AddSubObjective(findDivingGear);
}
else if (!findDivingGear.CanBeCompleted)
{
abandon = true;
return;
}
}
var weldingTool = character.Inventory.FindItemByTag("weldingtool");
if (weldingTool == null)
@@ -72,31 +92,48 @@ namespace Barotrauma
var repairTool = weldingTool.GetComponent<RepairTool>();
if (repairTool == null) { return; }
Vector2 standPosition = GetStandPosition();
Vector2 gapDiff = leak.WorldPosition - character.WorldPosition;
var humanoidController = character.AnimController as HumanoidAnimController;
if (!character.AnimController.InWater && character.AnimController is HumanoidAnimController &&
Math.Abs(gapDiff.X) < 100.0f && gapDiff.Y < 0.0f && gapDiff.Y > -150.0f)
// TODO: use the collider size/reach?
if (!character.AnimController.InWater && humanoidController != null && Math.Abs(gapDiff.X) < 100 && gapDiff.Y < 0.0f && gapDiff.Y > -150)
{
((HumanoidAnimController)character.AnimController).Crouching = true;
}
if (Math.Abs(gapDiff.X) > 100.0f || Math.Abs(gapDiff.Y) > 150.0f)
float armLength = humanoidController != null ? ConvertUnits.ToDisplayUnits(humanoidController.ArmLength) : 100;
bool cannotReach = gapDiff.Length() > armLength + repairTool.Range;
if (cannotReach)
{
var gotoObjective = new AIObjectiveGoTo(ConvertUnits.ToSimUnits(standPosition), character);
if (!gotoObjective.IsCompleted())
if (gotoObjective != null)
{
pathUnreachable = !gotoObjective.CanBeCompleted;
if (!pathUnreachable)
// Check if the objective is already removed -> completed/impossible
if (!subObjectives.Contains(gotoObjective))
{
gotoObjective = null;
}
}
else
{
gotoObjective = new AIObjectiveGoTo(ConvertUnits.ToSimUnits(GetStandPosition()), character);
if (!subObjectives.Contains(gotoObjective))
{
AddSubObjective(gotoObjective);
}
return;
}
}
AddSubObjective(new AIObjectiveOperateItem(repairTool, character, "", true, leak));
if (gotoObjective == null || gotoObjective.IsCompleted())
{
if (operateObjective == null)
{
operateObjective = new AIObjectiveOperateItem(repairTool, character, "", true, leak);
AddSubObjective(operateObjective);
}
else if (!subObjectives.Contains(operateObjective))
{
operateObjective = null;
}
}
}
private Vector2 GetStandPosition()
@@ -1,142 +1,45 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
using System.Collections.Generic;
namespace Barotrauma
{
class AIObjectiveFixLeaks : AIObjective
class AIObjectiveFixLeaks : AIObjectiveLoop<Gap>
{
const float UpdateGapListInterval = 5.0f;
public override string DebugTag => "fix leaks";
public override bool KeepDivingGearOn => true;
private double lastGapUpdate;
private AIObjectiveIdle idleObjective;
private AIObjectiveFindDivingGear findDivingGear;
private List<AIObjectiveFixLeak> objectiveList;
public AIObjectiveFixLeaks(Character character)
: base (character, "")
{
}
public override bool IsCompleted()
{
if (Timing.TotalTime > lastGapUpdate + UpdateGapListInterval || objectiveList == null)
{
UpdateGapList();
lastGapUpdate = Timing.TotalTime;
}
return objectiveList.Count == 0;
}
public AIObjectiveFixLeaks(Character character) : base (character, "") { }
public override float GetPriority(AIObjectiveManager objectiveManager)
{
if (Timing.TotalTime > lastGapUpdate + UpdateGapListInterval || objectiveList == null)
if (character.Submarine == null) { return 0; }
if (targets.None()) { return 0; }
if (objectiveManager.CurrentOrder == this)
{
UpdateGapList();
lastGapUpdate = Timing.TotalTime;
return AIObjectiveManager.OrderPriority;
}
float priority = 0.0f;
foreach (AIObjectiveFixLeak fixObjective in objectiveList)
{
//gaps from outside to inside significantly increase the priority
if (!fixObjective.Leak.IsRoomToRoom)
{
priority = Math.Max(priority + fixObjective.Leak.Open * 100.0f, 50.0f);
}
else
{
priority += fixObjective.Leak.Open * 10.0f;
}
if (priority >= 100.0f) break;
}
return Math.Min(priority, 100.0f);
return MathHelper.Lerp(0, AIObjectiveManager.OrderPriority, targets.Average(t => Average(t)));
}
protected override void Act(float deltaTime)
protected override void FindTargets()
{
if (Timing.TotalTime > lastGapUpdate + UpdateGapListInterval || objectiveList == null)
{
UpdateGapList();
lastGapUpdate = Timing.TotalTime;
}
if (objectiveList.Any())
{
if (!objectiveList[objectiveList.Count - 1].Leak.IsRoomToRoom)
{
if (findDivingGear == null) findDivingGear = new AIObjectiveFindDivingGear(character, true);
if (!findDivingGear.IsCompleted() && findDivingGear.CanBeCompleted)
{
findDivingGear.TryComplete(deltaTime);
return;
}
}
objectiveList[objectiveList.Count - 1].TryComplete(deltaTime);
if (!objectiveList[objectiveList.Count - 1].CanBeCompleted ||
objectiveList[objectiveList.Count - 1].IsCompleted())
{
objectiveList.RemoveAt(objectiveList.Count - 1);
}
}
else
{
if (idleObjective == null) idleObjective = new AIObjectiveIdle(character);
idleObjective.TryComplete(deltaTime);
}
base.FindTargets();
targets.Sort((x, y) => GetGapFixPriority(y).CompareTo(GetGapFixPriority(x)));
}
private void UpdateGapList()
protected override bool Filter(Gap gap)
{
if (objectiveList == null) { objectiveList = new List<AIObjectiveFixLeak>(); }
objectiveList.Clear();
foreach (Gap gap in Gap.GapList)
bool ignore = ignoreList.Contains(gap) || gap.ConnectedWall == null || gap.ConnectedDoor != null || gap.Open <= 0 || gap.linkedTo.All(l => l == null);
if (!ignore)
{
if (gap.ConnectedWall == null) { continue; }
if (gap.ConnectedDoor != null || gap.Open <= 0.0f) { continue; }
//not linked to a hull -> ignore
if (gap.linkedTo.All(l => l == null)) { continue; }
if (character.TeamID == Character.TeamType.None)
{
//TODO: make sure the gap isn't in another sub (outpost, respawn shuttle...?)
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 = Submarine.MainSub;
if (character.TeamID == Character.TeamType.Team2 && Submarine.MainSubs.Length > 1)
{
mySub = Submarine.MainSubs[1];
}
if (gap.Submarine != mySub) continue;
}
float gapPriority = GetGapFixPriority(gap);
int index = 0;
while (index < objectiveList.Count &&
GetGapFixPriority(objectiveList[index].Leak) < gapPriority)
{
index++;
}
objectiveList.Insert(index, new AIObjectiveFixLeak(gap, character));
if (gap.Submarine == null) { ignore = true; }
else if (gap.Submarine.TeamID != character.TeamID) { ignore = true; }
else if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(gap, true)) { ignore = true; }
}
return ignore;
}
private float GetGapFixPriority(Gap gap)
@@ -156,9 +59,9 @@ namespace Barotrauma
}
public override bool IsDuplicate(AIObjective otherObjective)
{
return otherObjective is AIObjectiveFixLeaks;
}
public override bool IsDuplicate(AIObjective otherObjective) => otherObjective is AIObjectiveFixLeaks;
protected override float Average(Gap gap) => gap.Open;
protected override IEnumerable<Gap> GetList() => Gap.GapList;
protected override AIObjective ObjectiveConstructor(Gap gap) => new AIObjectiveFixLeak(gap, character);
}
}
@@ -8,6 +8,8 @@ namespace Barotrauma
{
class AIObjectiveGetItem : AIObjective
{
public override string DebugTag => "get item";
public Func<Item, float> GetItemPriority;
//can be either tags or identifiers
@@ -17,8 +19,6 @@ namespace Barotrauma
private int currSearchIndex;
private bool canBeCompleted;
public bool IgnoreContainedItems;
private AIObjectiveGoTo goToObjective;
@@ -27,10 +27,8 @@ namespace Barotrauma
private bool equip;
public override bool CanBeCompleted
{
get { return canBeCompleted; }
}
private bool canBeCompleted = true;
public override bool CanBeCompleted => canBeCompleted;
public override float GetPriority(AIObjectiveManager objectiveManager)
{
@@ -45,7 +43,6 @@ namespace Barotrauma
public AIObjectiveGetItem(Character character, Item targetItem, bool equip = false)
: base(character, "")
{
canBeCompleted = true;
currSearchIndex = -1;
this.equip = equip;
this.targetItem = targetItem;
@@ -59,7 +56,6 @@ namespace Barotrauma
public AIObjectiveGetItem(Character character, string[] itemIdentifiers, bool equip = false)
: base(character, "")
{
canBeCompleted = true;
currSearchIndex = -1;
this.equip = equip;
this.itemIdentifiers = itemIdentifiers;
@@ -112,6 +108,7 @@ namespace Barotrauma
FindTargetItem();
if (targetItem == null || moveToTarget == null)
{
// TODO: cannot be completed?
character?.AIController?.SteeringManager?.Reset();
return;
}
@@ -184,12 +181,16 @@ namespace Barotrauma
{
if (itemIdentifiers == null)
{
if (targetItem == null) canBeCompleted = false;
if (targetItem == null)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot find the item, because neither identifiers nor item is was defined.");
#endif
canBeCompleted = false;
}
return;
}
float currDist = moveToTarget == null ? 0.0f : Vector2.DistanceSquared(moveToTarget.Position, character.Position);
for (int i = 0; i < 10 && currSearchIndex < Item.ItemList.Count - 1; i++)
{
currSearchIndex++;
@@ -234,7 +235,13 @@ namespace Barotrauma
}
//if searched through all the items and a target wasn't found, can't be completed
if (currSearchIndex >= Item.ItemList.Count - 1 && targetItem == null) canBeCompleted = false;
if (currSearchIndex >= Item.ItemList.Count - 1 && targetItem == null)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot find the item with the following identifier(s): {string.Join(", ", itemIdentifiers)}");
#endif
canBeCompleted = false;
}
}
public override bool IsDuplicate(AIObjective otherObjective)
@@ -1,5 +1,4 @@
using Barotrauma.Items.Components;
using FarseerPhysics;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
@@ -7,9 +6,14 @@ namespace Barotrauma
{
class AIObjectiveGoTo : AIObjective
{
public override string DebugTag => "go to";
private AIObjectiveFindDivingGear findDivingGear;
private Vector2 targetPos;
private bool repeat;
private bool cannotReach;
//how long until the path to the target is declared unreachable
private float waitUntilPathUnreachable;
@@ -40,24 +44,36 @@ namespace Barotrauma
{
get
{
if (FollowControlledCharacter && Character.Controlled == null) { return false; }
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 (!AllowGoingOutside && pathSteering.CurrentPath.HasOutdoorsNodes) { return false; }
return !pathSteering.CurrentPath.Unreachable;
bool canComplete = !cannotReach && !abandon;
if (FollowControlledCharacter && Character.Controlled == null) { canComplete = false; }
else if (Target != null && Target.Removed) { canComplete = false; }
else if (repeat || waitUntilPathUnreachable > 0.0f) { canComplete = true; }
else if (character.AIController.SteeringManager is IndoorsSteeringManager pathSteering)
{
//path doesn't exist (= hasn't been searched for yet), assume for now that the target is reachable TODO: add a timer?
if (pathSteering.CurrentPath == null) { canComplete = true; }
else if (!AllowGoingOutside && pathSteering.CurrentPath.HasOutdoorsNodes) { canComplete = false; }
if (canComplete)
{
canComplete = !pathSteering.CurrentPath.Unreachable;
}
}
if (!canComplete)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot reach the target.");
#endif
character.Speak(TextManager.Get("DialogCannotReach"), identifier: "cannotreach", minDurationBetweenSimilar: 10.0f);
character.AIController.SteeringManager.Reset();
}
return canComplete;
}
}
public Entity Target { get; private set; }
public Vector2 TargetPos => Target != null ? Target.SimPosition : targetPos;
public bool FollowControlledCharacter;
public AIObjectiveGoTo(Entity target, Character character, bool repeat = false, bool getDivingGearIfNeeded = true)
@@ -97,7 +113,7 @@ namespace Barotrauma
waitUntilPathUnreachable -= deltaTime;
if (character.SelectedConstruction != null && character.SelectedConstruction.GetComponent<Ladder>() == null)
if (!character.IsClimbing)
{
character.SelectedConstruction = null;
}
@@ -128,48 +144,32 @@ namespace Barotrauma
}
else
{
if (!AllowGoingOutside)
var indoorSteering = character.AIController.SteeringManager as IndoorsSteeringManager;
bool targetIsOutside = (Target != null && Target.Submarine == null) || (indoorSteering != null && indoorSteering.CurrentPath != null && indoorSteering.CurrentPath.HasOutdoorsNodes);
if (targetIsOutside && !AllowGoingOutside)
{
//if path is up-to-date and contains outdoors nodes, this path is unreachable
var pathSteering = character.AIController.SteeringManager as IndoorsSteeringManager;
if (pathSteering?.CurrentPath != null &&
Vector2.Distance(pathSteering.CurrentTarget, currTargetPos) < 1.0f &&
pathSteering.CurrentPath.HasOutdoorsNodes)
{
waitUntilPathUnreachable = 0.0f;
character.AIController.SteeringManager.Reset();
return;
}
cannotReach = true;
}
float normalSpeed = character.AnimController.GetCurrentSpeed(false);
character.AIController.SteeringManager.SteeringSeek(currTargetPos, normalSpeed);
if (getDivingGearIfNeeded && Target?.Submarine == null && AllowGoingOutside)
else
{
if (indoorsSteering.CurrentPath == null || indoorsSteering.CurrentPath.Unreachable)
character.AIController.SteeringManager.SteeringSeek(currTargetPos);
if (getDivingGearIfNeeded)
{
indoorsSteering.SteeringWander(normalSpeed);
}
else if (AllowGoingOutside &&
getDivingGearIfNeeded &&
indoorsSteering.CurrentPath != null &&
indoorsSteering.CurrentPath.HasOutdoorsNodes)
{
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));
if (targetIsOutside ||
Target is Hull h && HumanAIController.NeedsDivingGear(h) ||
Target is Item i && HumanAIController.NeedsDivingGear(i.CurrentHull) ||
Target is Character c && HumanAIController.NeedsDivingGear(c.CurrentHull))
{
if (findDivingGear == null)
{
findDivingGear = new AIObjectiveFindDivingGear(character, true);
AddSubObjective(findDivingGear);
}
else if (!findDivingGear.CanBeCompleted)
{
abandon = true;
}
}
}
}
}
@@ -1,5 +1,4 @@
using Barotrauma.Items.Components;
using FarseerPhysics;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
@@ -9,26 +8,24 @@ namespace Barotrauma
{
class AIObjectiveIdle : AIObjective
{
public override string DebugTag => "idle";
const float WallAvoidDistance = 150.0f;
private AITarget currentTarget;
private Hull 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()
{
return false;
}
public override bool IsCompleted() => false;
public override bool CanBeCompleted => true;
public override float GetPriority(AIObjectiveManager objectiveManager)
{
@@ -37,64 +34,59 @@ namespace Barotrauma
protected override void Act(float deltaTime)
{
var pathSteering = character.AIController.SteeringManager as IndoorsSteeringManager;
if (pathSteering == null) return;
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)
if (!character.IsClimbing)
{
character.SelectedConstruction = null;
}
if (character.AnimController.InWater)
{
//attempt to find a safer place if in water
if (findSafety == null) findSafety = new AIObjectiveFindSafety(character);
findSafety.TryComplete(deltaTime);
return;
}
if (currentTarget == null && (IsForbidden(character.CurrentHull) || HumanAIController.UnsafeHulls.Contains(character.CurrentHull)))
{
newTargetTimer = 0;
standStillTimer = 0;
}
if (character.AnimController.InWater || character.IsClimbing)
{
standStillTimer = 0;
}
if (newTargetTimer <= 0.0f)
{
currentTarget = FindRandomTarget();
currentTarget = FindRandomHull();
if (currentTarget != null)
{
Vector2 pos = character.SimPosition;
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);
string errorMsg = null;
#if DEBUG
bool isRoomNameFound = currentTarget.RoomName != null;
errorMsg = "(Character " + character.Name + " idling, target " + (isRoomNameFound ? currentTarget.RoomName : currentTarget.ToString()) + ")";
#endif
var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, errorMsg);
PathSteering.SetPath(path);
}
newTargetTimer = currentTarget == null ? 5.0f : 15.0f;
}
newTargetTimer -= deltaTime;
//wander randomly
// - if reached the end of the path
// - if the target is unreachable
// - if the path requires going outside
if (pathSteering == null || (pathSteering.CurrentPath != null &&
(pathSteering.CurrentPath.NextNode == null || pathSteering.CurrentPath.Unreachable || pathSteering.CurrentPath.HasOutdoorsNodes)))
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();
PathSteering.Reset();
return;
}
@@ -104,7 +96,7 @@ namespace Barotrauma
}
//steer away from edges of the hull
if (character.AnimController.CurrentHull != null)
if (character.AnimController.CurrentHull != null && !character.IsClimbing)
{
float leftDist = character.Position.X - character.AnimController.CurrentHull.Rect.X;
float rightDist = character.AnimController.CurrentHull.Rect.Right - character.Position.X;
@@ -113,95 +105,119 @@ namespace Barotrauma
{
if (Math.Abs(rightDist - leftDist) > WallAvoidDistance / 2)
{
pathSteering.SteeringManual(deltaTime, Vector2.UnitX * Math.Sign(rightDist - leftDist));
PathSteering.SteeringManual(deltaTime, Vector2.UnitX * Math.Sign(rightDist - leftDist));
}
else
{
pathSteering.Reset();
PathSteering.Reset();
return;
}
}
else if (leftDist < WallAvoidDistance)
{
pathSteering.SteeringManual(deltaTime, Vector2.UnitX * (WallAvoidDistance-leftDist)/WallAvoidDistance);
pathSteering.WanderAngle = 0.0f;
PathSteering.SteeringManual(deltaTime, Vector2.UnitX * (WallAvoidDistance-leftDist) / WallAvoidDistance);
PathSteering.WanderAngle = 0.0f;
return;
}
else if (rightDist < WallAvoidDistance)
{
pathSteering.SteeringManual(deltaTime, -Vector2.UnitX * (WallAvoidDistance-rightDist)/WallAvoidDistance);
pathSteering.WanderAngle = MathHelper.Pi;
PathSteering.SteeringManual(deltaTime, -Vector2.UnitX * (WallAvoidDistance-rightDist) / WallAvoidDistance);
PathSteering.WanderAngle = MathHelper.Pi;
return;
}
}
character.AIController.SteeringManager.SteeringWander(character.AnimController.GetCurrentSpeed(false));
//reset vertical steering to prevent dropping down from platforms etc
character.AIController.SteeringManager.ResetY();
character.AIController.SteeringManager.SteeringWander();
if (!character.IsClimbing)
{
//reset vertical steering to prevent dropping down from platforms etc
character.AIController.SteeringManager.ResetY();
}
return;
}
if (currentTarget?.Entity == null) return;
if (currentTarget.Entity.Removed)
if (currentTarget != null)
{
currentTarget = null;
return;
character.AIController.SteeringManager.SteeringSeek(currentTarget.SimPosition);
}
character.AIController.SteeringManager.SteeringSeek(currentTarget.SimPosition, character.AnimController.GetCurrentSpeed(true));
}
private AITarget FindRandomTarget()
{
//random chance of navigating back to the room where the character spawned
if (Rand.Int(5) == 1)
{
var idCard = character.Inventory.FindItemByIdentifier("idcard");
if (idCard == null) return null;
private readonly List<Hull> targetHulls = new List<Hull>(20);
private readonly List<float> hullWeights = new List<float>(20);
private Hull FindRandomHull()
{
var idCard = character.Inventory.FindItemByIdentifier("idcard");
Hull targetHull = null;
//random chance of navigating back to the room where the character spawned
if (Rand.Int(5) == 1 && idCard != 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;
if (idCard.HasTag(tag))
{
targetHull = wp.CurrentHull;
}
}
}
}
else
if (targetHull == null)
{
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 (character.Submarine != null)
targetHulls.Clear();
hullWeights.Clear();
foreach (var hull in Hull.hullList)
{
targetHulls.RemoveAll(h => h.Submarine != character.Submarine);
}
//remove ballast hulls
foreach (Item item in Item.ItemList)
{
if (item.HasTag("ballast") && targetHulls.Contains(item.CurrentHull))
if (HumanAIController.UnsafeHulls.Contains(hull)) { continue; }
if (hull.Submarine == null) { continue; }
if (hull.Submarine.TeamID != character.TeamID) { continue; }
// If the character is inside, only take connected hulls into account.
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(hull, true)) { continue; }
if (IsForbidden(hull)) { continue; }
// Ignore hulls that are too low to stand inside
if (character.AnimController is HumanoidAnimController animController)
{
targetHulls.Remove(item.CurrentHull);
if (hull.CeilingHeight < ConvertUnits.ToDisplayUnits(animController.HeadPosition.Value))
{
continue;
}
}
// Check that there is no unsafe or forbidden hulls on the way to the target
var path = PathSteering.PathFinder.FindPath(character.SimPosition, hull.SimPosition);
if (path.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull) || IsForbidden(n.CurrentHull))) { continue; }
// If we want to do a steering check, we should do it here, before setting the path
//if (path.Cost > 1000.0f) { continue; }
if (!targetHulls.Contains(hull))
{
targetHulls.Add(hull);
hullWeights.Add(hull.Volume);
}
}
//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 ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced);
}
return targetHull;
}
return null;
private bool IsForbidden(Hull hull)
{
if (hull == null) { return true; }
string hullName = hull.RoomName?.ToLowerInvariant();
bool isForbidden = hullName == "ballast" || hullName == "airlock";
foreach (Item item in Item.ItemList)
{
if (item.CurrentHull == hull && (item.HasTag("ballast") || item.HasTag("airlock")))
{
isForbidden = true;
break;
}
}
return isForbidden;
}
public override bool IsDuplicate(AIObjective otherObjective)
@@ -0,0 +1,122 @@
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
abstract class AIObjectiveLoop<T> : AIObjective
{
protected List<T> targets = new List<T>();
protected Dictionary<T, AIObjective> objectives = new Dictionary<T, AIObjective>();
protected HashSet<T> ignoreList = new HashSet<T>();
protected readonly float ignoreListClearInterval = 30;
private float ignoreListTimer;
protected readonly float targetUpdateInterval = 2;
private float targetUpdateTimer;
public AIObjectiveLoop(Character character, string option) : base(character, option)
{
FindTargets();
CreateObjectives();
}
protected override void Act(float deltaTime) { }
public override bool IsCompleted() => false;
public override bool CanBeCompleted => true;
public override void Update(AIObjectiveManager objectiveManager, float deltaTime)
{
base.Update(objectiveManager, deltaTime);
if (ignoreListTimer > ignoreListClearInterval)
{
ignoreList.Clear();
ignoreListTimer = 0;
UpdateTargets();
}
else
{
ignoreListTimer += deltaTime;
}
if (targetUpdateTimer > targetUpdateInterval)
{
targetUpdateTimer = 0;
UpdateTargets();
}
else
{
targetUpdateTimer += deltaTime;
}
// Sync objectives, subobjectives and targets
foreach (var objective in objectives)
{
var target = objective.Key;
if (!objective.Value.CanBeCompleted)
{
ignoreList.Add(target);
}
if (!targets.Contains(target))
{
subObjectives.Remove(objective.Value);
}
}
SyncRemovedObjectives(objectives, GetList());
if (objectives.None())
{
CreateObjectives();
}
}
public override float GetPriority(AIObjectiveManager objectiveManager)
{
if (character.Submarine == null) { return 0; }
if (targets.None()) { return 0; }
float avg = targets.Average(t => Average(t));
if (objectiveManager.CurrentOrder == this)
{
return AIObjectiveManager.OrderPriority - MathHelper.Max(0, AIObjectiveManager.OrderPriority - avg);
}
return MathHelper.Lerp(0, AIObjectiveManager.OrderPriority, avg / 100);
}
protected void UpdateTargets()
{
targets.Clear();
FindTargets();
CreateObjectives();
}
protected virtual void FindTargets()
{
foreach (T item in GetList())
{
if (Filter(item)) { continue; }
if (!targets.Contains(item))
{
targets.Add(item);
}
}
}
protected virtual void CreateObjectives()
{
foreach (T target in targets)
{
if (!objectives.TryGetValue(target, out AIObjective objective))
{
objective = ObjectiveConstructor(target);
objectives.Add(target, objective);
AddSubObjective(objective);
}
}
}
/// <summary>
/// List of all possible items of the specified type. Used for filtering the removed objectives.
/// </summary>
protected abstract IEnumerable<T> GetList();
protected abstract float Average(T target);
protected abstract AIObjective ObjectiveConstructor(T target);
protected abstract bool Filter(T target);
}
}
@@ -2,47 +2,58 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
class AIObjectiveManager
{
// TODO: expose
public const float OrderPriority = 50.0f;
// Constantly increases the priority of the selected objective, unless overridden
public const float baseDevotion = 2;
private List<AIObjective> objectives;
public List<AIObjective> Objectives { get; private set; }
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; }
}
public AIObjective CurrentObjective
{
get;
private set;
}
public AIObjective CurrentOrder { get; private set; }
public AIObjective CurrentObjective { get; private set; }
public AIObjectiveManager(Character character)
{
this.character = character;
objectives = new List<AIObjective>();
Objectives = new List<AIObjective>();
}
public void AddObjective(AIObjective objective)
{
if (objectives.Find(o => o.IsDuplicate(objective)) != null) return;
if (Objectives.Find(o => o.IsDuplicate(objective)) != null) return;
objectives.Add(objective);
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 Dictionary<AIObjective, CoroutineHandle> DelayedObjectives { get; private set; } = new Dictionary<AIObjective, CoroutineHandle>();
@@ -64,7 +75,7 @@ namespace Barotrauma
public T GetObjective<T>() where T : AIObjective
{
foreach (AIObjective objective in objectives)
foreach (AIObjective objective in Objectives)
{
if (objective is T) return (T)objective;
}
@@ -73,62 +84,85 @@ namespace Barotrauma
private AIObjective GetCurrentObjective()
{
if (CurrentOrder != null &&
(objectives.Count == 0 || currentOrder.GetPriority(this) > objectives[0].GetPriority(this)))
var firstObjective = Objectives.FirstOrDefault();
if (CurrentOrder != null && firstObjective != null && CurrentOrder.GetPriority(this) > firstObjective.GetPriority(this))
{
return CurrentOrder;
CurrentObjective = CurrentOrder;
}
return objectives.Count == 0 ? null : objectives[0];
else
{
CurrentObjective = firstObjective;
}
return CurrentObjective;
}
public float GetCurrentPriority()
{
var currentObjective = GetCurrentObjective();
return currentObjective == null ? 0.0f : currentObjective.GetPriority(this);
return CurrentObjective == null ? 0.0f : CurrentObjective.GetPriority(this);
}
public void UpdateObjectives()
public void UpdateObjectives(float deltaTime)
{
if (!objectives.Any()) return;
//remove completed objectives and ones that can't be completed
objectives = objectives.FindAll(o => !o.IsCompleted() && o.CanBeCompleted);
//sort objectives according to priority
objectives.Sort((x, y) => y.GetPriority(this).CompareTo(x.GetPriority(this)));
GetCurrentObjective()?.SortSubObjectives(this);
CurrentOrder?.Update(this, deltaTime);
for (int i = 0; i < Objectives.Count; i++)
{
var objective = Objectives[i];
if (objective.IsCompleted())
{
#if DEBUG
DebugConsole.NewMessage($"Removing objective {objective.DebugTag}, because it is completed.");
#endif
Objectives.Remove(objective);
}
else if (!objective.CanBeCompleted)
{
#if DEBUG
DebugConsole.NewMessage($"Removing objective {objective.DebugTag}, because it cannot be completed.");
#endif
Objectives.Remove(objective);
}
else
{
objective.Update(this, deltaTime);
}
}
GetCurrentObjective();
}
public void SortObjectives()
{
if (Objectives.Any())
{
Objectives.Sort((x, y) => y.GetPriority(this).CompareTo(x.GetPriority(this)));
}
CurrentObjective?.SortSubObjectives(this);
}
public void DoCurrentObjective(float deltaTime)
{
CurrentObjective = GetCurrentObjective();
if (CurrentObjective == null || (CurrentObjective.GetPriority(this) < OrderPriority && WaitTimer > 0.0f))
{
WaitTimer -= deltaTime;
character.AIController.SteeringManager.Reset();
return;
}
CurrentObjective?.TryComplete(deltaTime);
}
public void SetOrder(AIObjective objective)
{
currentOrder = objective;
CurrentOrder = objective;
}
public void SetOrder(Order order, string option, Character orderGiver)
{
currentOrder = null;
CurrentOrder = null;
if (order == null) return;
switch (order.AITag.ToLowerInvariant())
{
case "follow":
currentOrder = new AIObjectiveGoTo(orderGiver, character, true)
CurrentOrder = new AIObjectiveGoTo(orderGiver, character, true)
{
CloseEnough = 1.5f,
AllowGoingOutside = true,
@@ -137,13 +171,34 @@ namespace Barotrauma
};
break;
case "wait":
currentOrder = new AIObjectiveGoTo(character, character, true)
CurrentOrder = new AIObjectiveGoTo(character, character, true)
{
AllowGoingOutside = true
};
break;
case "fixleaks":
currentOrder = new AIObjectiveFixLeaks(character);
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;
case "chargebatteries":
currentOrder = new AIObjectiveChargeBatteries(character, option);
@@ -168,7 +223,7 @@ namespace Barotrauma
break;
default:
if (order.TargetItemComponent == null) return;
currentOrder = new AIObjectiveOperateItem(order.TargetItemComponent, character, option, false, null, order.UseController);
CurrentOrder = new AIObjectiveOperateItem(order.TargetItemComponent, character, option, false, null, order.UseController);
break;
}
}
@@ -7,6 +7,8 @@ namespace Barotrauma
{
class AIObjectiveOperateItem : AIObjective
{
public override string DebugTag => "operate item";
private ItemComponent component, controller;
private Entity operateTarget;
@@ -38,14 +40,16 @@ namespace Barotrauma
get { return operateTarget; }
}
public ItemComponent Component => component;
public override float GetPriority(AIObjectiveManager objectiveManager)
{
if (gotoObjective != null && !gotoObjective.CanBeCompleted) { return 0; }
if (objectiveManager.CurrentOrder == this)
{
return AIObjectiveManager.OrderPriority;
}
return 1.0f;
return base.GetPriority(objectiveManager);
}
public AIObjectiveOperateItem(ItemComponent item, Character character, string option, bool requireEquip, Entity operateTarget = null, bool useController = false)
@@ -158,7 +162,7 @@ namespace Barotrauma
AIObjectiveOperateItem operateItem = otherObjective as AIObjectiveOperateItem;
if (operateItem == null) return false;
return (operateItem.component == component);
return (operateItem.component == component ||otherObjective.Option == Option);
}
}
}
@@ -1,108 +1,68 @@
using Barotrauma.Items.Components;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Barotrauma
{
class AIObjectivePumpWater : AIObjective
class AIObjectivePumpWater : AIObjectiveLoop<Pump>
{
private const float FindPumpsInterval = 5.0f;
public override string DebugTag => "pump water";
public override bool KeepDivingGearOn => true;
private readonly IEnumerable<Pump> pumpList;
private string orderOption;
private List<Pump> pumps;
private float lastFindPumpsTime;
public AIObjectivePumpWater(Character character, string option)
: base(character, option)
public AIObjectivePumpWater(Character character, string option) : base(character, option)
{
orderOption = option;
pumpList = character.Submarine.GetItems(true).Select(i => i.GetComponent<Pump>()).Where(p => p != null);
}
public override float GetPriority(AIObjectiveManager objectiveManager)
{
if (Timing.TotalTime >= lastFindPumpsTime + FindPumpsInterval)
{
FindPumps();
}
if (objectiveManager.CurrentOrder == this && pumps.Count > 0)
if (character.Submarine == null) { return 0; }
if (objectiveManager.CurrentOrder == this && targets.Count > 0)
{
return AIObjectiveManager.OrderPriority;
}
return 0.0f;
}
public override bool IsCompleted()
public override bool IsDuplicate(AIObjective otherObjective) => otherObjective is AIObjectivePumpWater && otherObjective.Option == Option;
//availablePumps = allPumps.Where(p => !p.Item.HasTag("ballast") && p.Item.Connections.None(c => c.IsPower && p.Item.GetConnectedComponentsRecursive<Steering>(c).None())).ToList();
protected override void FindTargets()
{
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>();
if (option == null) { return; }
foreach (Item item in Item.ItemList)
{
//don't attempt to use pumps outside the sub
if (item.HasTag("ballast")) { continue; }
if (item.Submarine == null) { continue; }
if (item.Submarine.TeamID != character.TeamID) { continue; }
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(item, true)) { 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 (pump != null)
{
if (c.IsPower) continue;
if (item.GetConnectedComponentsRecursive<Steering>(c).Count > 0)
if (!ignoreList.Contains(pump))
{
connectedToSteering = true;
break;
if (option == "stoppumping")
{
if (!pump.IsActive || pump.FlowPercentage == 0.0f) { continue; }
}
else
{
if (!pump.Item.InWater) { continue; }
if (pump.IsActive && pump.FlowPercentage <= -90.0f) { continue; }
}
if (!targets.Contains(pump))
{
targets.Add(pump);
}
}
}
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));
}
}
protected override bool Filter(Pump pump) => true;
protected override IEnumerable<Pump> GetList() => pumpList;
protected override AIObjective ObjectiveConstructor(Pump pump) => new AIObjectiveOperateItem(pump, character, Option, false);
protected override float Average(Pump target) => 0;
}
}
@@ -2,100 +2,125 @@
using Microsoft.Xna.Framework;
using System;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
class AIObjectiveRepairItem : AIObjective
{
private Item item;
public AIObjectiveRepairItem(Character character, Item item)
: base(character, "")
public override string DebugTag => "repair item";
public override bool KeepDivingGearOn => true;
public Item Item { get; private set; }
private AIObjectiveGoTo goToObjective;
private float previousCondition = -1;
public AIObjectiveRepairItem(Character character, Item item) : base(character, "")
{
this.item = item;
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 = item.Prefab.Health - 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));
}
// TODO: priority list?
if (Item.Repairables.None()) { return 0; }
// Ignore items that are being repaired by someone else.
if (Item.Repairables.Any(r => r.CurrentFixer != null && r.CurrentFixer != character)) { return 0; }
// 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;
float distanceFactor = MathHelper.Lerp(1, 0.5f, MathUtils.InverseLerp(0, 10000, dist));
float damagePriority = MathHelper.Lerp(1, 0, (Item.Condition + 10) / Item.MaxCondition);
float successFactor = MathHelper.Lerp(0, 1, Item.Repairables.Average(r => r.DegreeOfSuccess(character)));
float isSelected = character.SelectedConstruction == Item ? 50 : 0;
float baseLevel = Math.Max(priority + isSelected, 1);
return MathHelper.Clamp(baseLevel * damagePriority * distanceFactor * successFactor, 0, 100);
}
public override bool CanBeCompleted => !abandon;
public override bool IsCompleted()
{
foreach (Repairable repairable in item.Repairables)
bool isCompleted = Item.IsFullCondition;
if (isCompleted)
{
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);
}
character?.Speak(TextManager.Get("DialogItemRepaired").Replace("[itemname]", item.Name), null, 0.0f, "itemrepaired", 10.0f);
return true;
return isCompleted;
}
public override bool IsDuplicate(AIObjective otherObjective)
{
return otherObjective is AIObjectiveRepairItem repairObjective && repairObjective.item == item;
return otherObjective is AIObjectiveRepairItem repairObjective && repairObjective.Item == Item;
}
protected override void Act(float deltaTime)
{
foreach (Repairable repairable in item.Repairables)
if (goToObjective != null && !subObjectives.Contains(goToObjective))
{
//make sure we have all the items required to fix the target item
foreach (var kvp in repairable.requiredItems)
if (!goToObjective.IsCompleted() && !goToObjective.CanBeCompleted)
{
foreach (RelatedItem requiredItem in kvp.Value)
abandon = true;
character?.Speak(TextManager.Get("DialogCannotRepair").Replace("[itemname]", Item.Name), null, 0.0f, "cannotrepair", 10.0f);
}
goToObjective = null;
}
foreach (Repairable repairable in Item.Repairables)
{
if (!repairable.HasRequiredItems(character, false))
{
//make sure we have all the items required to fix the target item
foreach (var kvp in repairable.requiredItems)
{
if (!character.Inventory.Items.Any(it => it != null && requiredItem.MatchesItem(it)))
foreach (RelatedItem requiredItem in kvp.Value)
{
AddSubObjective(new AIObjectiveGetItem(character, requiredItem.Identifiers, true));
return;
}
}
return;
}
}
if (character.CanInteractWith(item))
if (character.CanInteractWith(Item))
{
foreach (Repairable repairable in item.Repairables)
foreach (Repairable repairable in Item.Repairables)
{
if (character.SelectedConstruction != item) { item.TryInteract(character, true, true); }
repairable.CurrentFixer = character;
if (repairable.CurrentFixer != null && repairable.CurrentFixer != character)
{
// Someone else is repairing the target. Abandon the objective if the other is better at this then us.
abandon = repairable.DegreeOfSuccess(character) < repairable.DegreeOfSuccess(repairable.CurrentFixer);
}
if (!abandon)
{
if (character.SelectedConstruction != Item)
{
Item.TryInteract(character, true, true);
}
if (previousCondition == -1)
{
previousCondition = Item.Condition;
}
else if (Item.Condition < previousCondition)
{
// If the current condition is less than the previous condition, we can't complete the task, so let's abandon it. The item is probably deteriorating at a greater speed than we can repair it.
abandon = true;
character?.Speak(TextManager.Get("DialogRepairFailed").Replace("[itemname]", Item.Name), null, 0.0f, "repairfailed", 10.0f);
}
}
repairable.CurrentFixer = abandon && repairable.CurrentFixer == character ? null : character;
break;
}
}
else
else if (goToObjective == null || goToObjective.Target != Item)
{
AddSubObjective(new AIObjectiveGoTo(item, character));
previousCondition = -1;
if (goToObjective != null)
{
subObjectives.Remove(goToObjective);
}
goToObjective = new AIObjectiveGoTo(Item, character);
AddSubObjective(goToObjective);
}
}
}
@@ -1,68 +1,68 @@
using Barotrauma.Items.Components;
using System;
using System.Collections.Generic;
using System.Text;
using System.Collections.Generic;
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
namespace Barotrauma
{
class AIObjectiveRepairItems : AIObjective
class AIObjectiveRepairItems : AIObjectiveLoop<Item>
{
public override string DebugTag => "repair items";
public override bool KeepDivingGearOn => true;
/// <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 AIObjectiveRepairItems(Character character) : base(character, "") { }
public override float GetPriority(AIObjectiveManager objectiveManager)
// TODO: This can allow two active repair items objectives, if RequireAdequateSkills is not at the same value. We don't want that.
public override bool IsDuplicate(AIObjective otherObjective) => otherObjective is AIObjectiveRepairItems repairItems && repairItems.RequireAdequateSkills == RequireAdequateSkills;
protected override void CreateObjectives()
{
GetBrokenItems();
if (subObjectives.Count > 0 && objectiveManager.CurrentOrder == this)
foreach (var item in targets)
{
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 >= item.Prefab.Health) continue;
foreach (Repairable repairable in item.Repairables)
{
//ignore ones that are already fixed
if (item.Condition > repairable.ShowRepairUIThreshold) continue;
if (RequireAdequateSkills)
if (!objectives.TryGetValue(item, out AIObjective objective))
{
if (!repairable.HasRequiredSkills(character)) { continue; }
objective = ObjectiveConstructor(item);
objectives.Add(item, objective);
AddSubObjective(objective);
}
AddSubObjective(new AIObjectiveRepairItem(character, item));
break;
}
}
}
protected override bool Filter(Item item)
{
bool ignore = ignoreList.Contains(item) || item.IsFullCondition;
if (!ignore)
{
if (item.Submarine == null) { ignore = true; }
else if (item.Submarine.TeamID != character.TeamID) { ignore = true; }
else if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(item, true)) { ignore = true; }
else
{
if (item.Repairables.None()) { ignore = true; }
else
{
foreach (Repairable repairable in item.Repairables)
{
if (item.Condition > repairable.ShowRepairUIThreshold) { ignore = true; }
else if (RequireAdequateSkills && !repairable.HasRequiredSkills(character)) { ignore = true; }
if (ignore) { break; }
}
}
}
}
return ignore;
}
protected override float Average(Item item) => 100 - item.ConditionPercentage;
protected override IEnumerable<Item> GetList() => Item.ItemList;
protected override AIObjective ObjectiveConstructor(Item item) => new AIObjectiveRepairItem(character, item);
}
}
@@ -9,6 +9,10 @@ namespace Barotrauma
{
class AIObjectiveRescue : AIObjective
{
public override string DebugTag => "rescue";
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
const float TreatmentDelay = 0.5f;
private readonly Character targetCharacter;
@@ -102,7 +106,7 @@ namespace Barotrauma
}
return !character.AnimController.InWater && !targetCharacter.AnimController.InWater &&
AIObjectiveFindSafety.GetHullSafety(character.CurrentHull, character) > 50.0f;
HumanAIController.GetHullSafety(character.CurrentHull, character) > HumanAIController.HULL_SAFETY_THRESHOLD;
}
return false;
@@ -5,13 +5,16 @@ namespace Barotrauma
{
class AIObjectiveRescueAll : AIObjective
{
public override string DebugTag => "rescue all";
public override bool KeepDivingGearOn => true;
//only treat characters whose vitality is below this (0.8 = 80% of max vitality)
public const float VitalityThreshold = 0.8f;
private List<Character> rescueTargets;
public AIObjectiveRescueAll(Character character)
: base (character, "")
public AIObjectiveRescueAll(Character character) : base (character, "")
{
rescueTargets = new List<Character>();
}
@@ -23,10 +26,11 @@ namespace Barotrauma
public override float GetPriority(AIObjectiveManager objectiveManager)
{
if (character.Submarine == null) { return 0; }
GetRescueTargets();
if (!rescueTargets.Any()) { return 0.0f; }
if (objectiveManager.CurrentObjective == this)
if (objectiveManager.CurrentOrder == this)
{
return AIObjectiveManager.OrderPriority;
}
@@ -40,6 +44,7 @@ namespace Barotrauma
{
rescueTargets = Character.CharacterList.FindAll(c =>
c.AIController is HumanAIController &&
c.TeamID == character.TeamID &&
c != character &&
!c.IsDead &&
c.Vitality / c.MaxVitality < VitalityThreshold);
@@ -53,9 +58,7 @@ namespace Barotrauma
}
}
public override bool IsCompleted()
{
return false;
}
public override bool IsCompleted() => false;
public override bool CanBeCompleted => true;
}
}
@@ -155,7 +155,7 @@ namespace Barotrauma
}
}
public SteeringPath FindPath(Vector2 start, Vector2 end, string errorMsgStr)
public SteeringPath FindPath(Vector2 start, Vector2 end, string errorMsgStr = null)
{
float closestDist = 0.0f;
PathNode startNode = null;
@@ -33,19 +33,19 @@ namespace Barotrauma
wanderAngle = Rand.Range(0.0f, MathHelper.TwoPi);
}
public void SteeringSeek(Vector2 targetSimPos, float speed)
public void SteeringSeek(Vector2 targetSimPos, float weight = 1)
{
steering += DoSteeringSeek(targetSimPos, speed);
steering += DoSteeringSeek(targetSimPos, weight);
}
public void SteeringWander(float speed)
public void SteeringWander(float weight = 1)
{
steering += DoSteeringWander(speed);
steering += DoSteeringWander(weight);
}
public void SteeringAvoid(float deltaTime, float lookAheadDistance, float speed)
public void SteeringAvoid(float deltaTime, float lookAheadDistance, float weight = 1)
{
steering += DoSteeringAvoid(deltaTime, lookAheadDistance, speed);
steering += DoSteeringAvoid(deltaTime, lookAheadDistance, weight);
}
public void SteeringManual(float deltaTime, Vector2 velocity)
@@ -76,39 +76,36 @@ namespace Barotrauma
host.Steering = Vector2.Zero;
return;
}
float steeringSpeed = steering.Length();
if (steeringSpeed > speed)
if (steering.LengthSquared() > speed * speed)
{
steering = Vector2.Normalize(steering) * Math.Abs(speed);
}
host.Steering = steering;
}
protected virtual Vector2 DoSteeringSeek(Vector2 target, float speed)
protected virtual Vector2 DoSteeringSeek(Vector2 target, float weight)
{
Vector2 targetVel = target - host.SimPosition;
if (targetVel.LengthSquared() < 0.00001f) return Vector2.Zero;
targetVel = Vector2.Normalize(targetVel) * speed;
targetVel = Vector2.Normalize(targetVel) * weight;
Vector2 newSteering = targetVel - host.Steering;
if (newSteering == Vector2.Zero) return Vector2.Zero;
float steeringSpeed = (newSteering + host.Steering).Length();
if (steeringSpeed > Math.Abs(speed))
if (steeringSpeed > Math.Abs(weight))
{
newSteering = Vector2.Normalize(newSteering) * Math.Abs(speed);
newSteering = Vector2.Normalize(newSteering) * Math.Abs(weight);
}
return newSteering;
}
protected virtual Vector2 DoSteeringWander(float speed)
protected virtual Vector2 DoSteeringWander(float weight)
{
Vector2 circleCenter = (host.Steering == Vector2.Zero) ? Rand.Vector(speed) : host.Steering;
Vector2 circleCenter = (host.Steering == Vector2.Zero) ? Rand.Vector(weight) : host.Steering;
circleCenter = Vector2.Normalize(circleCenter) * CircleDistance;
Vector2 displacement = new Vector2(
@@ -122,15 +119,15 @@ namespace Barotrauma
Vector2 newSteering = circleCenter + displacement;
float steeringSpeed = (newSteering + host.Steering).Length();
if (steeringSpeed > speed)
if (steeringSpeed > weight)
{
newSteering = Vector2.Normalize(newSteering) * speed;
newSteering = Vector2.Normalize(newSteering) * weight;
}
return newSteering;
}
protected virtual Vector2 DoSteeringAvoid(float deltaTime, float lookAheadDistance, float speed)
protected virtual Vector2 DoSteeringAvoid(float deltaTime, float lookAheadDistance, float weight)
{
if (steering == Vector2.Zero || host.Steering == Vector2.Zero) return Vector2.Zero;
@@ -187,7 +184,7 @@ namespace Barotrauma
if (dist > maxDistance) return Vector2.Zero;
return -diff * (1.0f - dist / maxDistance) * speed;
return -diff * (1.0f - dist / maxDistance) * weight;
}
}
@@ -130,6 +130,9 @@ namespace Barotrauma
public bool Crouching;
private float upperArmLength = 0.0f, forearmLength = 0.0f;
public float ArmLength => upperArmLength + forearmLength;
public Vector2 RightHandIKPos
{
get;
@@ -1623,7 +1626,7 @@ namespace Barotrauma
Holdable holdable = item.GetComponent<Holdable>();
if (Anim != Animation.Climbing && !usingController && character.Stun <= 0.0f && aim && itemPos != Vector2.Zero)
if (!character.IsClimbing && !usingController && character.Stun <= 0.0f && aim && itemPos != Vector2.Zero)
{
Vector2 mousePos = ConvertUnits.ToSimUnits(character.SmoothedCursorPosition);
@@ -1653,7 +1656,7 @@ namespace Barotrauma
}
Vector2 transformedHoldPos = shoulder.WorldAnchorA;
if (itemPos == Vector2.Zero || Anim == Animation.Climbing || usingController)
if (itemPos == Vector2.Zero || character.IsClimbing || usingController)
{
if (character.SelectedItems[0] == item)
{
@@ -1742,7 +1745,7 @@ namespace Barotrauma
item.SetTransform(currItemPos, itemAngle + itemAngleRelativeToHoldAngle * Dir);
if (Anim == Animation.Climbing) return;
if (character.IsClimbing) return;
for (int i = 0; i < 2; i++)
{
@@ -988,6 +988,8 @@ namespace Barotrauma
public Vector2? OverrideMovement { get; set; }
public bool ForceRun { get; set; }
public bool IsClimbing => AnimController.Anim == AnimController.Animation.Climbing;
public Vector2 GetTargetMovement()
{
Vector2 targetMovement = Vector2.Zero;
@@ -1023,8 +1025,9 @@ namespace Barotrauma
!AnimController.IsMovingBackwards;
}
targetMovement *= AnimController.GetCurrentSpeed(run);
float maxSpeed = GetCurrentMaxSpeed(run);
float currentSpeed = AnimController.GetCurrentSpeed(run);
targetMovement *= currentSpeed;
float maxSpeed = ApplyTemporarySpeedLimits(currentSpeed);
targetMovement.X = MathHelper.Clamp(targetMovement.X, -maxSpeed, maxSpeed);
targetMovement.Y = MathHelper.Clamp(targetMovement.Y, -maxSpeed, maxSpeed);
@@ -1084,31 +1087,23 @@ namespace Barotrauma
speedMultipliers.Clear();
}
/// <summary>
/// Applies temporary limits to the speed (damage).
/// </summary>
public float GetCurrentMaxSpeed(bool run)
public float ApplyTemporarySpeedLimits(float speed)
{
float currMaxSpeed = AnimController.GetCurrentSpeed(run);
//?
//currMaxSpeed *= 1.5f;
var leftFoot = AnimController.GetLimb(LimbType.LeftFoot);
if (leftFoot != null)
{
float footAfflictionStrength = CharacterHealth.GetAfflictionStrength("damage", leftFoot, true);
currMaxSpeed *= MathHelper.Lerp(1.0f, 0.25f, MathHelper.Clamp(footAfflictionStrength / 100.0f, 0.0f, 1.0f));
speed *= MathHelper.Lerp(1.0f, 0.25f, MathHelper.Clamp(footAfflictionStrength / 100.0f, 0.0f, 1.0f));
}
var rightFoot = AnimController.GetLimb(LimbType.RightFoot);
if (rightFoot != null)
{
float footAfflictionStrength = CharacterHealth.GetAfflictionStrength("damage", rightFoot, true);
currMaxSpeed *= MathHelper.Lerp(1.0f, 0.25f, MathHelper.Clamp(footAfflictionStrength / 100.0f, 0.0f, 1.0f));
speed *= MathHelper.Lerp(1.0f, 0.25f, MathHelper.Clamp(footAfflictionStrength / 100.0f, 0.0f, 1.0f));
}
return currMaxSpeed;
return speed;
}
public void Control(float deltaTime, Camera cam)
@@ -2055,7 +2050,7 @@ namespace Barotrauma
prevAiChatMessages.Add(dummyMsg);
}
public void Speak(string message, ChatMessageType? messageType, float delay = 0.0f, string identifier = "", float minDurationBetweenSimilar = 0.0f)
public void Speak(string message, ChatMessageType? messageType = null, float delay = 0.0f, string identifier = "", float minDurationBetweenSimilar = 0.0f)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) return;
@@ -76,8 +76,8 @@ namespace Barotrauma
}
}
const float InsufficientOxygenThreshold = 30.0f;
const float LowOxygenThreshold = 50.0f;
public const float InsufficientOxygenThreshold = 30.0f;
public const float LowOxygenThreshold = 50.0f;
protected float minVitality, maxVitality;
public bool Unkillable;
@@ -441,28 +441,18 @@ namespace Barotrauma
public bool SectorHit(Vector2 armorSector, Vector2 simPosition)
{
if (armorSector == Vector2.Zero) { return false; }
// Can't get this to work properly.
//float rot = body.Rotation;
//if (Dir == -1) { rot -= MathHelper.Pi; }
//Vector2 armorLimits = new Vector2(rot - armorSector.X * Dir, rot - armorSector.Y * Dir);
//float mid = (armorLimits.X + armorLimits.Y) / 2;
//float angleDiff = MathUtils.GetShortestAngle(MathUtils.VectorToAngle(simPosition - SimPosition), mid);
//return (Math.Abs(angleDiff) < (armorSector.Y - armorSector.X) / 2);
// Alternative implementation
float offset = GetArmorSectorRotationOffset(armorSector, body.Rotation);
Vector2 forward = Vector2.Transform(-Vector2.UnitY, Matrix.CreateRotationZ(offset));
float hitAngle = VectorExtensions.Angle(forward, simPosition - SimPosition);
float sectorSize = MathHelper.ToDegrees(GetArmorSectorSize(armorSector));
float rotation = body.TransformedRotation;
float offset = (MathHelper.PiOver2 - GetArmorSectorRotationOffset(armorSector)) * Dir;
float hitAngle = VectorExtensions.Angle(VectorExtensions.Forward(rotation + offset), SimPosition - simPosition);
float sectorSize = GetArmorSectorSize(armorSector);
return hitAngle < sectorSize / 2;
}
protected float GetArmorSectorRotationOffset(Vector2 armorSector, float bodyRotation)
protected float GetArmorSectorRotationOffset(Vector2 armorSector)
{
float midAngle = MathUtils.GetMidAngle(armorSector.X, armorSector.Y);
float spritesheetOrientation = MathHelper.ToRadians(limbParams.Ragdoll.SpritesheetOrientation);
return bodyRotation + (midAngle + spritesheetOrientation) * Dir;
return midAngle + spritesheetOrientation;
}
protected float GetArmorSectorSize(Vector2 armorSector)