Files
LuaCsForBarotraumaEP/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs
2021-12-03 13:31:10 -03:00

499 lines
20 KiB
C#

using Barotrauma.Items.Components;
using Barotrauma.Networking;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
abstract partial class AIController : ISteerable
{
public bool Enabled;
public readonly Character Character;
// Update only when the value changes, not when it keeps the same.
protected AITarget _lastAiTarget;
// Updated each time the value is updated (also when the value is the same).
protected AITarget _previousAiTarget;
protected AITarget _selectedAiTarget;
public AITarget SelectedAiTarget
{
get { return _selectedAiTarget; }
protected set
{
_previousAiTarget = _selectedAiTarget;
_selectedAiTarget = value;
if (_selectedAiTarget != _previousAiTarget)
{
if (_previousAiTarget != null)
{
_lastAiTarget = _previousAiTarget;
}
OnTargetChanged(_previousAiTarget, _selectedAiTarget);
}
}
}
protected SteeringManager steeringManager;
public SteeringManager SteeringManager
{
get { return steeringManager; }
}
public Vector2 Steering
{
get { return Character.AnimController.TargetMovement; }
set { Character.AnimController.TargetMovement = value; }
}
public Vector2 SimPosition
{
get { return Character.SimPosition; }
}
public Vector2 WorldPosition
{
get { return Character.WorldPosition; }
}
public Vector2 Velocity
{
get { return Character.AnimController.Collider.LinearVelocity; }
}
public virtual bool CanEnterSubmarine
{
get { return true; }
}
public virtual bool CanFlip
{
get { return true; }
}
public virtual bool IsMentallyUnstable => false;
private IEnumerable<Hull> visibleHulls;
private float hullVisibilityTimer;
const float hullVisibilityInterval = 0.5f;
public IEnumerable<Hull> VisibleHulls
{
get
{
if (visibleHulls == null)
{
visibleHulls = Character.GetVisibleHulls();
}
return visibleHulls;
}
private set
{
visibleHulls = value;
}
}
public bool HasValidPath(bool requireNonDirty = false, bool requireUnfinished = true) =>
steeringManager is IndoorsSteeringManager pathSteering &&
pathSteering.CurrentPath != null &&
(!requireUnfinished || !pathSteering.CurrentPath.Finished) &&
!pathSteering.CurrentPath.Unreachable &&
(!requireNonDirty || !pathSteering.IsPathDirty);
public bool IsCurrentPathNullOrUnreachable => IsCurrentPathUnreachable || steeringManager is IndoorsSteeringManager pathSteering && pathSteering.CurrentPath == null;
public bool IsCurrentPathUnreachable => steeringManager is IndoorsSteeringManager pathSteering && !pathSteering.IsPathDirty && pathSteering.CurrentPath != null && pathSteering.CurrentPath.Unreachable;
public bool IsCurrentPathFinished => steeringManager is IndoorsSteeringManager pathSteering && !pathSteering.IsPathDirty && pathSteering.CurrentPath != null && pathSteering.CurrentPath.Finished;
protected readonly float colliderWidth;
protected readonly float minGapSize;
protected readonly float colliderLength;
protected readonly float avoidLookAheadDistance;
public AIController (Character c)
{
Character = c;
hullVisibilityTimer = Rand.Range(0f, hullVisibilityTimer);
Enabled = true;
var size = Character.AnimController.Collider.GetSize();
colliderWidth = size.X;
colliderLength = size.Y;
avoidLookAheadDistance = Math.Max(Math.Max(colliderWidth, colliderLength) * 3, 1.5f);
minGapSize = ConvertUnits.ToDisplayUnits(Math.Min(colliderWidth, colliderLength));
}
public virtual void OnHealed(Character healer, float healAmount) { }
public virtual void OnAttacked(Character attacker, AttackResult attackResult) { }
public virtual void SelectTarget(AITarget target) { }
public virtual void Update(float deltaTime)
{
if (hullVisibilityTimer > 0)
{
hullVisibilityTimer--;
}
else
{
hullVisibilityTimer = hullVisibilityInterval;
VisibleHulls = Character.GetVisibleHulls();
}
}
public virtual void Reset()
{
ResetAITarget();
}
protected void ResetAITarget()
{
_lastAiTarget = null;
_selectedAiTarget = null;
}
public void FaceTarget(ISpatialEntity target) => Character.AnimController.TargetDir = target.WorldPosition.X > Character.WorldPosition.X ? Direction.Right : Direction.Left;
public bool IsSteeringThroughGap { get; protected set; }
public virtual bool SteerThroughGap(Structure wall, WallSection section, Vector2 targetWorldPos, float deltaTime)
{
if (wall == null) { return false; }
if (section == null) { return false; }
Gap gap = section.gap;
if (gap == null) { return false; }
float maxDistance = Math.Min(wall.Rect.Width, wall.Rect.Height);
if (Vector2.DistanceSquared(Character.WorldPosition, targetWorldPos) > maxDistance * maxDistance) { return false; }
Hull targetHull = gap.FlowTargetHull;
if (targetHull == null) { return false; }
if (wall.IsHorizontal)
{
targetWorldPos.Y = targetHull.WorldRect.Y - targetHull.Rect.Height / 2;
}
else
{
targetWorldPos.X = targetHull.WorldRect.Center.X;
}
return SteerThroughGap(gap, targetWorldPos, deltaTime, maxDistance: -1);
}
public virtual bool SteerThroughGap(Gap gap, Vector2 targetWorldPos, float deltaTime, float maxDistance = -1)
{
Hull targetHull = gap.FlowTargetHull;
if (targetHull == null) { return false; }
if (maxDistance > 0)
{
if (Vector2.DistanceSquared(Character.WorldPosition, targetWorldPos) > maxDistance * maxDistance) { return false; }
}
if (SteeringManager is IndoorsSteeringManager pathSteering)
{
pathSteering.ResetPath();
}
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(targetWorldPos - Character.WorldPosition));
return true;
}
public bool CanPassThroughHole(Structure wall, int sectionIndex, int requiredHoleCount)
{
if (!wall.SectionBodyDisabled(sectionIndex)) { return false; }
int holeCount = 1;
for (int j = sectionIndex - 1; j > sectionIndex - requiredHoleCount; j--)
{
if (wall.SectionBodyDisabled(j))
{
holeCount++;
}
else
{
break;
}
}
for (int j = sectionIndex + 1; j < sectionIndex + requiredHoleCount; j++)
{
if (wall.SectionBodyDisabled(j))
{
holeCount++;
}
else
{
break;
}
}
return holeCount >= requiredHoleCount;
}
protected bool IsWallDisabled(Structure wall)
{
bool isDisabled = true;
for (int i = 0; i < wall.Sections.Length; i++)
{
if (!wall.SectionBodyDisabled(i))
{
isDisabled = false;
break;
}
}
return isDisabled;
}
private readonly HashSet<Item> unequippedItems = new HashSet<Item>();
public bool TakeItem(Item item, CharacterInventory targetInventory, bool equip, bool wear = false, bool dropOtherIfCannotMove = true, bool allowSwapping = false, bool storeUnequipped = false)
{
var pickable = item.GetComponent<Pickable>();
if (pickable == null) { return false; }
if (wear)
{
var wearable = item.GetComponent<Wearable>();
if (wearable != null)
{
pickable = wearable;
}
}
else
{
var holdable = item.GetComponent<Holdable>();
if (holdable != null)
{
pickable = holdable;
}
}
if (item.ParentInventory is ItemInventory itemInventory)
{
if (!itemInventory.Container.HasRequiredItems(Character, addMessage: false)) { return false; }
}
if (equip)
{
int targetSlot = -1;
//check if all the slots required by the item are free
foreach (InvSlotType slots in pickable.AllowedSlots)
{
if (slots.HasFlag(InvSlotType.Any)) { continue; }
for (int i = 0; i < targetInventory.Capacity; i++)
{
if (targetInventory is CharacterInventory characterInventory)
{
//slot not needed by the item, continue
if (!slots.HasFlag(characterInventory.SlotTypes[i])) { continue; }
}
targetSlot = i;
//slot free, continue
var otherItem = targetInventory.GetItemAt(i);
if (otherItem == null) { continue; }
//try to move the existing item to LimbSlot.Any and continue if successful
if (otherItem.AllowedSlots.Contains(InvSlotType.Any) && targetInventory.TryPutItem(otherItem, Character, CharacterInventory.anySlot))
{
if (storeUnequipped && targetInventory.Owner == Character)
{
unequippedItems.Add(otherItem);
}
continue;
}
if (dropOtherIfCannotMove)
{
//if everything else fails, simply drop the existing item
otherItem.Drop(Character);
}
}
}
return targetInventory.TryPutItem(item, targetSlot, allowSwapping, allowCombine: false, Character);
}
else
{
return targetInventory.TryPutItem(item, Character, CharacterInventory.anySlot);
}
}
public void UnequipEmptyItems(Item parentItem, bool avoidDroppingInSea = true) => UnequipEmptyItems(Character, parentItem, avoidDroppingInSea);
public void UnequipContainedItems(Item parentItem, Func<Item, bool> predicate = null, bool avoidDroppingInSea = true, int? unequipMax = null) => UnequipContainedItems(Character, parentItem, predicate, avoidDroppingInSea, unequipMax);
public static void UnequipEmptyItems(Character character, Item parentItem, bool avoidDroppingInSea = true) => UnequipContainedItems(character, parentItem, it => it.Condition <= 0, avoidDroppingInSea);
public static void UnequipContainedItems(Character character, Item parentItem, Func<Item, bool> predicate, bool avoidDroppingInSea = true, int? unequipMax = null)
{
var inventory = parentItem.OwnInventory;
if (inventory == null) { return; }
int removed = 0;
if (predicate == null || inventory.AllItems.Any(predicate))
{
foreach (Item containedItem in inventory.AllItemsMod)
{
if (containedItem == null) { continue; }
if (predicate == null || predicate(containedItem))
{
if (avoidDroppingInSea && !character.IsInFriendlySub)
{
// If we are not inside a friendly sub (= same team), try to put the item in the inventory instead dropping it.
if (character.Inventory.TryPutItem(containedItem, character, CharacterInventory.anySlot))
{
if (unequipMax.HasValue && ++removed >= unequipMax) { return; }
continue;
}
}
containedItem.Drop(character);
if (unequipMax.HasValue && ++removed >= unequipMax) { return; }
}
}
}
}
public void ReequipUnequipped()
{
foreach (var item in unequippedItems)
{
if (item != null && !item.Removed && Character.HasItem(item))
{
TakeItem(item, Character.Inventory, equip: true, wear: true, dropOtherIfCannotMove: true, allowSwapping: true, storeUnequipped: false);
}
}
unequippedItems.Clear();
}
#region Escape
public abstract bool Escape(float deltaTime);
public Gap EscapeTarget { get; private set; }
private readonly float escapeTargetSeekInterval = 2;
private float escapeTimer;
protected bool allGapsSearched;
protected readonly HashSet<Gap> unreachableGaps = new HashSet<Gap>();
protected bool UpdateEscape(float deltaTime, bool canAttackDoors)
{
IndoorsSteeringManager pathSteering = SteeringManager as IndoorsSteeringManager;
if (allGapsSearched)
{
escapeTimer -= deltaTime;
if (escapeTimer <= 0)
{
allGapsSearched = false;
}
}
if (Character.CurrentHull != null && pathSteering != null)
{
// Seek exit if inside
if (!allGapsSearched)
{
float closestDistance = 0;
foreach (Gap gap in Gap.GapList)
{
if (gap == null || gap.Removed) { continue; }
if (EscapeTarget == gap) { continue; }
if (unreachableGaps.Contains(gap)) { continue; }
if (gap.Submarine != Character.Submarine) { continue; }
if (gap.IsRoomToRoom) { continue; }
float multiplier = 1;
var door = gap.ConnectedDoor;
if (door != null)
{
if (!door.CanBeTraversed)
{
if (!door.HasAccess(Character))
{
if (!canAttackDoors) { continue; }
// Treat doors that don't have access to like they were farther, because it will take time to break them.
multiplier = 5;
}
}
}
else
{
if (gap.Open < 1) { continue; }
if (gap.Size < minGapSize) { continue; }
}
if (gap.FlowTargetHull == Character.CurrentHull)
{
// If the gap is in the same room, it's close enough.
EscapeTarget = gap;
break;
}
float distance = Vector2.DistanceSquared(Character.WorldPosition, gap.WorldPosition) * multiplier;
if (EscapeTarget == null || distance < closestDistance)
{
EscapeTarget = gap;
closestDistance = distance;
}
}
allGapsSearched = true;
escapeTimer = escapeTargetSeekInterval;
}
else if (EscapeTarget != null && EscapeTarget.FlowTargetHull != Character.CurrentHull)
{
if (IsCurrentPathUnreachable)
{
unreachableGaps.Add(EscapeTarget);
EscapeTarget = null;
allGapsSearched = false;
}
}
}
if (EscapeTarget != null)
{
var door = EscapeTarget.ConnectedDoor;
bool isClosedDoor = door != null && !door.IsOpen;
Vector2 diff = EscapeTarget.WorldPosition - Character.WorldPosition;
float sqrDist = diff.LengthSquared();
bool isClose = sqrDist < MathUtils.Pow2(100);
if (Character.CurrentHull == null || isClose && !isClosedDoor || pathSteering == null || IsCurrentPathNullOrUnreachable || IsCurrentPathFinished)
{
// Very close to the target, outside, or at the end of the path -> try to steer through the gap
SteeringManager.Reset();
pathSteering?.ResetPath();
Vector2 dir = Vector2.Normalize(diff);
if (Character.CurrentHull == null || isClose)
{
// Outside -> steer away from the target
if (EscapeTarget.FlowTargetHull != null)
{
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(EscapeTarget.WorldPosition - EscapeTarget.FlowTargetHull.WorldPosition));
}
else
{
SteeringManager.SteeringManual(deltaTime, -dir);
}
}
else
{
// Still inside -> steer towards the target
SteeringManager.SteeringManual(deltaTime, dir);
}
return sqrDist < MathUtils.Pow2(250);
}
else if (pathSteering != null)
{
pathSteering.SteeringSeek(EscapeTarget.SimPosition, weight: 1, minGapSize);
}
else
{
SteeringManager.SteeringSeek(EscapeTarget.SimPosition, 10);
}
}
else
{
// Can't find the target
EscapeTarget = null;
allGapsSearched = false;
unreachableGaps.Clear();
}
return false;
}
public void ResetEscape()
{
EscapeTarget = null;
allGapsSearched = false;
unreachableGaps.Clear();
}
#endregion
protected virtual void OnStateChanged(AIState from, AIState to) { }
protected virtual void OnTargetChanged(AITarget previousTarget, AITarget newTarget) { }
public virtual void ClientRead(IReadMessage msg) { }
public virtual void ServerWrite(IWriteMessage msg) { }
}
}