v0.12.0.2
This commit is contained in:
@@ -1,18 +1,17 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Barotrauma.Items.Components;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
public enum AIState { Idle, Attack, Escape, Eat, Flee, Avoid, Aggressive, PassiveAggressive, Protect, Observe, Freeze, Follow }
|
||||
|
||||
abstract partial class AIController : ISteerable
|
||||
{
|
||||
public bool Enabled;
|
||||
|
||||
public readonly Character Character;
|
||||
|
||||
private AIState state;
|
||||
|
||||
// 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).
|
||||
@@ -74,25 +73,6 @@ namespace Barotrauma
|
||||
get { return true; }
|
||||
}
|
||||
|
||||
public virtual AIObjectiveManager ObjectiveManager
|
||||
{
|
||||
get { return null; }
|
||||
}
|
||||
|
||||
public AIState State
|
||||
{
|
||||
get { return state; }
|
||||
set
|
||||
{
|
||||
if (state == value) { return; }
|
||||
PreviousState = state;
|
||||
OnStateChanged(state, value);
|
||||
state = value;
|
||||
}
|
||||
}
|
||||
|
||||
public AIState PreviousState { get; protected set; }
|
||||
|
||||
private IEnumerable<Hull> visibleHulls;
|
||||
private float hullVisibilityTimer;
|
||||
const float hullVisibilityInterval = 0.5f;
|
||||
@@ -112,6 +92,9 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
protected bool HasValidPath(bool requireNonDirty = false) =>
|
||||
steeringManager is IndoorsSteeringManager pathSteering && pathSteering.CurrentPath != null && !pathSteering.CurrentPath.Finished && !pathSteering.CurrentPath.Unreachable && (!requireNonDirty || !pathSteering.IsPathDirty);
|
||||
|
||||
public AIController (Character c)
|
||||
{
|
||||
Character = c;
|
||||
@@ -149,8 +132,152 @@ namespace Barotrauma
|
||||
|
||||
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, Inventory targetInventory, bool equip, bool dropOtherIfCannotMove = true, bool allowSwapping = false, bool storeUnequipped = false)
|
||||
{
|
||||
var pickable = item.GetComponent<Pickable>();
|
||||
if (pickable == null) { return false; }
|
||||
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 ReequipUnequipped()
|
||||
{
|
||||
foreach (var item in unequippedItems)
|
||||
{
|
||||
if (item != null && !item.Removed && Character.HasItem(item))
|
||||
{
|
||||
TakeItem(item, Character.Inventory, equip: true, dropOtherIfCannotMove: true, allowSwapping: true, storeUnequipped: false);
|
||||
}
|
||||
}
|
||||
unequippedItems.Clear();
|
||||
}
|
||||
|
||||
protected virtual void OnStateChanged(AIState from, AIState to) { }
|
||||
protected virtual void OnTargetChanged(AITarget previousTarget, AITarget newTarget) { }
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,30 +10,32 @@ using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
public enum AIState { Idle, Attack, Escape, Eat, Flee, Avoid, Aggressive, PassiveAggressive, Protect, Observe, Freeze, Follow }
|
||||
|
||||
partial class EnemyAIController : AIController
|
||||
{
|
||||
public static bool DisableEnemyAI;
|
||||
|
||||
private AIState _state;
|
||||
public AIState State
|
||||
{
|
||||
get { return _state; }
|
||||
set
|
||||
{
|
||||
if (_state == value) { return; }
|
||||
PreviousState = _state;
|
||||
OnStateChanged(_state, value);
|
||||
_state = value;
|
||||
}
|
||||
}
|
||||
|
||||
public AIState PreviousState { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enable the character to attack the outposts and the characters inside them. Disabled by default in normal levels, enabled in outpost levels.
|
||||
/// </summary>
|
||||
public bool TargetOutposts;
|
||||
|
||||
// TODO: use a struct?
|
||||
class WallTarget
|
||||
{
|
||||
public Vector2 Position;
|
||||
public Structure Structure;
|
||||
public int SectionIndex;
|
||||
|
||||
public WallTarget(Vector2 position, Structure structure = null, int sectionIndex = -1)
|
||||
{
|
||||
Position = position;
|
||||
Structure = structure;
|
||||
SectionIndex = sectionIndex;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly float updateTargetsInterval = 1;
|
||||
private readonly float updateMemoriesInverval = 1;
|
||||
private readonly float attackLimbResetInterval = 2;
|
||||
@@ -56,9 +58,6 @@ namespace Barotrauma
|
||||
|
||||
private FishAnimController FishAnimController => Character.AnimController as FishAnimController;
|
||||
|
||||
//a point in a wall which the Character is currently targeting
|
||||
private WallTarget wallTarget;
|
||||
|
||||
//the limb selected for the current attack
|
||||
private Limb _attackingLimb;
|
||||
public Limb AttackingLimb
|
||||
@@ -159,6 +158,11 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private readonly float maxSteeringBuffer = 5000;
|
||||
private readonly float minSteeringBuffer = 500;
|
||||
private readonly float steeringBufferIncreaseSpeed = 100;
|
||||
private float steeringBuffer;
|
||||
|
||||
public EnemyAIController(Character c, string seed) : base(c)
|
||||
{
|
||||
if (c.IsHuman)
|
||||
@@ -168,10 +172,9 @@ namespace Barotrauma
|
||||
if (Character.Params.Group.Equals("human", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Pet
|
||||
Character.TeamID = Character.TeamType.FriendlyNPC;
|
||||
Character.TeamID = CharacterTeamType.FriendlyNPC;
|
||||
}
|
||||
CharacterPrefab prefab = CharacterPrefab.FindBySpeciesName(c.SpeciesName);
|
||||
var mainElement = prefab.XDocument.Root.IsOverride() ? prefab.XDocument.Root.FirstElement() : prefab.XDocument.Root;
|
||||
var mainElement = c.Params.OriginalElement.IsOverride() ? c.Params.OriginalElement.FirstElement() : c.Params.OriginalElement;
|
||||
targetMemories = new Dictionary<AITarget, AITargetMemory>();
|
||||
steeringManager = outsideSteering;
|
||||
//allow targeting outposts and outpost NPCs in outpost levels
|
||||
@@ -188,7 +191,7 @@ namespace Barotrauma
|
||||
|
||||
if (aiElements.Count == 0)
|
||||
{
|
||||
DebugConsole.ThrowError("Error in file \"" + prefab.FilePath + "\" - no AI element found.");
|
||||
DebugConsole.ThrowError("Error in file \"" + c.Params.File + "\" - no AI element found.");
|
||||
outsideSteering = new SteeringManager(this);
|
||||
insideSteering = new IndoorsSteeringManager(this, false, false);
|
||||
return;
|
||||
@@ -229,7 +232,7 @@ namespace Barotrauma
|
||||
|
||||
ReevaluateAttacks();
|
||||
outsideSteering = new SteeringManager(this);
|
||||
insideSteering = new IndoorsSteeringManager(this, false, canAttackDoors);
|
||||
insideSteering = new IndoorsSteeringManager(this, Character.IsHumanoid, canAttackDoors);
|
||||
steeringManager = outsideSteering;
|
||||
State = AIState.Idle;
|
||||
|
||||
@@ -242,7 +245,24 @@ namespace Barotrauma
|
||||
myBodies = Character.AnimController.Limbs.Select(l => l.body.FarseerBody);
|
||||
}
|
||||
|
||||
public CharacterParams.AIParams AIParams => Character.Params.AI;
|
||||
private CharacterParams.AIParams _aiParams;
|
||||
public CharacterParams.AIParams AIParams
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_aiParams == null)
|
||||
{
|
||||
_aiParams = Character.Params.AI;
|
||||
if (_aiParams == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"No AI Params defined for {Character.SpeciesName}. AI disabled.");
|
||||
Enabled = false;
|
||||
_aiParams = new CharacterParams.AIParams(null, Character.Params);
|
||||
}
|
||||
}
|
||||
return _aiParams;
|
||||
}
|
||||
}
|
||||
private CharacterParams.TargetParams GetTargetParams(string targetTag) => AIParams.GetTarget(targetTag, false);
|
||||
private CharacterParams.TargetParams GetTargetParams(AITarget aiTarget) => GetTargetParams(GetTargetingTag(aiTarget));
|
||||
private string GetTargetingTag(AITarget aiTarget)
|
||||
@@ -321,7 +341,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
private float movementMargin;
|
||||
|
||||
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
if (DisableEnemyAI) { return; }
|
||||
@@ -341,14 +361,23 @@ namespace Barotrauma
|
||||
ignorePlatforms = height < allowedJumpHeight;
|
||||
}
|
||||
}
|
||||
if (Character.IsClimbing && PathSteering.IsNextLadderSameAsCurrent)
|
||||
{
|
||||
Character.AnimController.TargetMovement = new Vector2(0.0f, Math.Sign(Character.AnimController.TargetMovement.Y));
|
||||
}
|
||||
}
|
||||
Character.AnimController.IgnorePlatforms = ignorePlatforms;
|
||||
|
||||
//clients get the facing direction from the server
|
||||
if (Character.AnimController is HumanoidAnimController &&
|
||||
if (Math.Abs(Character.AnimController.movement.X) > 0.1f && !Character.AnimController.InWater &&
|
||||
(GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer || Character.Controlled == Character))
|
||||
{
|
||||
if (Math.Abs(Character.AnimController.movement.X) > 0.1f && !Character.AnimController.InWater)
|
||||
if (SelectedAiTarget?.Entity != null || escapeTarget != null)
|
||||
{
|
||||
Entity t = SelectedAiTarget?.Entity ?? escapeTarget;
|
||||
float referencePos = Vector2.DistanceSquared(Character.WorldPosition, t.WorldPosition) > 100 * 100 && HasValidPath(true) ? PathSteering.CurrentPath.CurrentNode.WorldPosition.X : t.WorldPosition.X;
|
||||
Character.AnimController.TargetDir = Character.WorldPosition.X < referencePos ? Direction.Right : Direction.Left;
|
||||
}
|
||||
else
|
||||
{
|
||||
Character.AnimController.TargetDir = Character.AnimController.movement.X > 0.0f ? Direction.Right : Direction.Left;
|
||||
}
|
||||
@@ -389,7 +418,7 @@ namespace Barotrauma
|
||||
FadeMemories(updateMemoriesInverval);
|
||||
updateMemoriesTimer = updateMemoriesInverval;
|
||||
}
|
||||
if (Character.HealthPercentage <= FleeHealthThreshold && SelectedAiTarget != null &&
|
||||
if (Math.Max(Character.HealthPercentage, 0) < FleeHealthThreshold && SelectedAiTarget != null &&
|
||||
SelectedAiTarget.Entity is Character target && (target.IsHuman && CanPerceive(SelectedAiTarget) || IsBeingChasedBy(target)))
|
||||
{
|
||||
// Keep fleeing if being chased
|
||||
@@ -408,7 +437,7 @@ namespace Barotrauma
|
||||
UpdateTargets(Character, out targetingParams);
|
||||
if (!IsLatchedOnSub)
|
||||
{
|
||||
UpdateWallTarget();
|
||||
UpdateWallTarget(requiredHoleCount);
|
||||
}
|
||||
updateTargetsTimer = updateTargetsInterval * Rand.Range(0.75f, 1.25f);
|
||||
if (SelectedAiTarget == null)
|
||||
@@ -423,21 +452,48 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
if (Character.CurrentHull == null)
|
||||
if (AIParams.Infiltrate)
|
||||
{
|
||||
if (steeringManager != outsideSteering)
|
||||
bool IsCloseEnoughToTargetSub(float threshold) => SelectedAiTarget?.Entity?.Submarine is Submarine sub && sub != null && Vector2.DistanceSquared(Character.WorldPosition, sub.WorldPosition) < MathUtils.Pow(Math.Max(sub.Borders.Size.X, sub.Borders.Size.Y) / 2 + threshold, 2);
|
||||
|
||||
if (Character.Submarine != null || HasValidPath() && IsCloseEnoughToTargetSub(maxSteeringBuffer) || IsCloseEnoughToTargetSub(steeringBuffer))
|
||||
{
|
||||
outsideSteering.Reset();
|
||||
if (steeringManager != insideSteering)
|
||||
{
|
||||
insideSteering.Reset();
|
||||
}
|
||||
steeringManager = insideSteering;
|
||||
steeringBuffer += steeringBufferIncreaseSpeed * deltaTime;
|
||||
}
|
||||
steeringManager = outsideSteering;
|
||||
else
|
||||
{
|
||||
if (steeringManager != outsideSteering)
|
||||
{
|
||||
outsideSteering.Reset();
|
||||
}
|
||||
steeringManager = outsideSteering;
|
||||
steeringBuffer = minSteeringBuffer;
|
||||
}
|
||||
steeringBuffer = Math.Clamp(steeringBuffer, minSteeringBuffer, maxSteeringBuffer);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (steeringManager != insideSteering)
|
||||
if (Character.Submarine != null)
|
||||
{
|
||||
insideSteering.Reset();
|
||||
if (steeringManager != insideSteering)
|
||||
{
|
||||
insideSteering.Reset();
|
||||
}
|
||||
steeringManager = insideSteering;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (steeringManager != outsideSteering)
|
||||
{
|
||||
outsideSteering.Reset();
|
||||
}
|
||||
steeringManager = outsideSteering;
|
||||
}
|
||||
steeringManager = insideSteering;
|
||||
}
|
||||
|
||||
bool useSteeringLengthAsMovementSpeed = State == AIState.Idle && Character.AnimController.InWater;
|
||||
@@ -519,12 +575,24 @@ namespace Barotrauma
|
||||
}
|
||||
if (State == AIState.Protect)
|
||||
{
|
||||
if (SelectedAiTarget.Entity is Character targetCharacter && targetCharacter.LastAttacker is Character attacker && !attacker.Removed && !attacker.IsDead)
|
||||
if (SelectedAiTarget.Entity is Character targetCharacter)
|
||||
{
|
||||
// Attack the character that attacked the target we are protecting
|
||||
ChangeTargetState(attacker, AIState.Attack, selectedTargetingParams.Priority * 2);
|
||||
SelectTarget(attacker.AiTarget);
|
||||
return;
|
||||
bool IsValid(Character.Attacker a)
|
||||
{
|
||||
Character c = a.Character;
|
||||
if (c.IsDead || c.Removed) { return false; }
|
||||
if (!IsFriendly(Character, c)) { return true; }
|
||||
// Only apply the threshold to friendly characters
|
||||
return a.Damage >= selectedTargetingParams.Threshold;
|
||||
}
|
||||
Character attacker = targetCharacter.LastAttackers.LastOrDefault(IsValid)?.Character;
|
||||
if (attacker != null)
|
||||
{
|
||||
// Attack the character that attacked the target we are protecting
|
||||
ChangeTargetState(attacker, AIState.Attack, selectedTargetingParams.Priority * 2);
|
||||
SelectTarget(attacker.AiTarget);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
float sqrDist = Vector2.DistanceSquared(WorldPosition, SelectedAiTarget.WorldPosition);
|
||||
@@ -622,8 +690,8 @@ namespace Barotrauma
|
||||
Character.AnimController.TargetMovement = Character.ApplyMovementLimits(Steering, targetMovement);
|
||||
if (Character.CurrentHull != null && Character.AnimController.InWater)
|
||||
{
|
||||
// Halve the swimming speed inside the sub
|
||||
Character.AnimController.TargetMovement *= 0.5f;
|
||||
// Limit the swimming speed inside the sub.
|
||||
Character.AnimController.TargetMovement = Character.AnimController.TargetMovement.ClampLength(5);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -691,6 +759,8 @@ namespace Barotrauma
|
||||
#endregion
|
||||
|
||||
#region Escape
|
||||
private readonly float escapeTargetSeekInterval = 2;
|
||||
private float escapeTimer;
|
||||
private Gap escapeTarget;
|
||||
private bool allGapsSearched;
|
||||
private readonly HashSet<Gap> unreachableGaps = new HashSet<Gap>();
|
||||
@@ -707,35 +777,62 @@ namespace Barotrauma
|
||||
}
|
||||
IndoorsSteeringManager pathSteering = SteeringManager as IndoorsSteeringManager;
|
||||
bool hasValidPath = pathSteering?.CurrentPath != null && !pathSteering.IsPathDirty && !pathSteering.CurrentPath.Unreachable;
|
||||
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.Open < 1 || gap.IsRoomToRoom) { continue; }
|
||||
bool canGetThrough = ConvertUnits.ToDisplayUnits(colliderWidth) < gap.Size;
|
||||
if (!canGetThrough) { continue; }
|
||||
if (escapeTarget == null)
|
||||
if (gap.IsRoomToRoom) { continue; }
|
||||
float multiplier = 1;
|
||||
var door = gap.ConnectedDoor;
|
||||
if (door != null)
|
||||
{
|
||||
escapeTarget = gap;
|
||||
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.FlowTargetHull == Character.CurrentHull)
|
||||
else
|
||||
{
|
||||
if (gap.Open < 1) { continue; }
|
||||
bool canGetThrough = ConvertUnits.ToDisplayUnits(colliderWidth) < gap.Size;
|
||||
if (!canGetThrough) { continue; }
|
||||
}
|
||||
if (gap.FlowTargetHull == Character.CurrentHull)
|
||||
{
|
||||
// If the gap is in the same room, it's close enough.
|
||||
escapeTarget = gap;
|
||||
break;
|
||||
}
|
||||
else if (Vector2.DistanceSquared(Character.SimPosition, gap.SimPosition) < Vector2.DistanceSquared(Character.SimPosition, escapeTarget.SimPosition))
|
||||
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)
|
||||
{
|
||||
@@ -760,36 +857,25 @@ namespace Barotrauma
|
||||
Vector2 escapeDir = Vector2.Normalize(SelectedAiTarget != null ? WorldPosition - SelectedAiTarget.WorldPosition : Character.AnimController.TargetMovement);
|
||||
if (!MathUtils.IsValid(escapeDir)) { escapeDir = Vector2.UnitY; }
|
||||
SteeringManager.SteeringManual(deltaTime, escapeDir);
|
||||
return;
|
||||
}
|
||||
else if (pathSteering != null)
|
||||
{
|
||||
if (canAttackDoors && hasValidPath)
|
||||
if (hasValidPath && canAttackDoors)
|
||||
{
|
||||
var door = pathSteering.CurrentPath.CurrentNode?.ConnectedDoor ?? pathSteering.CurrentPath.NextNode?.ConnectedDoor;
|
||||
if (door != null && !door.IsOpen && !door.IsBroken)
|
||||
if (door != null && !door.CanBeTraversed && !door.HasAccess(Character))
|
||||
{
|
||||
if (SelectedAiTarget != door.Item.AiTarget)
|
||||
if (SelectedAiTarget != door.Item.AiTarget || State != AIState.Attack)
|
||||
{
|
||||
SelectTarget(door.Item.AiTarget);
|
||||
SelectTarget(door.Item.AiTarget, selectedTargetMemory.Priority);
|
||||
State = AIState.Attack;
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
SteeringManager.SteeringSeek(escapeTarget.SimPosition, 5);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
SteeringManager.SteeringSeek(escapeTarget.SimPosition, 5);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
SteeringManager.SteeringSeek(escapeTarget.SimPosition, 10);
|
||||
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15);
|
||||
}
|
||||
SteeringManager.SteeringSeek(escapeTarget.SimPosition, 10);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -860,49 +946,9 @@ namespace Barotrauma
|
||||
|
||||
if (Character.AnimController.CanEnterSubmarine)
|
||||
{
|
||||
//targeting a wall section that can be passed through -> steer manually through the hole
|
||||
if (wallTarget != null && wallTarget.SectionIndex > -1 && CanPassThroughHole(wallTarget.Structure, wallTarget.SectionIndex))
|
||||
if (TrySteerThroughGaps(deltaTime))
|
||||
{
|
||||
WallSection section = wallTarget.Structure.GetSection(wallTarget.SectionIndex);
|
||||
Vector2 targetPos = wallTarget.Structure.SectionPosition(wallTarget.SectionIndex, true);
|
||||
if (section?.gap != null && SteerThroughGap(wallTarget.Structure, section, targetPos, deltaTime))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (SelectedAiTarget.Entity is Structure wall)
|
||||
{
|
||||
for (int i = 0; i < wall.Sections.Length; i++)
|
||||
{
|
||||
WallSection section = wall.Sections[i];
|
||||
if (CanPassThroughHole(wall, i) && section?.gap != null)
|
||||
{
|
||||
if (SteerThroughGap(wall, section, wall.SectionPosition(i, true), deltaTime))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (SelectedAiTarget.Entity is Item i)
|
||||
{
|
||||
var door = i.GetComponent<Door>();
|
||||
// Steer through the door manually if it's open or broken
|
||||
// Don't try to enter dry hulls if cannot walk or if the gap is too narrow
|
||||
if (door?.LinkedGap?.FlowTargetHull != null && !door.LinkedGap.IsRoomToRoom && (door.IsOpen || door.IsBroken))
|
||||
{
|
||||
if (Character.AnimController.CanWalk || door.LinkedGap.FlowTargetHull.WaterPercentage > 25)
|
||||
{
|
||||
if (door.LinkedGap.Size > ConvertUnits.ToDisplayUnits(colliderWidth))
|
||||
{
|
||||
LatchOntoAI?.DeattachFromBody(reset: true, cooldown: 2);
|
||||
Character.AnimController.ReleaseStuckLimbs();
|
||||
var velocity = Vector2.Normalize(door.LinkedGap.FlowTargetHull.WorldPosition - Character.WorldPosition);
|
||||
steeringManager.SteeringManual(deltaTime, velocity);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (SelectedAiTarget.Entity is Structure w && wallTarget == null)
|
||||
@@ -932,12 +978,6 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
if (Math.Abs(Character.AnimController.movement.X) > 0.1f && !Character.AnimController.InWater &&
|
||||
(GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer || Character.Controlled == Character))
|
||||
{
|
||||
Character.AnimController.TargetDir = Character.WorldPosition.X < attackWorldPos.X ? Direction.Right : Direction.Left;
|
||||
}
|
||||
|
||||
bool canAttack = true;
|
||||
bool pursue = false;
|
||||
if (IsCoolDownRunning)
|
||||
@@ -1119,30 +1159,34 @@ namespace Barotrauma
|
||||
}
|
||||
canAttack = AttackingLimb != null && AttackingLimb.attack.CoolDownTimer <= 0;
|
||||
}
|
||||
if (!Character.AnimController.SimplePhysicsEnabled && SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null && (!canAttackDoors || !canAttackWalls || !AIParams.TargetOuterWalls))
|
||||
if (!AIParams.Infiltrate)
|
||||
{
|
||||
if (wallTarget == null && Vector2.DistanceSquared(Character.WorldPosition, attackWorldPos) < 2000 * 2000)
|
||||
if (!Character.AnimController.SimplePhysicsEnabled && SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null && (!canAttackDoors || !canAttackWalls || !AIParams.TargetOuterWalls))
|
||||
{
|
||||
// Check that we are not bumping into a door or a wall
|
||||
Vector2 rayStart = SimPosition;
|
||||
if (Character.Submarine == null)
|
||||
if (wallTarget == null && Vector2.DistanceSquared(Character.WorldPosition, attackWorldPos) < 2000 * 2000)
|
||||
{
|
||||
rayStart -= SelectedAiTarget.Entity.Submarine.SimPosition;
|
||||
}
|
||||
Vector2 dir = SelectedAiTarget.WorldPosition - WorldPosition;
|
||||
Vector2 rayEnd = rayStart + dir.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 2);
|
||||
Body closestBody = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true);
|
||||
if (Submarine.LastPickedFraction != 1.0f && closestBody != null &&
|
||||
(!AIParams.TargetOuterWalls || !canAttackWalls && closestBody.UserData is Structure s && s.Submarine != null || !canAttackDoors && closestBody.UserData is Item i && i.Submarine != null && i.GetComponent<Door>() != null))
|
||||
{
|
||||
// Target is unreachable, there's a door or wall ahead
|
||||
State = AIState.Idle;
|
||||
IgnoreTarget(SelectedAiTarget);
|
||||
ResetAITarget();
|
||||
return;
|
||||
// Check that we are not bumping into a door or a wall
|
||||
Vector2 rayStart = SimPosition;
|
||||
if (Character.Submarine == null)
|
||||
{
|
||||
rayStart -= SelectedAiTarget.Entity.Submarine.SimPosition;
|
||||
}
|
||||
Vector2 dir = SelectedAiTarget.WorldPosition - WorldPosition;
|
||||
Vector2 rayEnd = rayStart + dir.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 2);
|
||||
Body closestBody = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true);
|
||||
if (Submarine.LastPickedFraction != 1.0f && closestBody != null &&
|
||||
(!AIParams.TargetOuterWalls || !canAttackWalls && closestBody.UserData is Structure s && s.Submarine != null || !canAttackDoors && closestBody.UserData is Item i && i.Submarine != null && i.GetComponent<Door>() != null))
|
||||
{
|
||||
// Target is unreachable, there's a door or wall ahead
|
||||
State = AIState.Idle;
|
||||
IgnoreTarget(SelectedAiTarget);
|
||||
ResetAITarget();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
float distance = 0;
|
||||
Limb attackTargetLimb = null;
|
||||
Character targetCharacter = SelectedAiTarget.Entity as Character;
|
||||
@@ -1202,6 +1246,16 @@ namespace Barotrauma
|
||||
// Check that we can reach the target
|
||||
distance = toTarget.Length();
|
||||
canAttack = distance < AttackingLimb.attack.Range;
|
||||
|
||||
// Crouch if the target is down (only humanoids), so that we can reach it.
|
||||
if (Character.AnimController is HumanoidAnimController humanoidAnimController && distance < AttackingLimb.attack.Range * 2)
|
||||
{
|
||||
if (Math.Abs(toTarget.Y) > AttackingLimb.attack.Range / 2 && Math.Abs(toTarget.X) <= AttackingLimb.attack.Range)
|
||||
{
|
||||
humanoidAnimController.Crouching = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (canAttack)
|
||||
{
|
||||
if (AttackingLimb.attack.Ranged)
|
||||
@@ -1303,26 +1357,27 @@ namespace Barotrauma
|
||||
if (targetCharacter == null || targetCharacter.CurrentHull != Character.CurrentHull)
|
||||
{
|
||||
var door = pathSteering.CurrentPath.CurrentNode?.ConnectedDoor ?? pathSteering.CurrentPath.NextNode?.ConnectedDoor;
|
||||
if (door != null && !door.IsOpen && !door.IsBroken)
|
||||
if (door != null && !door.CanBeTraversed && !door.HasAccess(Character))
|
||||
{
|
||||
if (door.Item.AiTarget != null && SelectedAiTarget != door.Item.AiTarget)
|
||||
{
|
||||
SelectTarget(door.Item.AiTarget, selectedTargetMemory.Priority);
|
||||
State = AIState.Attack;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Steer towards the target if in the same room and swimming
|
||||
if ((Character.AnimController.InWater || pursue || !Character.AnimController.CanWalk) &&
|
||||
(targetCharacter != null && VisibleHulls.Contains(targetCharacter.CurrentHull) || Character.CanSeeTarget(SelectedAiTarget.Entity)))
|
||||
if (Character.CurrentHull != null && ((Character.AnimController.InWater || pursue || !Character.AnimController.CanWalk) &&
|
||||
(targetCharacter != null && VisibleHulls.Contains(targetCharacter.CurrentHull))))
|
||||
{
|
||||
Vector2 myPos = Character.AnimController.SimplePhysicsEnabled ? Character.SimPosition : steeringLimb.SimPosition;
|
||||
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(steerPos - myPos));
|
||||
}
|
||||
else
|
||||
{
|
||||
SteeringManager.SteeringSeek(steerPos, 2);
|
||||
pathSteering.SteeringSeek(steerPos, 2, startNodeFilter: n => (n.Waypoint.CurrentHull == null) == (Character.CurrentHull == null), checkVisiblity: true);
|
||||
// Switch to Idle when cannot reach the target and if cannot damage the walls
|
||||
if ((!canAttackWalls || wallTarget == null) && !pathSteering.IsPathDirty && pathSteering.CurrentPath.Unreachable)
|
||||
{
|
||||
@@ -1363,6 +1418,16 @@ namespace Barotrauma
|
||||
}
|
||||
if (canAttack)
|
||||
{
|
||||
if (SelectedAiTarget.Entity is Item targetItem)
|
||||
{
|
||||
var door = targetItem.GetComponent<Door>();
|
||||
if (door != null && door.CanBeTraversed)
|
||||
{
|
||||
ResetAITarget();
|
||||
State = PreviousState;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!UpdateLimbAttack(deltaTime, AttackingLimb, attackSimPos, distance, attackTargetLimb))
|
||||
{
|
||||
IgnoreTarget(SelectedAiTarget);
|
||||
@@ -1370,38 +1435,6 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsSteeringThroughGap { get; private set; }
|
||||
private bool SteerThroughGap(Structure wall, WallSection section, Vector2 targetWorldPos, float deltaTime)
|
||||
{
|
||||
IsSteeringThroughGap = true;
|
||||
wallTarget = null;
|
||||
LatchOntoAI?.DeattachFromBody(reset: true, cooldown: 2);
|
||||
Character.AnimController.ReleaseStuckLimbs();
|
||||
Hull targetHull = section.gap?.FlowTargetHull;
|
||||
float maxDistance = Math.Min(wall.Rect.Width, wall.Rect.Height);
|
||||
if (Vector2.DistanceSquared(Character.WorldPosition, targetWorldPos) > maxDistance * maxDistance)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (targetHull != null)
|
||||
{
|
||||
// If already inside, target the hull, else target the wall.
|
||||
SelectedAiTarget = Character.CurrentHull != null ? targetHull.AiTarget : wall.AiTarget;
|
||||
if (wall.IsHorizontal)
|
||||
{
|
||||
targetWorldPos.Y = targetHull.WorldRect.Y - targetHull.Rect.Height / 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
targetWorldPos.X = targetHull.WorldRect.Center.X;
|
||||
}
|
||||
steeringManager.SteeringManual(deltaTime, Vector2.Normalize(targetWorldPos - Character.WorldPosition));
|
||||
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private readonly List<Limb> attackLimbs = new List<Limb>();
|
||||
private readonly List<float> weights = new List<float>();
|
||||
private Limb GetAttackLimb(Vector2 attackWorldPos, Limb ignoredLimb = null)
|
||||
@@ -1471,106 +1504,6 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateWallTarget()
|
||||
{
|
||||
wallTarget = null;
|
||||
if (SelectedAiTarget == null) { return; }
|
||||
if (SelectedAiTarget.Entity == null) { return; }
|
||||
//check if there's a wall between the target and the Character
|
||||
Vector2 rayStart = SimPosition;
|
||||
Vector2 rayEnd = SelectedAiTarget.SimPosition;
|
||||
if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null)
|
||||
{
|
||||
rayStart -= SelectedAiTarget.Entity.Submarine.SimPosition;
|
||||
}
|
||||
else if (SelectedAiTarget.Entity.Submarine == null && Character.Submarine != null)
|
||||
{
|
||||
rayEnd -= Character.Submarine.SimPosition;
|
||||
}
|
||||
Body closestBody = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true, ignoreSensors: CanEnterSubmarine, ignoreDisabledWalls: CanEnterSubmarine);
|
||||
if (Submarine.LastPickedFraction != 1.0f && closestBody != null)
|
||||
{
|
||||
if (closestBody.UserData is Structure wall && wall.Submarine != null && (wall.Submarine.Info.IsPlayer || wall.Submarine.Info.IsOutpost && TargetOutposts))
|
||||
{
|
||||
int sectionIndex = wall.FindSectionIndex(ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition));
|
||||
float sectionDamage = wall.SectionDamage(sectionIndex);
|
||||
for (int i = sectionIndex - 2; i <= sectionIndex + 2; i++)
|
||||
{
|
||||
if (wall.SectionBodyDisabled(i))
|
||||
{
|
||||
if (Character.AnimController.CanEnterSubmarine && CanPassThroughHole(wall, i))
|
||||
{
|
||||
sectionIndex = i;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
//otherwise ignore and keep breaking other sections
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (wall.SectionDamage(i) > sectionDamage)
|
||||
{
|
||||
sectionIndex = i;
|
||||
}
|
||||
}
|
||||
Vector2 sectionPos = wall.SectionPosition(sectionIndex);
|
||||
Vector2 attachTargetNormal;
|
||||
if (wall.IsHorizontal)
|
||||
{
|
||||
attachTargetNormal = new Vector2(0.0f, Math.Sign(WorldPosition.Y - wall.WorldPosition.Y));
|
||||
sectionPos.Y += (wall.BodyHeight <= 0.0f ? wall.Rect.Height : wall.BodyHeight) / 2 * attachTargetNormal.Y;
|
||||
}
|
||||
else
|
||||
{
|
||||
attachTargetNormal = new Vector2(Math.Sign(WorldPosition.X - wall.WorldPosition.X), 0.0f);
|
||||
sectionPos.X += (wall.BodyWidth <= 0.0f ? wall.Rect.Width : wall.BodyWidth) / 2 * attachTargetNormal.X;
|
||||
}
|
||||
LatchOntoAI?.SetAttachTarget(wall, ConvertUnits.ToSimUnits(sectionPos), attachTargetNormal);
|
||||
if (Character.AnimController.CanEnterSubmarine || !wall.SectionBodyDisabled(sectionIndex) && !IsWallDisabled(wall))
|
||||
{
|
||||
if (AIParams.TargetOuterWalls || wall.prefab.Tags.Contains("inner") || wall.Submarine != null && wall.Submarine == Character.Submarine)
|
||||
{
|
||||
if (wall.NoAITarget && Character.AnimController.CanEnterSubmarine)
|
||||
{
|
||||
// Blocked by a wall that shouldn't be targeted. The main intention here is to prevents monsters from entering the the tail and the nose pieces.
|
||||
IgnoreTarget(SelectedAiTarget);
|
||||
ResetAITarget();
|
||||
}
|
||||
else
|
||||
{
|
||||
wallTarget = new WallTarget(sectionPos, wall, sectionIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!Character.AnimController.CanEnterSubmarine && wallTarget == null)
|
||||
{
|
||||
if (closestBody.UserData is Structure w && w.Submarine != null || closestBody.UserData is Item i && i.Submarine != null)
|
||||
{
|
||||
// Cannot reach the target, because it's blocked by a disabled wall or a door
|
||||
State = AIState.Idle;
|
||||
IgnoreTarget(SelectedAiTarget);
|
||||
ResetAITarget();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private 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;
|
||||
}
|
||||
|
||||
public override void OnAttacked(Character attacker, AttackResult attackResult)
|
||||
{
|
||||
float reactionTime = Rand.Range(0.1f, 0.3f);
|
||||
@@ -1603,7 +1536,7 @@ namespace Barotrauma
|
||||
if (!isFriendly && attackResult.Damage > 0.0f)
|
||||
{
|
||||
bool canAttack = attacker.Submarine == Character.Submarine && canAttackCharacters || attacker.Submarine != null && canAttackWalls;
|
||||
if (Character.Params.AI.AttackWhenProvoked && canAttack)
|
||||
if (AIParams.AttackWhenProvoked && canAttack)
|
||||
{
|
||||
if (attacker.IsHusk)
|
||||
{
|
||||
@@ -1661,7 +1594,7 @@ namespace Barotrauma
|
||||
|
||||
// Only allow to react once. Otherwise would attack the target with only a fraction of a cooldown
|
||||
bool retaliate = !isFriendly && SelectedAiTarget != attacker.AiTarget && attacker.Submarine == Character.Submarine;
|
||||
bool avoidGunFire = Character.Params.AI.AvoidGunfire && attacker.Submarine != Character.Submarine;
|
||||
bool avoidGunFire = AIParams.AvoidGunfire && attacker.Submarine != Character.Submarine;
|
||||
|
||||
if (State == AIState.Attack && !IsCoolDownRunning)
|
||||
{
|
||||
@@ -1686,7 +1619,7 @@ namespace Barotrauma
|
||||
avoidTimer = AIParams.AvoidTime * Rand.Range(0.75f, 1.25f);
|
||||
SelectTarget(attacker.AiTarget);
|
||||
}
|
||||
if (Character.HealthPercentage <= FleeHealthThreshold)
|
||||
if (Math.Max(Character.HealthPercentage, 0) < FleeHealthThreshold)
|
||||
{
|
||||
State = AIState.Flee;
|
||||
avoidTimer = AIParams.MinFleeTime * Rand.Range(0.75f, 1.25f);
|
||||
@@ -1707,6 +1640,7 @@ namespace Barotrauma
|
||||
if (aiTarget != null && SelectedAiTarget != aiTarget)
|
||||
{
|
||||
SelectTarget(aiTarget, GetTargetMemory(SelectedAiTarget, true).Priority);
|
||||
State = AIState.Attack;
|
||||
}
|
||||
}
|
||||
IDamageable damageTarget = wallTarget != null ? wallTarget.Structure : SelectedAiTarget.Entity as IDamageable;
|
||||
@@ -1974,7 +1908,7 @@ namespace Barotrauma
|
||||
bool targetingFromOutsideToInside = item.CurrentHull != null && character.CurrentHull == null;
|
||||
if (targetingFromOutsideToInside)
|
||||
{
|
||||
if (door != null && !canAttackDoors || !canAttackWalls)
|
||||
if (door != null && (!canAttackDoors && !AIParams.Infiltrate) || !canAttackWalls)
|
||||
{
|
||||
// Can't reach
|
||||
continue;
|
||||
@@ -2122,16 +2056,14 @@ namespace Barotrauma
|
||||
bool isOutdoor = door.LinkedGap?.FlowTargetHull != null && !door.LinkedGap.IsRoomToRoom;
|
||||
// Ignore inner doors when outside
|
||||
if (character.CurrentHull == null && !isOutdoor) { continue; }
|
||||
bool isOpen = door.IsOpen || door.IsBroken;
|
||||
if (!isOpen && !canAttackDoors || (isOutdoor && !AIParams.TargetOuterWalls))
|
||||
bool isOpen = door.CanBeTraversed;
|
||||
if (!isOpen)
|
||||
{
|
||||
// Ignore doors that are not open if cannot attack doors or shouldn't target outer doors.
|
||||
continue;
|
||||
if (!canAttackDoors || isOutdoor && !AIParams.TargetOuterWalls) { continue; }
|
||||
}
|
||||
if (isOpen && (!Character.AnimController.CanEnterSubmarine || !AggressiveBoarding))
|
||||
else if (!Character.AnimController.CanEnterSubmarine)
|
||||
{
|
||||
// Ignore broken and open doors
|
||||
// Aggressive boarders don't ignore open doors, because they use them for getting in.
|
||||
// Ignore broken and open doors, if cannot enter submarine
|
||||
continue;
|
||||
}
|
||||
if (AggressiveBoarding)
|
||||
@@ -2157,8 +2089,9 @@ namespace Barotrauma
|
||||
if (targetingTag == null) { continue; }
|
||||
var targetParams = GetTargetParams(targetingTag);
|
||||
if (targetParams == null) { continue; }
|
||||
if (targetParams.IgnoreWhileInside && character.CurrentHull != null) { continue; }
|
||||
if (targetParams.IgnoreWhileOutside && character.CurrentHull == null) { continue; }
|
||||
if (targetParams.IgnoreInside && character.CurrentHull != null) { continue; }
|
||||
if (targetParams.IgnoreOutside && character.CurrentHull == null) { continue; }
|
||||
if (targetParams.IgnoreIncapacitated && targetCharacter != null && targetCharacter.IsIncapacitated) { continue; }
|
||||
if (targetParams.State == AIState.Observe || targetParams.State == AIState.Eat)
|
||||
{
|
||||
if (targetCharacter != null && targetCharacter.Submarine != Character.Submarine)
|
||||
@@ -2270,7 +2203,7 @@ namespace Barotrauma
|
||||
foreach (var gap in Character.CurrentHull.ConnectedGaps)
|
||||
{
|
||||
var door = gap.ConnectedDoor;
|
||||
if (door == null || !door.IsOpen && !door.IsBroken)
|
||||
if (door == null)
|
||||
{
|
||||
var wall = gap.ConnectedWall;
|
||||
if (wall != null)
|
||||
@@ -2293,7 +2226,7 @@ namespace Barotrauma
|
||||
newTarget = aiTarget;
|
||||
selectedTargetMemory = targetMemory;
|
||||
targetValue = valueModifier;
|
||||
targetingParams = GetTargetParams(targetingTag);
|
||||
targetingParams = targetParams;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2332,6 +2265,149 @@ namespace Barotrauma
|
||||
return SelectedAiTarget;
|
||||
}
|
||||
|
||||
class WallTarget
|
||||
{
|
||||
public Vector2 Position;
|
||||
public Structure Structure;
|
||||
public int SectionIndex;
|
||||
|
||||
public WallTarget(Vector2 position, Structure structure = null, int sectionIndex = -1)
|
||||
{
|
||||
Position = position;
|
||||
Structure = structure;
|
||||
SectionIndex = sectionIndex;
|
||||
}
|
||||
}
|
||||
|
||||
private WallTarget wallTarget;
|
||||
|
||||
private void UpdateWallTarget(int requiredHoleCount)
|
||||
{
|
||||
wallTarget = null;
|
||||
if (State == AIState.Flee || State == AIState.Escape) { return; }
|
||||
if (AIParams.Infiltrate && HasValidPath(requireNonDirty: true)) { return; }
|
||||
if (SelectedAiTarget == null) { return; }
|
||||
if (SelectedAiTarget.Entity == null) { return; }
|
||||
Vector2 rayStart = SimPosition;
|
||||
Vector2 rayEnd = SelectedAiTarget.SimPosition;
|
||||
if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null)
|
||||
{
|
||||
rayStart -= SelectedAiTarget.Entity.Submarine.SimPosition;
|
||||
}
|
||||
else if (SelectedAiTarget.Entity.Submarine == null && Character.Submarine != null)
|
||||
{
|
||||
rayEnd -= Character.Submarine.SimPosition;
|
||||
}
|
||||
Body closestBody = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true, ignoreSensors: CanEnterSubmarine, ignoreDisabledWalls: CanEnterSubmarine);
|
||||
if (Submarine.LastPickedFraction != 1.0f && closestBody != null)
|
||||
{
|
||||
if (closestBody.UserData is Structure wall && wall.Submarine != null && (Character.IsBot || wall.Submarine.Info.IsPlayer || wall.Submarine.Info.IsOutpost && TargetOutposts))
|
||||
{
|
||||
int sectionIndex = wall.FindSectionIndex(ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition));
|
||||
float sectionDamage = wall.SectionDamage(sectionIndex);
|
||||
for (int i = sectionIndex - 2; i <= sectionIndex + 2; i++)
|
||||
{
|
||||
if (wall.SectionBodyDisabled(i))
|
||||
{
|
||||
if (Character.AnimController.CanEnterSubmarine && CanPassThroughHole(wall, i, requiredHoleCount))
|
||||
{
|
||||
sectionIndex = i;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Ignore and keep breaking other sections
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (wall.SectionDamage(i) > sectionDamage)
|
||||
{
|
||||
sectionIndex = i;
|
||||
}
|
||||
}
|
||||
Vector2 sectionPos = wall.SectionPosition(sectionIndex);
|
||||
Vector2 attachTargetNormal;
|
||||
if (wall.IsHorizontal)
|
||||
{
|
||||
attachTargetNormal = new Vector2(0.0f, Math.Sign(WorldPosition.Y - wall.WorldPosition.Y));
|
||||
sectionPos.Y += (wall.BodyHeight <= 0.0f ? wall.Rect.Height : wall.BodyHeight) / 2 * attachTargetNormal.Y;
|
||||
}
|
||||
else
|
||||
{
|
||||
attachTargetNormal = new Vector2(Math.Sign(WorldPosition.X - wall.WorldPosition.X), 0.0f);
|
||||
sectionPos.X += (wall.BodyWidth <= 0.0f ? wall.Rect.Width : wall.BodyWidth) / 2 * attachTargetNormal.X;
|
||||
}
|
||||
LatchOntoAI?.SetAttachTarget(wall, ConvertUnits.ToSimUnits(sectionPos), attachTargetNormal);
|
||||
if (Character.AnimController.CanEnterSubmarine || !wall.SectionBodyDisabled(sectionIndex) && !IsWallDisabled(wall))
|
||||
{
|
||||
if (AIParams.TargetOuterWalls || wall.prefab.Tags.Contains("inner") || wall.Submarine != null && wall.Submarine == Character.Submarine)
|
||||
{
|
||||
if (wall.NoAITarget && Character.AnimController.CanEnterSubmarine)
|
||||
{
|
||||
// Blocked by a wall that shouldn't be targeted. The main intention here is to prevents monsters from entering the the tail and the nose pieces.
|
||||
IgnoreTarget(SelectedAiTarget);
|
||||
ResetAITarget();
|
||||
}
|
||||
else
|
||||
{
|
||||
wallTarget = new WallTarget(sectionPos, wall, sectionIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!Character.AnimController.CanEnterSubmarine && wallTarget == null)
|
||||
{
|
||||
if (closestBody.UserData is Structure w && w.Submarine != null || closestBody.UserData is Item i && i.Submarine != null)
|
||||
{
|
||||
// Cannot reach the target, because it's blocked by a disabled wall or a door
|
||||
State = AIState.Idle;
|
||||
IgnoreTarget(SelectedAiTarget);
|
||||
ResetAITarget();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool TrySteerThroughGaps(float deltaTime)
|
||||
{
|
||||
if (wallTarget != null && wallTarget.SectionIndex > -1 && CanPassThroughHole(wallTarget.Structure, wallTarget.SectionIndex, requiredHoleCount))
|
||||
{
|
||||
WallSection section = wallTarget.Structure.GetSection(wallTarget.SectionIndex);
|
||||
Vector2 targetPos = wallTarget.Structure.SectionPosition(wallTarget.SectionIndex, true);
|
||||
return section?.gap != null && SteerThroughGap(wallTarget.Structure, section, targetPos, deltaTime);
|
||||
}
|
||||
else if (SelectedAiTarget != null)
|
||||
{
|
||||
if (SelectedAiTarget.Entity is Structure wall)
|
||||
{
|
||||
for (int i = 0; i < wall.Sections.Length; i++)
|
||||
{
|
||||
WallSection section = wall.Sections[i];
|
||||
if (CanPassThroughHole(wall, i, requiredHoleCount) && section?.gap != null)
|
||||
{
|
||||
return SteerThroughGap(wall, section, wall.SectionPosition(i, true), deltaTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (SelectedAiTarget.Entity is Item i)
|
||||
{
|
||||
var door = i.GetComponent<Door>();
|
||||
// Don't try to enter dry hulls if cannot walk or if the gap is too narrow
|
||||
if (door?.LinkedGap?.FlowTargetHull != null && !door.LinkedGap.IsRoomToRoom && door.CanBeTraversed)
|
||||
{
|
||||
if (Character.AnimController.CanWalk || door.LinkedGap.FlowTargetHull.WaterPercentage > 25)
|
||||
{
|
||||
if (door.LinkedGap.Size > ConvertUnits.ToDisplayUnits(colliderWidth))
|
||||
{
|
||||
return SteerThroughGap(door.LinkedGap, door.LinkedGap.FlowTargetHull.WorldPosition, deltaTime, maxDistance: 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private AITargetMemory GetTargetMemory(AITarget target, bool addIfNotFound)
|
||||
{
|
||||
if (!targetMemories.TryGetValue(target, out AITargetMemory memory))
|
||||
@@ -2631,28 +2707,38 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanPassThroughHole(Structure wall, int sectionIndex)
|
||||
public override bool SteerThroughGap(Structure wall, WallSection section, Vector2 targetWorldPos, float deltaTime)
|
||||
{
|
||||
if (!wall.SectionBodyDisabled(sectionIndex)) return false;
|
||||
int holeCount = 1;
|
||||
for (int j = sectionIndex - 1; j > sectionIndex - requiredHoleCount; j--)
|
||||
wallTarget = null;
|
||||
LatchOntoAI?.DeattachFromBody(reset: true, cooldown: 2);
|
||||
Character.AnimController.ReleaseStuckLimbs();
|
||||
bool success = base.SteerThroughGap(wall, section, targetWorldPos, deltaTime);
|
||||
if (success)
|
||||
{
|
||||
if (wall.SectionBodyDisabled(j))
|
||||
holeCount++;
|
||||
else
|
||||
break;
|
||||
// If already inside, target the hull, else target the wall.
|
||||
SelectedAiTarget = Character.CurrentHull != null ? section.gap.AiTarget : wall.AiTarget;
|
||||
SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 1);
|
||||
}
|
||||
for (int j = sectionIndex + 1; j < sectionIndex + requiredHoleCount; j++)
|
||||
{
|
||||
if (wall.SectionBodyDisabled(j))
|
||||
holeCount++;
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
return holeCount >= requiredHoleCount;
|
||||
IsSteeringThroughGap = success;
|
||||
return success;
|
||||
}
|
||||
|
||||
public override bool SteerThroughGap(Gap gap, Vector2 targetWorldPos, float deltaTime, float maxDistance = -1)
|
||||
{
|
||||
wallTarget = null;
|
||||
LatchOntoAI?.DeattachFromBody(reset: true, cooldown: 2);
|
||||
Character.AnimController.ReleaseStuckLimbs();
|
||||
bool success = base.SteerThroughGap(gap, targetWorldPos, deltaTime, maxDistance);
|
||||
if (success)
|
||||
{
|
||||
SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 1);
|
||||
}
|
||||
IsSteeringThroughGap = success;
|
||||
return success;
|
||||
}
|
||||
|
||||
public bool CanPassThroughHole(Structure wall, int sectionIndex) => CanPassThroughHole(wall, sectionIndex, requiredHoleCount);
|
||||
|
||||
private readonly List<Limb> targetLimbs = new List<Limb>();
|
||||
public Limb GetTargetLimb(Limb attackLimb, Character target, LimbType targetLimbType = LimbType.None)
|
||||
{
|
||||
|
||||
@@ -29,7 +29,9 @@ namespace Barotrauma
|
||||
private float flipTimer;
|
||||
private const float FlipInterval = 0.5f;
|
||||
|
||||
public static float HULL_SAFETY_THRESHOLD = 50;
|
||||
public const float HULL_SAFETY_THRESHOLD = 40;
|
||||
public const float HULL_LOW_OXYGEN_PERCENTAGE = 30;
|
||||
|
||||
private static readonly float characterWaitOnSwitch = 5;
|
||||
|
||||
public readonly HashSet<Hull> UnreachableHulls = new HashSet<Hull>();
|
||||
@@ -60,10 +62,7 @@ namespace Barotrauma
|
||||
public IndoorsSteeringManager PathSteering => insideSteering as IndoorsSteeringManager;
|
||||
public HumanoidAnimController AnimController => Character.AnimController as HumanoidAnimController;
|
||||
|
||||
public override AIObjectiveManager ObjectiveManager
|
||||
{
|
||||
get { return objectiveManager; }
|
||||
}
|
||||
public AIObjectiveManager ObjectiveManager => objectiveManager;
|
||||
|
||||
public Order CurrentOrder
|
||||
{
|
||||
@@ -79,9 +78,7 @@ namespace Barotrauma
|
||||
|
||||
public float CurrentHullSafety { get; private set; } = 100;
|
||||
|
||||
private readonly Dictionary<Character, float> damageDoneByAttacker = new Dictionary<Character, float>();
|
||||
private readonly HashSet<Character> attackers = new HashSet<Character>();
|
||||
|
||||
private readonly Dictionary<Character, float> structureDamageAccumulator = new Dictionary<Character, float>();
|
||||
private readonly Dictionary<Hull, HullSafety> knownHulls = new Dictionary<Hull, HullSafety>();
|
||||
private class HullSafety
|
||||
{
|
||||
@@ -132,22 +129,6 @@ namespace Barotrauma
|
||||
{
|
||||
if (DisableCrewAI || Character.Removed) { return; }
|
||||
|
||||
//slowly forget about damage done by attackers
|
||||
foreach (Character enemy in attackers)
|
||||
{
|
||||
float cumulativeDamage = damageDoneByAttacker[enemy];
|
||||
if (cumulativeDamage > 0)
|
||||
{
|
||||
float reduction = deltaTime;
|
||||
if (cumulativeDamage < 2)
|
||||
{
|
||||
// If the damage is very low, let's not forget so quickly, or we can't cumulate the damage from repair tools (high frequency, low damage)
|
||||
reduction *= 0.5f;
|
||||
}
|
||||
damageDoneByAttacker[enemy] -= reduction;
|
||||
}
|
||||
}
|
||||
|
||||
bool isIncapacitated = Character.IsIncapacitated;
|
||||
if (freezeAI && !isIncapacitated)
|
||||
{
|
||||
@@ -188,7 +169,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
bool IsCloseEnoughToTargetSub(float threshold) => SelectedAiTarget?.Entity?.Submarine is Submarine sub && sub != null && Vector2.DistanceSquared(Character.WorldPosition, sub.WorldPosition) < MathUtils.Pow(Math.Max(sub.Borders.Size.X, sub.Borders.Size.Y) / 2 + threshold, 2);
|
||||
bool hasValidPath = steeringManager is IndoorsSteeringManager pathSteering && pathSteering.CurrentPath != null && !pathSteering.CurrentPath.Finished && !pathSteering.CurrentPath.Unreachable;
|
||||
bool hasValidPath = HasValidPath();
|
||||
|
||||
if (Character.Submarine == null && hasValidPath)
|
||||
{
|
||||
@@ -259,7 +240,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (Character.CurrentHull != null)
|
||||
{
|
||||
if (Character.TeamID == Character.TeamType.FriendlyNPC)
|
||||
if (Character.TeamID == CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
// Outpost npcs don't inform each other about threads, like crew members do.
|
||||
VisibleHulls.ForEach(h => RefreshHullSafety(h));
|
||||
@@ -386,10 +367,11 @@ namespace Barotrauma
|
||||
|
||||
if (isCarrying)
|
||||
{
|
||||
if (findItemState != FindItemState.OtherItem)
|
||||
if (findItemState == FindItemState.DivingSuit && ObjectiveManager.IsCurrentObjective<AIObjectiveIdle>())
|
||||
{
|
||||
if (ObjectiveManager.GetActiveObjective() is AIObjectiveGoTo gotoObjective && NeedsDivingGearOnPath(gotoObjective))
|
||||
{
|
||||
// Don't try to put the diving suit in a locker if the suit would be needed in any hull in the path to the locker.
|
||||
gotoObjective.Abandon = true;
|
||||
}
|
||||
}
|
||||
@@ -414,7 +396,7 @@ namespace Barotrauma
|
||||
{
|
||||
shouldKeepTheGearOn = false;
|
||||
}
|
||||
else if (Character.CurrentHull.Oxygen < CharacterHealth.LowOxygenThreshold)
|
||||
else if (Character.CurrentHull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 10)
|
||||
{
|
||||
shouldKeepTheGearOn = true;
|
||||
}
|
||||
@@ -558,39 +540,37 @@ namespace Barotrauma
|
||||
// Other items
|
||||
if (isCarrying) { return; }
|
||||
if (!ObjectiveManager.CurrentObjective.AllowAutomaticItemUnequipping || !ObjectiveManager.GetActiveObjective().AllowAutomaticItemUnequipping) { return; }
|
||||
foreach (var item in Character.Inventory.Items)
|
||||
|
||||
if (findItemState == FindItemState.None || findItemState == FindItemState.OtherItem)
|
||||
{
|
||||
if (item == null) { continue; }
|
||||
if (Character.HasEquippedItem(item) &&
|
||||
(Character.Inventory.IsInLimbSlot(item, InvSlotType.RightHand) ||
|
||||
Character.Inventory.IsInLimbSlot(item, InvSlotType.LeftHand) ||
|
||||
Character.Inventory.IsInLimbSlot(item, InvSlotType.RightHand | InvSlotType.LeftHand)))
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
var hand = i == 0 ? InvSlotType.RightHand : InvSlotType.LeftHand;
|
||||
Item item = Character.Inventory.GetItemInLimbSlot(hand);
|
||||
if (item == null) { continue; }
|
||||
|
||||
if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, new List<InvSlotType>() { InvSlotType.Any }))
|
||||
{
|
||||
if (findItemState == FindItemState.None || findItemState == FindItemState.OtherItem)
|
||||
findItemState = FindItemState.OtherItem;
|
||||
if (FindSuitableContainer(item, out Item targetContainer))
|
||||
{
|
||||
findItemState = FindItemState.OtherItem;
|
||||
if (FindSuitableContainer(item, out Item targetContainer))
|
||||
findItemState = FindItemState.None;
|
||||
itemIndex = 0;
|
||||
if (targetContainer != null)
|
||||
{
|
||||
findItemState = FindItemState.None;
|
||||
itemIndex = 0;
|
||||
if (targetContainer != null)
|
||||
var decontainObjective = new AIObjectiveDecontainItem(Character, item, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
|
||||
decontainObjective.Abandoned += () =>
|
||||
{
|
||||
var decontainObjective = new AIObjectiveDecontainItem(Character, item, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
|
||||
decontainObjective.Abandoned += () =>
|
||||
{
|
||||
ReequipUnequipped();
|
||||
IgnoredItems.Add(targetContainer);
|
||||
};
|
||||
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
item.Drop(Character);
|
||||
HandleRelocation(item);
|
||||
}
|
||||
ReequipUnequipped();
|
||||
IgnoredItems.Add(targetContainer);
|
||||
};
|
||||
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
item.Drop(Character);
|
||||
HandleRelocation(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -602,7 +582,7 @@ namespace Barotrauma
|
||||
|
||||
private void HandleRelocation(Item item)
|
||||
{
|
||||
if (item.Submarine?.TeamID == Character.TeamType.FriendlyNPC)
|
||||
if (item.Submarine?.TeamID == CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
if (itemsToRelocate.Contains(item)) { return; }
|
||||
itemsToRelocate.Add(item);
|
||||
@@ -627,7 +607,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (item.ParentInventory.Owner is Character c)
|
||||
{
|
||||
if (c.TeamID == Character.TeamType.Team1 || c.TeamID == Character.TeamType.Team2)
|
||||
if (c.TeamID == CharacterTeamType.Team1 || c.TeamID == CharacterTeamType.Team2)
|
||||
{
|
||||
// Taken by a player/bot (if npc or monster would take the item, we'd probably still want it to spawn back to the main sub.
|
||||
return;
|
||||
@@ -654,18 +634,6 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public void ReequipUnequipped()
|
||||
{
|
||||
foreach (var item in unequippedItems)
|
||||
{
|
||||
if (item != null && !item.Removed && Character.HasItem(item))
|
||||
{
|
||||
TakeItem(item, Character.Inventory, equip: true, dropOtherIfCannotMove: true, allowSwapping: true, storeUnequipped: false);
|
||||
}
|
||||
}
|
||||
unequippedItems.Clear();
|
||||
}
|
||||
|
||||
private enum FindItemState
|
||||
{
|
||||
None,
|
||||
@@ -681,23 +649,23 @@ namespace Barotrauma
|
||||
public static bool FindSuitableContainer(Character character, Item containableItem, List<Item> ignoredItems, ref int itemIndex, out Item suitableContainer)
|
||||
{
|
||||
suitableContainer = null;
|
||||
if (character.FindItem(ref itemIndex, out Item targetContainer, ignoredItems: ignoredItems, customPriorityFunction: i =>
|
||||
if (character.FindItem(ref itemIndex, out Item targetContainer, ignoredItems: ignoredItems, positionalReference: containableItem, customPriorityFunction: i =>
|
||||
{
|
||||
if (i.IsThisOrAnyContainerIgnoredByAI()) { return 0; }
|
||||
var container = i.GetComponent<ItemContainer>();
|
||||
if (container == null) { return 0; }
|
||||
if (container.Inventory.IsFull()) { return 0; }
|
||||
if (!container.Inventory.CanBePut(containableItem)) { return 0; }
|
||||
if (container.ShouldBeContained(containableItem, out bool isRestrictionsDefined))
|
||||
{
|
||||
if (isRestrictionsDefined)
|
||||
{
|
||||
return 4;
|
||||
return 10;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (containableItem.Prefab.IsContainerPreferred(container, out bool isPreferencesDefined, out bool isSecondary))
|
||||
if (containableItem.IsContainerPreferred(container, out bool isPreferencesDefined, out bool isSecondary))
|
||||
{
|
||||
return isPreferencesDefined ? isSecondary ? 2 : 3 : 1;
|
||||
return isPreferencesDefined ? isSecondary ? 2 : 5 : 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -749,6 +717,18 @@ namespace Barotrauma
|
||||
targetHull = hull;
|
||||
}
|
||||
}
|
||||
foreach (var ballastFlora in MapCreatures.Behavior.BallastFloraBehavior.EntityList)
|
||||
{
|
||||
if (ballastFlora.Parent?.Submarine != Character.Submarine) { continue; }
|
||||
if (!ballastFlora.HasBrokenThrough) { continue; }
|
||||
// Don't react to the first two branches, because they are usually in the very edges of the room.
|
||||
if (ballastFlora.Branches.Count(b => !b.Removed && b.Health > 0 && b.CurrentHull == hull) > 2)
|
||||
{
|
||||
var orderPrefab = Order.GetPrefab("reportballastflora");
|
||||
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
|
||||
targetHull = hull;
|
||||
}
|
||||
}
|
||||
if (!isFighting)
|
||||
{
|
||||
foreach (var gap in hull.ConnectedGaps)
|
||||
@@ -798,7 +778,7 @@ namespace Barotrauma
|
||||
}
|
||||
if (newOrder != null)
|
||||
{
|
||||
if (Character.TeamID == Character.TeamType.FriendlyNPC)
|
||||
if (Character.TeamID == CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName, givingOrderToSelf: false), ChatMessageType.Default,
|
||||
identifier: newOrder.Prefab.Identifier + (targetHull?.DisplayName ?? "null"),
|
||||
@@ -837,8 +817,8 @@ namespace Barotrauma
|
||||
Character.Speak(TextManager.Get("DialogBleeding"), null, Rand.Range(0.5f, 5.0f), "bleeding", 30.0f);
|
||||
}
|
||||
|
||||
if (Character.PressureTimer > 50.0f && Character.CurrentHull != null)
|
||||
{
|
||||
if (Character.PressureTimer > 50.0f && Character.CurrentHull?.DisplayName != null)
|
||||
{
|
||||
Character.Speak(TextManager.GetWithVariable("DialogPressure", "[roomname]", Character.CurrentHull.DisplayName, true), null, Rand.Range(0.5f, 5.0f), "pressure", 30.0f);
|
||||
}
|
||||
}
|
||||
@@ -895,15 +875,6 @@ namespace Barotrauma
|
||||
if (totalDamage <= 0.01f) { return; }
|
||||
if (Character.IsBot)
|
||||
{
|
||||
if (attacker != null)
|
||||
{
|
||||
if (!damageDoneByAttacker.ContainsKey(attacker))
|
||||
{
|
||||
damageDoneByAttacker[attacker] = 0.0f;
|
||||
}
|
||||
damageDoneByAttacker[attacker] += totalDamage;
|
||||
attackers.Add(attacker);
|
||||
}
|
||||
if (!freezeAI && !Character.IsDead && Character.IsIncapacitated)
|
||||
{
|
||||
// Removes the combat objective and resets all objectives.
|
||||
@@ -953,7 +924,7 @@ namespace Barotrauma
|
||||
foreach (Character otherCharacter in Character.CharacterList)
|
||||
{
|
||||
if (otherCharacter == Character || otherCharacter.IsDead || otherCharacter.IsUnconscious || otherCharacter.Removed ||
|
||||
otherCharacter.Info?.Job == null || otherCharacter.TeamID != Character.TeamType.FriendlyNPC ||
|
||||
otherCharacter.Info?.Job == null || otherCharacter.TeamID != CharacterTeamType.FriendlyNPC ||
|
||||
!(otherCharacter.AIController is HumanAIController otherHumanAI) ||
|
||||
otherCharacter.IsInstigator)
|
||||
{
|
||||
@@ -1045,7 +1016,7 @@ namespace Barotrauma
|
||||
// The guards don't react when the player attacks instigators.
|
||||
return c.IsSecurity ? AIObjectiveCombat.CombatMode.None : (Character.CombatAction != null ? Character.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.Retreat);
|
||||
}
|
||||
else if (attacker.TeamID == Character.TeamType.FriendlyNPC)
|
||||
else if (attacker.TeamID == CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
if (c.IsSecurity)
|
||||
{
|
||||
@@ -1078,7 +1049,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private void AddCombatObjective(AIObjectiveCombat.CombatMode mode, Character attacker, float delay = 0, Func<bool> abortCondition = null, Action onAbort = null, bool allowHoldFire = false)
|
||||
private void AddCombatObjective(AIObjectiveCombat.CombatMode mode, Character attacker, float delay = 0, Func<bool> abortCondition = null, Action onAbort = null, Action onCompleted = null, bool allowHoldFire = false)
|
||||
{
|
||||
if (mode == AIObjectiveCombat.CombatMode.None) { return; }
|
||||
if (Character.IsDead || Character.IsIncapacitated) { return; }
|
||||
@@ -1117,49 +1088,19 @@ namespace Barotrauma
|
||||
{
|
||||
objective.Abandoned += onAbort;
|
||||
}
|
||||
if (onCompleted != null)
|
||||
{
|
||||
objective.Completed += onCompleted;
|
||||
}
|
||||
return objective;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetOrder(Order order, string option, Character orderGiver, bool speak = true)
|
||||
{
|
||||
CurrentOrderOption = option;
|
||||
CurrentOrder = order;
|
||||
objectiveManager.SetOrder(order, option, orderGiver);
|
||||
if (ObjectiveManager.CurrentOrder != null && speak && Character.SpeechImpediment < 100.0f)
|
||||
{
|
||||
if (ObjectiveManager.CurrentOrder is AIObjectiveRepairItems repairItems && repairItems.Targets.None())
|
||||
{
|
||||
Character.Speak(TextManager.Get("DialogNoRepairTargets"), null, 3.0f, "norepairtargets");
|
||||
}
|
||||
else if (ObjectiveManager.CurrentOrder is AIObjectiveChargeBatteries chargeBatteries && chargeBatteries.Targets.None())
|
||||
{
|
||||
Character.Speak(TextManager.Get("DialogNoBatteries"), null, 3.0f, "nobatteries");
|
||||
}
|
||||
else if (ObjectiveManager.CurrentOrder is AIObjectiveExtinguishFires extinguishFires && extinguishFires.Targets.None())
|
||||
{
|
||||
Character.Speak(TextManager.Get("DialogNoFire"), null, 3.0f, "nofire");
|
||||
}
|
||||
else if (ObjectiveManager.CurrentOrder is AIObjectiveFixLeaks fixLeaks && fixLeaks.Targets.None())
|
||||
{
|
||||
Character.Speak(TextManager.Get("DialogNoLeaks"), null, 3.0f, "noleaks");
|
||||
}
|
||||
else if (ObjectiveManager.CurrentOrder is AIObjectiveFightIntruders fightIntruders && fightIntruders.Targets.None())
|
||||
{
|
||||
Character.Speak(TextManager.Get("DialogNoEnemies"), null, 3.0f, "noenemies");
|
||||
}
|
||||
else if (ObjectiveManager.CurrentOrder is AIObjectiveRescueAll rescueAll && rescueAll.Targets.None())
|
||||
{
|
||||
Character.Speak(TextManager.Get("DialogNoRescueTargets"), null, 3.0f, "norescuetargets");
|
||||
}
|
||||
else if (ObjectiveManager.CurrentOrder is AIObjectivePumpWater pumpWater && pumpWater.Targets.None())
|
||||
{
|
||||
Character.Speak(TextManager.Get("DialogNoPumps"), null, 3.0f, "nopumps");
|
||||
}
|
||||
else
|
||||
{
|
||||
Character.Speak(TextManager.Get("DialogAffirmative"), null, 1.0f);
|
||||
}
|
||||
}
|
||||
objectiveManager.SetOrder(order, option, orderGiver, speak);
|
||||
}
|
||||
|
||||
public override void SelectTarget(AITarget target)
|
||||
@@ -1213,56 +1154,6 @@ namespace Barotrauma
|
||||
return true;
|
||||
}
|
||||
|
||||
private readonly HashSet<Item> unequippedItems = new HashSet<Item>();
|
||||
public bool TakeItem(Item item, Inventory targetInventory, bool equip, bool dropOtherIfCannotMove = true, bool allowSwapping = false, bool storeUnequipped = false)
|
||||
{
|
||||
var pickable = item.GetComponent<Pickable>();
|
||||
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.Items.Length; 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.Items[i];
|
||||
if (otherItem == null) { continue; }
|
||||
//try to move the existing item to LimbSlot.Any and continue if successful
|
||||
if (otherItem.AllowedSlots.Contains(InvSlotType.Any) && 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 static bool NeedsDivingGear(Hull hull, out bool needsSuit)
|
||||
{
|
||||
needsSuit = false;
|
||||
@@ -1274,7 +1165,7 @@ namespace Barotrauma
|
||||
needsSuit = true;
|
||||
return true;
|
||||
}
|
||||
if (hull.WaterPercentage > 60 || hull.Oxygen < CharacterHealth.LowOxygenThreshold)
|
||||
if (hull.WaterPercentage > 60 || hull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -1294,20 +1185,118 @@ namespace Barotrauma
|
||||
public static bool HasDivingMask(Character character, float conditionPercentage = 0) => HasItem(character, AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR, out _, AIObjectiveFindDivingGear.OXYGEN_SOURCE, conditionPercentage, requireEquipped: true);
|
||||
|
||||
private static List<Item> matchingItems = new List<Item>();
|
||||
public static bool HasItem(Character character, string tagOrIdentifier, out IEnumerable<Item> items, string containedTag = null, float conditionPercentage = 0, bool requireEquipped = false)
|
||||
|
||||
/// <summary>
|
||||
/// Note: uses a single list for matching items. The item is reused each time when the method is called. So if you use the method twice, and then refer to the first items, you'll actually get the second.
|
||||
/// To solve this, create a copy of the collection or change the code so that you first handle the first items and only after that query for the next items.
|
||||
/// </summary>
|
||||
public static bool HasItem(Character character, string tagOrIdentifier, out IEnumerable<Item> items, string containedTag = null, float conditionPercentage = 0, bool requireEquipped = false, bool recursive = true, Func<Item, bool> predicate = null)
|
||||
{
|
||||
matchingItems.Clear();
|
||||
items = matchingItems;
|
||||
if (character == null) { return false; }
|
||||
if (character.Inventory == null) { return false; }
|
||||
matchingItems = character.Inventory.FindAllItems(i => i.Prefab.Identifier == tagOrIdentifier || i.HasTag(tagOrIdentifier), recursive: true, matchingItems);
|
||||
items = matchingItems;
|
||||
return matchingItems.Any(i => i != null &&
|
||||
matchingItems = character.Inventory.FindAllItems(i => (i.Prefab.Identifier == tagOrIdentifier || i.HasTag(tagOrIdentifier)) &&
|
||||
i.ConditionPercentage >= conditionPercentage &&
|
||||
(!requireEquipped || character.HasEquippedItem(i)) &&
|
||||
(containedTag == null ||
|
||||
(i.OwnInventory?.Items != null &&
|
||||
i.OwnInventory.Items.Any(it => it != null && it.HasTag(containedTag) && it.ConditionPercentage > conditionPercentage))));
|
||||
(predicate == null || predicate(i)), recursive, matchingItems);
|
||||
items = matchingItems;
|
||||
return matchingItems.Any(i => i != null && (containedTag == null || i.ContainedItems.Any(it => it.HasTag(containedTag) && it.ConditionPercentage > conditionPercentage)));
|
||||
}
|
||||
|
||||
public static void StructureDamaged(Structure structure, float damageAmount, Character character)
|
||||
{
|
||||
const float MaxDamagePerSecond = 5.0f;
|
||||
const float MaxDamagePerFrame = MaxDamagePerSecond * (float)Timing.Step;
|
||||
|
||||
const float WarningThreshold = 5.0f;
|
||||
const float ArrestThreshold = 20.0f;
|
||||
const float KillThreshold = 50.0f;
|
||||
|
||||
if (character == null || damageAmount <= 0.0f) { return; }
|
||||
if (structure?.Submarine == null || !structure.Submarine.Info.IsOutpost || character.TeamID == structure.Submarine.TeamID) { return; }
|
||||
//structure not indestructible = something that's "meant" to be destroyed, like an ice wall in mines
|
||||
if (!structure.Prefab.IndestructibleInOutposts) { return; }
|
||||
|
||||
bool someoneSpoke = false;
|
||||
float maxAccumulatedDamage = 0.0f;
|
||||
foreach (Character otherCharacter in Character.CharacterList)
|
||||
{
|
||||
if (otherCharacter == character || otherCharacter.TeamID == character.TeamID || otherCharacter.IsDead ||
|
||||
otherCharacter.Info?.Job == null ||
|
||||
!(otherCharacter.AIController is HumanAIController otherHumanAI) ||
|
||||
!otherHumanAI.VisibleHulls.Contains(character.CurrentHull))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!otherCharacter.CanSeeCharacter(character)) { continue; }
|
||||
|
||||
if (!otherHumanAI.structureDamageAccumulator.ContainsKey(character)) { otherHumanAI.structureDamageAccumulator.Add(character, 0.0f); }
|
||||
float prevAccumulatedDamage = otherHumanAI.structureDamageAccumulator[character];
|
||||
otherHumanAI.structureDamageAccumulator[character] += MathHelper.Clamp(damageAmount, -MaxDamagePerFrame, MaxDamagePerFrame);
|
||||
float accumulatedDamage = Math.Max(otherHumanAI.structureDamageAccumulator[character], maxAccumulatedDamage);
|
||||
maxAccumulatedDamage = Math.Max(accumulatedDamage, maxAccumulatedDamage);
|
||||
|
||||
if (GameMain.GameSession?.Campaign?.Map?.CurrentLocation != null)
|
||||
{
|
||||
var reputationLoss = damageAmount * Reputation.ReputationLossPerWallDamage;
|
||||
GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.Value -= reputationLoss;
|
||||
}
|
||||
|
||||
if (accumulatedDamage <= WarningThreshold) { return; }
|
||||
|
||||
if (accumulatedDamage > WarningThreshold && prevAccumulatedDamage <= WarningThreshold &&
|
||||
!someoneSpoke && !character.IsIncapacitated && character.Stun <= 0.0f)
|
||||
{
|
||||
//if the damage is still fairly low, wait and see if the character keeps damaging the walls to the point where we need to intervene
|
||||
if (accumulatedDamage < ArrestThreshold)
|
||||
{
|
||||
if (otherHumanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveIdle>())
|
||||
{
|
||||
(otherHumanAI.ObjectiveManager.CurrentObjective as AIObjectiveIdle)?.FaceTargetAndWait(character, 5.0f);
|
||||
}
|
||||
}
|
||||
otherCharacter.Speak(TextManager.Get("dialogdamagewallswarning"), null, Rand.Range(0.5f, 1.0f), "damageoutpostwalls", 10.0f);
|
||||
someoneSpoke = true;
|
||||
}
|
||||
// React if we are security
|
||||
if ((accumulatedDamage > ArrestThreshold && prevAccumulatedDamage <= ArrestThreshold) ||
|
||||
(accumulatedDamage > KillThreshold && prevAccumulatedDamage <= KillThreshold))
|
||||
{
|
||||
var combatMode = accumulatedDamage > KillThreshold ? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Arrest;
|
||||
if (!TriggerSecurity(otherHumanAI, combatMode))
|
||||
{
|
||||
// Else call the others
|
||||
foreach (Character security in Character.CharacterList.Where(c => c.TeamID == otherCharacter.TeamID).OrderByDescending(c => Vector2.DistanceSquared(character.WorldPosition, c.WorldPosition)))
|
||||
{
|
||||
if (!TriggerSecurity(security.AIController as HumanAIController, combatMode))
|
||||
{
|
||||
// Only alert one guard at a time
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool TriggerSecurity(HumanAIController humanAI, AIObjectiveCombat.CombatMode combatMode)
|
||||
{
|
||||
if (humanAI == null) { return false; }
|
||||
if (!humanAI.Character.IsSecurity) { return false; }
|
||||
if (humanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveCombat>()) { return false; }
|
||||
humanAI.AddCombatObjective(combatMode, character, delay: GetReactionTime(), allowHoldFire: true, onCompleted: () =>
|
||||
{
|
||||
//if the target is arrested successfully, reset the damage accumulator
|
||||
foreach (Character anyCharacter in Character.CharacterList)
|
||||
{
|
||||
if (anyCharacter.AIController is HumanAIController anyAI)
|
||||
{
|
||||
anyAI.structureDamageAccumulator?.Remove(character);
|
||||
}
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static void ItemTaken(Item item, Character character)
|
||||
@@ -1316,7 +1305,7 @@ namespace Barotrauma
|
||||
Character thief = character;
|
||||
bool someoneSpoke = false;
|
||||
|
||||
if (item.SpawnedInOutpost && thief.TeamID != Character.TeamType.FriendlyNPC && !item.HasTag("handlocker"))
|
||||
if (item.SpawnedInOutpost && thief.TeamID != CharacterTeamType.FriendlyNPC && !item.HasTag("handlocker"))
|
||||
{
|
||||
foreach (Character otherCharacter in Character.CharacterList)
|
||||
{
|
||||
@@ -1495,11 +1484,13 @@ namespace Barotrauma
|
||||
humanAI.ObjectiveManager.GetObjective<T1>()?.ReportedTargets.Remove(target));
|
||||
}
|
||||
|
||||
public float GetDamageDoneByAttacker(Character attacker)
|
||||
public float GetDamageDoneByAttacker(Character otherCharacter)
|
||||
{
|
||||
if (!damageDoneByAttacker.TryGetValue(attacker, out float dmg))
|
||||
float dmg = 0;
|
||||
Character.Attacker attacker = Character.LastAttackers.LastOrDefault(a => a.Character == otherCharacter);
|
||||
if (attacker != null)
|
||||
{
|
||||
dmg = 0;
|
||||
dmg = attacker.Damage;
|
||||
}
|
||||
return dmg;
|
||||
}
|
||||
@@ -1550,9 +1541,10 @@ namespace Barotrauma
|
||||
{
|
||||
if (hull == null) { return 0; }
|
||||
if (hull.LethalPressure > 0 && character.PressureProtection <= 0) { return 0; }
|
||||
// TODO: take the visiblehulls into account?
|
||||
float oxygenFactor = ignoreOxygen ? 1 : MathHelper.Lerp(0.25f, 1, hull.OxygenPercentage / 100);
|
||||
float waterFactor = ignoreWater ? 1 : MathHelper.Lerp(1, 0.25f, hull.WaterPercentage / 100);
|
||||
// Oxygen factor should be 1 with 70% oxygen or more and 0.1 when the oxygen level is 30% or lower.
|
||||
// With insufficient oxygen, the safety of the hull should be 39, all the other factors aside. So, just below the HULL_SAFETY_THRESHOLD.
|
||||
float oxygenFactor = ignoreOxygen ? 1 : MathHelper.Lerp((HULL_SAFETY_THRESHOLD - 1) / 100, 1, MathUtils.InverseLerp(HULL_LOW_OXYGEN_PERCENTAGE, 100 - HULL_LOW_OXYGEN_PERCENTAGE, hull.OxygenPercentage));
|
||||
float waterFactor = ignoreWater ? 1 : MathHelper.Lerp(1, HULL_SAFETY_THRESHOLD / 2 / 100, hull.WaterPercentage / 100);
|
||||
if (!character.NeedsAir)
|
||||
{
|
||||
oxygenFactor = 1;
|
||||
@@ -1637,7 +1629,7 @@ namespace Barotrauma
|
||||
if (!teamGood) { return false; }
|
||||
bool speciesGood = other.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.Group);
|
||||
if (!speciesGood) { return false; }
|
||||
if (me.TeamID == Character.TeamType.FriendlyNPC && other.TeamID == Character.TeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign)
|
||||
if (me.TeamID == CharacterTeamType.FriendlyNPC && other.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign)
|
||||
{
|
||||
var reputation = campaign.Map?.CurrentLocation?.Reputation;
|
||||
if (reputation != null && reputation.NormalizedValue < Reputation.HostileThreshold)
|
||||
@@ -1651,7 +1643,7 @@ namespace Barotrauma
|
||||
private static bool IsOnFriendlyTeam(GameMode mode, Character me, Character other)
|
||||
{
|
||||
// Only enemies are in the Team "None"
|
||||
bool friendlyTeam = me.TeamID != Character.TeamType.None && other.TeamID != Character.TeamType.None;
|
||||
bool friendlyTeam = me.TeamID != CharacterTeamType.None && other.TeamID != CharacterTeamType.None;
|
||||
// When playing a combat mission, we need to be on the same team to be friendlies
|
||||
if (friendlyTeam && mode is MissionMode mm && mm.Mission is CombatMission)
|
||||
{
|
||||
|
||||
@@ -50,9 +50,9 @@ namespace Barotrauma
|
||||
/// Returns true if the current or the next node is in ladders.
|
||||
/// </summary>
|
||||
public bool InLadders =>
|
||||
currentPath != null &&
|
||||
currentPath.CurrentNode != null && (currentPath.CurrentNode.Ladders != null && !currentPath.CurrentNode.Ladders.Item.NonInteractable ||
|
||||
(currentPath.NextNode != null && currentPath.NextNode.Ladders != null && !currentPath.NextNode.Ladders.Item.NonInteractable));
|
||||
currentPath != null && currentPath.CurrentNode != null &&
|
||||
(currentPath.CurrentNode.Ladders != null && currentPath.CurrentNode.Ladders.Item.IsInteractable(character) ||
|
||||
(currentPath.NextNode != null && currentPath.NextNode.Ladders != null && currentPath.NextNode.Ladders.Item.IsInteractable(character)));
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if any node in the path is in stairs
|
||||
@@ -70,7 +70,7 @@ namespace Barotrauma
|
||||
if (currentPath.NextNode == null) { return false; }
|
||||
var currentLadder = currentPath.CurrentNode.Ladders;
|
||||
if (currentLadder == null) { return false; }
|
||||
if (currentLadder.Item.NonInteractable) { return false; }
|
||||
if (!currentLadder.Item.IsInteractable(character)) { return false; }
|
||||
var nextLadder = GetNextLadder();
|
||||
return nextLadder != null && nextLadder == currentLadder;
|
||||
}
|
||||
@@ -123,7 +123,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (currentPath == null) { return null; }
|
||||
if (currentPath.NextNode == null) { return null; }
|
||||
if (currentPath.NextNode.Ladders != null && !currentPath.NextNode.Ladders.Item.NonInteractable)
|
||||
if (currentPath.NextNode.Ladders != null && currentPath.NextNode.Ladders.Item.IsInteractable(character))
|
||||
{
|
||||
return currentPath.NextNode.Ladders;
|
||||
}
|
||||
@@ -134,7 +134,7 @@ namespace Barotrauma
|
||||
{
|
||||
var node = currentPath.Nodes[index];
|
||||
if (node == null) { return null; }
|
||||
if (node.Ladders != null && !node.Ladders.Item.NonInteractable)
|
||||
if (node.Ladders != null && node.Ladders.Item.IsInteractable(character))
|
||||
{
|
||||
return node.Ladders;
|
||||
}
|
||||
@@ -146,7 +146,7 @@ namespace Barotrauma
|
||||
{
|
||||
node = currentPath.Nodes[index];
|
||||
if (node == null) { return null; }
|
||||
if (node.Ladders != null && !node.Ladders.Item.NonInteractable)
|
||||
if (node.Ladders != null && node.Ladders.Item.IsInteractable(character))
|
||||
{
|
||||
return node.Ladders;
|
||||
}
|
||||
@@ -295,7 +295,7 @@ namespace Barotrauma
|
||||
// Only humanoids can climb ladders
|
||||
bool canClimb = character.AnimController is HumanoidAnimController && !character.LockHands;
|
||||
Ladder currentLadder = currentPath.CurrentNode.Ladders;
|
||||
if (currentLadder != null && currentLadder.Item.NonInteractable)
|
||||
if (currentLadder != null && !currentLadder.Item.IsInteractable(character))
|
||||
{
|
||||
currentLadder = null;
|
||||
}
|
||||
@@ -303,7 +303,7 @@ namespace Barotrauma
|
||||
var ladders = currentLadder ?? nextLadder;
|
||||
if (canClimb && !isDiving && ladders != null && character.SelectedConstruction != ladders.Item)
|
||||
{
|
||||
if (IsNextNodeLadder || currentPath.CurrentIndex == currentPath.Nodes.Count - 1)
|
||||
if (IsNextNodeLadder || currentPath.Finished)
|
||||
{
|
||||
if (character.CanInteractWith(ladders.Item))
|
||||
{
|
||||
@@ -361,7 +361,7 @@ namespace Barotrauma
|
||||
nextLadder.Item.TryInteract(character, false, true);
|
||||
}
|
||||
}
|
||||
if (nextLadder != null || isAboveFloor)
|
||||
if (isAboveFloor || nextLadderSameAsCurrent)
|
||||
{
|
||||
currentPath.SkipToNextNode();
|
||||
}
|
||||
@@ -387,8 +387,7 @@ namespace Barotrauma
|
||||
character.SelectedConstruction = null;
|
||||
}
|
||||
var door = currentPath.CurrentNode.ConnectedDoor;
|
||||
bool blockedByDoor = door != null && !door.IsOpen && !door.IsBroken;
|
||||
if (!blockedByDoor)
|
||||
if (door == null || door.CanBeTraversed)
|
||||
{
|
||||
float multiplier = MathHelper.Lerp(1, 10, MathHelper.Clamp(collider.LinearVelocity.Length() / 10, 0, 1));
|
||||
float targetDistance = collider.GetSize().X * multiplier;
|
||||
@@ -421,10 +420,9 @@ namespace Barotrauma
|
||||
bool isAboveFeet = currentPath.CurrentNode.SimPosition.Y > colliderBottom.Y;
|
||||
bool isNotTooHigh = currentPath.CurrentNode.SimPosition.Y < colliderBottom.Y + characterHeight;
|
||||
var door = currentPath.CurrentNode.ConnectedDoor;
|
||||
bool blockedByDoor = door != null && !door.IsOpen && !door.IsBroken;
|
||||
float margin = MathHelper.Lerp(1, 10, MathHelper.Clamp(Math.Abs(velocity.X) / 10, 0, 1));
|
||||
float targetDistance = Math.Max(collider.radius * margin, minWidth);
|
||||
if (horizontalDistance < targetDistance && isAboveFeet && isNotTooHigh && !blockedByDoor)
|
||||
if (horizontalDistance < targetDistance && isAboveFeet && isNotTooHigh && (door == null || door.CanBeTraversed))
|
||||
{
|
||||
currentPath.SkipToNextNode();
|
||||
}
|
||||
@@ -438,18 +436,20 @@ namespace Barotrauma
|
||||
|
||||
private bool CanAccessDoor(Door door, Func<Controller, bool> buttonFilter = null)
|
||||
{
|
||||
if (door.IsOpen) { return true; }
|
||||
if (door.Item.NonInteractable) { return false; }
|
||||
if (CanBreakDoors) { return true; }
|
||||
if (door.IsStuck || door.IsJammed) { return false; }
|
||||
if (!canOpenDoors || character.LockHands) { return false; }
|
||||
if (door.IsOpen || door.IsBroken) { return true; }
|
||||
if (!door.Item.IsInteractable(character)) { return false; }
|
||||
if (!CanBreakDoors)
|
||||
{
|
||||
if (door.IsStuck || door.IsJammed) { return false; }
|
||||
if (!canOpenDoors || character.LockHands) { return false; }
|
||||
}
|
||||
if (door.HasIntegratedButtons)
|
||||
{
|
||||
return door.CanBeOpenedWithoutTools(character);
|
||||
return door.HasAccess(character) || CanBreakDoors;
|
||||
}
|
||||
else
|
||||
{
|
||||
return door.Item.GetConnectedComponents<Controller>(true).Any(b => !b.Item.NonInteractable && b.HasAccess(character) && (buttonFilter == null || buttonFilter(b)));
|
||||
return door.Item.GetConnectedComponents<Controller>(true).Any(b => b.HasAccess(character) && (buttonFilter == null || buttonFilter(b))) || CanBreakDoors;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -624,18 +624,19 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
bool nextNodeAboveWaterLevel = nextNode.Waypoint.CurrentHull != null && nextNode.Waypoint.CurrentHull.Surface < nextNode.Waypoint.Position.Y;
|
||||
//non-humanoids can't climb up ladders
|
||||
if (!(character.AnimController is HumanoidAnimController))
|
||||
{
|
||||
if (node.Waypoint.Ladders != null && nextNode.Waypoint.Ladders != null && (nextNode.Waypoint.Ladders.Item.NonInteractable || character.LockHands)||
|
||||
if (node.Waypoint.Ladders != null && nextNode.Waypoint.Ladders != null && (!nextNode.Waypoint.Ladders.Item.IsInteractable(character) || character.LockHands)||
|
||||
(nextNode.Position.Y - node.Position.Y > 1.0f && //more than one sim unit to climb up
|
||||
nextNode.Waypoint.CurrentHull != null && nextNode.Waypoint.CurrentHull.Surface < nextNode.Waypoint.Position.Y)) //upper node not underwater
|
||||
nextNodeAboveWaterLevel)) //upper node not underwater
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (node.Waypoint != null && node.Waypoint.CurrentHull != null)
|
||||
if (node.Waypoint.CurrentHull != null)
|
||||
{
|
||||
var hull = node.Waypoint.CurrentHull;
|
||||
if (hull.FireSources.Count > 0)
|
||||
@@ -645,23 +646,26 @@ namespace Barotrauma
|
||||
penalty += fs.Size.X * 10.0f;
|
||||
}
|
||||
}
|
||||
if (character.NeedsAir && hull.WaterVolume / hull.Rect.Width > 100.0f)
|
||||
if (character.NeedsAir)
|
||||
{
|
||||
if (!HumanAIController.HasDivingSuit(character))
|
||||
if (hull.WaterVolume / hull.Rect.Width > 100.0f)
|
||||
{
|
||||
penalty += 500.0f;
|
||||
if (!HumanAIController.HasDivingSuit(character))
|
||||
{
|
||||
penalty += 500.0f;
|
||||
}
|
||||
}
|
||||
if (character.PressureProtection < 10.0f && hull.WaterVolume > hull.Volume)
|
||||
{
|
||||
penalty += 1000.0f;
|
||||
}
|
||||
}
|
||||
if (character.PressureProtection < 10.0f && hull.WaterVolume > hull.Volume)
|
||||
{
|
||||
penalty += 1000.0f;
|
||||
}
|
||||
}
|
||||
|
||||
float yDist = Math.Abs(node.Position.Y - nextNode.Position.Y);
|
||||
if (node.Waypoint.Ladders == null && nextNode.Waypoint.Ladders == null)
|
||||
{
|
||||
penalty += yDist * 10.0f;
|
||||
float yDist = Math.Abs(node.Position.Y - nextNode.Position.Y);
|
||||
if (nextNodeAboveWaterLevel && node.Waypoint.Ladders == null && nextNode.Waypoint.Ladders == null && node.Waypoint.Stairs == null && nextNode.Waypoint.Stairs == null)
|
||||
{
|
||||
penalty += yDist * 10.0f;
|
||||
}
|
||||
}
|
||||
|
||||
return penalty;
|
||||
|
||||
@@ -178,7 +178,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (Timing.TotalTime < GameMain.GameSession.RoundStartTime + 120.0f &&
|
||||
speaker?.CurrentHull != null &&
|
||||
speaker.TeamID == Character.TeamType.FriendlyNPC &&
|
||||
speaker.TeamID == CharacterTeamType.FriendlyNPC &&
|
||||
Character.CharacterList.Any(c => c.TeamID != speaker.TeamID && c.CurrentHull == speaker.CurrentHull))
|
||||
{
|
||||
currentFlags.Add("EnterOutpost");
|
||||
@@ -213,7 +213,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
if (speaker.TeamID == Character.TeamType.FriendlyNPC && speaker.Submarine != null && speaker.Submarine.Info.IsOutpost)
|
||||
if (speaker.TeamID == CharacterTeamType.FriendlyNPC && speaker.Submarine != null && speaker.Submarine.Info.IsOutpost)
|
||||
{
|
||||
currentFlags.Add("OutpostNPC");
|
||||
}
|
||||
|
||||
@@ -220,7 +220,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (!AllowOutsideSubmarine && character.Submarine == null) { return false; }
|
||||
if (AllowInAnySub) { return true; }
|
||||
if (AllowInFriendlySubs && character.Submarine.TeamID == Character.TeamType.FriendlyNPC) { return true; }
|
||||
if (AllowInFriendlySubs && character.Submarine.TeamID == CharacterTeamType.FriendlyNPC) { return true; }
|
||||
return character.Submarine.TeamID == character.TeamID || character.Submarine.DockedTo.Any(sub => sub.TeamID == character.TeamID);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ namespace Barotrauma
|
||||
if (battery == null) { return false; }
|
||||
var item = battery.Item;
|
||||
if (item.IgnoreByAI) { return false; }
|
||||
if (item.NonInteractable) { return false; }
|
||||
if (!item.IsInteractable(character)) { return false; }
|
||||
if (item.Submarine == null) { return false; }
|
||||
if (item.CurrentHull == null) { return false; }
|
||||
if (item.Submarine.TeamID != character.TeamID) { return false; }
|
||||
|
||||
+5
@@ -58,6 +58,11 @@ namespace Barotrauma
|
||||
{
|
||||
// Only continue when the get item sub objectives have been completed.
|
||||
if (subObjectives.Any()) { return; }
|
||||
if (item.IgnoreByAI)
|
||||
{
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
if (HumanAIController.FindSuitableContainer(character, item, ignoredContainers, ref itemIndex, out Item suitableContainer))
|
||||
{
|
||||
itemIndex = 0;
|
||||
|
||||
+30
-16
@@ -12,15 +12,24 @@ namespace Barotrauma
|
||||
public override bool AllowAutomaticItemUnequipping => false;
|
||||
public override bool ForceOrderPriority => false;
|
||||
|
||||
public readonly Item prioritizedItem;
|
||||
public readonly List<Item> prioritizedItems = new List<Item>();
|
||||
|
||||
public AIObjectiveCleanupItems(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1, Item prioritizedItem = null)
|
||||
public AIObjectiveCleanupItems(Character character, AIObjectiveManager objectiveManager, Item prioritizedItem = null, float priorityModifier = 1)
|
||||
: base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
this.prioritizedItem = prioritizedItem;
|
||||
if (prioritizedItem != null)
|
||||
{
|
||||
prioritizedItems.Add(prioritizedItem);
|
||||
}
|
||||
}
|
||||
|
||||
protected override float TargetEvaluation() => Targets.Any() ? AIObjectiveManager.RunPriority - 1 : 0;
|
||||
public AIObjectiveCleanupItems(Character character, AIObjectiveManager objectiveManager, IEnumerable<Item> prioritizedItems, float priorityModifier = 1)
|
||||
: base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
this.prioritizedItems.AddRange(prioritizedItems.Where(i => i != null));
|
||||
}
|
||||
|
||||
protected override float TargetEvaluation() => Targets.Any() ? (objectiveManager.CurrentOrder == this ? AIObjectiveManager.OrderPriority : AIObjectiveManager.RunPriority - 1) : 0;
|
||||
|
||||
protected override bool Filter(Item target)
|
||||
{
|
||||
@@ -38,7 +47,7 @@ namespace Barotrauma
|
||||
protected override AIObjective ObjectiveConstructor(Item item)
|
||||
=> new AIObjectiveCleanupItem(item, character, objectiveManager, priorityModifier: PriorityModifier)
|
||||
{
|
||||
IsPriority = prioritizedItem == item
|
||||
IsPriority = prioritizedItems.Contains(item)
|
||||
};
|
||||
|
||||
protected override void OnObjectiveCompleted(AIObjective objective, Item target)
|
||||
@@ -56,12 +65,19 @@ namespace Barotrauma
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool IsValidContainer(Item item, Character character) =>
|
||||
!item.IgnoreByAI && item.IsInteractable(character) && item.HasTag("allowcleanup") && item.ParentInventory == null && item.OwnInventory != null && item.OwnInventory.AllItems.Any() && IsItemInsideValidSubmarine(item, character);
|
||||
|
||||
public static bool IsValidTarget(Item item, Character character, bool checkInventory)
|
||||
{
|
||||
if (item == null) { return false; }
|
||||
if (item.IgnoreByAI) { return false; }
|
||||
if (item.NonInteractable) { return false; }
|
||||
if (item.ParentInventory != null) { return false; }
|
||||
if (!item.IsInteractable(character)) { return false; }
|
||||
if (item.SpawnedInOutpost) { return false; }
|
||||
if (item.ParentInventory != null)
|
||||
{
|
||||
if (item.Container == null || !IsValidContainer(item.Container, character)) { return false; }
|
||||
}
|
||||
if (character != null && !IsItemInsideValidSubmarine(item, character)) { return false; }
|
||||
var pickable = item.GetComponent<Pickable>();
|
||||
if (pickable == null) { return false; }
|
||||
@@ -96,18 +112,16 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (var slotType in inv.SlotTypes)
|
||||
{
|
||||
if (allowedSlot.HasFlag(slotType))
|
||||
if (!allowedSlot.HasFlag(slotType)) { continue; }
|
||||
for (int i = 0; i < inv.Capacity; i++)
|
||||
{
|
||||
for (int i = 0; i < inv.Capacity; i++)
|
||||
canEquip = true;
|
||||
if (allowedSlot.HasFlag(inv.SlotTypes[i]) && inv.GetItemAt(i) != null)
|
||||
{
|
||||
canEquip = true;
|
||||
if (allowedSlot.HasFlag(inv.SlotTypes[i]) && inv.Items[i] != null)
|
||||
{
|
||||
canEquip = false;
|
||||
break;
|
||||
}
|
||||
canEquip = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+51
-33
@@ -140,7 +140,7 @@ namespace Barotrauma
|
||||
|
||||
public override float GetPriority()
|
||||
{
|
||||
if (character.TeamID == Character.TeamType.FriendlyNPC && Enemy != null)
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC && Enemy != null)
|
||||
{
|
||||
if (Enemy.Submarine == null || (Enemy.Submarine.TeamID != character.TeamID && Enemy.Submarine != character.Submarine))
|
||||
{
|
||||
@@ -238,7 +238,9 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsLoaded(ItemComponent weapon) => weapon.HasRequiredContainedItems(character, addMessage: false);
|
||||
private bool IsLoaded(ItemComponent weapon, bool checkContainedItems = true) =>
|
||||
weapon.HasRequiredContainedItems(character, addMessage: false) &&
|
||||
(!checkContainedItems || weapon.Item.OwnInventory == null || weapon.Item.OwnInventory.AllItems.Any(i => i.Condition > 0));
|
||||
|
||||
private bool TryArm()
|
||||
{
|
||||
@@ -260,21 +262,21 @@ namespace Barotrauma
|
||||
// No weapons
|
||||
break;
|
||||
}
|
||||
if (!character.Inventory.Items.Contains(Weapon) || WeaponComponent == null)
|
||||
if (!character.Inventory.Contains(Weapon) || WeaponComponent == null)
|
||||
{
|
||||
// Not in the inventory anymore or cannot find the weapon component
|
||||
allWeapons.Remove(WeaponComponent);
|
||||
Weapon = null;
|
||||
continue;
|
||||
}
|
||||
if (IsLoaded(WeaponComponent))
|
||||
if (IsLoaded(WeaponComponent, checkContainedItems: true))
|
||||
{
|
||||
// All good, the weapon is loaded
|
||||
break;
|
||||
}
|
||||
if (Reload(seekAmmo: false))
|
||||
{
|
||||
// All good, reloading successful
|
||||
// All good, we can use the weapon.
|
||||
break;
|
||||
}
|
||||
else
|
||||
@@ -304,7 +306,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
bool isAllowedToSeekWeapons = !EnemyIsClose() && character.TeamID != Character.TeamType.FriendlyNPC && IsOffensiveOrArrest;
|
||||
bool isAllowedToSeekWeapons = !EnemyIsClose() && character.TeamID != CharacterTeamType.FriendlyNPC && IsOffensiveOrArrest;
|
||||
if (!isAllowedToSeekWeapons)
|
||||
{
|
||||
if (WeaponComponent == null)
|
||||
@@ -369,7 +371,7 @@ namespace Barotrauma
|
||||
|
||||
bool CheckWeapon(bool seekAmmo)
|
||||
{
|
||||
if (!character.Inventory.Items.Contains(Weapon) || WeaponComponent == null)
|
||||
if (!character.Inventory.Contains(Weapon) || WeaponComponent == null)
|
||||
{
|
||||
// Not in the inventory anymore or cannot find the weapon component
|
||||
return false;
|
||||
@@ -564,21 +566,20 @@ namespace Barotrauma
|
||||
container.ContainableItems.Any(containable => containable.Identifiers.Any(id => id.Equals(mobileBatteryTag))));
|
||||
// If there's no such container, assume that the melee weapon can stun without a battery.
|
||||
return containers.None() || containers.Any(container =>
|
||||
(container as ItemContainer)?.Inventory.Items.Any(i => i != null && i.HasTag(mobileBatteryTag) && i.Condition > 0.0f) ?? false);
|
||||
(container as ItemContainer)?.Inventory.AllItems.Any(i => i != null && i.HasTag(mobileBatteryTag) && i.Condition > 0.0f) ?? false);
|
||||
}
|
||||
}
|
||||
|
||||
private HashSet<ItemComponent> FindWeaponsFromInventory()
|
||||
{
|
||||
weapons.Clear();
|
||||
foreach (var item in character.Inventory.Items)
|
||||
foreach (var item in character.Inventory.AllItems)
|
||||
{
|
||||
if (item == null) { continue; }
|
||||
if (ignoredWeapons.Contains(item)) { continue; }
|
||||
GetWeapons(item, weapons);
|
||||
if (item.OwnInventory != null)
|
||||
{
|
||||
item.OwnInventory.Items.ForEach(i => GetWeapons(i, weapons));
|
||||
item.OwnInventory.AllItems.ForEach(i => GetWeapons(i, weapons));
|
||||
}
|
||||
}
|
||||
return weapons;
|
||||
@@ -598,7 +599,7 @@ namespace Barotrauma
|
||||
|
||||
private void Unequip()
|
||||
{
|
||||
if (!character.LockHands && character.SelectedItems.Contains(Weapon))
|
||||
if (!character.LockHands && character.HeldItems.Contains(Weapon))
|
||||
{
|
||||
if (!Weapon.AllowedSlots.Contains(InvSlotType.Any) || !character.Inventory.TryPutItem(Weapon, character, new List<InvSlotType>() { InvSlotType.Any }))
|
||||
{
|
||||
@@ -617,7 +618,7 @@ namespace Barotrauma
|
||||
if (!character.HasEquippedItem(Weapon))
|
||||
{
|
||||
Weapon.TryInteract(character, forceSelectKey: true);
|
||||
var slots = Weapon.AllowedSlots.FindAll(s => s == InvSlotType.LeftHand || s == InvSlotType.RightHand || s == (InvSlotType.LeftHand | InvSlotType.RightHand));
|
||||
var slots = Weapon.AllowedSlots.Where(s => s == InvSlotType.LeftHand || s == InvSlotType.RightHand || s == (InvSlotType.LeftHand | InvSlotType.RightHand));
|
||||
if (character.Inventory.TryPutItem(Weapon, character, slots))
|
||||
{
|
||||
aimTimer = Rand.Range(0.5f, 1f);
|
||||
@@ -651,7 +652,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
retreatTarget = findSafety.FindBestHull(HumanAIController.VisibleHulls, allowChangingTheSubmarine: character.TeamID != Character.TeamType.FriendlyNPC);
|
||||
retreatTarget = findSafety.FindBestHull(HumanAIController.VisibleHulls, allowChangingTheSubmarine: character.TeamID != CharacterTeamType.FriendlyNPC);
|
||||
findHullTimer = findHullInterval * Rand.Range(0.9f, 1.1f);
|
||||
}
|
||||
}
|
||||
@@ -724,7 +725,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
if (character.TeamID == Character.TeamType.FriendlyNPC)
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
ItemPrefab prefab = ItemPrefab.Find(null, "handcuffs");
|
||||
if (prefab != null)
|
||||
@@ -769,9 +770,9 @@ namespace Barotrauma
|
||||
#endif
|
||||
}
|
||||
// Confiscate stolen goods.
|
||||
foreach (var item in Enemy.Inventory.Items)
|
||||
foreach (var item in Enemy.Inventory.AllItemsMod)
|
||||
{
|
||||
if (item == null || item == handCuffs) { continue; }
|
||||
if (item == handCuffs) { continue; }
|
||||
if (item.StolenDuringRound)
|
||||
{
|
||||
item.Drop(character);
|
||||
@@ -814,33 +815,50 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
private bool Reload(bool seekAmmo)
|
||||
{
|
||||
if (WeaponComponent == null) { return false; }
|
||||
if (!WeaponComponent.requiredItems.ContainsKey(RelatedItem.RelationType.Contained)) { return false; }
|
||||
var containedItems = Weapon.OwnInventory?.Items;
|
||||
if (containedItems == null) { return true; }
|
||||
// Drop empty ammo
|
||||
foreach (Item containedItem in containedItems)
|
||||
if (WeaponComponent == null) { return false; }
|
||||
if (Weapon.OwnInventory == null) { return true; }
|
||||
// Eject empty ammo
|
||||
if (Weapon.OwnInventory.AllItems.Any(it => it.Condition <= 0.0f))
|
||||
{
|
||||
if (containedItem == null) { continue; }
|
||||
if (containedItem.Condition <= 0)
|
||||
foreach (Item containedItem in Weapon.OwnInventory.AllItemsMod)
|
||||
{
|
||||
containedItem.Drop(character);
|
||||
if (containedItem.Condition <= 0)
|
||||
{
|
||||
if (character.Submarine == null)
|
||||
{
|
||||
// If we are outside of main sub, try to put the ammo in the inventory instead dropping it in the sea.
|
||||
if (character.Inventory.TryPutItem(containedItem, character, CharacterInventory.anySlot))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
containedItem.Drop(character);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RelatedItem item = null;
|
||||
Item ammunition = null;
|
||||
string[] ammunitionIdentifiers = null;
|
||||
foreach (RelatedItem requiredItem in WeaponComponent.requiredItems[RelatedItem.RelationType.Contained])
|
||||
if (WeaponComponent.requiredItems.ContainsKey(RelatedItem.RelationType.Contained))
|
||||
{
|
||||
ammunition = containedItems.FirstOrDefault(it => it != null && it.Condition > 0 && requiredItem.MatchesItem(it));
|
||||
if (ammunition != null)
|
||||
foreach (RelatedItem requiredItem in WeaponComponent.requiredItems[RelatedItem.RelationType.Contained])
|
||||
{
|
||||
// Ammunition still remaining
|
||||
return true;
|
||||
ammunition = Weapon.OwnInventory.AllItems.FirstOrDefault(it => it.Condition > 0 && requiredItem.MatchesItem(it));
|
||||
if (ammunition != null)
|
||||
{
|
||||
// Ammunition still remaining
|
||||
return true;
|
||||
}
|
||||
item = requiredItem;
|
||||
ammunitionIdentifiers = requiredItem.Identifiers;
|
||||
}
|
||||
item = requiredItem;
|
||||
ammunitionIdentifiers = requiredItem.Identifiers;
|
||||
}
|
||||
else if (WeaponComponent is MeleeWeapon meleeWeapon)
|
||||
{
|
||||
ammunitionIdentifiers = meleeWeapon.PreferredContainedItems;
|
||||
}
|
||||
|
||||
// No ammo
|
||||
if (ammunition == null)
|
||||
{
|
||||
|
||||
+6
-7
@@ -67,14 +67,14 @@ namespace Barotrauma
|
||||
}
|
||||
if (item != null)
|
||||
{
|
||||
return container.Inventory.Items.Contains(item);
|
||||
return container.Inventory.Contains(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
int containedItemCount = 0;
|
||||
foreach (Item i in container.Inventory.Items)
|
||||
foreach (Item it in container.Inventory.AllItems)
|
||||
{
|
||||
if (i != null && CheckItem(i))
|
||||
if (CheckItem(it))
|
||||
{
|
||||
containedItemCount++;
|
||||
}
|
||||
@@ -83,7 +83,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private bool CheckItem(Item i) => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id)) && i.ConditionPercentage >= ConditionLevel;
|
||||
private bool CheckItem(Item i) => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id)) && i.ConditionPercentage >= ConditionLevel && !i.IsThisOrAnyContainerIgnoredByAI();
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
@@ -102,11 +102,10 @@ namespace Barotrauma
|
||||
}
|
||||
if (character.CanInteractWith(container.Item, checkLinked: false))
|
||||
{
|
||||
if (RemoveEmpty)
|
||||
if (RemoveEmpty && container.Inventory.AllItems.Any(it => it.Condition <= 0.0f))
|
||||
{
|
||||
foreach (var emptyItem in container.Inventory.Items)
|
||||
foreach (var emptyItem in container.Inventory.AllItemsMod)
|
||||
{
|
||||
if (emptyItem == null) { continue; }
|
||||
if (emptyItem.Condition <= 0)
|
||||
{
|
||||
emptyItem.Drop(character);
|
||||
|
||||
+7
-2
@@ -58,12 +58,17 @@ namespace Barotrauma
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
Item itemToDecontain = targetItem ?? sourceContainer.Inventory.FindItem(i => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id)), recursive: false);
|
||||
Item itemToDecontain = targetItem ?? sourceContainer.Inventory.FindItem(i => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id) && !i.IgnoreByAI), recursive: false);
|
||||
if (itemToDecontain == null)
|
||||
{
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
if (itemToDecontain.IgnoreByAI)
|
||||
{
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
if (targetContainer == null)
|
||||
{
|
||||
if (sourceContainer == null)
|
||||
@@ -77,7 +82,7 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (targetContainer.Inventory.Items.Contains(itemToDecontain))
|
||||
else if (targetContainer.Inventory.Contains(itemToDecontain))
|
||||
{
|
||||
IsCompleted = true;
|
||||
return;
|
||||
|
||||
+5
-4
@@ -111,9 +111,10 @@ namespace Barotrauma
|
||||
float xDist = Math.Abs(character.WorldPosition.X - fs.WorldPosition.X) - fs.DamageRange;
|
||||
float yDist = Math.Abs(character.WorldPosition.Y - fs.WorldPosition.Y);
|
||||
bool inRange = xDist + yDist < extinguisher.Range;
|
||||
bool canSee = HumanAIController.VisibleHulls.Contains(fs.Hull) || character.CanSeeTarget(fs);
|
||||
bool move = !inRange || !canSee;
|
||||
if ((inRange && canSee) || useExtinquisherTimer > 0)
|
||||
// Use the hull position, because the fire x pos is sometimes inside a wall -> the bot can't ever see it and continues running towards the wall.
|
||||
ISpatialEntity lookTarget = character.CurrentHull == targetHull || character.CurrentHull.linkedTo.Contains(targetHull) ? targetHull : fs as ISpatialEntity;
|
||||
bool move = !inRange || !character.CanSeeTarget(lookTarget);
|
||||
if ((inRange && character.CanSeeTarget(lookTarget)) || useExtinquisherTimer > 0)
|
||||
{
|
||||
useExtinquisherTimer += deltaTime;
|
||||
if (useExtinquisherTimer > 2.0f)
|
||||
@@ -148,7 +149,7 @@ namespace Barotrauma
|
||||
onAbandon: () => Abandon = true,
|
||||
onCompleted: () => RemoveSubObjective(ref gotoObjective)))
|
||||
{
|
||||
gotoObjective.requiredCondition = () => HumanAIController.VisibleHulls.Contains(fs.Hull);
|
||||
gotoObjective.requiredCondition = () => targetHull == null || character.CanSeeTarget(targetHull);
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
-1
@@ -38,7 +38,6 @@ namespace Barotrauma
|
||||
public static bool IsValidTarget(Hull hull, Character character)
|
||||
{
|
||||
if (hull == null) { return false; }
|
||||
if (hull.IgnoreByAI) { return false; }
|
||||
if (hull.FireSources.None()) { return false; }
|
||||
if (hull.Submarine == null) { return false; }
|
||||
if (character.Submarine == null) { return false; }
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ namespace Barotrauma
|
||||
protected override AIObjective ObjectiveConstructor(Character target)
|
||||
{
|
||||
var combatObjective = new AIObjectiveCombat(character, target, AIObjectiveCombat.CombatMode.Offensive, objectiveManager, PriorityModifier);
|
||||
if (character.TeamID == Character.TeamType.FriendlyNPC && target.TeamID == Character.TeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign)
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC && target.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign)
|
||||
{
|
||||
var reputation = campaign.Map?.CurrentLocation?.Reputation;
|
||||
if (reputation != null && reputation.NormalizedValue < Reputation.HostileThreshold)
|
||||
|
||||
+20
-12
@@ -1,5 +1,6 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.Extensions;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -56,7 +57,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!DropEmptyTanks(character, targetItem, out Item[] containedItems))
|
||||
if (!EjectEmptyTanks(character, targetItem, out var containedItems))
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.ThrowError($"{character.Name}: AIObjectiveFindDivingGear failed - the item \"" + targetItem + "\" has no proper inventory");
|
||||
@@ -70,8 +71,11 @@ namespace Barotrauma
|
||||
// Seek oxygen that has min 10% condition left.
|
||||
TryAddSubObjective(ref getOxygen, () =>
|
||||
{
|
||||
character.Speak(TextManager.Get("DialogGetOxygenTank"), null, 0, "getoxygentank", 30.0f);
|
||||
return new AIObjectiveContainItem(character, OXYGEN_SOURCE, targetItem.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == Character.TeamType.FriendlyNPC)
|
||||
if (!HumanAIController.HasItem(character, "oxygensource", out _, conditionPercentage: 10))
|
||||
{
|
||||
character.Speak(TextManager.Get("DialogGetOxygenTank"), null, 0, "getoxygentank", 30.0f);
|
||||
}
|
||||
return new AIObjectiveContainItem(character, OXYGEN_SOURCE, targetItem.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
AllowToFindDivingGear = false,
|
||||
AllowDangerousPressure = true,
|
||||
@@ -83,7 +87,7 @@ namespace Barotrauma
|
||||
// Try to seek any oxygen sources.
|
||||
TryAddSubObjective(ref getOxygen, () =>
|
||||
{
|
||||
return new AIObjectiveContainItem(character, OXYGEN_SOURCE, targetItem.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == Character.TeamType.FriendlyNPC)
|
||||
return new AIObjectiveContainItem(character, OXYGEN_SOURCE, targetItem.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
AllowToFindDivingGear = false,
|
||||
AllowDangerousPressure = true
|
||||
@@ -100,18 +104,22 @@ namespace Barotrauma
|
||||
/// <summary>
|
||||
/// Returns false only when no inventory can be found from the item.
|
||||
/// </summary>
|
||||
public static bool DropEmptyTanks(Character actor, Item target, out Item[] containedItems)
|
||||
public static bool EjectEmptyTanks(Character actor, Item target, out IEnumerable<Item> containedItems)
|
||||
{
|
||||
containedItems = target.OwnInventory?.Items;
|
||||
if (containedItems == null)
|
||||
containedItems = target.OwnInventory?.AllItems;
|
||||
if (containedItems == null) { return false; }
|
||||
foreach (Item containedItem in target.OwnInventory.AllItemsMod)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
foreach (Item containedItem in containedItems)
|
||||
{
|
||||
if (containedItem == null) { continue; }
|
||||
if (containedItem.Condition <= 0.0f)
|
||||
{
|
||||
if (actor.Submarine == null)
|
||||
{
|
||||
// If we are outside of main sub, try to put the tank in the inventory instead dropping it in the sea.
|
||||
if (actor.Inventory.TryPutItem(containedItem, actor, CharacterInventory.anySlot))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
containedItem.Drop(actor);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -168,7 +168,7 @@ namespace Barotrauma
|
||||
{
|
||||
searchHullTimer = SearchHullInterval * Rand.Range(0.9f, 1.1f);
|
||||
previousSafeHull = currentSafeHull;
|
||||
currentSafeHull = FindBestHull(allowChangingTheSubmarine: character.TeamID != Character.TeamType.FriendlyNPC);
|
||||
currentSafeHull = FindBestHull(allowChangingTheSubmarine: character.TeamID != CharacterTeamType.FriendlyNPC);
|
||||
cannotFindSafeHull = currentSafeHull == null || HumanAIController.NeedsDivingGear(currentSafeHull, out _);
|
||||
if (currentSafeHull == null)
|
||||
{
|
||||
@@ -359,7 +359,7 @@ namespace Barotrauma
|
||||
hullSafety *= distanceFactor;
|
||||
// If the target is not inside a friendly submarine, considerably reduce the hull safety.
|
||||
// Intentionally exclude wrecks from this check
|
||||
if (hull.Submarine.TeamID != character.TeamID && hull.Submarine.TeamID != Character.TeamType.FriendlyNPC)
|
||||
if (hull.Submarine.TeamID != character.TeamID && hull.Submarine.TeamID != CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
hullSafety /= 10;
|
||||
}
|
||||
|
||||
+11
-9
@@ -64,7 +64,7 @@ namespace Barotrauma
|
||||
var weldingTool = character.Inventory.FindItemByTag("weldingequipment", true);
|
||||
if (weldingTool == null)
|
||||
{
|
||||
TryAddSubObjective(ref getWeldingTool, () => new AIObjectiveGetItem(character, "weldingequipment", objectiveManager, equip: true, spawnItemIfNotFound: character.TeamID == Character.TeamType.FriendlyNPC),
|
||||
TryAddSubObjective(ref getWeldingTool, () => new AIObjectiveGetItem(character, "weldingequipment", objectiveManager, equip: true, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC),
|
||||
onAbandon: () =>
|
||||
{
|
||||
if (objectiveManager.IsCurrentOrder<AIObjectiveFixLeaks>())
|
||||
@@ -78,8 +78,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
var containedItems = weldingTool.OwnInventory?.Items;
|
||||
if (containedItems == null)
|
||||
if (weldingTool.OwnInventory == null)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.ThrowError($"{character.Name}: AIObjectiveFixLeak failed - the item \"" + weldingTool + "\" has no proper inventory");
|
||||
@@ -88,17 +87,20 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
// Drop empty tanks
|
||||
foreach (Item containedItem in containedItems)
|
||||
if (weldingTool.OwnInventory.AllItems.Any(it => it.Condition <= 0.0f))
|
||||
{
|
||||
if (containedItem == null) { continue; }
|
||||
if (containedItem.Condition <= 0.0f)
|
||||
foreach (Item containedItem in weldingTool.OwnInventory.AllItemsMod)
|
||||
{
|
||||
containedItem.Drop(character);
|
||||
if (containedItem.Condition <= 0.0f)
|
||||
{
|
||||
containedItem.Drop(character);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (containedItems.None(i => i != null && i.HasTag("weldingfuel") && i.Condition > 0.0f))
|
||||
|
||||
if (weldingTool.OwnInventory.AllItems.None(i => i.HasTag("weldingfuel") && i.Condition > 0.0f))
|
||||
{
|
||||
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfuel", weldingTool.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == Character.TeamType.FriendlyNPC),
|
||||
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfuel", weldingTool.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC),
|
||||
onAbandon: () => Abandon = true,
|
||||
onCompleted: () => RemoveSubObjective(ref refuelObjective));
|
||||
return;
|
||||
|
||||
+8
-4
@@ -177,7 +177,7 @@ namespace Barotrauma
|
||||
}
|
||||
else if (moveToTarget is Item parentItem)
|
||||
{
|
||||
canInteract = character.CanInteractWith(parentItem, out _, checkLinked: false);
|
||||
canInteract = character.CanInteractWith(parentItem, checkLinked: false);
|
||||
}
|
||||
if (canInteract)
|
||||
{
|
||||
@@ -256,7 +256,7 @@ namespace Barotrauma
|
||||
if (mySub == null) { continue; }
|
||||
if (!AllowStealing)
|
||||
{
|
||||
if (character.TeamID == Character.TeamType.FriendlyNPC != item.SpawnedInOutpost) { continue; }
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC != item.SpawnedInOutpost) { continue; }
|
||||
}
|
||||
if (!CheckItem(item)) { continue; }
|
||||
if (ignoredContainerIdentifiers != null && item.Container != null)
|
||||
@@ -276,6 +276,10 @@ namespace Barotrauma
|
||||
itemPriority = GetItemPriority(item);
|
||||
}
|
||||
Entity rootInventoryOwner = item.GetRootInventoryOwner();
|
||||
if (rootInventoryOwner is Item ownerItem)
|
||||
{
|
||||
if (!ownerItem.IsInteractable(character)) { continue; }
|
||||
}
|
||||
Vector2 itemPos = (rootInventoryOwner ?? item).WorldPosition;
|
||||
float yDist = Math.Abs(character.WorldPosition.Y - itemPos.Y);
|
||||
yDist = yDist > 100 ? yDist * 5 : 0;
|
||||
@@ -308,7 +312,7 @@ namespace Barotrauma
|
||||
Entity.Spawner.AddToSpawnQueue(prefab, character.Inventory, onSpawned: (Item spawnedItem) =>
|
||||
{
|
||||
targetItem = spawnedItem;
|
||||
if (character.TeamID == Character.TeamType.FriendlyNPC && (character.Submarine?.Info.IsOutpost ?? false))
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC && (character.Submarine?.Info.IsOutpost ?? false))
|
||||
{
|
||||
spawnedItem.SpawnedInOutpost = true;
|
||||
}
|
||||
@@ -347,7 +351,7 @@ namespace Barotrauma
|
||||
|
||||
private bool CheckItem(Item item)
|
||||
{
|
||||
if (item.NonInteractable) { return false; }
|
||||
if (!item.IsInteractable(character)) { return false; }
|
||||
if (item.IsThisOrAnyContainerIgnoredByAI()) { return false; }
|
||||
if (ignoredItems.Contains(item)) { return false; };
|
||||
if (item.Condition < TargetCondition) { return false; }
|
||||
|
||||
+154
-47
@@ -2,6 +2,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -34,6 +35,9 @@ namespace Barotrauma
|
||||
public float extraDistanceOutsideSub;
|
||||
private float _closeEnough = 50;
|
||||
private readonly float minDistance = 50;
|
||||
private readonly float seekGapsInterval = 1;
|
||||
private float seekGapsTimer;
|
||||
|
||||
/// <summary>
|
||||
/// Display units
|
||||
/// </summary>
|
||||
@@ -116,6 +120,8 @@ namespace Barotrauma
|
||||
return Priority;
|
||||
}
|
||||
|
||||
private readonly float avoidLookAheadDistance = 5;
|
||||
|
||||
public AIObjectiveGoTo(ISpatialEntity target, Character character, AIObjectiveManager objectiveManager, bool repeat = false, bool getDivingGearIfNeeded = true, float priorityModifier = 1, float closeEnough = 0)
|
||||
: base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
@@ -208,14 +214,14 @@ namespace Barotrauma
|
||||
{
|
||||
Abandon = true;
|
||||
}
|
||||
else if (waitUntilPathUnreachable < 0)
|
||||
else if (SteeringManager == PathSteering && PathSteering.CurrentPath != null && PathSteering.CurrentPath.Unreachable && !PathSteering.IsPathDirty)
|
||||
{
|
||||
if (SteeringManager == PathSteering && PathSteering.CurrentPath != null && PathSteering.CurrentPath.Unreachable && !PathSteering.IsPathDirty)
|
||||
SteeringManager.Reset();
|
||||
if (waitUntilPathUnreachable < 0)
|
||||
{
|
||||
if (repeat)
|
||||
{
|
||||
SpeakCannotReach();
|
||||
SteeringManager.Reset();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -223,12 +229,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Abandon)
|
||||
{
|
||||
SpeakCannotReach();
|
||||
SteeringManager.Reset();
|
||||
}
|
||||
else
|
||||
if (!Abandon)
|
||||
{
|
||||
if (getDivingGearIfNeeded && !character.LockHands)
|
||||
{
|
||||
@@ -286,52 +287,134 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!character.AnimController.InWater)
|
||||
if (character.AnimController.InWater)
|
||||
{
|
||||
useScooter = false;
|
||||
checkScooterTimer = 0;
|
||||
}
|
||||
else if (checkScooterTimer <= 0)
|
||||
{
|
||||
useScooter = false;
|
||||
checkScooterTimer = checkScooterTime;
|
||||
string scooterTag = "scooter";
|
||||
string batteryTag = "mobilebattery";
|
||||
Item scooter = null;
|
||||
bool isScooterEquipped = false;
|
||||
float closeEnough = 250;
|
||||
float squaredDistance = Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition);
|
||||
bool shouldUseScooter = squaredDistance > closeEnough * closeEnough && (!mimic ||
|
||||
(Target is Character targetCharacter && targetCharacter.HasEquippedItem(scooterTag, allowBroken: false)) || squaredDistance > Math.Pow(closeEnough * 2, 2));
|
||||
if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> equippedScooters, batteryTag, requireEquipped: true))
|
||||
if (character.CurrentHull == null)
|
||||
{
|
||||
scooter = equippedScooters.FirstOrDefault();
|
||||
isScooterEquipped = scooter != null;
|
||||
}
|
||||
else if (shouldUseScooter && HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> scooters, batteryTag, requireEquipped: false))
|
||||
{
|
||||
scooter = scooters.FirstOrDefault();
|
||||
if (scooter != null)
|
||||
if (seekGapsTimer > 0)
|
||||
{
|
||||
isScooterEquipped = HumanAIController.TakeItem(scooter, character.Inventory, equip: true, dropOtherIfCannotMove: false, allowSwapping: true, storeUnequipped: false);
|
||||
}
|
||||
}
|
||||
if (scooter != null && isScooterEquipped)
|
||||
{
|
||||
if (shouldUseScooter)
|
||||
{
|
||||
useScooter = true;
|
||||
seekGapsTimer -= deltaTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Unequip
|
||||
character.Inventory.TryPutItem(scooter, character, CharacterInventory.anySlot);
|
||||
SeekGaps(maxDistance: 500);
|
||||
seekGapsTimer = seekGapsInterval * Rand.Range(0.1f, 1.1f);
|
||||
if (TargetGap != null)
|
||||
{
|
||||
// Check that nothing is blocking the way
|
||||
Vector2 rayStart = character.SimPosition;
|
||||
Vector2 rayEnd = TargetGap.SimPosition;
|
||||
if (TargetGap.Submarine != null && character.Submarine == null)
|
||||
{
|
||||
rayStart -= TargetGap.Submarine.SimPosition;
|
||||
}
|
||||
else if (TargetGap.Submarine == null && character.Submarine != null)
|
||||
{
|
||||
rayEnd -= character.Submarine.SimPosition;
|
||||
}
|
||||
var closestBody = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true);
|
||||
if (closestBody != null)
|
||||
{
|
||||
TargetGap = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
TargetGap = null;
|
||||
}
|
||||
if (TargetGap != null)
|
||||
{
|
||||
if (TargetGap.FlowTargetHull != null && HumanAIController.SteerThroughGap(TargetGap, TargetGap.FlowTargetHull.WorldPosition, deltaTime))
|
||||
{
|
||||
SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 1);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
TargetGap = null;
|
||||
}
|
||||
}
|
||||
if (checkScooterTimer <= 0)
|
||||
{
|
||||
useScooter = false;
|
||||
checkScooterTimer = checkScooterTime;
|
||||
string scooterTag = "scooter";
|
||||
string batteryTag = "mobilebattery";
|
||||
Item scooter = null;
|
||||
float closeEnough = 250;
|
||||
float squaredDistance = Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition);
|
||||
bool shouldUseScooter = squaredDistance > closeEnough * closeEnough && (!mimic ||
|
||||
(Target is Character targetCharacter && targetCharacter.HasEquippedItem(scooterTag, allowBroken: false)) || squaredDistance > Math.Pow(closeEnough * 2, 2));
|
||||
if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> equippedScooters, recursive: false, requireEquipped: true))
|
||||
{
|
||||
// Currently equipped scooter
|
||||
scooter = equippedScooters.FirstOrDefault();
|
||||
}
|
||||
else if (shouldUseScooter)
|
||||
{
|
||||
bool hasBattery = false;
|
||||
if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> nonEquippedScooters, containedTag: batteryTag, conditionPercentage: 1, requireEquipped: false))
|
||||
{
|
||||
// Non-equipped scooter with a battery
|
||||
scooter = nonEquippedScooters.FirstOrDefault();
|
||||
hasBattery = true;
|
||||
}
|
||||
else if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> _nonEquippedScooters, requireEquipped: false))
|
||||
{
|
||||
// Non-equipped scooter without a battery
|
||||
scooter = _nonEquippedScooters.FirstOrDefault();
|
||||
// Non-recursive so that the bots won't take batteries from other items. Also means that they can't find batteries inside containers. Not sure how to solve this.
|
||||
hasBattery = HumanAIController.HasItem(character, batteryTag, out _, requireEquipped: false, conditionPercentage: 1, recursive: false);
|
||||
}
|
||||
if (scooter != null && hasBattery)
|
||||
{
|
||||
// Equip only if we have a battery available
|
||||
HumanAIController.TakeItem(scooter, character.Inventory, equip: true, dropOtherIfCannotMove: false, allowSwapping: true, storeUnequipped: false);
|
||||
}
|
||||
}
|
||||
bool isScooterEquipped = scooter != null && character.HasEquippedItem(scooter);
|
||||
if (scooter != null && isScooterEquipped)
|
||||
{
|
||||
if (shouldUseScooter)
|
||||
{
|
||||
useScooter = true;
|
||||
// Check the battery
|
||||
if (scooter.ContainedItems.None(i => i.Condition > 0))
|
||||
{
|
||||
// Try to switch batteries
|
||||
if (HumanAIController.HasItem(character, batteryTag, out IEnumerable<Item> batteries, conditionPercentage: 1, recursive: false))
|
||||
{
|
||||
scooter.ContainedItems.ForEachMod(emptyBattery => character.Inventory.TryPutItem(emptyBattery, character, CharacterInventory.anySlot));
|
||||
if (!scooter.Combine(batteries.OrderByDescending(b => b.Condition).First(), character))
|
||||
{
|
||||
useScooter = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
useScooter = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!useScooter)
|
||||
{
|
||||
// Unequip
|
||||
character.Inventory.TryPutItem(scooter, character, CharacterInventory.anySlot);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
checkScooterTimer -= deltaTime;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
checkScooterTimer -= deltaTime;
|
||||
TargetGap = null;
|
||||
useScooter = false;
|
||||
checkScooterTimer = 0;
|
||||
}
|
||||
if (SteeringManager == PathSteering)
|
||||
{
|
||||
@@ -347,7 +430,7 @@ namespace Barotrauma
|
||||
nodeFilter,
|
||||
CheckVisibility);
|
||||
|
||||
if (!isInside && PathSteering.CurrentPath == null || PathSteering.IsPathDirty || PathSteering.CurrentPath.Unreachable)
|
||||
if (!isInside && (PathSteering.CurrentPath == null || PathSteering.IsPathDirty || PathSteering.CurrentPath.Unreachable))
|
||||
{
|
||||
if (useScooter)
|
||||
{
|
||||
@@ -358,7 +441,7 @@ namespace Barotrauma
|
||||
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(Target.WorldPosition - character.WorldPosition));
|
||||
if (character.AnimController.InWater)
|
||||
{
|
||||
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: 5, weight: 2);
|
||||
SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -378,7 +461,7 @@ namespace Barotrauma
|
||||
SteeringManager.SteeringSeek(character.GetRelativeSimPosition(Target), 10);
|
||||
if (character.AnimController.InWater)
|
||||
{
|
||||
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: 5, weight: 15);
|
||||
SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 15);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -439,6 +522,27 @@ namespace Barotrauma
|
||||
return null;
|
||||
}
|
||||
|
||||
public Gap TargetGap { get; private set; }
|
||||
private void SeekGaps(float maxDistance)
|
||||
{
|
||||
Gap selectedGap = null;
|
||||
float selectedDistance = -1;
|
||||
foreach (Gap gap in Gap.GapList)
|
||||
{
|
||||
if (gap.Open < 1) { continue; }
|
||||
if (gap.FlowTargetHull == null) { continue; }
|
||||
if (gap.Submarine != Target.Submarine) { continue; }
|
||||
float distance = Vector2.DistanceSquared(character.WorldPosition, gap.WorldPosition);
|
||||
if (distance > maxDistance * maxDistance) { continue; }
|
||||
if (selectedGap == null || distance < selectedDistance)
|
||||
{
|
||||
selectedGap = gap;
|
||||
selectedDistance = distance;
|
||||
}
|
||||
}
|
||||
TargetGap = selectedGap;
|
||||
}
|
||||
|
||||
public bool IsCloseEnough
|
||||
{
|
||||
get
|
||||
@@ -507,6 +611,7 @@ namespace Barotrauma
|
||||
{
|
||||
PathSteering.ResetPath();
|
||||
}
|
||||
SpeakCannotReach();
|
||||
base.OnAbandon();
|
||||
}
|
||||
|
||||
@@ -530,6 +635,8 @@ namespace Barotrauma
|
||||
{
|
||||
base.Reset();
|
||||
findDivingGear = null;
|
||||
seekGapsTimer = 0;
|
||||
TargetGap = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
-4
@@ -21,7 +21,7 @@ namespace Barotrauma
|
||||
set
|
||||
{
|
||||
behavior = value;
|
||||
if (behavior == BehaviorType.StayInHull && character.TeamID != Character.TeamType.FriendlyNPC)
|
||||
if (behavior == BehaviorType.StayInHull && character.TeamID != CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
DebugConsole.NewMessage($"AIObjectiveIdle.BehaviorType.StayInHull is implemented only for outpost NPCs. Using passive behavior for {character.Name} ({character.Info.Job.Prefab.Identifier})", color: Color.Red);
|
||||
behavior = BehaviorType.Passive;
|
||||
@@ -203,7 +203,7 @@ namespace Barotrauma
|
||||
|
||||
if (currentTarget != null && !currentTargetIsInvalid)
|
||||
{
|
||||
if (character.TeamID == Character.TeamType.FriendlyNPC)
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
if (currentTarget.Submarine.TeamID != character.TeamID)
|
||||
{
|
||||
@@ -260,7 +260,7 @@ namespace Barotrauma
|
||||
{
|
||||
//choose a random available hull
|
||||
currentTarget = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced);
|
||||
bool isInWrongSub = character.TeamID == Character.TeamType.FriendlyNPC && character.Submarine.TeamID != character.TeamID;
|
||||
bool isInWrongSub = character.TeamID == CharacterTeamType.FriendlyNPC && character.Submarine.TeamID != character.TeamID;
|
||||
bool isCurrentHullAllowed = !isInWrongSub && !IsForbidden(character.CurrentHull);
|
||||
var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, errorMsgStr: $"AIObjectiveIdle {character.DisplayName}", nodeFilter: node =>
|
||||
{
|
||||
@@ -402,6 +402,14 @@ namespace Barotrauma
|
||||
PathSteering.Wander(deltaTime);
|
||||
}
|
||||
|
||||
public void FaceTargetAndWait(ISpatialEntity target, float waitTime)
|
||||
{
|
||||
standStillTimer = waitTime;
|
||||
HumanAIController.FaceTarget(target);
|
||||
currentTarget = null;
|
||||
SetTargetTimerHigh();
|
||||
}
|
||||
|
||||
private void FindTargetHulls()
|
||||
{
|
||||
targetHulls.Clear();
|
||||
@@ -411,7 +419,7 @@ namespace Barotrauma
|
||||
if (HumanAIController.UnsafeHulls.Contains(hull)) { continue; }
|
||||
if (hull.Submarine == null) { continue; }
|
||||
if (character.Submarine == null) { break; }
|
||||
if (character.TeamID == Character.TeamType.FriendlyNPC)
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
if (hull.Submarine.TeamID != character.TeamID)
|
||||
{
|
||||
|
||||
+71
-9
@@ -127,10 +127,14 @@ namespace Barotrauma
|
||||
{
|
||||
var orderPrefab = Order.GetPrefab(autonomousObjective.identifier);
|
||||
if (orderPrefab == null) { throw new Exception($"Could not find a matching prefab by the identifier: '{autonomousObjective.identifier}'"); }
|
||||
var item = orderPrefab.MustSetTarget ? orderPrefab.GetMatchingItems(character.Submarine, mustBelongToPlayerSub: false, requiredTeam: character.Info.TeamID)?.GetRandom() : null;
|
||||
Item item = null;
|
||||
if (orderPrefab.MustSetTarget)
|
||||
{
|
||||
item = orderPrefab.GetMatchingItems(character.Submarine, mustBelongToPlayerSub: false, requiredTeam: character.Info.TeamID, interactableFor: character)?.GetRandom();
|
||||
}
|
||||
var order = new Order(orderPrefab, item ?? character.CurrentHull as Entity, orderPrefab.GetTargetItemComponent(item), orderGiver: character);
|
||||
if (order == null) { continue; }
|
||||
if (autonomousObjective.ignoreAtOutpost && Level.IsLoadedOutpost && character.TeamID != Character.TeamType.FriendlyNPC) { continue; }
|
||||
if (autonomousObjective.ignoreAtOutpost && Level.IsLoadedOutpost && character.TeamID != CharacterTeamType.FriendlyNPC) { continue; }
|
||||
var objective = CreateObjective(order, autonomousObjective.option, character, isAutonomous: true, autonomousObjective.priorityModifier);
|
||||
if (objective != null && objective.CanBeCompleted)
|
||||
{
|
||||
@@ -273,7 +277,8 @@ namespace Barotrauma
|
||||
CurrentOrder = objective;
|
||||
}
|
||||
|
||||
public void SetOrder(Order order, string option, Character orderGiver)
|
||||
private CoroutineHandle speakRoutine;
|
||||
public void SetOrder(Order order, string option, Character orderGiver, bool speak)
|
||||
{
|
||||
if (character.IsDead)
|
||||
{
|
||||
@@ -294,6 +299,49 @@ namespace Barotrauma
|
||||
{
|
||||
// This should be redundant, because all the objectives are reset when they are selected as active.
|
||||
CurrentOrder.Reset();
|
||||
if (speak)
|
||||
{
|
||||
character.Speak(TextManager.Get("DialogAffirmative"), null, 1.0f);
|
||||
if (speakRoutine != null)
|
||||
{
|
||||
CoroutineManager.StopCoroutines(speakRoutine);
|
||||
}
|
||||
speakRoutine = CoroutineManager.InvokeAfter(() =>
|
||||
{
|
||||
if (GameMain.GameSession == null || Level.Loaded == null) { return; }
|
||||
if (CurrentOrder != null && character.SpeechImpediment < 100.0f)
|
||||
{
|
||||
if (CurrentOrder is AIObjectiveRepairItems repairItems && repairItems.Targets.None())
|
||||
{
|
||||
character.Speak(TextManager.Get("DialogNoRepairTargets"), null, 3.0f, "norepairtargets");
|
||||
}
|
||||
else if (CurrentOrder is AIObjectiveChargeBatteries chargeBatteries && chargeBatteries.Targets.None())
|
||||
{
|
||||
character.Speak(TextManager.Get("DialogNoBatteries"), null, 3.0f, "nobatteries");
|
||||
}
|
||||
else if (CurrentOrder is AIObjectiveExtinguishFires extinguishFires && extinguishFires.Targets.None())
|
||||
{
|
||||
character.Speak(TextManager.Get("DialogNoFire"), null, 3.0f, "nofire");
|
||||
}
|
||||
else if (CurrentOrder is AIObjectiveFixLeaks fixLeaks && fixLeaks.Targets.None())
|
||||
{
|
||||
character.Speak(TextManager.Get("DialogNoLeaks"), null, 3.0f, "noleaks");
|
||||
}
|
||||
else if (CurrentOrder is AIObjectiveFightIntruders fightIntruders && fightIntruders.Targets.None())
|
||||
{
|
||||
character.Speak(TextManager.Get("DialogNoEnemies"), null, 3.0f, "noenemies");
|
||||
}
|
||||
else if (CurrentOrder is AIObjectiveRescueAll rescueAll && rescueAll.Targets.None())
|
||||
{
|
||||
character.Speak(TextManager.Get("DialogNoRescueTargets"), null, 3.0f, "norescuetargets");
|
||||
}
|
||||
else if (CurrentOrder is AIObjectivePumpWater pumpWater && pumpWater.Targets.None())
|
||||
{
|
||||
character.Speak(TextManager.Get("DialogNoPumps"), null, 3.0f, "nopumps");
|
||||
}
|
||||
}
|
||||
}, 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,8 +368,7 @@ namespace Barotrauma
|
||||
case "wait":
|
||||
newObjective = new AIObjectiveGoTo(order.TargetSpatialEntity ?? character, character, this, repeat: true, priorityModifier: priorityModifier)
|
||||
{
|
||||
AllowGoingOutside = order.TargetSpatialEntity == null ? character.CurrentHull == null :
|
||||
character.Submarine == null || character.Submarine != order.TargetSpatialEntity.Submarine
|
||||
AllowGoingOutside = character.Submarine == null || (order.TargetSpatialEntity != null && character.Submarine != order.TargetSpatialEntity.Submarine)
|
||||
};
|
||||
break;
|
||||
case "fixleaks":
|
||||
@@ -345,7 +392,7 @@ namespace Barotrauma
|
||||
case "pumpwater":
|
||||
if (order.TargetItemComponent is Pump targetPump)
|
||||
{
|
||||
if (order.TargetItemComponent.Item.NonInteractable) { return null; }
|
||||
if (!order.TargetItemComponent.Item.IsInteractable(character)) { return null; }
|
||||
newObjective = new AIObjectiveOperateItem(targetPump, character, this, option, false, priorityModifier: priorityModifier)
|
||||
{
|
||||
IsLoop = true,
|
||||
@@ -370,7 +417,7 @@ namespace Barotrauma
|
||||
var steering = (order?.TargetEntity as Item)?.GetComponent<Steering>();
|
||||
if (steering != null) { steering.PosToMaintain = steering.Item.Submarine?.WorldPosition; }
|
||||
if (order.TargetItemComponent == null) { return null; }
|
||||
if (order.TargetItemComponent.Item.NonInteractable) { return null; }
|
||||
if (!order.TargetItemComponent.Item.IsInteractable(character)) { return null; }
|
||||
newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option,
|
||||
requireEquip: false, useController: order.UseController, controller: order.ConnectedController, priorityModifier: priorityModifier)
|
||||
{
|
||||
@@ -403,11 +450,26 @@ namespace Barotrauma
|
||||
};
|
||||
break;
|
||||
case "cleanupitems":
|
||||
newObjective = new AIObjectiveCleanupItems(character, this, priorityModifier, order.TargetEntity as Item);
|
||||
if (order.TargetEntity is Item targetItem)
|
||||
{
|
||||
if (targetItem.HasTag("allowcleanup") && targetItem.ParentInventory == null && targetItem.OwnInventory != null)
|
||||
{
|
||||
// Target all items inside the container
|
||||
newObjective = new AIObjectiveCleanupItems(character, this, targetItem.OwnInventory.AllItems, priorityModifier);
|
||||
}
|
||||
else
|
||||
{
|
||||
newObjective = new AIObjectiveCleanupItems(character, this, targetItem, priorityModifier);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
newObjective = new AIObjectiveCleanupItems(character, this, priorityModifier: priorityModifier);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (order.TargetItemComponent == null) { return null; }
|
||||
if (order.TargetItemComponent.Item.NonInteractable) { return null; }
|
||||
if (!order.TargetItemComponent.Item.IsInteractable(character)) { return null; }
|
||||
newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option,
|
||||
requireEquip: false, useController: order.UseController, controller: order.ConnectedController, priorityModifier: priorityModifier)
|
||||
{
|
||||
|
||||
+10
-23
@@ -69,7 +69,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (!isOrder)
|
||||
{
|
||||
if (reactor.LastUserWasPlayer && character.TeamID != Character.TeamType.FriendlyNPC ||
|
||||
if (reactor.LastUserWasPlayer && character.TeamID != CharacterTeamType.FriendlyNPC ||
|
||||
HumanAIController.IsTrueForAnyCrewMember(c =>
|
||||
c.ObjectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder.GetTarget() == target))
|
||||
{
|
||||
@@ -101,7 +101,8 @@ namespace Barotrauma
|
||||
targetItem.Submarine != character.Submarine && !isOrder ||
|
||||
targetItem.CurrentHull.FireSources.Any() ||
|
||||
HumanAIController.IsItemOperatedByAnother(target, out _) ||
|
||||
Character.CharacterList.Any(c => c.CurrentHull == targetItem.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c)))
|
||||
Character.CharacterList.Any(c => c.CurrentHull == targetItem.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))
|
||||
|| component.Item.IgnoreByAI || (useController && controller.Item.IgnoreByAI))
|
||||
{
|
||||
Priority = 0;
|
||||
}
|
||||
@@ -137,7 +138,7 @@ namespace Barotrauma
|
||||
throw new Exception("target null");
|
||||
#endif
|
||||
}
|
||||
else if (target.Item.NonInteractable)
|
||||
else if (!target.Item.IsInteractable(character))
|
||||
{
|
||||
Abandon = true;
|
||||
}
|
||||
@@ -157,21 +158,6 @@ namespace Barotrauma
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
// If this is not an order...
|
||||
if (objectiveManager.CurrentOrder != this)
|
||||
{
|
||||
// Don't allow to operate an item that someone with a better skills already operates
|
||||
if (HumanAIController.IsItemOperatedByAnother(target, out _))
|
||||
{
|
||||
// Don't abandon
|
||||
return;
|
||||
}
|
||||
if (component.Item.IgnoreByAI || (useController && controller.Item.IgnoreByAI))
|
||||
{
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (operateTarget != null)
|
||||
{
|
||||
if (HumanAIController.IsTrueForAnyCrewMember(other => other != HumanAIController && other.ObjectiveManager.GetActiveObjective() is AIObjectiveOperateItem operateObjective && operateObjective.operateTarget == operateTarget))
|
||||
@@ -215,7 +201,7 @@ namespace Barotrauma
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
else if (!character.Inventory.Items.Contains(component.Item))
|
||||
else if (!character.Inventory.Contains(component.Item))
|
||||
{
|
||||
TryAddSubObjective(ref getItemObjective, () => new AIObjectiveGetItem(character, component.Item, objectiveManager, equip: true),
|
||||
onAbandon: () => Abandon = true,
|
||||
@@ -241,13 +227,14 @@ namespace Barotrauma
|
||||
continue;
|
||||
}
|
||||
//equip slot already taken
|
||||
if (character.Inventory.Items[i] != null)
|
||||
var existingItem = character.Inventory.GetItemAt(i);
|
||||
if (existingItem != null)
|
||||
{
|
||||
//try to put the item in an Any slot, and drop it if that fails
|
||||
if (!character.Inventory.Items[i].AllowedSlots.Contains(InvSlotType.Any) ||
|
||||
!character.Inventory.TryPutItem(character.Inventory.Items[i], character, new List<InvSlotType>() { InvSlotType.Any }))
|
||||
if (!existingItem.AllowedSlots.Contains(InvSlotType.Any) ||
|
||||
!character.Inventory.TryPutItem(existingItem, character, new List<InvSlotType>() { InvSlotType.Any }))
|
||||
{
|
||||
character.Inventory.Items[i].Drop(character);
|
||||
existingItem.Drop(character);
|
||||
}
|
||||
}
|
||||
if (character.Inventory.TryPutItem(component.Item, i, true, false, character))
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (pump == null) { return false; }
|
||||
if (pump.Item.IgnoreByAI) { return false; }
|
||||
if (pump.Item.NonInteractable) { return false; }
|
||||
if (!pump.Item.IsInteractable(character)) { return false; }
|
||||
if (pump.Item.HasTag("ballast")) { return false; }
|
||||
if (pump.Item.Submarine == null) { return false; }
|
||||
if (pump.Item.CurrentHull == null) { return false; }
|
||||
|
||||
+26
-11
@@ -33,10 +33,14 @@ namespace Barotrauma
|
||||
|
||||
public override float GetPriority()
|
||||
{
|
||||
if (!IsAllowed)
|
||||
if (!IsAllowed || Item.IgnoreByAI)
|
||||
{
|
||||
Priority = 0;
|
||||
Abandon = true;
|
||||
if (IsRepairing())
|
||||
{
|
||||
Item.Repairables.ForEach(r => r.StopRepairing(character));
|
||||
}
|
||||
return Priority;
|
||||
}
|
||||
// TODO: priority list?
|
||||
@@ -107,8 +111,7 @@ namespace Barotrauma
|
||||
}
|
||||
if (repairTool != null)
|
||||
{
|
||||
var containedItems = repairTool.Item.OwnInventory?.Items;
|
||||
if (containedItems == null)
|
||||
if (repairTool.Item.OwnInventory == null)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.ThrowError($"{character.Name}: AIObjectiveRepairItem failed - the item \"" + repairTool + "\" has no proper inventory");
|
||||
@@ -116,27 +119,39 @@ namespace Barotrauma
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
// Drop empty tanks
|
||||
foreach (Item containedItem in containedItems)
|
||||
// Eject empty tanks
|
||||
if (repairTool.Item.OwnInventory.AllItems.Any(it => it.Condition <= 0.0f))
|
||||
{
|
||||
if (containedItem == null) { continue; }
|
||||
if (containedItem.Condition <= 0.0f)
|
||||
foreach (Item containedItem in repairTool.Item.OwnInventory.AllItemsMod)
|
||||
{
|
||||
containedItem.Drop(character);
|
||||
if (containedItem == null) { continue; }
|
||||
if (containedItem.Condition <= 0.0f)
|
||||
{
|
||||
if (character.Submarine == null)
|
||||
{
|
||||
// If we are outside of main sub, try to put the tank in the inventory instead dropping it in the sea.
|
||||
if (character.Inventory.TryPutItem(containedItem, character, CharacterInventory.anySlot))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
containedItem.Drop(character);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RelatedItem item = null;
|
||||
Item fuel = null;
|
||||
foreach (RelatedItem requiredItem in repairTool.requiredItems[RelatedItem.RelationType.Contained])
|
||||
{
|
||||
item = requiredItem;
|
||||
fuel = containedItems.FirstOrDefault(it => it != null && it.Condition > 0.0f && requiredItem.MatchesItem(it));
|
||||
fuel = repairTool.Item.OwnInventory.AllItems.FirstOrDefault(it => it.Condition > 0.0f && requiredItem.MatchesItem(it));
|
||||
if (fuel != null) { break; }
|
||||
}
|
||||
if (fuel == null)
|
||||
{
|
||||
RemoveSubObjective(ref goToObjective);
|
||||
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, item.Identifiers, repairTool.Item.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == Character.TeamType.FriendlyNPC),
|
||||
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, item.Identifiers, repairTool.Item.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC),
|
||||
onCompleted: () => RemoveSubObjective(ref refuelObjective),
|
||||
onAbandon: () => Abandon = true);
|
||||
return;
|
||||
@@ -229,7 +244,7 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (RelatedItem requiredItem in kvp.Value)
|
||||
{
|
||||
foreach (var item in character.Inventory.Items)
|
||||
foreach (var item in character.Inventory.AllItems)
|
||||
{
|
||||
if (requiredItem.MatchesItem(item))
|
||||
{
|
||||
|
||||
+1
-1
@@ -149,7 +149,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (item == null) { return false; }
|
||||
if (item.IgnoreByAI) { return false; }
|
||||
if (item.NonInteractable) { return false; }
|
||||
if (!item.IsInteractable(character)) { return false; }
|
||||
if (item.IsFullCondition) { return false; }
|
||||
if (item.CurrentHull == null) { return false; }
|
||||
if (item.Submarine == null || character.Submarine == null) { return false; }
|
||||
|
||||
+16
-5
@@ -85,7 +85,7 @@ namespace Barotrauma
|
||||
Item suit = suits.FirstOrDefault();
|
||||
if (suit != null)
|
||||
{
|
||||
AIObjectiveFindDivingGear.DropEmptyTanks(character, suit, out _);
|
||||
AIObjectiveFindDivingGear.EjectEmptyTanks(character, suit, out _);
|
||||
}
|
||||
}
|
||||
else if (HumanAIController.HasItem(targetCharacter, AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR, out IEnumerable<Item> masks, requireEquipped: true))
|
||||
@@ -93,7 +93,7 @@ namespace Barotrauma
|
||||
Item mask = masks.FirstOrDefault();
|
||||
if (mask != null)
|
||||
{
|
||||
AIObjectiveFindDivingGear.DropEmptyTanks(character, mask, out _);
|
||||
AIObjectiveFindDivingGear.EjectEmptyTanks(character, mask, out _);
|
||||
}
|
||||
}
|
||||
bool ShouldRemoveDivingSuit() => targetCharacter.OxygenAvailable < CharacterHealth.InsufficientOxygenThreshold && targetCharacter.CurrentHull?.LethalPressure <= 0;
|
||||
@@ -101,7 +101,7 @@ namespace Barotrauma
|
||||
{
|
||||
suits.ForEach(suit => suit.Drop(character));
|
||||
}
|
||||
else if (suits.Any() && suits.None(s => s.OwnInventory?.Items != null && s.OwnInventory.Items.Any(it => it != null && it.HasTag(AIObjectiveFindDivingGear.OXYGEN_SOURCE) && it.ConditionPercentage > 0)))
|
||||
else if (suits.Any() && suits.None(s => s.OwnInventory?.AllItems != null && s.OwnInventory.AllItems.Any(it => it.HasTag(AIObjectiveFindDivingGear.OXYGEN_SOURCE) && it.ConditionPercentage > 0)))
|
||||
{
|
||||
// The target has a suit equipped with an empty oxygen tank.
|
||||
// Can't remove the suit, because the target needs it.
|
||||
@@ -331,9 +331,13 @@ namespace Barotrauma
|
||||
character.DeselectCharacter();
|
||||
RemoveSubObjective(ref getItemObjective);
|
||||
TryAddSubObjective(ref getItemObjective,
|
||||
constructor: () => new AIObjectiveGetItem(character, suitableItemIdentifiers.ToArray(), objectiveManager, equip: true, spawnItemIfNotFound: character.TeamID == Character.TeamType.FriendlyNPC),
|
||||
constructor: () => new AIObjectiveGetItem(character, suitableItemIdentifiers.ToArray(), objectiveManager, equip: true, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC),
|
||||
onCompleted: () => RemoveSubObjective(ref getItemObjective),
|
||||
onAbandon: () => RemoveSubObjective(ref getItemObjective));
|
||||
onAbandon: () =>
|
||||
{
|
||||
Abandon = true;
|
||||
character.Speak(TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, formatCapitals: false), identifier: "cannottreatpatient", minDurationBetweenSimilar: 20.0f);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -427,6 +431,13 @@ namespace Barotrauma
|
||||
replaceOxygenObjective = null;
|
||||
safeHull = null;
|
||||
ignoreOxygen = false;
|
||||
character.SelectedCharacter = null;
|
||||
}
|
||||
|
||||
public override void OnDeselected()
|
||||
{
|
||||
character.SelectedCharacter = null;
|
||||
base.OnDeselected();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-2
@@ -14,7 +14,7 @@ namespace Barotrauma
|
||||
public override bool AllowInAnySub => true;
|
||||
|
||||
private const float vitalityThreshold = 75;
|
||||
private const float vitalityThresholdForOrders = 85;
|
||||
private const float vitalityThresholdForOrders = 90;
|
||||
public static float GetVitalityThreshold(AIObjectiveManager manager, Character character, Character target)
|
||||
{
|
||||
if (manager == null)
|
||||
@@ -23,7 +23,10 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
return character == target || manager.CurrentOrder is AIObjectiveRescueAll ? vitalityThresholdForOrders : vitalityThreshold;
|
||||
// When targeting player characters, always treat them when ordered, else use the threshold so that minor/non-severe damage is ignored.
|
||||
// If we ignore any damage when the player orders a bot to do healings, it's observed to cause confusion among the players.
|
||||
// On the other hand, if the bots too eagerly heal characters when it's not nevessary, it's inefficient and can feel frustrating, because it can't be controlled.
|
||||
return character == target || manager.CurrentOrder is AIObjectiveRescueAll ? (target.IsPlayer ? 100 : vitalityThresholdForOrders) : vitalityThreshold;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +59,10 @@ namespace Barotrauma
|
||||
public Order Prefab { get; private set; }
|
||||
|
||||
public readonly string Name;
|
||||
/// <summary>
|
||||
/// Name that can be used with the contextual version of the order
|
||||
/// </summary>
|
||||
public readonly string ContextualName;
|
||||
|
||||
public readonly Sprite SymbolSprite;
|
||||
|
||||
@@ -97,7 +101,6 @@ namespace Barotrauma
|
||||
public bool TargetAllCharacters { get; }
|
||||
public bool IsReport => TargetAllCharacters && !MustSetTarget;
|
||||
|
||||
|
||||
public readonly float FadeOutTime;
|
||||
|
||||
public Entity TargetEntity;
|
||||
@@ -119,9 +122,9 @@ namespace Barotrauma
|
||||
private readonly Dictionary<string, Sprite> minimapIcons;
|
||||
public Dictionary<string, Sprite> MinimapIcons => IsPrefab ? minimapIcons : Prefab.minimapIcons;
|
||||
|
||||
public readonly float Weight;
|
||||
public readonly bool MustSetTarget;
|
||||
public readonly string AppropriateSkill;
|
||||
public readonly bool Hidden;
|
||||
|
||||
public bool HasOptions => (IsPrefab ? Options : Prefab.Options).Length > 1;
|
||||
public bool IsPrefab { get; private set; }
|
||||
@@ -159,6 +162,11 @@ namespace Barotrauma
|
||||
public int? WallSectionIndex { get; }
|
||||
public bool IsIgnoreOrder { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Should the order icon be drawn when the order target is inside a container
|
||||
/// </summary>
|
||||
public bool DrawIconWhenContained { get; }
|
||||
|
||||
public static void Init()
|
||||
{
|
||||
Prefabs = new Dictionary<string, Order>();
|
||||
@@ -239,7 +247,8 @@ namespace Barotrauma
|
||||
private Order(XElement orderElement)
|
||||
{
|
||||
Identifier = orderElement.GetAttributeString("identifier", "");
|
||||
Name = TextManager.Get("OrderName." + Identifier, true) ?? "Name not found";
|
||||
Name = TextManager.Get("OrderName." + Identifier, returnNull: true) ?? "Name not found";
|
||||
ContextualName = TextManager.Get("OrderNameContextual." + Identifier, returnNull: true) ?? Name;
|
||||
|
||||
string targetItemType = orderElement.GetAttributeString("targetitemtype", "");
|
||||
if (!string.IsNullOrWhiteSpace(targetItemType))
|
||||
@@ -267,6 +276,7 @@ namespace Barotrauma
|
||||
if (!string.IsNullOrWhiteSpace(category)) { this.Category = (OrderCategory)Enum.Parse(typeof(OrderCategory), category, true); }
|
||||
MustSetTarget = orderElement.GetAttributeBool("mustsettarget", false);
|
||||
AppropriateSkill = orderElement.GetAttributeString("appropriateskill", null);
|
||||
Hidden = orderElement.GetAttributeBool("hidden", false);
|
||||
|
||||
var optionNames = TextManager.Get("OrderOptions." + Identifier, true)?.Split(',', ',') ??
|
||||
orderElement.GetAttributeStringArray("optionnames", new string[0]);
|
||||
@@ -315,6 +325,7 @@ namespace Barotrauma
|
||||
IsPrefab = true;
|
||||
MustManuallyAssign = orderElement.GetAttributeBool("mustmanuallyassign", false);
|
||||
IsIgnoreOrder = Identifier == "ignorethis" || Identifier == "unignorethis";
|
||||
DrawIconWhenContained = orderElement.GetAttributeBool("displayiconwhencontained", false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -324,23 +335,26 @@ namespace Barotrauma
|
||||
{
|
||||
Prefab = prefab.Prefab ?? prefab;
|
||||
|
||||
Name = prefab.Name;
|
||||
Identifier = prefab.Identifier;
|
||||
ItemComponentType = prefab.ItemComponentType;
|
||||
CanTypeBeSubclass = prefab.CanTypeBeSubclass;
|
||||
TargetItems = prefab.TargetItems;
|
||||
Options = prefab.Options;
|
||||
SymbolSprite = prefab.SymbolSprite;
|
||||
Color = prefab.Color;
|
||||
UseController = prefab.UseController;
|
||||
TargetAllCharacters = prefab.TargetAllCharacters;
|
||||
AppropriateJobs = prefab.AppropriateJobs;
|
||||
FadeOutTime = prefab.FadeOutTime;
|
||||
MustSetTarget = prefab.MustSetTarget;
|
||||
AppropriateSkill = prefab.AppropriateSkill;
|
||||
Category = prefab.Category;
|
||||
MustManuallyAssign = prefab.MustManuallyAssign;
|
||||
IsIgnoreOrder = prefab.IsIgnoreOrder;
|
||||
Name = prefab.Name;
|
||||
ContextualName = prefab.ContextualName;
|
||||
Identifier = prefab.Identifier;
|
||||
ItemComponentType = prefab.ItemComponentType;
|
||||
CanTypeBeSubclass = prefab.CanTypeBeSubclass;
|
||||
TargetItems = prefab.TargetItems;
|
||||
Options = prefab.Options;
|
||||
SymbolSprite = prefab.SymbolSprite;
|
||||
Color = prefab.Color;
|
||||
UseController = prefab.UseController;
|
||||
TargetAllCharacters = prefab.TargetAllCharacters;
|
||||
AppropriateJobs = prefab.AppropriateJobs;
|
||||
FadeOutTime = prefab.FadeOutTime;
|
||||
MustSetTarget = prefab.MustSetTarget;
|
||||
AppropriateSkill = prefab.AppropriateSkill;
|
||||
Category = prefab.Category;
|
||||
MustManuallyAssign = prefab.MustManuallyAssign;
|
||||
IsIgnoreOrder = prefab.IsIgnoreOrder;
|
||||
DrawIconWhenContained = prefab.DrawIconWhenContained;
|
||||
Hidden = prefab.Hidden;
|
||||
|
||||
OrderGiver = orderGiver;
|
||||
TargetEntity = targetEntity;
|
||||
@@ -351,9 +365,7 @@ namespace Barotrauma
|
||||
ConnectedController = targetItem.Item?.FindController();
|
||||
if (ConnectedController == null)
|
||||
{
|
||||
#if DEBUG
|
||||
throw new Exception("Tried to use controller, but couldn't find one");
|
||||
#endif
|
||||
DebugConsole.AddWarning("AI: Tried to use a controller for operating an item, but couldn't find any.");
|
||||
UseController = false;
|
||||
}
|
||||
}
|
||||
@@ -433,7 +445,8 @@ namespace Barotrauma
|
||||
return firstMatchingComponent != null;
|
||||
}
|
||||
|
||||
public List<Item> GetMatchingItems(Submarine submarine, bool mustBelongToPlayerSub, Character.TeamType? requiredTeam = null)
|
||||
/// <param name="interactableFor">Only returns items which are interactable for this character</param>
|
||||
public List<Item> GetMatchingItems(Submarine submarine, bool mustBelongToPlayerSub, CharacterTeamType? requiredTeam = null, Character interactableFor = null)
|
||||
{
|
||||
List<Item> matchingItems = new List<Item>();
|
||||
if (submarine == null) { return matchingItems; }
|
||||
@@ -456,16 +469,23 @@ namespace Barotrauma
|
||||
{
|
||||
matchingItems.RemoveAll(i => i.Components.None(c => c.GetType() == ItemComponentType) && !i.TryFindController(out _));
|
||||
}
|
||||
if (interactableFor != null)
|
||||
{
|
||||
matchingItems.RemoveAll(it => !it.IsInteractable(interactableFor) ||
|
||||
(UseController && it.FindController() is Controller c && !c.Item.IsInteractable(interactableFor)));
|
||||
}
|
||||
}
|
||||
return matchingItems;
|
||||
}
|
||||
|
||||
public List<Item> GetMatchingItems(bool mustBelongToPlayerSub)
|
||||
|
||||
/// <param name="interactableFor">Only returns items which are interactable for this character</param>
|
||||
public List<Item> GetMatchingItems(bool mustBelongToPlayerSub, Character interactableFor = null)
|
||||
{
|
||||
Submarine submarine = Character.Controlled != null && Character.Controlled.TeamID == Character.TeamType.Team2 && Submarine.MainSubs.Length > 1 ?
|
||||
Submarine submarine = Character.Controlled != null && Character.Controlled.TeamID == CharacterTeamType.Team2 && Submarine.MainSubs.Length > 1 ?
|
||||
Submarine.MainSubs[1] :
|
||||
Submarine.MainSub;
|
||||
return GetMatchingItems(submarine, mustBelongToPlayerSub);
|
||||
return GetMatchingItems(submarine, mustBelongToPlayerSub, interactableFor: interactableFor);
|
||||
}
|
||||
|
||||
public string GetOptionName(string id)
|
||||
|
||||
@@ -254,11 +254,8 @@ namespace Barotrauma
|
||||
{
|
||||
if (AiController.Character.Inventory != null)
|
||||
{
|
||||
var items = AiController.Character.Inventory.Items;
|
||||
for (int i = 0; i < items.Length; i++)
|
||||
foreach (Item item in AiController.Character.Inventory.AllItems)
|
||||
{
|
||||
var item = items[i];
|
||||
if (item == null) { continue; }
|
||||
var tag = item.GetComponent<NameTag>();
|
||||
if (tag != null && !string.IsNullOrWhiteSpace(tag.WrittenName))
|
||||
{
|
||||
@@ -358,7 +355,7 @@ namespace Barotrauma
|
||||
|
||||
XElement petElement = new XElement("pet",
|
||||
new XAttribute("speciesname", c.SpeciesName),
|
||||
new XAttribute("ownerid", petBehavior.Owner?.ID ?? Entity.NullEntityID),
|
||||
new XAttribute("ownerhash", petBehavior.Owner?.Info?.GetIdentifier() ?? 0),
|
||||
new XAttribute("seed", c.Seed));
|
||||
|
||||
var petBehaviorElement = new XElement("petbehavior",
|
||||
@@ -387,16 +384,19 @@ namespace Barotrauma
|
||||
{
|
||||
string speciesName = subElement.GetAttributeString("speciesname", "");
|
||||
string seed = subElement.GetAttributeString("seed", "123");
|
||||
ushort ownerID = (ushort)subElement.GetAttributeInt("ownerid", 0);
|
||||
int ownerHash = subElement.GetAttributeInt("ownerhash", 0);
|
||||
Vector2 spawnPos = Vector2.Zero;
|
||||
Character owner = Entity.FindEntityByID(ownerID) as Character;
|
||||
if (owner != null)
|
||||
Character owner = Character.CharacterList.Find(c => c.Info?.GetIdentifier() == ownerHash);
|
||||
if (owner != null && owner.Submarine?.Info.Type == SubmarineType.Player)
|
||||
{
|
||||
spawnPos = owner.WorldPosition;
|
||||
}
|
||||
else
|
||||
{
|
||||
var spawnPoint = WayPoint.WayPointList.Where(wp => wp.SpawnType == SpawnType.Human && wp.Submarine?.Info.Type == SubmarineType.Player).GetRandom();
|
||||
//try to find a spawnpoint in the main sub
|
||||
var spawnPoint = WayPoint.WayPointList.Where(wp => wp.SpawnType == SpawnType.Human && wp.Submarine == Submarine.MainSub).GetRandom();
|
||||
//if not found, try any player sub (shuttle/drone etc)
|
||||
spawnPoint ??= WayPoint.WayPointList.Where(wp => wp.SpawnType == SpawnType.Human && wp.Submarine?.Info.Type == SubmarineType.Player).GetRandom();
|
||||
spawnPos = spawnPoint?.WorldPosition ?? Submarine.MainSub.WorldPosition;
|
||||
}
|
||||
var pet = Character.Create(speciesName, spawnPos, seed);
|
||||
|
||||
@@ -131,7 +131,7 @@ namespace Barotrauma
|
||||
if (container == null) { continue; }
|
||||
for (int i = 0; i < container.Inventory.Capacity; i++)
|
||||
{
|
||||
if (container.Inventory.Items[i] != null) { continue; }
|
||||
if (container.Inventory.GetItemAt(i) != null) { continue; }
|
||||
if (MapEntityPrefab.List.GetRandom(e => e is ItemPrefab i && container.CanBeContained(i) &&
|
||||
Config.ForbiddenAmmunition.None(id => id.Equals(i.Identifier, StringComparison.OrdinalIgnoreCase)), Rand.RandSync.Server) is ItemPrefab ammoPrefab)
|
||||
{
|
||||
@@ -319,25 +319,33 @@ namespace Barotrauma
|
||||
private readonly List<Hull> populatedHulls = new List<Hull>();
|
||||
private float cellSpawnTimer;
|
||||
|
||||
private float CellSpawnTime => Config.AgentSpawnDelay;
|
||||
private float CellSpawnRandomFactor => Config.AgentSpawnDelayRandomFactor;
|
||||
private int MinCellsPerBrainRoom => Config.MinAgentsPerBrainRoom;
|
||||
private int MaxCellsPerRoom => Config.MaxAgentsPerRoom;
|
||||
private int MinCellsOutside => Config.MinAgentsOutside;
|
||||
private int MaxCellsOutside => Config.MaxAgentsOutside;
|
||||
private int MinCellsInside => Config.MinAgentsInside;
|
||||
private int MaxCellsInside => Config.MaxAgentsInside;
|
||||
private int MaxCellCount => Config.MaxAgentCount;
|
||||
private int MinCellsPerBrainRoom => CalculateCellCount(0, Config.MinAgentsPerBrainRoom);
|
||||
private int MaxCellsPerRoom => CalculateCellCount(1, Config.MaxAgentsPerRoom);
|
||||
private int MinCellsOutside => CalculateCellCount(0, Config.MinAgentsOutside);
|
||||
private int MaxCellsOutside => CalculateCellCount(0, Config.MaxAgentsOutside);
|
||||
private int MinCellsInside => CalculateCellCount(2, Config.MinAgentsInside);
|
||||
private int MaxCellsInside => CalculateCellCount(3, Config.MaxAgentsInside);
|
||||
private int MaxCellCount => CalculateCellCount(5, Config.MaxAgentCount);
|
||||
private float MinWaterLevel => Config.MinWaterLevel;
|
||||
|
||||
private int CalculateCellCount(int minValue, int maxValue)
|
||||
{
|
||||
if (maxValue == 0) { return 0; }
|
||||
return (int)Math.Round(MathHelper.Lerp(minValue, maxValue, Level.Loaded.Difficulty * 0.01f * Config.AgentSpawnCountDifficultyMultiplier));
|
||||
}
|
||||
|
||||
private float GetSpawnTime() =>
|
||||
Math.Max(Config.AgentSpawnDelay * Rand.Range(Config.AgentSpawnDelayRandomFactor, 1 + Config.AgentSpawnDelayRandomFactor)
|
||||
/ (Math.Max(Level.Loaded.Difficulty, 1) * 0.01f * Config.AgentSpawnDelayDifficultyMultiplier), Config.AgentSpawnDelay);
|
||||
|
||||
void UpdateReinforcements(float deltaTime)
|
||||
{
|
||||
if (protectiveCells.Count >= MaxCellCount || spawnOrgans.Count == 0) { return; }
|
||||
if (spawnOrgans.Count == 0) { return; }
|
||||
cellSpawnTimer -= deltaTime;
|
||||
if (cellSpawnTimer < 0)
|
||||
{
|
||||
TrySpawnCell(out _, spawnOrgans.GetRandom());
|
||||
cellSpawnTimer = CellSpawnTime * Rand.Range(CellSpawnRandomFactor, 1 + CellSpawnRandomFactor);
|
||||
cellSpawnTimer = GetSpawnTime();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,7 +372,7 @@ namespace Barotrauma
|
||||
cell = Character.Create(Config.DefensiveAgent, targetEntity.WorldPosition, ToolBox.RandomSeed(8), hasAi: true, createNetworkEvent: true);
|
||||
protectiveCells.Add(cell);
|
||||
cell.OnDeath += OnCellDeath;
|
||||
cellSpawnTimer = CellSpawnTime * Rand.Range(CellSpawnRandomFactor, 1 + CellSpawnRandomFactor);
|
||||
cellSpawnTimer = GetSpawnTime();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,12 @@ namespace Barotrauma
|
||||
[Serialize(0.5f, false)]
|
||||
public float AgentSpawnDelayRandomFactor { get; private set; }
|
||||
|
||||
[Serialize(1f, false)]
|
||||
public float AgentSpawnDelayDifficultyMultiplier { get; private set; }
|
||||
|
||||
[Serialize(1f, false)]
|
||||
public float AgentSpawnCountDifficultyMultiplier { get; private set; }
|
||||
|
||||
[Serialize(0, false)]
|
||||
public int MinAgentsPerBrainRoom { get; private set; }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user