Unstable v0.10.600.0

This commit is contained in:
Juan Pablo Arce
2020-10-01 12:19:24 -03:00
parent 20a69375ca
commit ebe1ce1427
217 changed files with 4284 additions and 1547 deletions
@@ -3,7 +3,7 @@ using System.Collections.Generic;
namespace Barotrauma
{
public enum AIState { Idle, Attack, Escape, Eat, Flee, Avoid, Aggressive, PassiveAggressive, Protect }
public enum AIState { Idle, Attack, Escape, Eat, Flee, Avoid, Aggressive, PassiveAggressive, Protect, Observe }
abstract partial class AIController : ISteerable
{
@@ -147,6 +147,8 @@ namespace Barotrauma
_selectedAiTarget = null;
}
public void FaceTarget(ISpatialEntity target) => Character.AnimController.TargetDir = target.WorldPosition.X > Character.WorldPosition.X ? Direction.Right : Direction.Left;
protected virtual void OnStateChanged(AIState from, AIState to) { }
protected virtual void OnTargetChanged(AITarget previousTarget, AITarget newTarget) { }
@@ -38,7 +38,7 @@ namespace Barotrauma
{
if (float.IsNaN(value))
{
DebugConsole.ThrowError("Attempted to set the SoundRange of an AITarget to NaN.\n" + Environment.StackTrace);
DebugConsole.ThrowError("Attempted to set the SoundRange of an AITarget to NaN.\n" + Environment.StackTrace.CleanupStackTrace());
return;
}
soundRange = MathHelper.Clamp(value, MinSoundRange, MaxSoundRange);
@@ -52,7 +52,7 @@ namespace Barotrauma
{
if (float.IsNaN(value))
{
DebugConsole.ThrowError("Attempted to set the SightRange of an AITarget to NaN.\n" + Environment.StackTrace);
DebugConsole.ThrowError("Attempted to set the SightRange of an AITarget to NaN.\n" + Environment.StackTrace.CleanupStackTrace());
return;
}
sightRange = MathHelper.Clamp(value, MinSightRange, MaxSightRange);
@@ -74,7 +74,7 @@ namespace Barotrauma
{
if (!MathUtils.IsValid(value))
{
string errorMsg = "Invalid AITarget sector direction (" + value + ")\n" + Environment.StackTrace;
string errorMsg = "Invalid AITarget sector direction (" + value + ")\n" + Environment.StackTrace.CleanupStackTrace();
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("AITarget.SectorDir:" + entity?.ToString(), GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
return;
@@ -113,11 +113,11 @@ namespace Barotrauma
if (entity == null || entity.Removed)
{
#if DEBUG
DebugConsole.ThrowError("Attempted to access a removed AITarget\n" + Environment.StackTrace);
DebugConsole.ThrowError("Attempted to access a removed AITarget\n" + Environment.StackTrace.CleanupStackTrace());
#endif
GameAnalyticsManager.AddErrorEventOnce("AITarget.WorldPosition:EntityRemoved",
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Attempted to access a removed AITarget\n" + Environment.StackTrace);
"Attempted to access a removed AITarget\n" + Environment.StackTrace.CleanupStackTrace());
return Vector2.Zero;
}
@@ -132,11 +132,11 @@ namespace Barotrauma
if (entity == null || entity.Removed)
{
#if DEBUG
DebugConsole.ThrowError("Attempted to access a removed AITarget\n" + Environment.StackTrace);
DebugConsole.ThrowError("Attempted to access a removed AITarget\n" + Environment.StackTrace.CleanupStackTrace());
#endif
GameAnalyticsManager.AddErrorEventOnce("AITarget.WorldPosition:EntityRemoved",
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Attempted to access a removed AITarget\n" + Environment.StackTrace);
"Attempted to access a removed AITarget\n" + Environment.StackTrace.CleanupStackTrace());
return Vector2.Zero;
}
@@ -56,6 +56,8 @@ namespace Barotrauma
private float AggressionHurt => Character.Params.AI.AggressionHurt;
private bool AggressiveBoarding => Character.Params.AI.AggressiveBoarding;
private FishAnimController FishAnimController => Character.AnimController as FishAnimController;
//a point in a wall which the Character is currently targeting
private WallTarget wallTarget;
@@ -70,10 +72,6 @@ namespace Barotrauma
_attackingLimb = value;
attackVector = null;
Reverse = _attackingLimb != null && _attackingLimb.attack.Reverse;
if (Character.AnimController is FishAnimController fishController)
{
fishController.reverse = Reverse;
}
}
}
@@ -95,6 +93,7 @@ namespace Barotrauma
private readonly float avoidTime = 3;
private float avoidTimer;
private float observeTimer;
public bool StayInsideLevel = true;
@@ -107,7 +106,7 @@ namespace Barotrauma
{
get
{
var target = GetTarget(CharacterPrefab.HumanSpeciesName);
var target = GetTargetParams(CharacterPrefab.HumanSpeciesName);
return target != null && target.Priority > 0.0f && (target.State == AIState.Attack || target.State == AIState.Aggressive);
}
}
@@ -116,7 +115,7 @@ namespace Barotrauma
{
get
{
var target = GetTarget("room");
var target = GetTargetParams("room");
return target != null && target.Priority > 0.0f && (target.State == AIState.Attack || target.State == AIState.Aggressive);
}
}
@@ -145,7 +144,16 @@ namespace Barotrauma
public bool IsBeingChasedBy(Character c) => c.AIController is EnemyAIController enemyAI && enemyAI.SelectedAiTarget?.Entity is Character && (enemyAI.State == AIState.Aggressive || enemyAI.State == AIState.Attack);
private bool IsBeingChased => SelectedAiTarget?.Entity is Character targetCharacter && IsBeingChasedBy(targetCharacter);
public bool Reverse { get; private set; }
private bool reverse;
public bool Reverse
{
get { return reverse; }
private set
{
reverse = value;
FishAnimController.reverse = reverse;
}
}
public EnemyAIController(Character c, string seed) : base(c)
{
@@ -214,7 +222,69 @@ namespace Barotrauma
}
private CharacterParams.AIParams AIParams => Character.Params.AI;
private CharacterParams.TargetParams GetTarget(string targetTag) => AIParams.GetTarget(targetTag, false);
private CharacterParams.TargetParams GetTargetParams(string targetTag) => AIParams.GetTarget(targetTag, false);
private CharacterParams.TargetParams GetTargetParams(AITarget aiTarget) => GetTargetParams(GetTargetingTag(aiTarget));
private string GetTargetingTag(AITarget aiTarget)
{
if (aiTarget.Entity == null) { return null; }
string targetingTag = null;
if (aiTarget.Entity is Character targetCharacter)
{
if (targetCharacter.IsDead)
{
targetingTag = "dead";
}
else if (AIParams.TryGetTarget(targetCharacter.SpeciesName, out CharacterParams.TargetParams tP))
{
targetingTag = tP.Tag;
}
else if (targetCharacter.AIController is EnemyAIController enemy)
{
if (targetCharacter.IsHusk && AIParams.HasTag("husk"))
{
targetingTag = "husk";
}
else
{
if (enemy.CombatStrength > CombatStrength)
{
targetingTag = "stronger";
}
else if (enemy.CombatStrength < CombatStrength)
{
targetingTag = "weaker";
}
}
}
}
else if (aiTarget.Entity is Item targetItem)
{
foreach (var prio in AIParams.Targets)
{
if (targetItem.HasTag(prio.Tag))
{
targetingTag = prio.Tag;
break;
}
}
if (targetingTag == null)
{
if (targetItem.GetComponent<Sonar>() != null)
{
targetingTag = "sonar";
}
}
}
else if (aiTarget.Entity is Structure)
{
targetingTag = "wall";
}
else if (aiTarget.Entity is Hull)
{
targetingTag = "room";
}
return targetingTag;
}
public override void SelectTarget(AITarget target) => SelectTarget(target, 100);
@@ -295,8 +365,9 @@ namespace Barotrauma
updateMemoriesTimer = updateMemoriesInverval;
}
if (Character.HealthPercentage <= FleeHealthThreshold && SelectedAiTarget != null &&
SelectedAiTarget.Entity is Character target && (target.IsPlayer || IsBeingChasedBy(target)))
SelectedAiTarget.Entity is Character target && (target.IsHuman && CanPerceive(SelectedAiTarget) || IsBeingChasedBy(target)))
{
// Keep fleeing if being chased
State = AIState.Flee;
wallTarget = null;
}
@@ -348,6 +419,7 @@ namespace Barotrauma
steeringManager = insideSteering;
}
bool useSteeringLengthAsMovementSpeed = State == AIState.Idle && Character.AnimController.InWater;
bool run = false;
switch (State)
{
@@ -425,7 +497,7 @@ namespace Barotrauma
State = AIState.Idle;
return;
}
if (SelectedAiTarget.Entity is Character targetCharacter && targetCharacter.LastAttacker is Character attacker)
if (SelectedAiTarget.Entity is Character targetCharacter && targetCharacter.LastAttacker is Character attacker && !attacker.Removed && !attacker.IsDead)
{
// Attack the character that attacked the target we are protecting
ChangeTargetState(attacker, AIState.Attack, selectedTargetingParams.Priority * 2);
@@ -446,6 +518,63 @@ namespace Barotrauma
UpdateIdle(deltaTime);
}
break;
case AIState.Observe:
if (SelectedAiTarget == null || SelectedAiTarget.Entity == null || SelectedAiTarget.Entity.Removed)
{
State = AIState.Idle;
return;
}
run = false;
sqrDist = Vector2.DistanceSquared(WorldPosition, SelectedAiTarget.WorldPosition);
reactDist = selectedTargetingParams != null && selectedTargetingParams.ReactDistance > 0 ? selectedTargetingParams.ReactDistance : GetPerceivingRange(SelectedAiTarget);
float halfReactDist = reactDist / 2;
float attackDist = selectedTargetingParams != null && selectedTargetingParams.AttackDistance > 0 ? selectedTargetingParams.AttackDistance : halfReactDist;
if (sqrDist > Math.Pow(reactDist, 2))
{
// Too far to react
UpdateIdle(deltaTime);
}
else if (sqrDist < Math.Pow(attackDist + movementMargin, 2))
{
movementMargin = attackDist;
SteeringManager.Reset();
if (Character.AnimController.InWater)
{
useSteeringLengthAsMovementSpeed = true;
Vector2 dir = Vector2.Normalize(SelectedAiTarget.WorldPosition - Character.WorldPosition);
if (sqrDist < Math.Pow(attackDist * 0.75f, 2))
{
// Keep the distance, if too close
dir = -dir;
useSteeringLengthAsMovementSpeed = false;
Reverse = true;
run = true;
}
else
{
Reverse = false;
}
SteeringManager.SteeringManual(deltaTime, dir * 0.2f);
}
else
{
FaceTarget(SelectedAiTarget.Entity);
}
observeTimer -= deltaTime;
if (observeTimer < 0)
{
IgnoreTarget(SelectedAiTarget);
State = AIState.Idle;
ResetAITarget();
}
}
else
{
run = sqrDist > Math.Pow(attackDist * 2, 2);
movementMargin = MathHelper.Clamp(movementMargin -= deltaTime, 0, attackDist);
UpdateFollow(deltaTime);
}
break;
default:
throw new NotImplementedException();
}
@@ -465,7 +594,8 @@ namespace Barotrauma
SteerInsideLevel(deltaTime);
float speed = Character.AnimController.GetCurrentSpeed(run && Character.CanRun);
steeringManager.Update(speed);
Character.AnimController.TargetMovement = Character.ApplyMovementLimits(Steering, State == AIState.Idle && Character.AnimController.InWater ? Steering.Length() : speed);
float targetMovement = useSteeringLengthAsMovementSpeed ? Steering.Length() : speed;
Character.AnimController.TargetMovement = Character.ApplyMovementLimits(Steering, targetMovement);
if (Character.CurrentHull != null && Character.AnimController.InWater)
{
// Halve the swimming speed inside the sub
@@ -601,7 +731,7 @@ namespace Barotrauma
{
// Steer away from the target if in the same room
Vector2 escapeDir = Vector2.Normalize(SelectedAiTarget != null ? WorldPosition - SelectedAiTarget.WorldPosition : Character.AnimController.TargetMovement);
if (!MathUtils.IsValid(escapeDir)) escapeDir = Vector2.UnitY;
if (!MathUtils.IsValid(escapeDir)) { escapeDir = Vector2.UnitY; }
SteeringManager.SteeringManual(deltaTime, escapeDir);
}
else if (pathSteering != null)
@@ -922,7 +1052,7 @@ namespace Barotrauma
}
if (!Character.AnimController.SimplePhysicsEnabled && SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null && (!canAttackDoors || !canAttackWalls || !AIParams.TargetOuterWalls))
{
if (Vector2.DistanceSquared(Character.WorldPosition, attackWorldPos) < 2000 * 2000)
if (wallTarget == null && Vector2.DistanceSquared(Character.WorldPosition, attackWorldPos) < 2000 * 2000)
{
// Check that we are not bumping into a door or a wall
Vector2 rayStart = SimPosition;
@@ -1193,7 +1323,7 @@ namespace Barotrauma
}
if (AIParams.RandomAttack)
{
selectedLimb = ToolBox.SelectWeightedRandom(attackLimbs, weights, Rand.RandSync.Server);
selectedLimb = ToolBox.SelectWeightedRandom(attackLimbs, weights, Rand.RandSync.Unsynced);
attackLimbs.Clear();
weights.Clear();
}
@@ -1268,7 +1398,7 @@ namespace Barotrauma
LatchOntoAI?.SetAttachTarget(wall.Submarine.PhysicsBody.FarseerBody, wall.Submarine, ConvertUnits.ToSimUnits(sectionPos), attachTargetNormal);
if (Character.AnimController.CanEnterSubmarine || !wall.SectionBodyDisabled(sectionIndex) && !IsWallDisabled(wall))
{
if (AIParams.TargetOuterWalls || wall.prefab.Tags.Contains("inner"))
if (AIParams.TargetOuterWalls || wall.prefab.Tags.Contains("inner") || wall.Submarine != null && wall.Submarine == Character.Submarine)
{
wallTarget = new WallTarget(sectionPos, wall, sectionIndex);
}
@@ -1309,7 +1439,7 @@ namespace Barotrauma
bool wasLatched = IsLatchedOnSub;
Character.AnimController.ReleaseStuckLimbs();
LatchOntoAI?.DeattachFromBody(cooldown: 1);
if (attacker == null || attacker.AiTarget == null) { return; }
if (attacker == null || attacker.AiTarget == null || attacker.Removed || attacker.IsDead) { return; }
bool isFriendly = IsFriendly(Character, attacker);
if (wasLatched)
{
@@ -1414,6 +1544,11 @@ namespace Barotrauma
avoidTimer = avoidTime * Rand.Range(0.75f, 1.25f);
SelectTarget(attacker.AiTarget);
}
if (Character.HealthPercentage <= FleeHealthThreshold)
{
avoidTimer = Rand.Range(15, 30);
SelectTarget(attacker.AiTarget);
}
}
// 10 dmg, 100 health -> 0.1
@@ -1534,7 +1669,7 @@ namespace Barotrauma
{
return;
}
steeringManager.SteeringManual(deltaTime, dir);
SteeringManager.SteeringSeek(Character.GetRelativeSimPosition(SelectedAiTarget.Entity), 5);
if (Character.AnimController.InWater)
{
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15);
@@ -1557,6 +1692,7 @@ namespace Barotrauma
foreach (AITarget aiTarget in AITarget.List)
{
if (!aiTarget.Enabled) { continue; }
if (aiTarget.Entity == null) { continue; }
if (ignoredTargets.Contains(aiTarget)) { continue; }
if (Level.Loaded != null && aiTarget.WorldPosition.Y > Level.Loaded.Size.Y)
{
@@ -1626,7 +1762,7 @@ namespace Barotrauma
}
}
}
else if (aiTarget.Entity != null)
else
{
// Ignore all structures and items inside wrecks
if (aiTarget.Entity.Submarine != null && aiTarget.Entity.Submarine.Info.IsWreck) { continue; }
@@ -1784,8 +1920,10 @@ namespace Barotrauma
{
targetingTag = "door";
}
if (door.Item.Submarine == null) { continue;}
if (door.Item.Submarine == null) { continue; }
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))
{
@@ -1795,26 +1933,22 @@ namespace Barotrauma
if (isOpen && (!Character.AnimController.CanEnterSubmarine || !AggressiveBoarding))
{
// Ignore broken and open doors
// Aggressive boarders don't ignore open doors, because they use them for get in.
// Aggressive boarders don't ignore open doors, because they use them for getting in.
continue;
}
if (AggressiveBoarding)
{
// Increase the priority if the character is outside and the door is from outside to inside
if (character.CurrentHull == null && isOutdoor)
if (character.CurrentHull == null)
{
valueModifier *= isOpen ? 5 : 1;
}
else
{
// Inside
// Inside -> ignore open doors and outer doors
valueModifier *= isOpen || isOutdoor ? 0 : 1;
}
}
else if (character.CurrentHull == null)
{
valueModifier = isOutdoor ? 1 : 0;
}
}
else if (aiTarget.Entity is IDamageable targetDamageable && targetDamageable.Health <= 0.0f)
{
@@ -1823,8 +1957,13 @@ namespace Barotrauma
}
if (targetingTag == null) { continue; }
var targetParams = GetTarget(targetingTag);
var targetParams = GetTargetParams(targetingTag);
if (targetParams == null) { continue; }
if (targetCharacter != null && targetCharacter.Submarine != Character.Submarine && targetParams.State == AIState.Observe)
{
// Don't allow to observe characters that are inside a different submarine / outside when we are inside.
continue;
}
valueModifier *= targetParams.Priority;
if (valueModifier == 0.0f) { continue; }
@@ -1939,7 +2078,7 @@ namespace Barotrauma
newTarget = aiTarget;
selectedTargetMemory = targetMemory;
targetValue = valueModifier;
targetingParams = GetTarget(targetingTag);
targetingParams = GetTargetParams(targetingTag);
}
}
@@ -1999,7 +2138,7 @@ namespace Barotrauma
{
_selectedAiTarget = null;
}
else if (CanPerceive(_selectedAiTarget, distSquared: Vector2.DistanceSquared(Character.WorldPosition, _selectedAiTarget.WorldPosition)))
else if (CanPerceive(_selectedAiTarget))
{
var memory = GetTargetMemory(_selectedAiTarget, false);
if (memory != null)
@@ -2040,7 +2179,7 @@ namespace Barotrauma
removals.ForEach(r => targetMemories.Remove(r));
}
private readonly float targetIgnoreTime = 5;
private readonly float targetIgnoreTime = 10;
private float targetIgnoreTimer;
private readonly HashSet<AITarget> ignoredTargets = new HashSet<AITarget>();
public void IgnoreTarget(AITarget target)
@@ -2168,6 +2307,17 @@ namespace Barotrauma
}
#endregion
protected override void OnTargetChanged(AITarget previousTarget, AITarget newTarget)
{
base.OnTargetChanged(previousTarget, newTarget);
if (newTarget == null) { return; }
var targetParams = GetTargetParams(newTarget);
if (targetParams != null)
{
observeTimer = targetParams.Timer * Rand.Range(0.75f, 1.25f);
}
}
protected override void OnStateChanged(AIState from, AIState to)
{
LatchOntoAI?.DeattachFromBody();
@@ -2189,13 +2339,17 @@ namespace Barotrauma
private bool CanPerceive(AITarget target, float dist = -1, float distSquared = -1)
{
if (distSquared > -1)
if (dist > 0)
{
return distSquared <= MathUtils.Pow(target.SightRange * Sight, 2) || distSquared <= MathUtils.Pow(target.SoundRange * Hearing, 2);
return dist <= target.SightRange * Sight || dist <= target.SoundRange * Hearing;
}
else
{
return dist <= target.SightRange * Sight || dist <= target.SoundRange * Hearing;
if (distSquared < 0)
{
distSquared = Vector2.DistanceSquared(Character.WorldPosition, target.WorldPosition);
}
return distSquared <= MathUtils.Pow(target.SightRange * Sight, 2) || distSquared <= MathUtils.Pow(target.SoundRange * Hearing, 2);
}
}
@@ -35,6 +35,14 @@ namespace Barotrauma
public readonly HashSet<Hull> UnsafeHulls = new HashSet<Hull>();
public readonly List<Item> IgnoredItems = new List<Item>();
private float respondToAttackTimer;
private const float RespondToAttackInterval = 1.0f;
/// <summary>
/// List of previous attacks done to this character
/// </summary>
private readonly Dictionary<Character, AttackResult> previousAttackResults = new Dictionary<Character, AttackResult>();
private class HullSafety
{
public float safety;
@@ -138,6 +146,17 @@ namespace Barotrauma
}
if (isIncapacitated) { return; }
respondToAttackTimer -= deltaTime;
if (respondToAttackTimer <= 0.0f)
{
foreach (var previousAttackResult in previousAttackResults)
{
RespondToAttack(previousAttackResult.Key, previousAttackResult.Value);
}
previousAttackResults.Clear();
respondToAttackTimer = RespondToAttackInterval;
}
base.Update(deltaTime);
foreach (var values in knownHulls)
@@ -316,6 +335,7 @@ namespace Barotrauma
{
if (Character.LockHands) { return; }
if (ObjectiveManager.CurrentObjective == null) { return; }
if (Character.CurrentHull == null) { return; }
bool oxygenLow = !Character.AnimController.HeadInWater && Character.OxygenAvailable < CharacterHealth.LowOxygenThreshold;
bool isCarrying = ObjectiveManager.HasActiveObjective<AIObjectiveContainItem>() || ObjectiveManager.HasActiveObjective<AIObjectiveDecontainItem>();
@@ -349,7 +369,9 @@ namespace Barotrauma
if (!NeedsDivingGear(Character.CurrentHull, out bool needsSuit) || !needsSuit || oxygenLow)
{
bool shouldKeepTheGearOn = Character.AnimController.HeadInWater
|| Character.Submarine.TeamID != Character.TeamID && Character.Submarine.TeamID != Character.TeamType.FriendlyNPC
|| ObjectiveManager.IsCurrentObjective<AIObjectiveFindSafety>()
|| ObjectiveManager.CurrentOrder is AIObjectiveGoTo goTo && goTo.Target == Character // wait order
|| ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn);
if (oxygenLow && Character.CurrentHull.Oxygen > 0)
{
@@ -425,12 +447,14 @@ namespace Barotrauma
{
var decontainObjective = new AIObjectiveDecontainItem(Character, divingSuit, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>())
{
DropIfFailsToContain = false
DropIfFails = false
};
decontainObjective.Abandoned += () =>
{
ReequipUnequipped();
IgnoredItems.Add(targetContainer);
};
decontainObjective.Completed += () => ReequipUnequipped();
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
return;
}
@@ -451,7 +475,7 @@ namespace Barotrauma
{
if (!mask.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(mask, Character, new List<InvSlotType>() { InvSlotType.Any }))
{
if (oxygenLow || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
if (ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
{
mask.Drop(Character);
}
@@ -465,7 +489,12 @@ namespace Barotrauma
if (targetContainer != null)
{
var decontainObjective = new AIObjectiveDecontainItem(Character, mask, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
decontainObjective.Abandoned += () => IgnoredItems.Add(targetContainer);
decontainObjective.Abandoned += () =>
{
ReequipUnequipped();
IgnoredItems.Add(targetContainer);
};
decontainObjective.Completed += () => ReequipUnequipped();
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
return;
}
@@ -476,6 +505,10 @@ namespace Barotrauma
}
}
}
else
{
ReequipUnequipped();
}
}
}
}
@@ -504,7 +537,11 @@ namespace Barotrauma
if (targetContainer != null)
{
var decontainObjective = new AIObjectiveDecontainItem(Character, item, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
decontainObjective.Abandoned += () => IgnoredItems.Add(targetContainer);
decontainObjective.Abandoned += () =>
{
ReequipUnequipped();
IgnoredItems.Add(targetContainer);
};
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
return;
}
@@ -519,6 +556,18 @@ 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,
@@ -528,10 +577,13 @@ namespace Barotrauma
}
private FindItemState findItemState;
private int itemIndex;
public bool FindSuitableContainer(Item containableItem, out Item suitableContainer)
public bool FindSuitableContainer(Item containableItem, out Item suitableContainer) => FindSuitableContainer(Character, containableItem, IgnoredItems, ref itemIndex, out suitableContainer);
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, customPriorityFunction: i =>
{
var container = i.GetComponent<ItemContainer>();
if (container == null) { return 0; }
@@ -683,6 +735,38 @@ namespace Barotrauma
public override void OnAttacked(Character attacker, AttackResult attackResult)
{
if (Character.IsDead) { return; }
if (attacker == null || Character.IsPlayer)
{
// The player characters need to "respond" to the attack always, because the update loop doesn't run for them.
// Otherwise other NPCs totally ignore when player characters are attacked.
RespondToAttack(attacker, attackResult);
return;
}
if (previousAttackResults.ContainsKey(attacker))
{
foreach (Affliction newAffliction in attackResult.Afflictions)
{
var matchingAffliction = previousAttackResults[attacker].Afflictions.Find(a => a.Prefab == newAffliction.Prefab && a.Source == newAffliction.Source);
if (matchingAffliction == null)
{
previousAttackResults[attacker].Afflictions.Add(newAffliction);
}
else
{
matchingAffliction.Strength += newAffliction.Strength;
}
}
previousAttackResults[attacker] = new AttackResult(previousAttackResults[attacker].Afflictions, previousAttackResults[attacker].HitLimb);
}
else
{
previousAttackResults.Add(attacker, attackResult);
}
}
private void RespondToAttack(Character attacker, AttackResult attackResult)
{
// excluding poisons etc
float realDamage = attackResult.Damage;
// including poisons etc
@@ -691,7 +775,7 @@ namespace Barotrauma
{
totalDamage -= affliction.Prefab.KarmaChangeOnApplied * affliction.Strength;
}
if (totalDamage <= 0) { return; }
if (totalDamage <= 0.01f) { return; }
if (Character.IsBot)
{
if (attacker != null)
@@ -735,7 +819,7 @@ namespace Barotrauma
return;
}
float cumulativeDamage = GetDamageDoneByAttacker(attacker);
if (!Character.IsSecurity && attacker.IsBot && !attacker.IsInstigator)
if (!Character.IsSecurity && attacker.IsBot && Character.CombatAction == null)
{
if (cumulativeDamage > 1)
{
@@ -834,7 +918,17 @@ namespace Barotrauma
}
else
{
if (attacker.TeamID == Character.TeamType.FriendlyNPC)
// If there are any enemies around, just ignore the friendly fire
if (Character.CharacterList.Any(ch => ch.Submarine == Character.Submarine && !ch.Removed && !ch.IsDead && !ch.IsIncapacitated && !IsFriendly(ch) && VisibleHulls.Contains(ch.CurrentHull)))
{
return AIObjectiveCombat.CombatMode.None;
}
if (Character.IsInstigator && attacker.IsPlayer)
{
// 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)
{
if (c.IsSecurity)
{
@@ -847,11 +941,7 @@ namespace Barotrauma
}
else
{
if (Character.IsInstigator)
{
return c.IsSecurity ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Retreat;
}
else if (cumulativeDamage > dmgThreshold)
if (cumulativeDamage > dmgThreshold)
{
if (c.IsSecurity)
{
@@ -1006,39 +1096,51 @@ namespace Barotrauma
return true;
}
public bool TryToMoveItem(Item item, Inventory targetInventory, bool dropIfCannotMove = 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 (pickable == null) { return false; }
int targetSlot = -1;
//check if all the slots required by the item are free
foreach (InvSlotType slots in pickable.AllowedSlots)
if (equip)
{
if (slots.HasFlag(InvSlotType.Any)) { continue; }
for (int i = 0; i < targetInventory.Items.Length; i++)
int targetSlot = -1;
//check if all the slots required by the item are free
foreach (InvSlotType slots in pickable.AllowedSlots)
{
if (targetInventory is CharacterInventory characterInventory)
if (slots.HasFlag(InvSlotType.Any)) { continue; }
for (int i = 0; i < targetInventory.Items.Length; i++)
{
//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, new List<InvSlotType>() { InvSlotType.Any }))
{
continue;
}
if (dropIfCannotMove)
{
//if everything else fails, simply drop the existing item
otherItem.Drop(Character);
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);
}
return targetInventory.TryPutItem(item, targetSlot, false, false, Character);
}
public static bool NeedsDivingGear(Hull hull, out bool needsSuit)
@@ -1107,7 +1209,7 @@ namespace Barotrauma
}
//if (!otherCharacter.IsFacing(thief.WorldPosition)) { continue; }
if (!otherCharacter.CanSeeCharacter(thief)) { continue; }
if (!someoneSpoke)
if (!someoneSpoke && !character.IsIncapacitated && character.Stun <= 0.0f)
{
if (!item.StolenDuringRound && GameMain.GameSession?.Campaign?.Map?.CurrentLocation != null)
{
@@ -1306,7 +1408,7 @@ namespace Barotrauma
// Use the cached visible hulls
visibleHulls = VisibleHulls;
}
bool ignoreFire = objectiveManager.HasActiveObjective<AIObjectiveExtinguishFire>();
bool ignoreFire = objectiveManager.CurrentOrder is AIObjectiveExtinguishFires extinguishOrder && extinguishOrder.Priority > 0 || objectiveManager.HasActiveObjective<AIObjectiveExtinguishFire>();
bool ignoreWater = HasDivingSuit(character);
bool ignoreOxygen = ignoreWater || HasDivingMask(character);
bool ignoreEnemies = ObjectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>();
@@ -1391,13 +1493,10 @@ namespace Barotrauma
return hullSafety.safety;
}
public void FaceTarget(ISpatialEntity target) => Character.AnimController.TargetDir = target.WorldPosition.X > Character.WorldPosition.X ? Direction.Right : Direction.Left;
public static bool IsFriendly(Character me, Character other, bool onlySameTeam = false)
{
bool sameTeam = me.TeamID == other.TeamID;
// Only enemies are in the Team "None"
bool friendlyTeam = me.TeamID != Character.TeamType.None && other.TeamID != Character.TeamType.None;
bool friendlyTeam = IsOnFriendlyTeam(GameMain.GameSession?.GameMode, me, other);
bool teamGood = sameTeam || friendlyTeam && !onlySameTeam;
if (!teamGood) { return false; }
bool speciesGood = other.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.Group);
@@ -1413,6 +1512,18 @@ namespace Barotrauma
return true;
}
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;
// 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)
{
friendlyTeam = me.TeamID == other.TeamID;
}
return friendlyTeam;
}
public static bool IsActive(Character other) => other != null && !other.Removed && !other.IsDead && !other.IsUnconscious;
public static bool IsTrueForAllCrewMembers(Character character, Func<HumanAIController, bool> predicate)
@@ -191,7 +191,7 @@ namespace Barotrauma
}
pathFinder.InsideSubmarine = character.Submarine != null;
var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", startNodeFilter, endNodeFilter, nodeFilter, checkVisibility: checkVisibility);
bool useNewPath = needsNewPath || currentPath == null || currentPath.CurrentNode == null;
bool useNewPath = needsNewPath || currentPath == null || currentPath.CurrentNode == null || findPathTimer < -1;
if (!useNewPath && currentPath != null && currentPath.CurrentNode != null && newPath.Nodes.Any() && !newPath.Unreachable)
{
// It's possible that the current path was calculated from a start point that is no longer valid.
@@ -15,6 +15,7 @@ namespace Barotrauma
public virtual bool IgnoreUnsafeHulls => false;
public virtual bool AbandonWhenCannotCompleteSubjectives => true;
public virtual bool AllowSubObjectiveSorting => false;
public virtual bool ForceOrderPriority => true;
/// <summary>
/// Can there be multiple objective instaces of the same type?
@@ -34,6 +35,7 @@ namespace Barotrauma
public virtual bool AllowAutomaticItemUnequipping => false;
public virtual bool AllowOutsideSubmarine => false;
public virtual bool AllowInFriendlySubs => false;
public virtual bool AllowInAnySub => false;
protected readonly List<AIObjective> subObjectives = new List<AIObjective>();
private float _cumulatedDevotion;
@@ -198,12 +200,10 @@ namespace Barotrauma
{
get
{
if (AllowOutsideSubmarine) { return true; }
if (character.Submarine == null) { return false; }
return
character.Submarine.TeamID == character.TeamID ||
(AllowInFriendlySubs && character.Submarine.TeamID == Character.TeamType.FriendlyNPC) ||
character.Submarine.DockedTo.Any(sub => sub.TeamID == character.TeamID);
if (!AllowOutsideSubmarine && character.Submarine == null) { return false; }
if (AllowInAnySub) { return true; }
if (AllowInFriendlySubs && character.Submarine.TeamID == Character.TeamType.FriendlyNPC) { return true; }
return character.Submarine.TeamID == character.TeamID || character.Submarine.DockedTo.Any(sub => sub.TeamID == character.TeamID);
}
}
@@ -212,12 +212,14 @@ namespace Barotrauma
/// </summary>
public virtual float GetPriority()
{
bool isOrder = objectiveManager.CurrentOrder == this;
if (!IsAllowed)
{
Priority = 0;
Abandon = !isOrder;
return Priority;
}
if (objectiveManager.CurrentOrder == this)
if (isOrder)
{
Priority = AIObjectiveManager.OrderPriority;
}
@@ -306,7 +308,7 @@ namespace Barotrauma
return true;
}
#if DEBUG
DebugConsole.ThrowError("Attempted to add a duplicate subobjective!\n" + Environment.StackTrace);
DebugConsole.ThrowError("Attempted to add a duplicate subobjective!\n" + Environment.StackTrace.CleanupStackTrace());
#endif
return false;
}
@@ -0,0 +1,124 @@
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
class AIObjectiveCleanupItem : AIObjective
{
public override string DebugTag => "cleanup item";
public override bool KeepDivingGearOn => true;
public override bool AllowAutomaticItemUnequipping => false;
public readonly Item item;
public bool IsPriority { get; set; }
private readonly List<Item> ignoredContainers = new List<Item>();
private AIObjectiveDecontainItem decontainObjective;
private int itemIndex = 0;
public AIObjectiveCleanupItem(Item item, Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
this.item = item;
}
public override float GetPriority()
{
if (!IsAllowed)
{
Priority = 0;
Abandon = true;
return Priority;
}
else
{
float distanceFactor = 0.9f;
if (!IsPriority && item.CurrentHull != character.CurrentHull)
{
float yDist = Math.Abs(character.WorldPosition.Y - item.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 5 : 0;
float dist = Math.Abs(character.WorldPosition.X - item.WorldPosition.X) + yDist;
distanceFactor = MathHelper.Lerp(0.9f, 0, MathUtils.InverseLerp(0, 5000, dist));
}
bool isSelected = character.HasItem(item);
float selectedBonus = isSelected ? 100 - MaxDevotion : 0;
float devotion = (CumulatedDevotion + selectedBonus) / 100;
float reduction = IsPriority ? 1 : isSelected ? 2 : 3;
float max = MathHelper.Min(AIObjectiveManager.OrderPriority - reduction, 90);
Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (distanceFactor * PriorityModifier), 0, 1));
}
return Priority;
}
protected override void Act(float deltaTime)
{
// Only continue when the get item sub objectives have been completed.
if (subObjectives.Any()) { return; }
if (HumanAIController.FindSuitableContainer(character, item, ignoredContainers, ref itemIndex, out Item suitableContainer))
{
itemIndex = 0;
if (suitableContainer != null)
{
bool equip = item.HasTag(AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR) || (
item.GetComponent<Wearable>() == null &&
item.AllowedSlots.None(s =>
s == InvSlotType.Card ||
s == InvSlotType.Head ||
s == InvSlotType.Headset ||
s == InvSlotType.InnerClothes ||
s == InvSlotType.OuterClothes));
TryAddSubObjective(ref decontainObjective, () => new AIObjectiveDecontainItem(character, item, objectiveManager, targetContainer: suitableContainer.GetComponent<ItemContainer>())
{
Equip = equip,
DropIfFails = true
},
onCompleted: () =>
{
if (equip)
{
HumanAIController.ReequipUnequipped();
}
IsCompleted = true;
},
onAbandon: () =>
{
if (equip)
{
HumanAIController.ReequipUnequipped();
}
if (decontainObjective != null && decontainObjective.ContainObjective != null && decontainObjective.ContainObjective.CanBeCompleted)
{
ignoredContainers.Add(suitableContainer);
}
else
{
Abandon = true;
}
});
}
else
{
Abandon = true;
}
}
else
{
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
}
}
protected override bool Check() => IsCompleted;
public override void Reset()
{
base.Reset();
ignoredContainers.Clear();
itemIndex = 0;
decontainObjective = null;
}
}
}
@@ -0,0 +1,85 @@
using Barotrauma.Items.Components;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
class AIObjectiveCleanupItems : AIObjectiveLoop<Item>
{
public override string DebugTag => "cleanup items";
public override bool KeepDivingGearOn => true;
public override bool AllowAutomaticItemUnequipping => false;
public override bool ForceOrderPriority => false;
public readonly Item prioritizedItem;
public AIObjectiveCleanupItems(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1, Item prioritizedItem = null)
: base(character, objectiveManager, priorityModifier)
{
this.prioritizedItem = prioritizedItem;
}
protected override float TargetEvaluation() => Targets.Any() ? AIObjectiveManager.RunPriority - 1 : 0;
protected override bool Filter(Item target)
{
// If the target was selected as a valid target, we'll have to accept it so that the objective can be completed.
// The validity changes when a character picks the item up.
if (!IsValidTarget(target, character)) { return Objectives.ContainsKey(target) && IsItemInsideValidSubmarine(target, character); }
if (target.CurrentHull.FireSources.Count > 0) { return false; }
// Don't repair items in rooms that have enemies inside.
if (Character.CharacterList.Any(c => c.CurrentHull == target.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; }
return true;
}
protected override IEnumerable<Item> GetList() => Item.ItemList;
protected override AIObjective ObjectiveConstructor(Item item)
=> new AIObjectiveCleanupItem(item, character, objectiveManager, priorityModifier: PriorityModifier)
{
IsPriority = prioritizedItem == item
};
protected override void OnObjectiveCompleted(AIObjective objective, Item target)
=> HumanAIController.RemoveTargets<AIObjectiveCleanupItems, Item>(character, target);
private static bool IsItemInsideValidSubmarine(Item item, Character character)
{
if (item.CurrentHull == null) { return false; }
if (item.Submarine == null) { return false; }
if (item.Submarine.TeamID != character.TeamID) { return false; }
if (character.Submarine != null)
{
if (!character.Submarine.IsConnectedTo(item.Submarine)) { return false; }
}
return true;
}
public static bool IsValidTarget(Item item, Character character)
{
if (item == null) { return false; }
if (item.NonInteractable) { return false; }
if (item.ParentInventory != null) { return false; }
if (character != null && !IsItemInsideValidSubmarine(item, character)) { return false; }
//var rootContainer = item.GetRootContainer();
//// Only target items lying on the ground (= not inside a container) (do we need this check?)
//if (rootContainer != null) { return false; }
var pickable = item.GetComponent<Pickable>();
if (pickable == null) { return false; }
var wire = item.GetComponent<Wire>();
if (wire != null)
{
if (wire.Connections.Any()) { return false; }
}
else
{
var connectionPanel = item.GetComponent<ConnectionPanel>();
if (connectionPanel != null && connectionPanel.Connections.Any(c => c.Wires.Any(w => w != null)))
{
return false;
}
}
return item.Prefab.PreferredContainers.Any();
}
}
}
@@ -15,6 +15,7 @@ namespace Barotrauma
public override bool KeepDivingGearOn => true;
public override bool IgnoreUnsafeHulls => true;
public override bool AllowOutsideSubmarine => true;
public override bool AllowInAnySub => true;
private readonly CombatMode initialMode;
@@ -54,8 +55,8 @@ namespace Barotrauma
if (_weaponComponent == null)
{
_weaponComponent =
Weapon.GetComponent<RangedWeapon>() as ItemComponent ??
Weapon.GetComponent<MeleeWeapon>() as ItemComponent ??
Weapon.GetComponent<RangedWeapon>() ??
Weapon.GetComponent<MeleeWeapon>() ??
Weapon.GetComponent<RepairTool>() as ItemComponent;
}
return _weaponComponent;
@@ -144,6 +145,7 @@ namespace Barotrauma
if (Enemy.Submarine == null || (Enemy.Submarine.TeamID != character.TeamID && Enemy.Submarine != character.Submarine))
{
Priority = 0;
Abandon = true;
return Priority;
}
}
@@ -760,11 +762,7 @@ namespace Barotrauma
if (HumanAIController.HasItem(character, "handlocker", out IEnumerable<Item> matchingItems) && Enemy.Stun > 0 && character.CanInteractWith(Enemy))
{
var handCuffs = matchingItems.First();
if (HumanAIController.TryToMoveItem(handCuffs, Enemy.Inventory))
{
handCuffs.Equip(Enemy);
}
else
if (!HumanAIController.TakeItem(handCuffs, Enemy.Inventory, equip: true))
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Failed to handcuff the target.", Color.Red);
@@ -777,7 +775,7 @@ namespace Barotrauma
if (item.StolenDuringRound)
{
item.Drop(character);
character.Inventory.TryPutItem(item, character, new List<InvSlotType>() { InvSlotType.Any });
character.Inventory.TryPutItem(item, character, CharacterInventory.anySlot);
}
}
character.Speak(TextManager.Get("DialogTargetArrested"), null, 3.0f, "targetarrested", 30.0f);
@@ -885,7 +883,11 @@ namespace Barotrauma
private void Attack(float deltaTime)
{
character.CursorPosition = Enemy.Position;
character.CursorPosition = Enemy.WorldPosition;
if (character.Submarine != null)
{
character.CursorPosition -= character.Submarine.Position;
}
visibilityCheckTimer -= deltaTime;
if (visibilityCheckTimer <= 0.0f)
{
@@ -142,6 +142,7 @@ namespace Barotrauma
}
else
{
// TODO: should we just use GetItem?
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(container.Item, character, objectiveManager, getDivingGearIfNeeded: AllowToFindDivingGear)
{
DialogueIdentifier = "dialogcannotreachtarget",
@@ -153,27 +154,34 @@ namespace Barotrauma
}
else
{
// No matching items in the inventory, try to get an item
TryAddSubObjective(ref getItemObjective, () =>
new AIObjectiveGetItem(character, itemIdentifiers, objectiveManager, equip: Equip, checkInventory: checkInventory, spawnItemIfNotFound: spawnItemIfNotFound)
{
GetItemPriority = GetItemPriority,
ignoredContainerIdentifiers = ignoredContainerIdentifiers,
ignoredItems = containedItems,
AllowToFindDivingGear = AllowToFindDivingGear,
AllowDangerousPressure = AllowDangerousPressure,
TargetCondition = ConditionLevel
}, onAbandon: () =>
{
Abandon = true;
}, onCompleted: () =>
{
if (getItemObjective?.TargetItem != null)
if (character.Submarine == null)
{
Abandon = true;
}
else
{
// No matching items in the inventory, try to get an item
TryAddSubObjective(ref getItemObjective, () =>
new AIObjectiveGetItem(character, itemIdentifiers, objectiveManager, equip: Equip, checkInventory: checkInventory, spawnItemIfNotFound: spawnItemIfNotFound)
{
containedItems.Add(getItemObjective.TargetItem);
}
RemoveSubObjective(ref getItemObjective);
});
GetItemPriority = GetItemPriority,
ignoredContainerIdentifiers = ignoredContainerIdentifiers,
ignoredItems = containedItems,
AllowToFindDivingGear = AllowToFindDivingGear,
AllowDangerousPressure = AllowDangerousPressure,
TargetCondition = ConditionLevel
}, onAbandon: () =>
{
Abandon = true;
}, onCompleted: () =>
{
if (getItemObjective?.TargetItem != null)
{
containedItems.Add(getItemObjective.TargetItem);
}
RemoveSubObjective(ref getItemObjective);
});
}
}
}
@@ -16,9 +16,12 @@ namespace Barotrauma
private ItemContainer targetContainer;
private readonly Item targetItem;
private AIObjectiveGoTo goToObjective;
private AIObjectiveGetItem getItemObjective;
private AIObjectiveContainItem containObjective;
public AIObjectiveGetItem GetItemObjective => getItemObjective;
public AIObjectiveContainItem ContainObjective => containObjective;
public bool Equip { get; set; }
/// <summary>
@@ -26,7 +29,7 @@ namespace Barotrauma
/// In both cases abandons the objective.
/// Note that has no effect if the target container was not defined (always drops) -> completes when the item is dropped.
/// </summary>
public bool DropIfFailsToContain { get; set; } = true;
public bool DropIfFails { get; set; } = true;
public AIObjectiveDecontainItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, ItemContainer sourceContainer = null, ItemContainer targetContainer = null, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
@@ -74,54 +77,30 @@ namespace Barotrauma
return;
}
}
else
else if (targetContainer.Inventory.Items.Contains(itemToDecontain))
{
if (targetContainer.Inventory.Items.Contains(itemToDecontain))
{
IsCompleted = true;
return;
}
IsCompleted = true;
return;
}
if (goToObjective == null && !itemToDecontain.IsOwnedBy(character))
if (getItemObjective == null && !itemToDecontain.IsOwnedBy(character))
{
if (sourceContainer == null)
{
Abandon = true;
return;
}
if (!character.CanInteractWith(sourceContainer.Item, out _, checkLinked: false))
{
TryAddSubObjective(ref goToObjective,
constructor: () => new AIObjectiveGoTo(sourceContainer.Item, character, objectiveManager)
{
// If the container changes, the item is no longer where it was
abortCondition = () => itemToDecontain.Container != sourceContainer.Item,
DialogueIdentifier = "dialogcannotreachtarget",
TargetName = sourceContainer.Item.Name
},
onAbandon: () => Abandon = true);
return;
}
TryAddSubObjective(ref getItemObjective,
constructor: () => new AIObjectiveGetItem(character, targetItem, objectiveManager, Equip),
onAbandon: () => Abandon = true);
return;
}
if (targetContainer != null)
{
TryAddSubObjective(ref containObjective,
constructor: () => new AIObjectiveContainItem(character, itemToDecontain, targetContainer, objectiveManager)
{
Equip = this.Equip,
Equip = Equip,
RemoveEmpty = false,
GetItemPriority = this.GetItemPriority,
GetItemPriority = GetItemPriority,
ignoredContainerIdentifiers = sourceContainer != null ? new string[] { sourceContainer.Item.Prefab.Identifier } : null
},
onCompleted: () => IsCompleted = true,
onAbandon: () =>
{
if (DropIfFailsToContain)
{
itemToDecontain.Drop(character);
}
Abandon = true;
});
onAbandon: () => Abandon = true);
}
else
{
@@ -133,8 +112,17 @@ namespace Barotrauma
public override void Reset()
{
base.Reset();
goToObjective = null;
getItemObjective = null;
containObjective = null;
}
protected override void OnAbandon()
{
base.OnAbandon();
if (DropIfFails && targetItem != null && targetItem.IsOwnedBy(character))
{
targetItem.Drop(character);
}
}
}
}
@@ -13,6 +13,8 @@ namespace Barotrauma
public override bool ConcurrentObjectives => true;
public override bool KeepDivingGearOn => true;
public override bool AllowInAnySub => true;
private readonly Hull targetHull;
private AIObjectiveGetItem getExtinguisherObjective;
@@ -30,12 +32,15 @@ namespace Barotrauma
if (!IsAllowed)
{
Priority = 0;
Abandon = true;
return Priority;
}
if (!objectiveManager.IsCurrentOrder<AIObjectiveExtinguishFires>()
&& Character.CharacterList.Any(c => c.CurrentHull == targetHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c)))
bool isOrder = objectiveManager.IsCurrentOrder<AIObjectiveExtinguishFires>();
if (!isOrder && Character.CharacterList.Any(c => c.CurrentHull == targetHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c)))
{
// Don't go into rooms with any enemies, unless it's an order
Priority = 0;
Abandon = true;
}
else
{
@@ -43,14 +48,22 @@ namespace Barotrauma
yDist = yDist > 100 ? yDist * 3 : 0;
float dist = Math.Abs(character.WorldPosition.X - targetHull.WorldPosition.X) + yDist;
float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, dist));
if (targetHull == character.CurrentHull)
if (targetHull == character.CurrentHull || HumanAIController.VisibleHulls.Contains(targetHull))
{
distanceFactor = 1;
}
float severity = AIObjectiveExtinguishFires.GetFireSeverity(targetHull);
float severityFactor = MathHelper.Lerp(0, 1, severity / 100);
float devotion = CumulatedDevotion / 100;
Priority = MathHelper.Lerp(0, 100, MathHelper.Clamp(devotion + (severityFactor * distanceFactor * PriorityModifier), 0, 1));
if (severity > 0.5f && !isOrder)
{
// Ignore severe fires unless ordered. (Let the fire drain all the oxygen instead).
Priority = 0;
Abandon = true;
}
else
{
float devotion = CumulatedDevotion / 100;
Priority = MathHelper.Lerp(0, 100, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1));
}
}
return Priority;
}
@@ -131,13 +144,16 @@ namespace Barotrauma
if (move)
{
//go to the first firesource
TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(fs, character, objectiveManager, closeEnough: extinguisher.Range / 2)
if (TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(fs, character, objectiveManager, closeEnough: extinguisher.Range / 2)
{
DialogueIdentifier = "dialogcannotreachfire",
TargetName = fs.Hull.DisplayName
},
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref gotoObjective)))
{
DialogueIdentifier = "dialogcannotreachfire",
TargetName = fs.Hull.DisplayName
},
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref gotoObjective));
gotoObjective.requiredCondition = () => HumanAIController.VisibleHulls.Contains(fs.Hull);
}
}
else
{
@@ -1,6 +1,8 @@
using System.Linq;
using System;
using System.Linq;
using System.Collections.Generic;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
@@ -8,14 +10,22 @@ namespace Barotrauma
{
public override string DebugTag => "extinguish fires";
public override bool ForceRun => true;
public override bool AllowInAnySub => true;
public AIObjectiveExtinguishFires(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { }
protected override bool Filter(Hull hull) => IsValidTarget(hull, character);
protected override float TargetEvaluation() => Targets.Sum(t => GetFireSeverity(t));
protected override float TargetEvaluation() =>
// If any target is visible -> 100 priority
Targets.Any(t => t == character.CurrentHull || HumanAIController.VisibleHulls.Contains(t)) ? 100 :
// Else based on the fire severity
Targets.Sum(t => GetFireSeverity(t) * 100);
public static float GetFireSeverity(Hull hull) => hull.FireSources.Sum(fs => fs.Size.X);
/// <summary>
/// 0-1 based on the horizontal size of all of the fires in the hull.
/// </summary>
public static float GetFireSeverity(Hull hull) => MathHelper.Lerp(0, 1, MathUtils.InverseLerp(0, Math.Min(hull.Rect.Width, 1000), hull.FireSources.Sum(fs => fs.Size.X)));
protected override IEnumerable<Hull> GetList() => Hull.hullList;
@@ -31,22 +41,7 @@ namespace Barotrauma
if (hull.FireSources.None()) { return false; }
if (hull.Submarine == null) { return false; }
if (character.Submarine == null) { return false; }
if (!character.Submarine.IsConnectedTo(hull.Submarine)) { return false; }
if (character.AIController is HumanAIController humanAI)
{
if (hull.Submarine.TeamID != character.TeamID)
{
if (humanAI.ObjectiveManager.IsCurrentOrder<AIObjectiveExtinguishFires>())
{
// For orders, allow targets in the current sub (for example if the bot is inside an outpost or a wreck)
if (hull.Submarine != character.Submarine) { return false; }
}
else
{
return false;
}
}
}
if (!character.Submarine.IsEntityFoundOnThisSub(hull, includingConnectedSubs: true)) { return false; }
return true;
}
}
@@ -17,7 +17,7 @@ namespace Barotrauma
private Item targetItem;
public static float MIN_OXYGEN = 10;
public static string HEAVY_DIVING_GEAR = "heavydiving";
public static string HEAVY_DIVING_GEAR = "deepdiving";
public static string LIGHT_DIVING_GEAR = "lightdiving";
public static string OXYGEN_SOURCE = "oxygensource";
@@ -14,8 +14,9 @@ namespace Barotrauma
public override bool IgnoreUnsafeHulls => true;
public override bool ConcurrentObjectives => true;
public override bool AllowOutsideSubmarine => true;
public override bool AllowInAnySub => true;
public override bool AbandonWhenCannotCompleteSubjectives => false;
public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace); }
public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace.CleanupStackTrace()); }
// TODO: expose?
const float priorityIncrease = 100;
@@ -12,6 +12,7 @@ namespace Barotrauma
public override string DebugTag => "fix leak";
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
public override bool AllowInAnySub => true;
public Gap Leak { get; private set; }
@@ -35,11 +36,7 @@ namespace Barotrauma
if (!IsAllowed)
{
Priority = 0;
return Priority;
}
if (Leak.Removed || Leak.Open <= 0)
{
Priority = 0;
Abandon = true;
}
else if (HumanAIController.IsTrueForAnyCrewMember(other => other != HumanAIController && other.ObjectiveManager.GetActiveObjective<AIObjectiveFixLeak>()?.Leak == Leak))
{
@@ -116,7 +113,7 @@ namespace Barotrauma
{
HumanAIController.AnimController.Crouching = true;
}
float reach = repairTool.Range + ConvertUnits.ToDisplayUnits(((HumanoidAnimController)character.AnimController).ArmLength);
float reach = CalculateReach(repairTool, character);
bool canOperate = toLeak.LengthSquared() < reach * reach;
if (canOperate)
{
@@ -144,7 +141,7 @@ namespace Barotrauma
onAbandon: () =>
{
if (Check()) { IsCompleted = true; }
else if ((Leak.WorldPosition - character.WorldPosition).LengthSquared() > reach * reach * 2)
else if ((Leak.WorldPosition - character.WorldPosition).LengthSquared() > MathUtils.Pow(reach * 2, 2))
{
// Too far
Abandon = true;
@@ -167,5 +164,14 @@ namespace Barotrauma
gotoObjective = null;
operateObjective = null;
}
public static float CalculateReach(RepairTool repairTool, Character character)
{
float armLength = ConvertUnits.ToDisplayUnits(((HumanoidAnimController)character.AnimController).ArmLength);
// This is an approximation, because we don't know the exact reach until the pose is taken.
// And even then the actual range depends on the direction we are aiming to.
// Found out that without any multiplier the value (209) is often too short.
return repairTool.Range + armLength * 1.2f;
}
}
}
@@ -9,6 +9,7 @@ namespace Barotrauma
public override string DebugTag => "fix leaks";
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
public override bool AllowInAnySub => true;
private Hull PrioritizedHull { get; set; }
public AIObjectiveFixLeaks(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1, Hull prioritizedHull = null) : base(character, objectiveManager, priorityModifier)
@@ -71,12 +72,9 @@ namespace Barotrauma
{
if (gap == null) { return false; }
if (gap.ConnectedWall == null || gap.ConnectedDoor != null || gap.Open <= 0 || gap.linkedTo.All(l => l == null)) { return false; }
if (gap.Submarine == null) { return false; }
if (gap.Submarine.TeamID != character.TeamID) { return false; }
if (character.Submarine != null)
{
if (!character.Submarine.IsConnectedTo(gap.Submarine)) { return false; }
}
if (gap.Submarine == null || character.Submarine == null) { return false; }
// Don't allow going into another sub, unless it's connected and of the same team and type.
if (!character.Submarine.IsEntityFoundOnThisSub(gap, includingConnectedSubs: true)) { return false; }
return true;
}
}
@@ -37,6 +37,7 @@ namespace Barotrauma
public static float DefaultReach = 100;
public bool AllowToFindDivingGear { get; set; } = true;
public bool MustBeSpecificItem { get; set; }
public AIObjectiveGetItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, bool equip = true, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
@@ -84,6 +85,11 @@ namespace Barotrauma
Abandon = true;
return;
}
if (character.Submarine == null)
{
Abandon = true;
return;
}
if (identifiersOrTags != null && !isDoneSeeking)
{
if (checkInventory)
@@ -109,7 +115,10 @@ namespace Barotrauma
}
}
FindTargetItem();
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
if (!objectiveManager.IsCurrentOrder<AIObjectiveGoTo>())
{
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
}
return;
}
}
@@ -137,7 +146,8 @@ namespace Barotrauma
if (originalTarget == null)
{
// Try again
Reset();
ignoredItems.Add(targetItem);
ResetInternal();
}
else
{
@@ -176,12 +186,8 @@ namespace Barotrauma
return;
}
if (HumanAIController.TryToMoveItem(targetItem, character.Inventory))
if (HumanAIController.TakeItem(targetItem, character.Inventory, equip, storeUnequipped: true))
{
if (equip)
{
targetItem.Equip(character);
}
IsCompleted = true;
}
else
@@ -207,8 +213,16 @@ namespace Barotrauma
},
onAbandon: () =>
{
ignoredItems.Add(targetItem);
Reset();
if (originalTarget == null)
{
// Try again
ignoredItems.Add(targetItem);
ResetInternal();
}
else
{
Abandon = true;
}
},
onCompleted: () => RemoveSubObjective(ref goToObjective));
}
@@ -235,13 +249,13 @@ namespace Barotrauma
Submarine mySub = character.Submarine;
if (itemSub == null) { continue; }
if (mySub == null) { continue; }
if (itemSub.TeamID != mySub.TeamID && itemSub.TeamID != character.TeamID) { continue; }
if (!CheckItem(item)) { continue; }
if (ignoredContainerIdentifiers != null && item.Container != null)
{
if (ignoredContainerIdentifiers.Contains(item.ContainerIdentifier)) { continue; }
}
if (!mySub.IsConnectedTo(itemSub)) { continue; }
// Don't allow going into another sub, unless it's connected and of the same team and type.
if (!character.Submarine.IsEntityFoundOnThisSub(item, includingConnectedSubs: true)) { continue; }
if (character.IsItemTakenBySomeoneElse(item)) { continue; }
float itemPriority = 1;
if (GetItemPriority != null)
@@ -272,7 +286,7 @@ namespace Barotrauma
if (!(MapEntityPrefab.List.FirstOrDefault(me => me is ItemPrefab ip && identifiersOrTags.Any(id => id == ip.Identifier || ip.Tags.Contains(id))) is ItemPrefab prefab))
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot find the item with the following identifier(s): {string.Join(", ", identifiersOrTags)}, tried to spawn the item but no matching item prefabs were found.", Color.Yellow);
DebugConsole.NewMessage($"{character.Name}: Cannot find the item with the following identifier(s) or tag(s): {string.Join(", ", identifiersOrTags)}, tried to spawn the item but no matching item prefabs were found.", Color.Yellow);
#endif
Abandon = true;
}
@@ -291,7 +305,7 @@ namespace Barotrauma
else
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot find the item with the following identifier(s): {string.Join(", ", identifiersOrTags)}", Color.Yellow);
DebugConsole.NewMessage($"{character.Name}: Cannot find the item with the following identifier(s) or tag(s): {string.Join(", ", identifiersOrTags)}", Color.Yellow);
#endif
Abandon = true;
}
@@ -330,11 +344,28 @@ namespace Barotrauma
public override void Reset()
{
base.Reset();
ResetInternal();
}
/// <summary>
/// Does not reset the ignored items list
/// </summary>
private void ResetInternal()
{
goToObjective = null;
targetItem = originalTarget;
moveToTarget = targetItem?.GetRootInventoryOwner();
isDoneSeeking = false;
currSearchIndex = 0;
}
protected override void OnAbandon()
{
base.OnAbandon();
if (objectiveManager.CurrentOrder != null)
{
character.Speak(TextManager.Get("DialogCannotFindItem"), null, 0.0f, "cannotfinditem", 10.0f);
}
}
}
}
@@ -1,5 +1,6 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
@@ -50,6 +51,7 @@ namespace Barotrauma
public override bool AbandonWhenCannotCompleteSubjectives => !repeat;
public override bool AllowOutsideSubmarine => AllowGoingOutside;
public override bool AllowInAnySub => true;
public string DialogueIdentifier { get; set; }
public string TargetName { get; set; }
@@ -60,17 +62,27 @@ namespace Barotrauma
public override float GetPriority()
{
bool isOrder = objectiveManager.CurrentOrder == this;
if (!IsAllowed)
{
Priority = 0;
Abandon = !isOrder;
return Priority;
}
if (followControlledCharacter && Character.Controlled == null)
{
Priority = 0;
Abandon = !isOrder;
}
if (Target is Entity e && e.Removed)
{
Priority = 0;
Abandon = !isOrder;
}
if (IgnoreIfTargetDead && Target is Character character && character.IsDead)
{
Priority = 0;
Abandon = !isOrder;
}
else
{
@@ -84,7 +96,7 @@ namespace Barotrauma
}
else
{
Priority = objectiveManager.CurrentOrder == this ? AIObjectiveManager.OrderPriority : 10;
Priority = isOrder ? AIObjectiveManager.OrderPriority : 10;
}
}
return Priority;
@@ -93,7 +105,7 @@ namespace Barotrauma
public AIObjectiveGoTo(ISpatialEntity target, Character character, AIObjectiveManager objectiveManager, bool repeat = false, bool getDivingGearIfNeeded = true, float priorityModifier = 1, float closeEnough = 0)
: base(character, objectiveManager, priorityModifier)
{
this.Target = target;
Target = target;
this.repeat = repeat;
waitUntilPathUnreachable = 3.0f;
this.getDivingGearIfNeeded = getDivingGearIfNeeded;
@@ -260,6 +272,53 @@ namespace Barotrauma
}
}
}
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))
{
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)
{
isScooterEquipped = HumanAIController.TakeItem(scooter, character.Inventory, equip: true, dropOtherIfCannotMove: false, allowSwapping: true, storeUnequipped: false);
}
}
if (scooter != null && isScooterEquipped)
{
if (shouldUseScooter)
{
useScooter = true;
}
else
{
// Unequip
character.Inventory.TryPutItem(scooter, character, CharacterInventory.anySlot);
}
}
}
else
{
checkScooterTimer -= deltaTime;
}
if (SteeringManager == PathSteering)
{
Func<PathNode, bool> nodeFilter = null;
@@ -274,47 +333,90 @@ namespace Barotrauma
}, endNodeFilter, nodeFilter, CheckVisibility);
if (!isInside && PathSteering.CurrentPath == null || PathSteering.IsPathDirty || PathSteering.CurrentPath.Unreachable)
{
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(Target.WorldPosition - character.WorldPosition));
if (character.AnimController.InWater)
if (useScooter)
{
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: 5, weight: 15);
UseScooter(Target.WorldPosition);
}
else
{
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(Target.WorldPosition - character.WorldPosition));
if (character.AnimController.InWater)
{
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: 5, weight: 15);
}
}
}
else if (useScooter && PathSteering.CurrentPath?.CurrentNode != null)
{
UseScooter(PathSteering.CurrentPath.CurrentNode.WorldPosition);
}
}
else
{
SteeringManager.SteeringSeek(character.GetRelativeSimPosition(Target), 10);
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: 5, weight: 15);
if (useScooter)
{
UseScooter(Target.WorldPosition);
}
else
{
SteeringManager.SteeringSeek(character.GetRelativeSimPosition(Target), 10);
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: 5, weight: 15);
}
}
}
void UseScooter(Vector2 targetWorldPos)
{
SteeringManager.Reset();
character.CursorPosition = targetWorldPos;
if (character.Submarine != null)
{
character.CursorPosition -= character.Submarine.Position;
}
Vector2 dir = Vector2.Normalize(character.CursorPosition - character.Position);
if (!MathUtils.IsValid(dir)) { dir = Vector2.UnitY; }
SteeringManager.SteeringManual(1.0f, dir);
character.SetInput(InputType.Aim, false, true);
character.SetInput(InputType.Shoot, false, true);
}
}
public Hull GetTargetHull()
private bool useScooter;
private float checkScooterTimer;
private readonly float checkScooterTime = 0.2f;
public Hull GetTargetHull() => GetTargetHull(Target);
public static Hull GetTargetHull(ISpatialEntity target)
{
if (Target is Hull h)
if (target is Hull h)
{
return h;
}
else if (Target is Item i)
else if (target is Item i)
{
return i.CurrentHull;
}
else if (Target is Character c)
else if (target is Character c)
{
return c.CurrentHull;
}
else if (Target is Gap g)
else if (target is Gap g)
{
return g.FlowTargetHull;
}
else if (Target is WayPoint wp)
else if (target is WayPoint wp)
{
return wp.CurrentHull;
}
else if (Target is FireSource fs)
else if (target is FireSource fs)
{
return fs.Hull;
}
else if (target is OrderTarget ot)
{
return ot.Hull;
}
return null;
}
@@ -361,7 +463,7 @@ namespace Barotrauma
{
if (Target is Item item)
{
if (character.CanInteractWith(item, out _, checkLinked: false)) { IsCompleted = true; }
if (!character.IsClimbing && character.CanInteractWith(item, out _, checkLinked: false)) { IsCompleted = true; }
}
else if (Target is Character targetCharacter)
{
@@ -1,4 +1,5 @@
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
@@ -11,14 +12,14 @@ namespace Barotrauma
{
public override string DebugTag => "idle";
public override bool AllowAutomaticItemUnequipping => true;
public override bool AllowOutsideSubmarine => true;
public override bool AllowInAnySub => true;
private BehaviorType behavior;
public BehaviorType Behavior
{
get { return behavior; }
set
{
set
{
behavior = value;
switch (behavior)
{
@@ -27,16 +28,13 @@ namespace Barotrauma
newTargetIntervalMax = 20;
standStillMin = 2;
standStillMax = 10;
walkDurationMin = 5;
walkDurationMax = 10;
break;
case BehaviorType.Passive:
case BehaviorType.StayInHull:
newTargetIntervalMin = 60;
newTargetIntervalMax = 120;
standStillMin = 30;
standStillMax = 60;
walkDurationMin = 5;
walkDurationMax = 10;
break;
}
}
@@ -46,8 +44,8 @@ namespace Barotrauma
private float newTargetIntervalMax;
private float standStillMin;
private float standStillMax;
private float walkDurationMin;
private float walkDurationMax;
private readonly float walkDurationMin = 5;
private readonly float walkDurationMax = 10;
public enum BehaviorType
{
@@ -55,7 +53,7 @@ namespace Barotrauma
Passive,
StayInHull
}
public Hull TargetHull { get; set; }
private Hull currentTarget;
private float newTargetTimer;
@@ -86,7 +84,7 @@ namespace Barotrauma
protected override bool Check() => false;
public override bool CanBeCompleted => true;
public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace); }
public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace.CleanupStackTrace()); }
public readonly HashSet<string> PreferredOutpostModuleTypes = new HashSet<string>();
@@ -142,6 +140,8 @@ namespace Barotrauma
timerMargin = 0;
}
private bool IsSteeringFinished() => PathSteering.CurrentPath != null && (PathSteering.CurrentPath.Finished || PathSteering.CurrentPath.Unreachable);
protected override void Act(float deltaTime)
{
if (PathSteering == null) { return; }
@@ -164,12 +164,30 @@ namespace Barotrauma
character.DeselectCharacter();
}
if (behavior != BehaviorType.StayInHull)
if (!character.IsClimbing)
{
bool currentTargetIsInvalid = currentTarget == null || IsForbidden(currentTarget) ||
(PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull)));
character.SelectedConstruction = null;
}
bool IsSteeringFinished() => PathSteering.CurrentPath != null && (PathSteering.CurrentPath.Finished || PathSteering.CurrentPath.Unreachable);
CleanupItems(deltaTime);
if (behavior == BehaviorType.StayInHull)
{
currentTarget = TargetHull;
bool stayInHull = character.CurrentHull == currentTarget && IsSteeringFinished() && !character.IsClimbing;
if (stayInHull)
{
Wander(deltaTime);
}
else if (currentTarget != null)
{
PathSteering.SteeringSeek(character.GetRelativeSimPosition(currentTarget), weight: 1, nodeFilter: node => node.Waypoint.CurrentHull != null);
}
}
else
{
bool currentTargetIsInvalid = currentTarget == null || IsForbidden(currentTarget) ||
(PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull)));
if (currentTarget != null && !currentTargetIsInvalid)
{
@@ -226,7 +244,7 @@ namespace Barotrauma
searchingNewHull = true;
return;
}
else if (targetHulls.Count > 0)
else if (targetHulls.Any())
{
//choose a random available hull
currentTarget = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced);
@@ -242,12 +260,13 @@ namespace Barotrauma
});
if (path.Unreachable)
{
//can't go to this room, remove it from the list and try another room next frame
//can't go to this room, remove it from the list and try another room
int index = targetHulls.IndexOf(currentTarget);
targetHulls.RemoveAt(index);
hullWeights.RemoveAt(index);
PathSteering.Reset();
currentTarget = null;
SetTargetTimerLow();
return;
}
searchingNewHull = false;
@@ -263,47 +282,25 @@ namespace Barotrauma
{
character.AIController.SelectTarget(currentTarget.AiTarget);
string errorMsg = null;
#if DEBUG
#if DEBUG
bool isRoomNameFound = currentTarget.DisplayName != null;
errorMsg = "(Character " + character.Name + " idling, target " + (isRoomNameFound ? currentTarget.DisplayName : currentTarget.ToString()) + ")";
#endif
#endif
var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, errorMsgStr: errorMsg, nodeFilter: node => node.Waypoint.CurrentHull != null);
PathSteering.SetPath(path);
}
SetTargetTimerNormal();
}
}
newTargetTimer -= deltaTime;
}
//wander randomly
// - if reached the end of the path
// - if the target is unreachable
// - if the path requires going outside
if (!character.IsClimbing)
{
if (behavior == BehaviorType.StayInHull || SteeringManager != PathSteering || (PathSteering.CurrentPath != null &&
(PathSteering.CurrentPath.Finished || PathSteering.CurrentPath.Unreachable || PathSteering.CurrentPath.HasOutdoorsNodes)))
if (!character.IsClimbing && IsSteeringFinished())
{
Wander(deltaTime);
return;
}
character.SelectedConstruction = null;
}
if (currentTarget != null)
{
if (SteeringManager == PathSteering)
else if (currentTarget != null)
{
PathSteering.SteeringSeek(character.GetRelativeSimPosition(currentTarget), weight: 1, nodeFilter: node => node.Waypoint.CurrentHull != null);
}
else
{
character.AIController.SteeringManager.SteeringSeek(character.GetRelativeSimPosition(currentTarget));
}
}
else
{
Wander(deltaTime);
}
}
@@ -328,7 +325,7 @@ namespace Barotrauma
{
//if there are characters too close on both sides, don't try to steer away from them
//because it'll cause the character to spaz out trying to avoid both
if (tooCloseCharacter != null &&
if (tooCloseCharacter != null &&
Math.Sign(tooCloseCharacter.WorldPosition.X - character.WorldPosition.X) != Math.Sign(c.WorldPosition.X - character.WorldPosition.X))
{
tooCloseCharacter = null;
@@ -336,7 +333,7 @@ namespace Barotrauma
}
tooCloseCharacter = c;
}
HumanAIController.FaceTarget(c);
HumanAIController.FaceTarget(c);
}
}
@@ -344,7 +341,7 @@ namespace Barotrauma
{
Vector2 diff = character.WorldPosition - tooCloseCharacter.WorldPosition;
if (diff.LengthSquared() < 0.0001f) { diff = Rand.Vector(1.0f); }
if (Math.Abs(diff.X) > 0 &&
if (Math.Abs(diff.X) > 0 &&
(character.WorldPosition.X > currentHull.WorldRect.Right - 50 || character.WorldPosition.X < currentHull.WorldRect.Left + 50))
{
// Between a wall and a character -> move away
@@ -459,6 +456,48 @@ namespace Barotrauma
}
}
#region Cleaning
private readonly float checkItemsInterval = 1;
private float checkItemsTimer;
private readonly List<Item> itemsToClean = new List<Item>();
private readonly List<Item> ignoredItems = new List<Item>();
private void CleanupItems(float deltaTime)
{
if (checkItemsTimer <= 0)
{
checkItemsTimer = checkItemsInterval * Rand.Range(0.9f, 1.1f);
var hull = character.CurrentHull;
if (hull != null)
{
itemsToClean.Clear();
foreach (Item item in Item.ItemList)
{
if (item.CurrentHull != hull) { continue; }
if (AIObjectiveCleanupItems.IsValidTarget(item, character) && !ignoredItems.Contains(item))
{
itemsToClean.Add(item);
}
}
if (itemsToClean.Any())
{
var targetItem = itemsToClean.OrderBy(i => Math.Abs(character.WorldPosition.X - i.WorldPosition.X)).FirstOrDefault();
if (targetItem != null)
{
var cleanupObjective = new AIObjectiveCleanupItem(targetItem, character, objectiveManager, PriorityModifier);
cleanupObjective.Abandoned += () => ignoredItems.Add(targetItem);
subObjectives.Add(cleanupObjective);
}
}
}
}
else
{
checkItemsTimer -= deltaTime;
}
}
#endregion
public static bool IsForbidden(Hull hull)
{
if (hull == null) { return true; }
@@ -475,6 +514,9 @@ namespace Barotrauma
tooCloseCharacter = null;
targetHulls.Clear();
hullWeights.Clear();
checkItemsTimer = 0;;
itemsToClean.Clear();
ignoredItems.Clear();
autonomousObjectiveRetryTimer = 10;
}
}
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
namespace Barotrauma
@@ -46,7 +45,7 @@ namespace Barotrauma
public override bool AllowSubObjectiveSorting => true;
public virtual bool InverseTargetEvaluation => false;
public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace); }
public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace.CleanupStackTrace()); }
public override void Update(float deltaTime)
{
@@ -79,7 +78,12 @@ namespace Barotrauma
var target = objective.Key;
if (!Targets.Contains(target))
{
subObjectives.Remove(objective.Value);
var subObjective = objective.Value;
if (CurrentSubObjective == subObjective)
{
CurrentSubObjective.Abandon = !CurrentSubObjective.IsCompleted;
}
subObjectives.Remove(subObjective);
}
}
SyncRemovedObjectives(Objectives, GetList());
@@ -137,7 +141,7 @@ namespace Barotrauma
{
if (objectiveManager.CurrentOrder == this)
{
Priority = AIObjectiveManager.OrderPriority;
Priority = ForceOrderPriority ? AIObjectiveManager.OrderPriority : targetValue;
}
else
{
@@ -70,7 +70,7 @@ namespace Barotrauma
if (objective == null)
{
#if DEBUG
DebugConsole.ThrowError("Attempted to add a null objective to AIObjectiveManager\n" + Environment.StackTrace);
DebugConsole.ThrowError("Attempted to add a null objective to AIObjectiveManager\n" + Environment.StackTrace.CleanupStackTrace());
#endif
return;
}
@@ -128,8 +128,7 @@ 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;
var order = new Order(orderPrefab, item ?? character.CurrentHull as Entity,
item?.Components.FirstOrDefault(ic => ic.GetType() == orderPrefab.ItemComponentType), orderGiver: character);
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; }
var objective = CreateObjective(order, autonomousObjective.option, character, isAutonomous: true, autonomousObjective.priorityModifier);
@@ -147,7 +146,7 @@ namespace Barotrauma
if (objective == null)
{
#if DEBUG
DebugConsole.ThrowError($"{character.Name}: Attempted to add a null objective to AIObjectiveManager\n" + Environment.StackTrace);
DebugConsole.ThrowError($"{character.Name}: Attempted to add a null objective to AIObjectiveManager\n" + Environment.StackTrace.CleanupStackTrace());
#endif
return;
}
@@ -314,9 +313,10 @@ namespace Barotrauma
};
break;
case "wait":
newObjective = new AIObjectiveGoTo(order.TargetEntity ?? character, character, this, repeat: true, priorityModifier: priorityModifier)
newObjective = new AIObjectiveGoTo(order.TargetSpatialEntity ?? character, character, this, repeat: true, priorityModifier: priorityModifier)
{
AllowGoingOutside = character.CurrentHull == null
AllowGoingOutside = order.TargetSpatialEntity == null ? character.CurrentHull == null :
character.Submarine == null || character.Submarine != order.TargetSpatialEntity.Submarine
};
break;
case "fixleaks":
@@ -374,6 +374,32 @@ namespace Barotrauma
Override = orderGiver != null && orderGiver.IsPlayer
};
break;
case "setchargepct":
newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option, false, priorityModifier: priorityModifier)
{
IsLoop = false,
Override = character.CurrentOrder != null,
completionCondition = () =>
{
if (float.TryParse(option, out float pct))
{
var targetRatio = Math.Clamp(pct, 0f, 1f);
var currentRatio = (order.TargetItemComponent as PowerContainer).RechargeRatio;
return Math.Abs(targetRatio - currentRatio) < 0.05f;
}
return true;
}
};
break;
case "getitem":
newObjective = new AIObjectiveGetItem(character, order.TargetEntity as Item ?? order.TargetItemComponent?.Item, this, false, priorityModifier: priorityModifier)
{
MustBeSpecificItem = true
};
break;
case "cleanupitems":
newObjective = new AIObjectiveCleanupItems(character, this, priorityModifier, order.TargetEntity as Item);
break;
default:
if (order.TargetItemComponent == null) { return null; }
if (order.TargetItemComponent.Item.NonInteractable) { return null; }
@@ -11,6 +11,7 @@ namespace Barotrauma
public override string DebugTag => $"operate item {component.Name}";
public override bool AllowAutomaticItemUnequipping => true;
public override bool AllowMultipleInstances => true;
public override bool AllowInAnySub => true;
private ItemComponent component, controller;
private Entity operateTarget;
@@ -35,9 +36,11 @@ namespace Barotrauma
public override float GetPriority()
{
bool isOrder = objectiveManager.CurrentOrder == this;
if (!IsAllowed || character.LockHands)
{
Priority = 0;
Abandon = !isOrder;
return Priority;
}
if (component.Item.ConditionPercentage <= 0)
@@ -46,7 +49,6 @@ namespace Barotrauma
}
else
{
bool isOrder = objectiveManager.CurrentOrder == this;
if (isOrder)
{
Priority = AIObjectiveManager.OrderPriority;
@@ -172,7 +174,7 @@ namespace Barotrauma
}
if (target.CanBeSelected)
{
if (character.CanInteractWith(target.Item, out _, checkLinked: false))
if (!character.IsClimbing && character.CanInteractWith(target.Item, out _, checkLinked: false))
{
HumanAIController.FaceTarget(target.Item);
if (character.SelectedConstruction != target.Item)
@@ -189,7 +191,8 @@ namespace Barotrauma
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(target.Item, character, objectiveManager, closeEnough: 50)
{
DialogueIdentifier = "dialogcannotreachtarget",
TargetName = target.Item.Name
TargetName = target.Item.Name,
endNodeFilter = node => node.Waypoint.Ladders == null
},
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref goToObjective));
@@ -10,6 +10,8 @@ namespace Barotrauma
{
public override string DebugTag => "repair item";
public override bool AllowInAnySub => true;
public Item Item { get; private set; }
private AIObjectiveGoTo goToObjective;
@@ -34,6 +36,7 @@ namespace Barotrauma
if (!IsAllowed)
{
Priority = 0;
Abandon = true;
return Priority;
}
// TODO: priority list?
@@ -69,7 +72,7 @@ namespace Barotrauma
IsCompleted = Item.IsFullCondition;
if (IsCompleted && IsRepairing())
{
character?.Speak(TextManager.GetWithVariable("DialogItemRepaired", "[itemname]", Item.Name, true), null, 0.0f, "itemrepaired", 10.0f);
character.Speak(TextManager.GetWithVariable("DialogItemRepaired", "[itemname]", Item.Name, true), null, 0.0f, "itemrepaired", 10.0f);
}
return IsCompleted;
}
@@ -134,7 +137,7 @@ namespace Barotrauma
return;
}
}
if (character.CanInteractWith(Item, out _, checkLinked: false))
if (!character.IsClimbing && character.CanInteractWith(Item, out _, checkLinked: false))
{
HumanAIController.FaceTarget(Item);
if (repairTool != null)
@@ -235,7 +238,11 @@ namespace Barotrauma
private void OperateRepairTool(float deltaTime)
{
character.CursorPosition = Item.Position;
character.CursorPosition = Item.WorldPosition;
if (character.Submarine != null)
{
character.CursorPosition -= character.Submarine.Position;
}
if (repairTool.Item.RequireAimToUse)
{
character.SetInput(InputType.Aim, false, true);
@@ -24,11 +24,11 @@ namespace Barotrauma
private readonly Item prioritizedItem;
public override bool AllowMultipleInstances => true;
public override bool AllowInAnySub => true;
public readonly static float RequiredSuccessFactor = 0.4f;
public override bool IsDuplicate<T>(T otherObjective) =>
(otherObjective as AIObjective) is AIObjectiveRepairItems repairObjective && repairObjective.RequireAdequateSkills == RequireAdequateSkills;
public override bool IsDuplicate<T>(T otherObjective) => otherObjective is AIObjectiveRepairItems repairObjective && repairObjective.RequireAdequateSkills == RequireAdequateSkills;
public AIObjectiveRepairItems(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1, Item prioritizedItem = null)
: base(character, objectiveManager, priorityModifier)
@@ -151,13 +151,9 @@ namespace Barotrauma
if (item.NonInteractable) { return false; }
if (item.IsFullCondition) { return false; }
if (item.CurrentHull == null) { return false; }
if (item.Submarine == null) { return false; }
if (item.Submarine.TeamID != character.TeamID) { return false; }
if (item.Submarine == null || character.Submarine == null) { return false; }
if (!character.Submarine.IsEntityFoundOnThisSub(item, includingConnectedSubs: true)) { return false; }
if (item.Repairables.None()) { return false; }
if (character.Submarine != null)
{
if (!character.Submarine.IsConnectedTo(item.Submarine)) { return false; }
}
return true;
}
}
@@ -14,6 +14,7 @@ namespace Barotrauma
public override bool KeepDivingGearOn => true;
public override bool AllowOutsideSubmarine => true;
public override bool AllowInAnySub => true;
const float TreatmentDelay = 0.5f;
@@ -35,7 +36,7 @@ namespace Barotrauma
{
if (targetCharacter == null)
{
string errorMsg = $"{character.Name}: Attempted to create a Rescue objective with no target!\n" + Environment.StackTrace;
string errorMsg = $"{character.Name}: Attempted to create a Rescue objective with no target!\n" + Environment.StackTrace.CleanupStackTrace();
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("AIObjectiveRescue:ctor:targetnull", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
Abandon = true;
@@ -392,11 +393,13 @@ namespace Barotrauma
if (!IsAllowed)
{
Priority = 0;
Abandon = true;
return Priority;
}
if (character.LockHands || targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead)
{
Priority = 0;
Abandon = true;
}
else
{
@@ -11,6 +11,7 @@ namespace Barotrauma
public override bool ForceRun => true;
public override bool InverseTargetEvaluation => true;
public override bool AllowOutsideSubmarine => true;
public override bool AllowInAnySub => true;
private const float vitalityThreshold = 75;
private const float vitalityThresholdForOrders = 85;
@@ -38,7 +38,8 @@ namespace Barotrauma
}
public bool MatchesOrder(Order order, string option) =>
order.Identifier == Order.Identifier && option == OrderOption && order.TargetEntity == Order.TargetEntity;
order.Identifier == Order.Identifier &&
option == OrderOption;
}
class Order
@@ -54,7 +55,7 @@ namespace Barotrauma
}
return order;
}
public Order Prefab { get; private set; }
public readonly string Name;
@@ -62,7 +63,8 @@ namespace Barotrauma
public readonly Sprite SymbolSprite;
public readonly Type ItemComponentType;
public readonly string[] ItemIdentifiers;
public readonly bool CanTypeBeSubclass;
public readonly string[] TargetItems;
public readonly string Identifier;
@@ -89,14 +91,14 @@ namespace Barotrauma
color = value;
}
}
//if true, the order is issued to all available characters
public bool TargetAllCharacters;
public readonly float FadeOutTime;
public Entity TargetEntity;
public Entity TargetEntity;
public ItemComponent TargetItemComponent;
public readonly bool UseController;
public Controller ConnectedController;
@@ -123,6 +125,18 @@ namespace Barotrauma
public bool IsPrefab { get; private set; }
public readonly bool MustManuallyAssign;
public readonly OrderTarget TargetPosition;
private ISpatialEntity targetSpatialEntity;
public ISpatialEntity TargetSpatialEntity
{
get
{
if (targetSpatialEntity == null) { targetSpatialEntity = TargetEntity ?? TargetPosition as ISpatialEntity; }
return targetSpatialEntity;
}
}
public static void Init()
{
Prefabs = new Dictionary<string, Order>();
@@ -218,8 +232,9 @@ namespace Barotrauma
DebugConsole.ThrowError("Error in the order definitions: item component type " + targetItemType + " not found", e);
}
}
CanTypeBeSubclass = orderElement.GetAttributeBool("cantypebesubclass", false);
ItemIdentifiers = orderElement.GetAttributeStringArray("targetitemidentifiers", new string[0], trim: true, convertToLowerInvariant: true);
TargetItems = orderElement.GetAttributeStringArray("targetitems", new string[0], trim: true, convertToLowerInvariant: true);
color = orderElement.GetAttributeColor("color");
FadeOutTime = orderElement.GetAttributeFloat("fadeouttime", 0.0f);
UseController = orderElement.GetAttributeBool("usecontroller", false);
@@ -228,7 +243,6 @@ namespace Barotrauma
Options = orderElement.GetAttributeStringArray("options", new string[0]);
var category = orderElement.GetAttributeString("category", null);
if (!string.IsNullOrWhiteSpace(category)) { this.Category = (OrderCategory)Enum.Parse(typeof(OrderCategory), category, true); }
Weight = orderElement.GetAttributeFloat(0.0f, "weight");
MustSetTarget = orderElement.GetAttributeBool("mustsettarget", false);
AppropriateSkill = orderElement.GetAttributeString("appropriateskill", null);
@@ -279,7 +293,7 @@ namespace Barotrauma
IsPrefab = true;
MustManuallyAssign = orderElement.GetAttributeBool("mustmanuallyassign", false);
}
/// <summary>
/// Constructor for order instances
/// </summary>
@@ -290,7 +304,8 @@ namespace Barotrauma
Name = prefab.Name;
Identifier = prefab.Identifier;
ItemComponentType = prefab.ItemComponentType;
ItemIdentifiers = prefab.ItemIdentifiers;
CanTypeBeSubclass = prefab.CanTypeBeSubclass;
TargetItems = prefab.TargetItems;
Options = prefab.Options;
SymbolSprite = prefab.SymbolSprite;
Color = prefab.Color;
@@ -298,7 +313,6 @@ namespace Barotrauma
TargetAllCharacters = prefab.TargetAllCharacters;
AppropriateJobs = prefab.AppropriateJobs;
FadeOutTime = prefab.FadeOutTime;
Weight = prefab.Weight;
MustSetTarget = prefab.MustSetTarget;
AppropriateSkill = prefab.AppropriateSkill;
Category = prefab.Category;
@@ -325,6 +339,11 @@ namespace Barotrauma
IsPrefab = false;
}
public Order(Order prefab, OrderTarget target, Character orderGiver = null) : this(prefab, targetEntity: null, targetItem: null, orderGiver)
{
TargetPosition = target;
}
public bool HasAppropriateJob(Character character)
{
@@ -358,15 +377,38 @@ namespace Barotrauma
return msg;
}
/// <summary>
/// Get the target item component based on the target item type
/// </summary>
public ItemComponent GetTargetItemComponent(Item item)
{
if (item?.Components == null || ItemComponentType == null) { return null; }
foreach (ItemComponent component in item.Components)
{
if (component?.GetType() is Type componentType)
{
if (componentType == ItemComponentType) { return component; }
if (CanTypeBeSubclass && componentType.IsSubclassOf(ItemComponentType)) { return component; }
}
}
return null;
}
public bool TryGetTargetItemComponent(Item item, out ItemComponent firstMatchingComponent)
{
firstMatchingComponent = GetTargetItemComponent(item);
return firstMatchingComponent != null;
}
public List<Item> GetMatchingItems(Submarine submarine, bool mustBelongToPlayerSub, Character.TeamType? requiredTeam = null)
{
List<Item> matchingItems = new List<Item>();
if (submarine == null) { return matchingItems; }
if (ItemComponentType != null || ItemIdentifiers.Length > 0)
if (ItemComponentType != null || TargetItems.Length > 0)
{
matchingItems = ItemIdentifiers.Length > 0 ?
Item.ItemList.FindAll(it => ItemIdentifiers.Contains(it.Prefab.Identifier) || it.HasTag(ItemIdentifiers)) :
Item.ItemList.FindAll(it => it.Components.Any(ic => ic.GetType() == ItemComponentType));
matchingItems = TargetItems.Length > 0 ?
Item.ItemList.FindAll(it => TargetItems.Contains(it.Prefab.Identifier) || it.HasTag(TargetItems)) :
Item.ItemList.FindAll(it => TryGetTargetItemComponent(it, out _));
if (mustBelongToPlayerSub)
{
matchingItems.RemoveAll(it => it.Submarine?.Info != null && it.Submarine.Info.Type != SubmarineType.Player);