Unstable 1.8.4.0
This commit is contained in:
@@ -274,7 +274,7 @@ namespace Barotrauma
|
||||
public bool IsIgnoredAtOutpost()
|
||||
{
|
||||
if (!IgnoreAtOutpost) { return false; }
|
||||
if (!Level.IsLoadedFriendlyOutpost) { return false; }
|
||||
if (!Level.IsLoadedFriendlyOutpost && GameMain.GameSession.GameMode is not TestGameMode) { return false; }
|
||||
if (!character.IsOnPlayerTeam || character.IsFriendlyNPCTurnedHostile) { return false; }
|
||||
if (character.Submarine?.Info == null) { return false; }
|
||||
return character.Submarine.Info.IsOutpost && character.Submarine.TeamID == CharacterTeamType.FriendlyNPC;
|
||||
@@ -513,18 +513,19 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
private bool Check()
|
||||
{
|
||||
if (isCompleted) { return true; }
|
||||
if (AbortCondition != null && AbortCondition(this))
|
||||
{
|
||||
Abandon = true;
|
||||
return false;
|
||||
}
|
||||
return CheckObjectiveSpecific();
|
||||
return CheckObjectiveState();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Should return whether the objective is completed or not.
|
||||
/// </summary>
|
||||
protected abstract bool CheckObjectiveSpecific();
|
||||
protected abstract bool CheckObjectiveState();
|
||||
|
||||
private bool CheckState()
|
||||
{
|
||||
@@ -574,8 +575,6 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void SpeakAfterOrderReceived() { }
|
||||
|
||||
protected static bool CanPutInInventory(Character character, Item item, bool allowWearing)
|
||||
{
|
||||
if (item == null) { return false; }
|
||||
|
||||
+38
-18
@@ -45,22 +45,25 @@ namespace Barotrauma
|
||||
InitTimers();
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific() => false;
|
||||
protected override bool CheckObjectiveState() => false;
|
||||
|
||||
protected override float GetPriority()
|
||||
{
|
||||
if (character.IsClimbing)
|
||||
{
|
||||
// Target is climbing -> stop following the objective (soft abandon, without ignoring the target).
|
||||
Priority = 0;
|
||||
}
|
||||
else if (!Abandon && !IsCompleted && objectiveManager.IsOrder(this))
|
||||
{
|
||||
if (!Abandon && !IsCompleted && objectiveManager.IsOrder(this))
|
||||
{
|
||||
Priority = objectiveManager.GetOrderPriority(this);
|
||||
}
|
||||
else
|
||||
{
|
||||
Priority = AIObjectiveManager.LowestOrderPriority - 1;
|
||||
if (HumanAIController.CurrentHullSafety < HumanAIController.HULL_SAFETY_THRESHOLD || HumanAIController.CalculateObjectiveHullSafety(Target) < HumanAIController.HULL_SAFETY_THRESHOLD)
|
||||
{
|
||||
// Don't do inspections in unsafe hulls, because under a threat, bots are allowed to wear diving gear or hold fire extinguishers etc. Even if they are "stolen".
|
||||
Priority = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
Priority = AIObjectiveManager.LowestOrderPriority - 1;
|
||||
}
|
||||
}
|
||||
return Priority;
|
||||
}
|
||||
@@ -86,18 +89,18 @@ namespace Barotrauma
|
||||
onCompleted: () =>
|
||||
{
|
||||
RemoveSubObjective(ref goToObjective);
|
||||
if (character.IsClimbing)
|
||||
if (character.IsClimbing || HumanAIController.CurrentHullSafety < HumanAIController.HULL_SAFETY_THRESHOLD || HumanAIController.CalculateObjectiveHullSafety(Target) < HumanAIController.HULL_SAFETY_THRESHOLD)
|
||||
{
|
||||
// Shouldn't start inspecting characters when they climb, nor get here, because the priority should be 0,
|
||||
// but if this still happens, we'll have to abandon the objective
|
||||
// because it's not currently possible to hold to characters and ladders at the same time.
|
||||
// Don't do inspections in unsafe hulls, because under a threat, bots are allowed to wear diving gear or hold fire extinguishers etc. Even if they are "stolen".
|
||||
// Shouldn't start inspecting characters when they climb, but we can still get here, if they start climbing while we are moving at them.
|
||||
// If that happens, let's abandon the objective, because it's not currently possible to hold to characters and ladders at the same time.
|
||||
Abandon = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
currentState = State.Inspect;
|
||||
stolenItems.Clear();
|
||||
Target.Inventory.FindAllItems(it => it.Illegitimate, recursive: true, stolenItems);
|
||||
Target.Inventory.FindAllItems(it => IsItemIllegitimate(Target, it), recursive: true, stolenItems);
|
||||
character.Speak(TextManager.Get(Target.IsCriminal ? "dialogcheckstolenitems.criminal" : "dialogcheckstolenitems").Value);
|
||||
}
|
||||
},
|
||||
@@ -190,11 +193,23 @@ namespace Barotrauma
|
||||
var stolenItemsOnCharacter = stolenItems.Where(it => it.GetRootInventoryOwner() == Target);
|
||||
if (stolenItemsOnCharacter.Any())
|
||||
{
|
||||
character.Speak(TextManager.Get(character.IsCriminal ? "dialogcheckstolenitems.arrest.criminal" : "dialogcheckstolenitems.arrest").Value);
|
||||
Arrest(abortWhenItemsDropped: true, allowHoldFire: true);
|
||||
foreach (var stolenItem in stolenItemsOnCharacter)
|
||||
if (Target.IsBot)
|
||||
{
|
||||
HumanAIController.ApplyStealingReputationLoss(stolenItem);
|
||||
// Bots automatically comply and drop stolen items when being inspected.
|
||||
foreach (Item item in stolenItemsOnCharacter)
|
||||
{
|
||||
item.Drop(Target);
|
||||
}
|
||||
character.Speak(TextManager.Get("dialogcheckstolenitems.comply").Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
character.Speak(TextManager.Get(character.IsCriminal ? "dialogcheckstolenitems.arrest.criminal" : "dialogcheckstolenitems.arrest").Value);
|
||||
Arrest(abortWhenItemsDropped: true, allowHoldFire: true);
|
||||
foreach (var stolenItem in stolenItemsOnCharacter)
|
||||
{
|
||||
HumanAIController.ApplyStealingReputationLoss(stolenItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -242,5 +257,10 @@ namespace Barotrauma
|
||||
currentWarnDelay = Target.IsCriminal ? CriminalWarnDelay : NormalWarnDelay;
|
||||
warnTimer = currentWarnDelay;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks for illegitimate item, ignoring handcuffs equipped on the owner.
|
||||
/// </summary>
|
||||
public static bool IsItemIllegitimate(Character owner, Item item) => item.Illegitimate && (!item.HasTag(Tags.HandLockerItem) || !owner.HasEquippedItem(item));
|
||||
}
|
||||
}
|
||||
|
||||
+8
-8
@@ -18,11 +18,11 @@ namespace Barotrauma
|
||||
public bool IsPriority { get; set; }
|
||||
|
||||
private readonly List<Item> ignoredContainers = new List<Item>();
|
||||
private AIObjectiveDecontainItem decontainObjective;
|
||||
private AIObjectiveMoveItem moveItemObjective;
|
||||
private int itemIndex = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Allows decontainObjective to be interrupted if this objective gets abandoned (e.g. due to the item no longer being eligible for cleanup)
|
||||
/// Allows <see cref="moveItemObjective"/> to be interrupted if this objective gets abandoned (e.g. due to the item no longer being eligible for cleanup)
|
||||
/// </summary>
|
||||
protected override bool ConcurrentObjectives => true;
|
||||
|
||||
@@ -53,9 +53,9 @@ namespace Barotrauma
|
||||
float reduction = IsPriority ? 1 : isSelected ? 2 : 3;
|
||||
float max = AIObjectiveManager.LowestOrderPriority - reduction;
|
||||
Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (distanceFactor * PriorityModifier), 0, 1));
|
||||
if (decontainObjective == null)
|
||||
if (moveItemObjective == null)
|
||||
{
|
||||
// Halve the priority until there's a decontain objective (a valid container was found).
|
||||
// Halve the priority until there's a moveItemObjective (a valid container was found).
|
||||
Priority /= 2;
|
||||
}
|
||||
}
|
||||
@@ -79,7 +79,7 @@ namespace Barotrauma
|
||||
s == InvSlotType.OuterClothes ||
|
||||
s == InvSlotType.HealthInterface);
|
||||
|
||||
TryAddSubObjective(ref decontainObjective, () => new AIObjectiveDecontainItem(character, item, objectiveManager, targetContainer: suitableContainer.GetComponent<ItemContainer>())
|
||||
TryAddSubObjective(ref moveItemObjective, () => new AIObjectiveMoveItem(character, item, objectiveManager, targetContainer: suitableContainer.GetComponent<ItemContainer>())
|
||||
{
|
||||
Equip = equip,
|
||||
TakeWholeStack = true,
|
||||
@@ -99,7 +99,7 @@ namespace Barotrauma
|
||||
{
|
||||
HumanAIController.ReequipUnequipped();
|
||||
}
|
||||
if (decontainObjective != null && decontainObjective.ContainObjective != null && decontainObjective.ContainObjective.CanBeCompleted)
|
||||
if (moveItemObjective is { ContainObjective.CanBeCompleted: true })
|
||||
{
|
||||
ignoredContainers.Add(suitableContainer);
|
||||
}
|
||||
@@ -117,7 +117,7 @@ namespace Barotrauma
|
||||
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific()
|
||||
protected override bool CheckObjectiveState()
|
||||
{
|
||||
if (item.IgnoreByAI(character) || Item.DeconstructItems.Contains(item))
|
||||
{
|
||||
@@ -144,7 +144,7 @@ namespace Barotrauma
|
||||
base.Reset();
|
||||
ignoredContainers.Clear();
|
||||
itemIndex = 0;
|
||||
decontainObjective = null;
|
||||
moveItemObjective = null;
|
||||
}
|
||||
|
||||
public void DropTarget()
|
||||
|
||||
+2
@@ -81,6 +81,8 @@ namespace Barotrauma
|
||||
|
||||
public static bool IsItemInsideValidSubmarine(Item item, Character character)
|
||||
{
|
||||
if (item == null || item.Removed) { return false; }
|
||||
if (character == null || character.Removed) { return false; }
|
||||
if (item.CurrentHull == null) { return false; }
|
||||
if (item.Submarine == null) { return false; }
|
||||
if (item.Submarine.TeamID != character.TeamID) { return false; }
|
||||
|
||||
+266
-116
@@ -4,15 +4,18 @@ using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FarseerPhysics.Dynamics;
|
||||
using static Barotrauma.AIObjectiveFindSafety;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using FarseerPhysics;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveCombat : AIObjective
|
||||
{
|
||||
public override Identifier Identifier { get; set; } = "combat".ToIdentifier();
|
||||
|
||||
public override string DebugTag => $"{Identifier} ({Mode})";
|
||||
|
||||
public override bool KeepDivingGearOn => true;
|
||||
public override bool IgnoreUnsafeHulls => true;
|
||||
@@ -35,10 +38,9 @@ namespace Barotrauma
|
||||
private bool allowCooldown;
|
||||
|
||||
public Character Enemy { get; private set; }
|
||||
public bool HoldPosition { get; set; }
|
||||
|
||||
|
||||
private Item _weapon;
|
||||
private Item Weapon
|
||||
public Item Weapon
|
||||
{
|
||||
get { return _weapon; }
|
||||
set
|
||||
@@ -48,6 +50,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
private ItemComponent _weaponComponent;
|
||||
private bool hasValidRangedWeapon;
|
||||
private ItemComponent WeaponComponent
|
||||
{
|
||||
get
|
||||
@@ -74,7 +77,6 @@ namespace Barotrauma
|
||||
private float pathBackTimer;
|
||||
private const float DefaultCoolDown = 10.0f;
|
||||
private const float PathBackCheckTime = 1.0f;
|
||||
private IEnumerable<Body> myBodies;
|
||||
private float aimTimer;
|
||||
private float reloadTimer;
|
||||
private float spreadTimer;
|
||||
@@ -88,7 +90,9 @@ namespace Barotrauma
|
||||
private const float DistanceCheckInterval = 0.2f;
|
||||
private float distanceTimer;
|
||||
|
||||
private const float CloseDistanceThreshold = 300;
|
||||
private const float CloseDistance = 300;
|
||||
private const float MeleeDistance = 125;
|
||||
private const float TooCloseToShoot = 100;
|
||||
private const float FloorHeightApproximate = 100;
|
||||
|
||||
public bool AllowHoldFire;
|
||||
@@ -169,13 +173,7 @@ namespace Barotrauma
|
||||
public AIObjectiveCombat(Character character, Character enemy, CombatMode mode, AIObjectiveManager objectiveManager, float priorityModifier = 1, float coolDown = DefaultCoolDown)
|
||||
: base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
if (mode == CombatMode.None)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.ThrowError("Combat mode == None");
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
Debug.Assert(mode != CombatMode.None);
|
||||
Enemy = enemy;
|
||||
coolDownTimer = coolDown;
|
||||
findSafety = objectiveManager.GetObjective<AIObjectiveFindSafety>();
|
||||
@@ -230,6 +228,7 @@ namespace Barotrauma
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
base.Update(deltaTime);
|
||||
isAimBlocked = false;
|
||||
ignoreWeaponTimer -= deltaTime;
|
||||
checkWeaponsTimer -= deltaTime;
|
||||
if (reloadTimer > 0)
|
||||
@@ -257,7 +256,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific()
|
||||
protected override bool CheckObjectiveState()
|
||||
{
|
||||
if (character.Submarine is { TeamID: CharacterTeamType.FriendlyNPC } && character.Submarine == Enemy.Submarine)
|
||||
{
|
||||
@@ -332,68 +331,155 @@ namespace Barotrauma
|
||||
pathBackTimer -= deltaTime;
|
||||
}
|
||||
}
|
||||
if (standUpTimer > 0)
|
||||
{
|
||||
standUpTimer -= deltaTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Crouch by default so that others can shoot from behind. Disabled when the line of sight is blocked and while moving.
|
||||
allowCrouching = true;
|
||||
}
|
||||
if (HumanAIController.DebugAI)
|
||||
{
|
||||
BlockedPositions ??= new List<Vector2>();
|
||||
BlockedPositions.Clear();
|
||||
}
|
||||
if (seekAmmunitionObjective == null && seekWeaponObjective == null)
|
||||
{
|
||||
if (Mode != CombatMode.Retreat && TryArm())
|
||||
{
|
||||
OperateWeapon(deltaTime);
|
||||
}
|
||||
if (HoldPosition)
|
||||
{
|
||||
SteeringManager.Reset();
|
||||
}
|
||||
else if (seekAmmunitionObjective == null && seekWeaponObjective == null)
|
||||
isMoving = false;
|
||||
if (seekAmmunitionObjective == null && seekWeaponObjective == null)
|
||||
{
|
||||
Move(deltaTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private bool isMoving;
|
||||
private void Move(float deltaTime)
|
||||
{
|
||||
switch (Mode)
|
||||
if (Mode == CombatMode.Retreat)
|
||||
{
|
||||
case CombatMode.Offensive:
|
||||
case CombatMode.Arrest:
|
||||
Retreat(deltaTime);
|
||||
}
|
||||
else if (character.IsOnPlayerTeam && !Enemy.IsPlayer && objectiveManager.CurrentOrder is AIObjectiveGoTo gotoObjective)
|
||||
{
|
||||
if (gotoObjective.IsWaitOrder && WeaponComponent is MeleeWeapon && IsEnemyClose(CloseDistance))
|
||||
{
|
||||
// Ordered to wait near the enemy with a melee weapon -> engage.
|
||||
Engage(deltaTime);
|
||||
break;
|
||||
case CombatMode.Defensive:
|
||||
if (character.IsOnPlayerTeam && !Enemy.IsPlayer && objectiveManager.IsCurrentOrder<AIObjectiveGoTo>())
|
||||
}
|
||||
else
|
||||
{
|
||||
// Ordered to follow -> keep following.
|
||||
if (!gotoObjective.IsCloseEnough)
|
||||
{
|
||||
if ((character.CurrentHull == null || character.CurrentHull == Enemy.CurrentHull) && sqrDistance < 200 * 200)
|
||||
isMoving = true;
|
||||
}
|
||||
gotoObjective.FaceTargetOnCompleted = false;
|
||||
gotoObjective.ForceAct(deltaTime);
|
||||
if (!character.AnimController.InWater && IsEnemyClose(CloseDistance))
|
||||
{
|
||||
// If close to the enemy, face it, so that we can attack it.
|
||||
HumanAIController.FaceTarget(Enemy);
|
||||
HumanAIController.AutoFaceMovement = false;
|
||||
if (!gotoObjective.ShouldRun(true))
|
||||
{
|
||||
Engage(deltaTime);
|
||||
ForceWalk = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (Mode)
|
||||
{
|
||||
case CombatMode.Defensive:
|
||||
Retreat(deltaTime);
|
||||
break;
|
||||
case CombatMode.Offensive when hasValidRangedWeapon && IsEnemyClose(CloseDistance):
|
||||
// Too close to the enemy -> try to back off.
|
||||
Hull currentHull = character.CurrentHull;
|
||||
bool backOff = currentHull != null;
|
||||
Vector2 escapeVel = Vector2.Zero;
|
||||
if (backOff)
|
||||
{
|
||||
int previousEnemyDir = 0;
|
||||
foreach (Character enemy in Character.CharacterList)
|
||||
{
|
||||
if (!HumanAIController.IsActive(enemy) || HumanAIController.IsFriendly(enemy) || enemy.IsHandcuffed) { continue; }
|
||||
if (enemy.CurrentHull == null) { continue; }
|
||||
if (currentHull != enemy.CurrentHull && !currentHull.linkedTo.Contains(enemy.CurrentHull)) { continue; }
|
||||
Vector2 dir = character.Position - enemy.Position;
|
||||
int enemyDir = Math.Sign(dir.X);
|
||||
if (enemyDir == 0)
|
||||
{
|
||||
// Exactly at the same pos.
|
||||
if (previousEnemyDir != 0)
|
||||
{
|
||||
// Another enemy at either side -> Ignore this enemy.
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Just choose either direction.
|
||||
enemyDir = Rand.Value() > 0.5f ? 1 : -1;
|
||||
}
|
||||
}
|
||||
if (previousEnemyDir != 0 && enemyDir != previousEnemyDir)
|
||||
{
|
||||
// Don't back off when there are enemies in different directions, because that's doomed.
|
||||
backOff = false;
|
||||
break;
|
||||
}
|
||||
previousEnemyDir = enemyDir;
|
||||
// This formula is slightly modified from AIObjectiveFindSafety.UpdateSimpleEscape().
|
||||
float distMultiplier = MathHelper.Clamp(MeleeDistance / Vector2.Distance(enemy.Position, character.Position), 0.1f, 10.0f);
|
||||
escapeVel += new Vector2(enemyDir * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier);
|
||||
}
|
||||
if (escapeVel == Vector2.Zero)
|
||||
{
|
||||
backOff = false;
|
||||
}
|
||||
if (backOff)
|
||||
{
|
||||
// Only move if we haven't reached the edge of the room.
|
||||
float left = currentHull.Rect.X + 50;
|
||||
float right = currentHull.Rect.Right - 50;
|
||||
backOff = escapeVel.X < 0 && character.Position.X > left || escapeVel.X > 0 && character.Position.X < right;
|
||||
}
|
||||
}
|
||||
if (backOff)
|
||||
{
|
||||
BackOff();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Keep following the goto target
|
||||
var gotoObjective = objectiveManager.GetOrder<AIObjectiveGoTo>();
|
||||
if (gotoObjective != null)
|
||||
{
|
||||
gotoObjective.ForceAct(deltaTime);
|
||||
if (!character.AnimController.InWater)
|
||||
{
|
||||
HumanAIController.FaceTarget(Enemy);
|
||||
ForceWalk = true;
|
||||
HumanAIController.AutoFaceMovement = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
SteeringManager.Reset();
|
||||
}
|
||||
Engage(deltaTime);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Retreat(deltaTime);
|
||||
}
|
||||
break;
|
||||
case CombatMode.Retreat:
|
||||
Retreat(deltaTime);
|
||||
break;
|
||||
default:
|
||||
throw new NotImplementedException();
|
||||
|
||||
void BackOff()
|
||||
{
|
||||
RemoveFollowTarget();
|
||||
isMoving = true;
|
||||
if (!IsEnemyClose(MeleeDistance))
|
||||
{
|
||||
ForceWalk = true;
|
||||
}
|
||||
HumanAIController.FaceTarget(Enemy);
|
||||
HumanAIController.AutoFaceMovement = false;
|
||||
character.ReleaseSecondaryItem();
|
||||
character.AIController.SteeringManager.SteeringManual(deltaTime, escapeVel);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
Engage(deltaTime);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -408,7 +494,8 @@ namespace Barotrauma
|
||||
bool isAllowedToSeekWeapons = character.IsHostileEscortee || character.IsPrisoner || // Prisoners and terrorists etc are always allowed to seek new weapons.
|
||||
(character.IsInFriendlySub // Other characters need to be on a friendly sub in order to "know" where the weapons are. This also prevents NPCs "stealing" player items.
|
||||
&& IsOffensiveOrArrest // = Defensive or retreating AI shouldn't seek new weapons.
|
||||
&& !character.IsInstigator); // Instigators (= aggressive NPCs spawned with events) shouldn't seek new weapons, because we don't want them to grab e.g. an smg, if they spawn with a wrench or something.
|
||||
&& !character.IsInstigator // Instigators (= aggressive NPCs spawned with events) shouldn't seek new weapons, because we don't want them to grab e.g. an smg, if they spawn with a wrench or something.
|
||||
&& objectiveManager.CurrentOrder is not AIObjectiveGoTo); // if ordered to wait/follow, shouldn't go seeking new weapons.
|
||||
if (checkWeaponsTimer < 0)
|
||||
{
|
||||
checkWeaponsTimer = CheckWeaponsInterval;
|
||||
@@ -434,7 +521,12 @@ namespace Barotrauma
|
||||
// All good, the weapon is loaded
|
||||
break;
|
||||
}
|
||||
bool seekAmmo = isAllowedToSeekWeapons && seekAmmunitionObjective == null && !IsEnemyClose(CloseDistanceThreshold);
|
||||
bool seekAmmo = isAllowedToSeekWeapons && seekAmmunitionObjective == null;
|
||||
if (seekAmmo)
|
||||
{
|
||||
// Bots set to arrest the target are always allowed to seek (or spawn) more ammo, because otherwise they might not be able to stun the target and need to use lethal weapons.
|
||||
seekAmmo = Mode == CombatMode.Arrest || !IsEnemyClose(CloseDistance);
|
||||
}
|
||||
if (Reload(seekAmmo: seekAmmo))
|
||||
{
|
||||
// All good, we can use the weapon.
|
||||
@@ -480,7 +572,7 @@ namespace Barotrauma
|
||||
Mode = CombatMode.Retreat;
|
||||
}
|
||||
}
|
||||
else if (seekAmmunitionObjective == null && (WeaponComponent == null || (WeaponComponent.CombatPriority < GoodWeaponPriority && !IsEnemyClose(CloseDistanceThreshold))))
|
||||
else if (seekAmmunitionObjective == null && (WeaponComponent == null || (WeaponComponent.CombatPriority < GoodWeaponPriority && !IsEnemyClose(CloseDistance))))
|
||||
{
|
||||
// No weapon or only a poor weapon equipped -> try to find better.
|
||||
RemoveSubObjective(ref retreatObjective);
|
||||
@@ -489,7 +581,7 @@ namespace Barotrauma
|
||||
constructor: () => new AIObjectiveGetItem(character, "weapon".ToIdentifier(), objectiveManager, equip: true, checkInventory: false)
|
||||
{
|
||||
AllowStealing = HumanAIController.IsMentallyUnstable,
|
||||
AbortCondition = obj => IsEnemyClose(200),
|
||||
AbortCondition = _ => IsEnemyClose(CloseDistance / 2),
|
||||
EvaluateCombatPriority = false, // Use a custom formula instead
|
||||
GetItemPriority = i =>
|
||||
{
|
||||
@@ -582,6 +674,12 @@ namespace Barotrauma
|
||||
|
||||
private void OperateWeapon(float deltaTime)
|
||||
{
|
||||
if (isMoving && character.IsClimbing)
|
||||
{
|
||||
// Don't climb and shoot at the same time, because it messes up the aiming.
|
||||
ClearInputs();
|
||||
return;
|
||||
}
|
||||
switch (Mode)
|
||||
{
|
||||
case CombatMode.Offensive:
|
||||
@@ -790,17 +888,22 @@ namespace Barotrauma
|
||||
return containers.None() || containers.Any(container =>
|
||||
(container as ItemContainer)?.Inventory.AllItems.Any(i => i != null && i.HasTag(mobileBatteryTag) && i.Condition > 0.0f) ?? false);
|
||||
}
|
||||
|
||||
|
||||
private Item GetWeapon(IEnumerable<ItemComponent> weaponList, out ItemComponent weaponComponent)
|
||||
{
|
||||
hasValidRangedWeapon = false;
|
||||
weaponComponent = null;
|
||||
float bestPriority = 0;
|
||||
float lethalDmg = -1;
|
||||
bool prioritizeMelee = IsEnemyClose(50) || EnemyAIController.IsLatchedTo(Enemy, character);
|
||||
bool isCloseToEnemy = prioritizeMelee || IsEnemyClose(CloseDistanceThreshold);
|
||||
foreach (var weapon in weaponList)
|
||||
bool prioritizeMelee = IsEnemyClose(TooCloseToShoot) || EnemyAIController.IsLatchedTo(Enemy, character);
|
||||
bool isCloseToEnemy = prioritizeMelee || IsEnemyClose(CloseDistance);
|
||||
foreach (ItemComponent weapon in weaponList)
|
||||
{
|
||||
float priority = GetWeaponPriority(weapon, prioritizeMelee, canSeekAmmo: !isCloseToEnemy, out lethalDmg);
|
||||
if (priority >= GoodWeaponPriority && weapon is RangedWeapon or RepairTool)
|
||||
{
|
||||
hasValidRangedWeapon = true;
|
||||
}
|
||||
if (priority > bestPriority)
|
||||
{
|
||||
weaponComponent = weapon;
|
||||
@@ -898,23 +1001,13 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Unequip()
|
||||
|
||||
private void UnequipWeapon()
|
||||
{
|
||||
if (!character.LockHands && character.HeldItems.Contains(Weapon))
|
||||
{
|
||||
if (!Weapon.AllowedSlots.Contains(InvSlotType.Any) || !character.Inventory.TryPutItem(Weapon, character, new List<InvSlotType>() { InvSlotType.Any }))
|
||||
{
|
||||
if (Weapon.AllowedSlots.Contains(InvSlotType.Bag))
|
||||
{
|
||||
if (character.Inventory.TryPutItem(Weapon, character, new List<InvSlotType>() { InvSlotType.Bag }))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
Weapon.Drop(character);
|
||||
}
|
||||
}
|
||||
if (Weapon == null) { return; }
|
||||
if (character.LockHands) { return; }
|
||||
if (character.HeldItems.Contains(Weapon)) { return; }
|
||||
character.Unequip(Weapon);
|
||||
}
|
||||
|
||||
private bool Equip()
|
||||
@@ -929,7 +1022,15 @@ namespace Barotrauma
|
||||
ClearInputs();
|
||||
Weapon.TryInteract(character, forceSelectKey: true);
|
||||
var slots = Weapon.AllowedSlots.Where(CharacterInventory.IsHandSlotType);
|
||||
if (character.Inventory.TryPutItem(Weapon, character, slots))
|
||||
bool successfullyEquipped = character.TryPutItem(Weapon, slots);
|
||||
if (!successfullyEquipped && character.HasHandsFull(out (Item leftHandItem, Item rightHandItem) items))
|
||||
{
|
||||
// Unequip and try again.
|
||||
character.Unequip(items.leftHandItem);
|
||||
character.Unequip(items.rightHandItem);
|
||||
successfullyEquipped = character.TryPutItem(Weapon, slots);
|
||||
}
|
||||
if (successfullyEquipped)
|
||||
{
|
||||
SetAimTimer(Rand.Range(0.2f, 0.4f) / AimSpeed);
|
||||
SetReloadTime(WeaponComponent);
|
||||
@@ -950,6 +1051,7 @@ namespace Barotrauma
|
||||
|
||||
private void Retreat(float deltaTime)
|
||||
{
|
||||
isMoving = true;
|
||||
if (!Enemy.IsHuman && !character.IsInFriendlySub)
|
||||
{
|
||||
// Only relevant when we are retreating from monsters and are not inside a friendly sub.
|
||||
@@ -1047,6 +1149,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (sqrDistance > MathUtils.Pow2(meleeWeapon.Range))
|
||||
{
|
||||
isMoving = true;
|
||||
character.ReleaseSecondaryItem();
|
||||
// Swim towards the target
|
||||
SteeringManager.Reset();
|
||||
@@ -1097,7 +1200,7 @@ namespace Barotrauma
|
||||
}
|
||||
});
|
||||
if (followTargetObjective == null) { return; }
|
||||
if (Mode == CombatMode.Arrest && Enemy.IsKnockedDown && !arrestingRegistered)
|
||||
if (Mode == CombatMode.Arrest && Enemy.IsKnockedDownOrRagdolled && !arrestingRegistered)
|
||||
{
|
||||
bool hasHandCuffs = HumanAIController.HasItem(character, Tags.HandLockerItem, out _);
|
||||
if (!hasHandCuffs && character.TeamID == CharacterTeamType.FriendlyNPC)
|
||||
@@ -1122,12 +1225,20 @@ namespace Barotrauma
|
||||
followTargetObjective.CloseEnough =
|
||||
WeaponComponent switch
|
||||
{
|
||||
RangedWeapon => 1000,
|
||||
RangedWeapon => isAimBlocked ? BlockedDistance : 1000,
|
||||
MeleeWeapon mw => mw.Range,
|
||||
RepairTool rt => rt.Range,
|
||||
_ => 50
|
||||
};
|
||||
}
|
||||
if (isAimBlocked)
|
||||
{
|
||||
ForceWalk = true;
|
||||
}
|
||||
if (!followTargetObjective.IsCloseEnough)
|
||||
{
|
||||
isMoving = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveFollowTarget()
|
||||
@@ -1145,19 +1256,23 @@ namespace Barotrauma
|
||||
|
||||
private void OnArrestTargetReached()
|
||||
{
|
||||
if (!Enemy.IsKnockedDown)
|
||||
if (!Enemy.IsKnockedDownOrRagdolled)
|
||||
{
|
||||
RemoveFollowTarget();
|
||||
return;
|
||||
}
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
// Confiscate stolen goods and all weapons
|
||||
foreach (var item in Enemy.Inventory.AllItemsMod)
|
||||
{
|
||||
// Ignore handcuffs already on the target.
|
||||
if (item.HasTag(Tags.HandLockerItem) && Enemy.HasEquippedItem(item)) { continue; }
|
||||
if (item.Illegitimate || item.HasTag(Tags.Weapon) || item.HasTag(Tags.Poison) || GetWeaponComponent(item) is { CombatPriority: > 0 })
|
||||
AIObjectiveFindThieves.MarkTargetAsInspected(character);
|
||||
bool confiscateItem = AIObjectiveCheckStolenItems.IsItemIllegitimate(Enemy, item);
|
||||
if (!confiscateItem && Enemy.IsActingOffensively)
|
||||
{
|
||||
// Confiscate any weapons or items that can be used offensively.
|
||||
confiscateItem = item.HasTag(Tags.Weapon) || item.HasTag(Tags.Poison) || GetWeaponComponent(item) is { CombatPriority: > 0 };
|
||||
}
|
||||
if (confiscateItem)
|
||||
{
|
||||
item.Drop(character);
|
||||
character.Inventory.TryPutItem(item, character, CharacterInventory.AnySlot);
|
||||
@@ -1172,7 +1287,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
if (matchingItems.Any() &&
|
||||
!Enemy.IsUnconscious && Enemy.IsKnockedDown && character.CanInteractWith(Enemy) && !Enemy.LockHands)
|
||||
!Enemy.IsUnconscious && Enemy.IsKnockedDownOrRagdolled && character.CanInteractWith(Enemy) && !Enemy.LockHands)
|
||||
{
|
||||
var handCuffs = matchingItems.First();
|
||||
if (!HumanAIController.TakeItem(handCuffs, Enemy.Inventory, equip: true, wear: true))
|
||||
@@ -1205,7 +1320,8 @@ namespace Barotrauma
|
||||
RemoveFollowTarget();
|
||||
var itemContainer = Weapon.GetComponent<ItemContainer>();
|
||||
TryAddSubObjective(ref seekAmmunitionObjective,
|
||||
constructor: () => new AIObjectiveContainItem(character, ammunitionIdentifiers, itemContainer, objectiveManager)
|
||||
constructor: () => new AIObjectiveContainItem(character, ammunitionIdentifiers, itemContainer, objectiveManager,
|
||||
spawnItemIfNotFound: !character.IsOnPlayerTeam && character.AIController.HasInfiniteItemSpawns(ammunitionIdentifiers))
|
||||
{
|
||||
ItemCount = itemContainer.MainContainerCapacity * itemContainer.MaxStackSize,
|
||||
checkInventory = false,
|
||||
@@ -1227,10 +1343,9 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
private bool Reload(bool seekAmmo)
|
||||
{
|
||||
if (WeaponComponent == null) { return false; }
|
||||
if (WeaponComponent == null) { return false; }
|
||||
if (Weapon.OwnInventory == null) { return true; }
|
||||
// Eject empty ammo
|
||||
HumanAIController.UnequipEmptyItems(Weapon);
|
||||
HumanAIController.UnequipEmptyItems(Weapon, allowDestroying: !character.IsOnPlayerTeam);
|
||||
ImmutableHashSet<Identifier> ammunitionIdentifiers = null;
|
||||
if (WeaponComponent.RequiredItems.ContainsKey(RelatedItem.RelationType.Contained))
|
||||
{
|
||||
@@ -1270,7 +1385,7 @@ namespace Barotrauma
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if (!HoldPosition && IsOffensiveOrArrest && seekAmmo && ammunitionIdentifiers != null)
|
||||
else if (IsOffensiveOrArrest && seekAmmo && ammunitionIdentifiers != null)
|
||||
{
|
||||
// Inventory not drawn = it's not interactable
|
||||
// If the weapon is empty and the inventory is inaccessible, it can't be reloaded
|
||||
@@ -1280,6 +1395,20 @@ namespace Barotrauma
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool isAimBlocked;
|
||||
private float _blockedDistance;
|
||||
private float BlockedDistance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_blockedDistance <= 0)
|
||||
{
|
||||
_blockedDistance = CloseDistance * Rand.Range(1.0f, 1.3f);
|
||||
}
|
||||
return _blockedDistance;
|
||||
}
|
||||
}
|
||||
public List<Vector2> BlockedPositions;
|
||||
private void Attack(float deltaTime)
|
||||
{
|
||||
character.CursorPosition = Enemy.WorldPosition;
|
||||
@@ -1322,8 +1451,6 @@ namespace Barotrauma
|
||||
aimTimer -= deltaTime;
|
||||
return;
|
||||
}
|
||||
if (reloadTimer > 0) { return; }
|
||||
if (holdFireCondition != null && holdFireCondition()) { return; }
|
||||
sqrDistance = Vector2.DistanceSquared(character.WorldPosition, Enemy.WorldPosition);
|
||||
distanceTimer = DistanceCheckInterval;
|
||||
if (WeaponComponent is MeleeWeapon meleeWeapon)
|
||||
@@ -1353,9 +1480,11 @@ namespace Barotrauma
|
||||
if (closeEnough && Enemy.WorldPosition.Y < character.WorldPosition.Y && yDiff > 25)
|
||||
{
|
||||
// The target is probably knocked down? -> try to reach it by crouching.
|
||||
HumanAIController.AnimController.Crouching = true;
|
||||
HumanAIController.AnimController.Crouch();
|
||||
}
|
||||
}
|
||||
if (reloadTimer > 0) { return; }
|
||||
if (holdFireCondition != null && holdFireCondition()) { return; }
|
||||
if (closeEnough)
|
||||
{
|
||||
UseWeapon(deltaTime);
|
||||
@@ -1371,36 +1500,59 @@ namespace Barotrauma
|
||||
{
|
||||
if (WeaponComponent is RepairTool repairTool)
|
||||
{
|
||||
if (sqrDistance > repairTool.Range * repairTool.Range) { return; }
|
||||
float reach = AIObjectiveFixLeak.CalculateReach(repairTool, character);
|
||||
if (sqrDistance > reach * reach) { return; }
|
||||
}
|
||||
float aimFactor = MathHelper.PiOver2 * (1 - AimAccuracy);
|
||||
if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.WorldPosition - Weapon.WorldPosition) < MathHelper.PiOver4 + aimFactor)
|
||||
{
|
||||
myBodies ??= character.AnimController.Limbs.Select(l => l.body.FarseerBody);
|
||||
// Check that we don't hit friendlies. No need to check the walls, because there's a separate check for that at 1096 (which intentionally has a small delay)
|
||||
var pickedBodies = Submarine.PickBodies(Weapon.SimPosition, Submarine.GetRelativeSimPosition(from: Weapon, to: Enemy), myBodies, Physics.CollisionCharacter);
|
||||
var pickedBodies = Submarine.PickBodies(Weapon.SimPosition, Submarine.GetRelativeSimPosition(from: Weapon, to: Enemy),
|
||||
ignoredBodies: character.AnimController.LimbBodies,
|
||||
Physics.CollisionCharacter);
|
||||
|
||||
foreach (var body in pickedBodies)
|
||||
{
|
||||
Character target = body.UserData switch
|
||||
if (body.UserData is Limb limb)
|
||||
{
|
||||
Character c => c,
|
||||
Limb limb => limb.character,
|
||||
_ => null
|
||||
};
|
||||
if (target != null && target != Enemy && HumanAIController.IsFriendly(target))
|
||||
{
|
||||
return;
|
||||
Character target = limb.character;
|
||||
if (target != null && target != Enemy && HumanAIController.IsFriendly(target))
|
||||
{
|
||||
// Blocked by a friendly target.
|
||||
isAimBlocked = true;
|
||||
if (HumanAIController.DebugAI)
|
||||
{
|
||||
BlockedPositions.Add(ConvertUnits.ToDisplayUnits(body.Position));
|
||||
}
|
||||
// Stand up, so that we might shoot past the friendlies that are crouching.
|
||||
allowCrouching = false;
|
||||
standUpTimer = StandUpCooldown;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
UseWeapon(deltaTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool allowCrouching;
|
||||
private float standUpTimer;
|
||||
private const float StandUpCooldown = 5;
|
||||
private void UseWeapon(float deltaTime)
|
||||
{
|
||||
// Never allow friendly crew (bots) to attack with deadly weapons.
|
||||
if (Mode == CombatMode.Arrest && isLethalWeapon && character.IsOnPlayerTeam && Enemy.IsOnPlayerTeam) { return; }
|
||||
// Enable this to debug intentional friendly fire.
|
||||
// if (isLethalWeapon && character.TeamID == Enemy.TeamID && character.IsOnPlayerTeam)
|
||||
// {
|
||||
// // Never allow friendly crew (bots) to attack with deadly weapons (this check should be redundant)
|
||||
// Debugger.Break();
|
||||
// return;
|
||||
// }
|
||||
if (allowCrouching && !isMoving && !character.AnimController.InWater && WeaponComponent is not MeleeWeapon)
|
||||
{
|
||||
HumanAIController.AnimController.Crouch();
|
||||
}
|
||||
character.SetInput(InputType.Shoot, hit: false, held: true);
|
||||
Weapon.Use(deltaTime, user: character);
|
||||
SetReloadTime(WeaponComponent);
|
||||
@@ -1420,11 +1572,8 @@ namespace Barotrauma
|
||||
break;
|
||||
}
|
||||
case MeleeWeapon mw:
|
||||
{
|
||||
if (character.AnimController is HumanoidAnimController { Crouching: false })
|
||||
{
|
||||
reloadTime = mw.Reload;
|
||||
}
|
||||
{
|
||||
reloadTime = mw.Reload;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1485,7 +1634,7 @@ namespace Barotrauma
|
||||
}
|
||||
if (ShouldUnequipWeapon)
|
||||
{
|
||||
Unequip();
|
||||
UnequipWeapon();
|
||||
}
|
||||
SteeringManager?.Reset();
|
||||
}
|
||||
@@ -1495,7 +1644,7 @@ namespace Barotrauma
|
||||
base.OnAbandon();
|
||||
if (ShouldUnequipWeapon)
|
||||
{
|
||||
Unequip();
|
||||
UnequipWeapon();
|
||||
}
|
||||
SteeringManager?.Reset();
|
||||
}
|
||||
@@ -1516,6 +1665,7 @@ namespace Barotrauma
|
||||
hasAimed = false;
|
||||
holdFireTimer = 0;
|
||||
pathBackTimer = 0;
|
||||
standUpTimer = 0;
|
||||
isLethalWeapon = false;
|
||||
canSeeTarget = false;
|
||||
seekWeaponObjective = null;
|
||||
|
||||
+9
-5
@@ -48,6 +48,8 @@ namespace Barotrauma
|
||||
public int? RemoveMax { get; set; }
|
||||
|
||||
public bool MoveWholeStack { get; set; }
|
||||
|
||||
public bool AllowStealing { get; set; }
|
||||
|
||||
private int _itemCount = 1;
|
||||
public int ItemCount
|
||||
@@ -77,9 +79,8 @@ namespace Barotrauma
|
||||
this.container = container;
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific()
|
||||
protected override bool CheckObjectiveState()
|
||||
{
|
||||
if (IsCompleted) { return true; }
|
||||
if (container?.Item == null || !container.Item.HasAccess(character))
|
||||
{
|
||||
Abandon = true;
|
||||
@@ -148,11 +149,11 @@ namespace Barotrauma
|
||||
|
||||
if (RemoveExisting || (RemoveExistingWhenNecessary && !CanBePut(container.Inventory, TargetSlot, ItemToContain)))
|
||||
{
|
||||
HumanAIController.UnequipContainedItems(container.Item, predicate: RemoveExistingPredicate, unequipMax: RemoveMax);
|
||||
HumanAIController.UnequipContainedItems(container.Item, predicate: RemoveExistingPredicate, unequipMax: RemoveMax, allowDestroying: spawnItemIfNotFound);
|
||||
}
|
||||
else if (RemoveEmpty)
|
||||
{
|
||||
HumanAIController.UnequipEmptyItems(container.Item);
|
||||
HumanAIController.UnequipEmptyItems(container.Item, allowDestroying: spawnItemIfNotFound);
|
||||
}
|
||||
Inventory originalInventory = ItemToContain.ParentInventory;
|
||||
var slots = originalInventory?.FindIndices(ItemToContain);
|
||||
@@ -196,6 +197,7 @@ namespace Barotrauma
|
||||
{
|
||||
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(container.Item, character, objectiveManager, getDivingGearIfNeeded: AllowToFindDivingGear)
|
||||
{
|
||||
DialogueIdentifier = AIObjectiveGoTo.DialogCannotReachTarget,
|
||||
TargetName = container.Item.Name,
|
||||
AbortCondition = obj =>
|
||||
container?.Item == null || container.Item.Removed || !container.Item.HasAccess(character) ||
|
||||
@@ -232,7 +234,9 @@ namespace Barotrauma
|
||||
return (RemoveEmpty ? container.CanBeContained(potentialItem) : container.Inventory.CanBePut(potentialItem)) && container.ShouldBeContained(potentialItem, out _);
|
||||
},
|
||||
ItemCount = ItemCount,
|
||||
TakeWholeStack = MoveWholeStack
|
||||
TakeWholeStack = MoveWholeStack,
|
||||
ContainTarget = container,
|
||||
AllowStealing = AllowStealing
|
||||
}, onAbandon: () =>
|
||||
{
|
||||
Abandon = true;
|
||||
|
||||
+35
-12
@@ -14,7 +14,8 @@ namespace Barotrauma
|
||||
|
||||
private Deconstructor deconstructor;
|
||||
|
||||
private AIObjectiveDecontainItem decontainObjective;
|
||||
private AIObjectiveMoveItem moveItemObjective;
|
||||
private AIObjectiveGoTo gotoObjective;
|
||||
|
||||
public AIObjectiveDeconstructItem(Item item, Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
|
||||
: base(character, objectiveManager, priorityModifier)
|
||||
@@ -36,8 +37,8 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
TryAddSubObjective(ref decontainObjective,
|
||||
constructor: () => new AIObjectiveDecontainItem(character, Item, objectiveManager,
|
||||
TryAddSubObjective(ref moveItemObjective,
|
||||
constructor: () => new AIObjectiveMoveItem(character, Item, objectiveManager,
|
||||
sourceContainer: Item.Container?.GetComponent<ItemContainer>(), targetContainer: deconstructor.InputContainer, priorityModifier: PriorityModifier)
|
||||
{
|
||||
Equip = true,
|
||||
@@ -45,15 +46,25 @@ namespace Barotrauma
|
||||
},
|
||||
onCompleted: () =>
|
||||
{
|
||||
StartDeconstructor();
|
||||
//make sure the item gets moved to the main sub if the crew leaves while a bot is deconstructing something in the outpost
|
||||
if (deconstructor.Item.Submarine is { Info.IsOutpost: true })
|
||||
if (character.CanInteractWith(deconstructor.Item))
|
||||
{
|
||||
HumanAIController.HandleRelocation(Item);
|
||||
deconstructor.RelocateOutputToMainSub = true;
|
||||
StartDeconstruction();
|
||||
}
|
||||
IsCompleted = true;
|
||||
RemoveSubObjective(ref decontainObjective);
|
||||
else
|
||||
{
|
||||
TryAddSubObjective(ref gotoObjective,
|
||||
constructor: () => new AIObjectiveGoTo(Item, character, objectiveManager, priorityModifier: PriorityModifier),
|
||||
onCompleted: () =>
|
||||
{
|
||||
StartDeconstruction();
|
||||
RemoveSubObjective(ref gotoObjective);
|
||||
},
|
||||
onAbandon: () =>
|
||||
{
|
||||
Abandon = true;
|
||||
});
|
||||
}
|
||||
RemoveSubObjective(ref moveItemObjective);
|
||||
},
|
||||
onAbandon: () =>
|
||||
{
|
||||
@@ -61,6 +72,18 @@ namespace Barotrauma
|
||||
});
|
||||
}
|
||||
|
||||
private void StartDeconstruction()
|
||||
{
|
||||
StartDeconstructor();
|
||||
//make sure the item gets moved to the main sub if the crew leaves while a bot is deconstructing something in the outpost
|
||||
if (deconstructor.Item.Submarine is { Info.IsOutpost: true })
|
||||
{
|
||||
HumanAIController.HandleRelocation(Item);
|
||||
deconstructor.RelocateOutputToMainSub = true;
|
||||
}
|
||||
IsCompleted = true;
|
||||
}
|
||||
|
||||
private Deconstructor FindDeconstructor()
|
||||
{
|
||||
Deconstructor closestDeconstructor = null;
|
||||
@@ -86,7 +109,7 @@ namespace Barotrauma
|
||||
deconstructor.SetActive(active: true, user: character, createNetworkEvent: true);
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific()
|
||||
protected override bool CheckObjectiveState()
|
||||
{
|
||||
if (Item.IgnoreByAI(character))
|
||||
{
|
||||
@@ -102,7 +125,7 @@ namespace Barotrauma
|
||||
public override void Reset()
|
||||
{
|
||||
base.Reset();
|
||||
decontainObjective = null;
|
||||
moveItemObjective = null;
|
||||
}
|
||||
|
||||
public void DropTarget()
|
||||
|
||||
+4
-2
@@ -59,13 +59,15 @@ namespace Barotrauma
|
||||
|
||||
protected override bool IsValidTarget(Item target)
|
||||
{
|
||||
if (target == null || target.Removed) { return false; }
|
||||
// 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, checkInventory: true))
|
||||
{
|
||||
return Objectives.ContainsKey(target) && AIObjectiveCleanupItems.IsItemInsideValidSubmarine(target, character);
|
||||
}
|
||||
if (target.CurrentHull.FireSources.Count > 0) { return false; }
|
||||
//note that the item can be outside hulls and still be a valid target - it can be in the character's inventory
|
||||
if (target.CurrentHull != null && target.CurrentHull.FireSources.Count > 0) { return false; }
|
||||
|
||||
foreach (Character c in Character.CharacterList)
|
||||
{
|
||||
@@ -96,7 +98,7 @@ namespace Barotrauma
|
||||
|
||||
private static bool IsValidTarget(Item item, Character character, bool checkInventory)
|
||||
{
|
||||
if (item == null) { return false; }
|
||||
if (item == null || item.Removed) { return false; }
|
||||
if (item.GetRootInventoryOwner() == character) { return true; }
|
||||
return AIObjectiveCleanupItems.IsValidTarget(
|
||||
item,
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
public override bool CanBeCompleted => true;
|
||||
protected override bool CheckObjectiveSpecific() => false;
|
||||
protected override bool CheckObjectiveState() => false;
|
||||
|
||||
// escape timer is set to 60 by default to allow players to locate prisoners in time
|
||||
private float escapeTimer = 60f;
|
||||
|
||||
+40
-39
@@ -39,58 +39,57 @@ namespace Barotrauma
|
||||
// Don't go into rooms with any enemies, unless it's an order
|
||||
Priority = 0;
|
||||
Abandon = true;
|
||||
return Priority;
|
||||
}
|
||||
else
|
||||
// Prioritize fires that currently damage the character.
|
||||
bool inDamageRange = targetHull.FireSources.Any(fs => fs.IsInDamageRange(character, fs.DamageRange));
|
||||
float severity = inDamageRange ? 1.0f : AIObjectiveExtinguishFires.GetFireSeverity(targetHull);
|
||||
float characterY = character.CurrentHull?.WorldPosition.Y ?? character.WorldPosition.Y;
|
||||
float distanceFactor = targetHull == character.CurrentHull ? 1.0f
|
||||
: HumanAIController.VisibleHulls.Contains(targetHull) ? 0.75f : 0.0f;
|
||||
|
||||
if (distanceFactor <= 0.0f)
|
||||
{
|
||||
float characterY = character.CurrentHull?.WorldPosition.Y ?? character.WorldPosition.Y;
|
||||
|
||||
float distanceFactor = 1.0f;
|
||||
if (targetHull != character.CurrentHull &&
|
||||
!HumanAIController.VisibleHulls.Contains(targetHull))
|
||||
{
|
||||
distanceFactor =
|
||||
GetDistanceFactor(
|
||||
new Vector2(character.WorldPosition.Y, characterY),
|
||||
targetHull.WorldPosition,
|
||||
verticalDistanceMultiplier: 3,
|
||||
maxDistance: 5000,
|
||||
factorAtMaxDistance: 0.1f);
|
||||
}
|
||||
float severity = AIObjectiveExtinguishFires.GetFireSeverity(targetHull);
|
||||
if (severity > 0.75f && !isOrder &&
|
||||
targetHull.RoomName != null &&
|
||||
!targetHull.RoomName.Contains("reactor", StringComparison.OrdinalIgnoreCase) &&
|
||||
!targetHull.RoomName.Contains("engine", StringComparison.OrdinalIgnoreCase) &&
|
||||
!targetHull.RoomName.Contains("command", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Ignore severe fires to prevent casualities unless ordered to extinguish.
|
||||
Priority = 0;
|
||||
Abandon = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
float devotion = CumulatedDevotion / 100;
|
||||
Priority = MathHelper.Lerp(0, AIObjectiveManager.MaxObjectivePriority, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1));
|
||||
}
|
||||
distanceFactor =
|
||||
GetDistanceFactor(
|
||||
new Vector2(character.WorldPosition.Y, characterY),
|
||||
targetHull.WorldPosition,
|
||||
verticalDistanceMultiplier: 3,
|
||||
maxDistance: 5000,
|
||||
factorAtMaxDistance: 0.1f);
|
||||
}
|
||||
|
||||
if (!inDamageRange && severity > 0.75f && distanceFactor < 0.75f && !isOrder && character.IsOnPlayerTeam &&
|
||||
targetHull.RoomName != null &&
|
||||
!targetHull.RoomName.Contains("reactor", StringComparison.OrdinalIgnoreCase) &&
|
||||
!targetHull.RoomName.Contains("engine", StringComparison.OrdinalIgnoreCase) &&
|
||||
!targetHull.RoomName.Contains("command", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Bots in the player crew ignore severe fires that are not close to the target to prevent casualties unless ordered to extinguish.
|
||||
Priority = 0;
|
||||
Abandon = true;
|
||||
return Priority;
|
||||
}
|
||||
float devotion = CumulatedDevotion / 100;
|
||||
Priority = MathHelper.Lerp(0, AIObjectiveManager.MaxObjectivePriority, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1));
|
||||
return Priority;
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific() => targetHull.FireSources.None();
|
||||
protected override bool CheckObjectiveState() => targetHull.FireSources.None();
|
||||
|
||||
private float sinTime;
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
var extinguisherItem = character.Inventory.FindItemByTag("fireextinguisher".ToIdentifier());
|
||||
var extinguisherItem = character.Inventory.FindItemByTag(Tags.FireExtinguisher);
|
||||
if (extinguisherItem == null || extinguisherItem.Condition <= 0.0f || !character.HasEquippedItem(extinguisherItem))
|
||||
{
|
||||
TryAddSubObjective(ref getExtinguisherObjective, () =>
|
||||
{
|
||||
if (character.IsOnPlayerTeam && !character.HasEquippedItem("fireextinguisher".ToIdentifier(), allowBroken: false))
|
||||
if (character.IsOnPlayerTeam && !character.HasEquippedItem(Tags.FireExtinguisher, allowBroken: false))
|
||||
{
|
||||
character.Speak(TextManager.Get("DialogFindExtinguisher").Value, null, 2.0f, "findextinguisher".ToIdentifier(), 30.0f);
|
||||
character.Speak(TextManager.Get("DialogFindExtinguisher").Value, null, 2.0f, Tags.FireExtinguisher, 30.0f);
|
||||
}
|
||||
var getItemObjective = new AIObjectiveGetItem(character, "fireextinguisher".ToIdentifier(), objectiveManager, equip: true)
|
||||
var getItemObjective = new AIObjectiveGetItem(character, Tags.FireExtinguisher, objectiveManager, equip: true)
|
||||
{
|
||||
AllowStealing = true,
|
||||
// If the item is inside an unsafe hull, decrease the priority
|
||||
@@ -124,7 +123,9 @@ namespace Barotrauma
|
||||
break;
|
||||
}
|
||||
float xDist = Math.Abs(character.WorldPosition.X - fs.WorldPosition.X);
|
||||
float yDist = Math.Abs(character.CurrentHull.WorldPosition.Y - targetHull.WorldPosition.Y);
|
||||
// If fire source and the character are on the same level, it's better to ignore the y-axis (e.g. it doesn't matter if we stand or crouch), as the fire size is rectangular.
|
||||
// If we'd do this while climbing, the character would often get too close to the fire.
|
||||
float yDist = !character.IsClimbing && MathUtils.NearlyEqual(character.CurrentHull.WorldPosition.Y, targetHull.WorldPosition.Y) ? 0.0f : Math.Abs(character.CurrentHull.WorldPosition.Y - fs.WorldPosition.Y);
|
||||
float dist = xDist + yDist;
|
||||
bool inRange = dist < extinguisher.Range;
|
||||
bool isInDamageRange = fs.IsInDamageRange(character, fs.DamageRange) && character.CanSeeTarget(targetHull);
|
||||
@@ -153,8 +154,8 @@ namespace Barotrauma
|
||||
{
|
||||
if (TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(fs, character, objectiveManager, closeEnough: extinguisher.Range * 0.8f)
|
||||
{
|
||||
DialogueIdentifier = "dialogcannotreachfire".ToIdentifier(),
|
||||
TargetName = fs.Hull.DisplayName,
|
||||
DialogueIdentifier = AIObjectiveGoTo.DialogCannotReachFire,
|
||||
TargetName = fs.Hull.DisplayName
|
||||
},
|
||||
onAbandon: () => Abandon = true,
|
||||
onCompleted: () => RemoveSubObjective(ref gotoObjective)))
|
||||
|
||||
+2
-2
@@ -59,7 +59,7 @@ namespace Barotrauma
|
||||
public static bool IsValidTarget(Character target, Character character, bool targetCharactersInOtherSubs)
|
||||
{
|
||||
if (target == null || target.Removed) { return false; }
|
||||
if (target.IsDead) { return false; }
|
||||
if (target.IsDead || target.InDetectable) { return false; }
|
||||
if (target.IsUnconscious && target.Params.Health.ConstantHealthRegeneration <= 0.0f) { return false; }
|
||||
if (target == character) { return false; }
|
||||
if (target.Submarine == null) { return false; }
|
||||
@@ -75,7 +75,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
if (target.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI)) { return false; }
|
||||
if (target.IsHandcuffed && target.IsKnockedDown) { return false; }
|
||||
if (target.IsHandcuffed) { return false; }
|
||||
if (EnemyAIController.IsLatchedToSomeoneElse(target, character)) { return false; }
|
||||
return true;
|
||||
}
|
||||
|
||||
+79
-71
@@ -21,9 +21,9 @@ namespace Barotrauma
|
||||
private Item targetItem;
|
||||
private int? oxygenSourceSlotIndex;
|
||||
|
||||
public const float MIN_OXYGEN = 10;
|
||||
private const float MinOxygen = 10;
|
||||
|
||||
protected override bool CheckObjectiveSpecific() =>
|
||||
protected override bool CheckObjectiveState() =>
|
||||
targetItem != null && character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head);
|
||||
|
||||
public AIObjectiveFindDivingGear(Character character, bool needsDivingSuit, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier)
|
||||
@@ -39,83 +39,98 @@ namespace Barotrauma
|
||||
TrySetTargetItem(character.Inventory.FindItem(
|
||||
it => it.HasTag(Tags.HeavyDivingGear) && IsSuitablePressureProtection(it, Tags.HeavyDivingGear, character), recursive: true));
|
||||
}
|
||||
if (targetItem == null ||
|
||||
!character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head) &&
|
||||
targetItem.ContainedItems.Any(it => IsSuitableContainedOxygenSource(it)))
|
||||
|
||||
bool findDivingGear = targetItem == null ||
|
||||
(!character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head) && targetItem.ContainedItems.Any(IsSuitableContainedOxygenSource));
|
||||
|
||||
if (findDivingGear)
|
||||
{
|
||||
bool mustFindMorePressureProtection =
|
||||
!objectiveManager.FailedToFindDivingGearForDepth &&
|
||||
character.Inventory.FindItem(
|
||||
it => it.HasTag(Tags.HeavyDivingGear) && !IsSuitablePressureProtection(it, Tags.HeavyDivingGear, character), recursive: true) != null;
|
||||
TryAddSubObjective(ref getDivingGear, () =>
|
||||
bool mustFindMorePressureProtection = !objectiveManager.FailedToFindDivingGearForDepth &&
|
||||
character.Inventory.FindItem(it => it.HasTag(Tags.HeavyDivingGear) && !IsSuitablePressureProtection(it, Tags.HeavyDivingGear, character), recursive: true) != null;
|
||||
|
||||
if (gearTag == Tags.LightDivingGear)
|
||||
{
|
||||
if (targetItem == null && character.IsOnPlayerTeam)
|
||||
if (character.GetEquippedItem(Tags.HeavyDivingGear, slotType: InvSlotType.OuterClothes | InvSlotType.InnerClothes) is Item divingSuit && divingSuit.ContainedItems.None(IsSuitableContainedOxygenSource))
|
||||
{
|
||||
character.Speak(TextManager.Get("DialogGetDivingGear").Value, null, 0.0f, "getdivinggear".ToIdentifier(), 30.0f);
|
||||
// A special case: we are already wearing a suit without enough oxygen, but seeking for a mask, because a suit is not really needed.
|
||||
// This would result into wearing boh the mask and the suit (because the suit shouldn't be unequipped in this situation), which is a bit weird and also suboptimal, because the mask uses the oxygen 2x faster.
|
||||
// So, let's target the diving suit and try to find oxygen instead.
|
||||
targetItem = divingSuit;
|
||||
findDivingGear = false;
|
||||
}
|
||||
var getItemObjective = new AIObjectiveGetItem(character, gearTag, objectiveManager, equip: true)
|
||||
}
|
||||
if (findDivingGear)
|
||||
{
|
||||
TryAddSubObjective(ref getDivingGear, () =>
|
||||
{
|
||||
AllowStealing = HumanAIController.NeedsDivingGear(character.CurrentHull, out _),
|
||||
AllowToFindDivingGear = false,
|
||||
AllowDangerousPressure = true,
|
||||
EquipSlotType = InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head,
|
||||
Wear = true
|
||||
};
|
||||
if (gearTag == Tags.HeavyDivingGear)
|
||||
{
|
||||
if (mustFindMorePressureProtection)
|
||||
if (targetItem == null && character.IsOnPlayerTeam)
|
||||
{
|
||||
//if we're looking for a suit specifically because the current suit isn't enough,
|
||||
//let's ignore unsuitable suits altogether...
|
||||
getItemObjective.ItemFilter = it => IsSuitablePressureProtection(it, gearTag, character);
|
||||
character.Speak(TextManager.Get("DialogGetDivingGear").Value, null, 0.0f, "getdivinggear".ToIdentifier(), 30.0f);
|
||||
}
|
||||
else
|
||||
var getItemObjective = new AIObjectiveGetItem(character, gearTag, objectiveManager, equip: true)
|
||||
{
|
||||
//...Otherwise it's fine to give a very small priority
|
||||
//to inadequate suits (a suit not adequate for the depth is better than no suit)
|
||||
getItemObjective.GetItemPriority = it => IsSuitablePressureProtection(it, gearTag, character) ? 1000.0f : 1.0f;
|
||||
}
|
||||
getItemObjective.GetItemPriority = it =>
|
||||
AllowStealing = HumanAIController.NeedsDivingGear(character.CurrentHull, out _),
|
||||
AllowToFindDivingGear = false,
|
||||
AllowDangerousPressure = true,
|
||||
EquipSlotType = InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head,
|
||||
Wear = true
|
||||
};
|
||||
if (gearTag == Tags.HeavyDivingGear)
|
||||
{
|
||||
if (IsSuitablePressureProtection(it, gearTag, character))
|
||||
if (mustFindMorePressureProtection)
|
||||
{
|
||||
return 1000.0f;
|
||||
//if we're looking for a suit specifically because the current suit isn't enough,
|
||||
//let's ignore unsuitable suits altogether...
|
||||
getItemObjective.ItemFilter = it => IsSuitablePressureProtection(it, gearTag, character);
|
||||
}
|
||||
else
|
||||
{
|
||||
//if we're looking for a suit specifically because the current suit isn't enough,
|
||||
//let's ignore unsuitable suits altogether. Otherwise it's fine to give a very small priority
|
||||
//...Otherwise it's fine to give a very small priority
|
||||
//to inadequate suits (a suit not adequate for the depth is better than no suit)
|
||||
return mustFindMorePressureProtection ? 0.0f : 1.0f;
|
||||
getItemObjective.GetItemPriority = it => IsSuitablePressureProtection(it, gearTag, character) ? 1000.0f : 1.0f;
|
||||
}
|
||||
};
|
||||
}
|
||||
return getItemObjective;
|
||||
},
|
||||
onAbandon: () =>
|
||||
{
|
||||
if (mustFindMorePressureProtection) { objectiveManager.FailedToFindDivingGearForDepth = true; }
|
||||
Abandon = true;
|
||||
},
|
||||
onCompleted: () =>
|
||||
{
|
||||
RemoveSubObjective(ref getDivingGear);
|
||||
if (gearTag == Tags.HeavyDivingGear && HumanAIController.HasItem(character, Tags.LightDivingGear, out IEnumerable<Item> masks, requireEquipped: true))
|
||||
{
|
||||
foreach (Item mask in masks)
|
||||
{
|
||||
if (mask != targetItem)
|
||||
getItemObjective.GetItemPriority = it =>
|
||||
{
|
||||
character.Inventory.TryPutItem(mask, character, CharacterInventory.AnySlot);
|
||||
if (IsSuitablePressureProtection(it, gearTag, character))
|
||||
{
|
||||
return 1000.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
//if we're looking for a suit specifically because the current suit isn't enough,
|
||||
//let's ignore unsuitable suits altogether. Otherwise it's fine to give a very small priority
|
||||
//to inadequate suits (a suit not adequate for the depth is better than no suit)
|
||||
return mustFindMorePressureProtection ? 0.0f : 1.0f;
|
||||
}
|
||||
};
|
||||
}
|
||||
return getItemObjective;
|
||||
},
|
||||
onAbandon: () =>
|
||||
{
|
||||
if (mustFindMorePressureProtection) { objectiveManager.FailedToFindDivingGearForDepth = true; }
|
||||
Abandon = true;
|
||||
},
|
||||
onCompleted: () =>
|
||||
{
|
||||
RemoveSubObjective(ref getDivingGear);
|
||||
if (gearTag == Tags.HeavyDivingGear && HumanAIController.HasItem(character, Tags.LightDivingGear, out IEnumerable<Item> masks, requireEquipped: true))
|
||||
{
|
||||
foreach (Item mask in masks)
|
||||
{
|
||||
if (mask != targetItem)
|
||||
{
|
||||
character.Inventory.TryPutItem(mask, character, CharacterInventory.AnySlot);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
if (!findDivingGear)
|
||||
{
|
||||
float min = GetMinOxygen(character);
|
||||
if (targetItem.OwnInventory != null && targetItem.OwnInventory.AllItems.None(it => IsSuitableContainedOxygenSource(it)))
|
||||
if (targetItem.OwnInventory != null && targetItem.OwnInventory.AllItems.None(IsSuitableContainedOxygenSource))
|
||||
{
|
||||
TryAddSubObjective(ref getOxygen, () =>
|
||||
{
|
||||
@@ -139,9 +154,10 @@ namespace Barotrauma
|
||||
{
|
||||
AllowToFindDivingGear = false,
|
||||
AllowDangerousPressure = true,
|
||||
ConditionLevel = MIN_OXYGEN,
|
||||
ConditionLevel = MinOxygen,
|
||||
RemoveExistingWhenNecessary = true,
|
||||
TargetSlot = oxygenSourceSlotIndex
|
||||
TargetSlot = oxygenSourceSlotIndex,
|
||||
AllowStealing = HumanAIController.NeedsDivingGear(character.CurrentHull, out _)
|
||||
};
|
||||
if (container.HasSubContainers)
|
||||
{
|
||||
@@ -184,7 +200,7 @@ namespace Barotrauma
|
||||
int ReportOxygenTankCount()
|
||||
{
|
||||
if (character.Submarine != Submarine.MainSub) { return 1; }
|
||||
int remainingOxygenTanks = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(Tags.OxygenSource) && i.Condition > 1);
|
||||
int remainingOxygenTanks = Submarine.MainSub?.GetItems(false).Count(i => i.HasTag(Tags.OxygenSource) && i.Condition > 1) ?? 0;
|
||||
if (remainingOxygenTanks == 0)
|
||||
{
|
||||
character.Speak(TextManager.Get("DialogOutOfOxygenTanks").Value, null, 0.0f, "outofoxygentanks".ToIdentifier(), 30.0f);
|
||||
@@ -212,7 +228,6 @@ namespace Barotrauma
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private bool IsSuitableContainedOxygenSource(Item item)
|
||||
{
|
||||
return
|
||||
@@ -226,14 +241,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (targetItem == item) { return; }
|
||||
targetItem = item;
|
||||
if (targetItem != null)
|
||||
{
|
||||
oxygenSourceSlotIndex = targetItem.GetComponent<ItemContainer>()?.FindSuitableSubContainerIndex(Tags.OxygenSource);
|
||||
}
|
||||
else
|
||||
{
|
||||
oxygenSourceSlotIndex = null;
|
||||
}
|
||||
oxygenSourceSlotIndex = targetItem?.GetComponent<ItemContainer>()?.FindSuitableSubContainerIndex(Tags.OxygenSource);
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
@@ -251,7 +259,7 @@ namespace Barotrauma
|
||||
// The margin helps us to survive, because we might need some oxygen before we can find more oxygen.
|
||||
// When we are venturing outside of our sub, let's just suppose that we have enough oxygen with us and optimize it so that we don't keep switching off half used tanks.
|
||||
float min = 0.01f;
|
||||
float minOxygen = character.IsInFriendlySub ? MIN_OXYGEN : min;
|
||||
float minOxygen = character.IsInFriendlySub ? MinOxygen : min;
|
||||
if (minOxygen > min && character.Inventory.AllItems.Any(i => i.HasTag(Tags.OxygenSource) && i.ConditionPercentage >= minOxygen))
|
||||
{
|
||||
// There's a valid oxygen tank in the inventory -> no need to swap the tank too early.
|
||||
|
||||
+119
-33
@@ -3,6 +3,7 @@ using FarseerPhysics;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
@@ -31,7 +32,7 @@ namespace Barotrauma
|
||||
|
||||
public AIObjectiveFindSafety(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { }
|
||||
|
||||
protected override bool CheckObjectiveSpecific() => false;
|
||||
protected override bool CheckObjectiveState() => false;
|
||||
public override bool CanBeCompleted => true;
|
||||
|
||||
private bool resetPriority;
|
||||
@@ -41,9 +42,9 @@ namespace Barotrauma
|
||||
if (character.CurrentHull == null)
|
||||
{
|
||||
Priority = (
|
||||
objectiveManager.HasOrder<AIObjectiveGoTo>(o => o.Priority > 0) ||
|
||||
objectiveManager.CurrentOrder is AIObjectiveGoTo ||
|
||||
objectiveManager.HasActiveObjective<AIObjectiveRescue>() ||
|
||||
objectiveManager.Objectives.Any(o => (o is AIObjectiveCombat || o is AIObjectiveReturn) && o.Priority > 0))
|
||||
objectiveManager.Objectives.Any(o => o is AIObjectiveCombat or AIObjectiveReturn && o.Priority > 0))
|
||||
&& ((!character.IsLowInOxygen && character.IsImmuneToPressure)|| HumanAIController.HasDivingSuit(character)) ? 0 : AIObjectiveManager.EmergencyObjectivePriority - 10;
|
||||
}
|
||||
else
|
||||
@@ -70,6 +71,11 @@ namespace Barotrauma
|
||||
Priority = AIObjectiveManager.MaxObjectivePriority;
|
||||
}
|
||||
}
|
||||
else if (objectiveManager.CurrentOrder is AIObjectiveGoTo { IsFollowOrder: true })
|
||||
{
|
||||
// Ordered to follow -> Don't flee from the enemies/fires (doesn't get here if we need more oxygen).
|
||||
Priority = 0;
|
||||
}
|
||||
else if ((objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() || objectiveManager.IsCurrentOrder<AIObjectiveReturn>()) &&
|
||||
character.Submarine != null && !character.IsOnFriendlyTeam(character.Submarine.TeamID))
|
||||
{
|
||||
@@ -82,7 +88,7 @@ namespace Barotrauma
|
||||
Priority = 0;
|
||||
}
|
||||
Priority = MathHelper.Clamp(Priority, 0, AIObjectiveManager.MaxObjectivePriority);
|
||||
if (divingGearObjective != null && !divingGearObjective.IsCompleted && divingGearObjective.CanBeCompleted)
|
||||
if (divingGearObjective is { IsCompleted: false, CanBeCompleted: true, Priority: > 0f })
|
||||
{
|
||||
// Boost the priority while seeking the diving gear
|
||||
Priority = Math.Max(Priority, Math.Min(AIObjectiveManager.EmergencyObjectivePriority - 1, AIObjectiveManager.MaxObjectivePriority));
|
||||
@@ -148,7 +154,13 @@ namespace Barotrauma
|
||||
bool shouldActOnSuffocation = character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false);
|
||||
if (!character.LockHands && (!dangerousPressure || shouldActOnSuffocation || cannotFindSafeHull))
|
||||
{
|
||||
bool needsDivingGear = HumanAIController.NeedsDivingGear(currentHull, out bool needsDivingSuit);
|
||||
bool needsDivingGear = HumanAIController.NeedsDivingGear(currentHull, out bool needsDivingSuit, objectiveManager);
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC && character.Submarine?.Info is { IsOutpost: true })
|
||||
{
|
||||
// In outposts, the NPCs don't try to use diving suits, because otherwise there's probably not enough for those trying to fix the leaks.
|
||||
// This is not a hard rule: the bots may still grab a suit, unless they find a diving mask.
|
||||
needsDivingSuit = false;
|
||||
}
|
||||
bool needsEquipment = shouldActOnSuffocation;
|
||||
if (needsDivingSuit)
|
||||
{
|
||||
@@ -306,10 +318,10 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
if (escapeVel != Vector2.Zero)
|
||||
if (escapeVel != Vector2.Zero && character.CurrentHull is Hull currentHull)
|
||||
{
|
||||
float left = character.CurrentHull.Rect.X + 50;
|
||||
float right = character.CurrentHull.Rect.Right - 50;
|
||||
float left = currentHull.Rect.X + 50;
|
||||
float right = currentHull.Rect.Right - 50;
|
||||
//only move if we haven't reached the edge of the room
|
||||
if (escapeVel.X < 0 && character.Position.X > left || escapeVel.X > 0 && character.Position.X < right)
|
||||
{
|
||||
@@ -339,6 +351,10 @@ namespace Barotrauma
|
||||
float bestHullValue = 0;
|
||||
bool bestHullIsAirlock = false;
|
||||
Hull potentialBestHull;
|
||||
|
||||
#if DEBUG
|
||||
private readonly Stopwatch stopWatch = new Stopwatch();
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Tries to find the best (safe, nearby) hull the character can find a path to.
|
||||
@@ -353,6 +369,9 @@ namespace Barotrauma
|
||||
bestHullIsAirlock = false;
|
||||
hulls.Clear();
|
||||
var connectedSubs = character.Submarine?.GetConnectedSubs();
|
||||
#if DEBUG
|
||||
stopWatch.Restart();
|
||||
#endif
|
||||
foreach (Hull hull in Hull.HullList)
|
||||
{
|
||||
if (hull.Submarine == null) { continue; }
|
||||
@@ -363,25 +382,66 @@ namespace Barotrauma
|
||||
if (ignoredHulls != null && ignoredHulls.Contains(hull)) { continue; }
|
||||
if (HumanAIController.UnreachableHulls.Contains(hull)) { continue; }
|
||||
if (connectedSubs != null && !connectedSubs.Contains(hull.Submarine)) { continue; }
|
||||
//sort the hulls based on distance and which sub they're in
|
||||
//tends to make the method much faster, because we find a potential hull earlier and can discard further-away hulls more easily
|
||||
//(for instance, an NPC in an outpost might otherwise go through all the hulls in the main sub first and do tons of expensive
|
||||
//path calculations, only to discard all of them when going through the hulls in the outpost)
|
||||
float hullSuitability = EstimateHullSuitability(character, hull);
|
||||
if (hulls.None())
|
||||
{
|
||||
hulls.Add(hull);
|
||||
}
|
||||
else
|
||||
{
|
||||
//sort the hulls first based on distance and a rough suitability estimation
|
||||
//tends to make the method much faster, because we find a potential hull earlier and can discard further-away hulls more easily
|
||||
//(for instance, an NPC in an outpost might otherwise go through all the hulls in the main sub first and do tons of expensive
|
||||
//path calculations, only to discard all of them when going through the hulls in the outpost)
|
||||
bool addLast = true;
|
||||
float hullSuitability = EstimateHullSuitability(hull);
|
||||
for (int i = 0; i < hulls.Count; i++)
|
||||
{
|
||||
if (hullSuitability > EstimateHullSuitability(character, hulls[i]))
|
||||
Hull otherHull = hulls[i];
|
||||
float otherHullSuitability = EstimateHullSuitability(otherHull);
|
||||
if (hullSuitability > otherHullSuitability)
|
||||
{
|
||||
hulls.Insert(i, hull);
|
||||
addLast = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (addLast)
|
||||
{
|
||||
hulls.Add(hull);
|
||||
}
|
||||
}
|
||||
|
||||
float EstimateHullSuitability(Hull h)
|
||||
{
|
||||
float distX = Math.Abs(h.WorldPosition.X - character.WorldPosition.X);
|
||||
float distY = Math.Abs(h.WorldPosition.Y - character.WorldPosition.Y);
|
||||
if (character.CurrentHull != null)
|
||||
{
|
||||
distY *= 3;
|
||||
}
|
||||
float dist = distX + distY;
|
||||
float suitability = -dist;
|
||||
const float suitabilityReduction = 10000.0f;
|
||||
if (h.Submarine != character.Submarine)
|
||||
{
|
||||
suitability -= suitabilityReduction;
|
||||
}
|
||||
if (character.CurrentHull != null)
|
||||
{
|
||||
if (h.AvoidStaying)
|
||||
{
|
||||
suitability -= suitabilityReduction;
|
||||
}
|
||||
if (HumanAIController.UnsafeHulls.Contains(h))
|
||||
{
|
||||
suitability -= suitabilityReduction;
|
||||
}
|
||||
if (HumanAIController.NeedsDivingGear(h, out _))
|
||||
{
|
||||
suitability -= suitabilityReduction;
|
||||
}
|
||||
}
|
||||
return suitability;
|
||||
}
|
||||
}
|
||||
if (hulls.None())
|
||||
@@ -390,19 +450,10 @@ namespace Barotrauma
|
||||
return HullSearchStatus.Finished;
|
||||
}
|
||||
hullSearchIndex = 0;
|
||||
}
|
||||
|
||||
static float EstimateHullSuitability(Character character, Hull hull)
|
||||
{
|
||||
float dist =
|
||||
Math.Abs(hull.WorldPosition.X - character.WorldPosition.X) +
|
||||
Math.Abs(hull.WorldPosition.Y - character.WorldPosition.Y) * 3;
|
||||
float suitability = -dist;
|
||||
if (hull.Submarine != character.Submarine)
|
||||
{
|
||||
suitability -= 10000.0f;
|
||||
}
|
||||
return suitability;
|
||||
#if DEBUG
|
||||
stopWatch.Stop();
|
||||
DebugConsole.Log($"({character.DisplayName}) Sorted hulls by suitability in {stopWatch.ElapsedMilliseconds} ms");
|
||||
#endif
|
||||
}
|
||||
|
||||
Hull potentialHull = hulls[hullSearchIndex];
|
||||
@@ -420,7 +471,7 @@ namespace Barotrauma
|
||||
if (hullSafety > bestHullValue)
|
||||
{
|
||||
//avoid airlock modules if not allowed to change the sub
|
||||
if (allowChangingSubmarine || !potentialHull.OutpostModuleTags.Any(t => t == "airlock"))
|
||||
if (allowChangingSubmarine || potentialHull.OutpostModuleTags.All(t => t != "airlock"))
|
||||
{
|
||||
// Don't allow to go outside if not already outside.
|
||||
var path = PathSteering.PathFinder.FindPath(character.SimPosition, character.GetRelativeSimPosition(potentialHull), character.Submarine, nodeFilter: node => node.Waypoint.CurrentHull != null);
|
||||
@@ -431,12 +482,47 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
// Each unsafe node reduces the hull safety value.
|
||||
// Ignore the current hull, because otherwise we couldn't find a path out.
|
||||
int unsafeNodes = path.Nodes.Count(n => n.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(n.CurrentHull));
|
||||
hullSafety /= 1 + unsafeNodes;
|
||||
// Check the path safety. Each unsafe node reduces the hull safety value.
|
||||
Hull previousHull = null;
|
||||
foreach (WayPoint node in path.Nodes)
|
||||
{
|
||||
Hull hull = node.CurrentHull;
|
||||
if (hull == previousHull)
|
||||
{
|
||||
// Let's evaluate each hull only once. If we'd want to make this foolproof, we'd have to add the checked hulls to a list,
|
||||
// yet in practice there shouldn't be a case where the path would get back to a hull once it has exited it.
|
||||
continue;
|
||||
}
|
||||
previousHull = hull;
|
||||
if (hull == character.CurrentHull)
|
||||
{
|
||||
// Ignore the current hull, because otherwise we couldn't find a path out.
|
||||
continue;
|
||||
}
|
||||
if (HumanAIController.UnsafeHulls.Contains(hull))
|
||||
{
|
||||
// Compare safety of the node hull to the current hull safety.
|
||||
float nodeHullSafety = HumanAIController.GetHullSafety(hull, hull.GetConnectedHulls(true, 1), character);
|
||||
if (nodeHullSafety < HumanAIController.HULL_SAFETY_THRESHOLD && nodeHullSafety < HumanAIController.CurrentHullSafety)
|
||||
{
|
||||
// If the node hull is considered unsafe and less safe than the current hull, let's ignore the target.
|
||||
hullSafety = 0;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Otherwise, each unsafe hull on the path reduces the safety of the target hull by 50% of their threat value.
|
||||
float hullThreat = 100 - nodeHullSafety;
|
||||
hullSafety -= hullThreat / 2;
|
||||
if (hullSafety <= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// If the target is not inside a friendly submarine, considerably reduce the hull safety.
|
||||
if (!character.Submarine.IsEntityFoundOnThisSub(potentialHull, true))
|
||||
if (!character.Submarine.IsEntityFoundOnThisSub(potentialHull, includingConnectedSubs: true))
|
||||
{
|
||||
hullSafety /= 10;
|
||||
}
|
||||
|
||||
+29
-12
@@ -83,7 +83,7 @@ namespace Barotrauma
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!IsValidTarget(target, character)) { return false; }
|
||||
if (!CheckTarget(target)) { return false; }
|
||||
float inspectDist = target.IsCriminal ? CriminalInspectDistance : inspectDistance;
|
||||
if (Vector2.DistanceSquared(target.WorldPosition, character.WorldPosition) > inspectDist * inspectDist) { return false; }
|
||||
if (lastInspectionTimes.TryGetValue(target, out double lastInspectionTime))
|
||||
@@ -145,26 +145,31 @@ namespace Barotrauma
|
||||
// Might be e.g. sitting on a chair.
|
||||
character.SelectedSecondaryItem = null;
|
||||
}
|
||||
foreach (var target in Character.CharacterList)
|
||||
if (HumanAIController.CurrentHullSafety >= HumanAIController.HULL_SAFETY_THRESHOLD)
|
||||
{
|
||||
if (!IsValidTarget(target, character)) { continue; }
|
||||
//if we spot someone wearing or holding stolen items, immediately check them (with 100% chance of spotting the stolen items)
|
||||
if (target.Inventory.AllItems.Any(it => it.Illegitimate && target.HasEquippedItem(it)) &&
|
||||
character.CanSeeTarget(target, seeThroughWindows: true))
|
||||
foreach (var target in Character.CharacterList)
|
||||
{
|
||||
AIObjectiveCheckStolenItems? existingObjective =
|
||||
objectiveManager.GetActiveObjectives<AIObjectiveCheckStolenItems>().FirstOrDefault(o => o.Target == target);
|
||||
if (existingObjective == null)
|
||||
if (!CheckTarget(target)) { continue; }
|
||||
//if we spot someone wearing or holding stolen items, immediately check them (with 100% chance of spotting the stolen items)
|
||||
if (target.Inventory.AllItems.Any(it => target.HasEquippedItem(it) && AIObjectiveCheckStolenItems.IsItemIllegitimate(target, it)) && character.CanSeeTarget(target, seeThroughWindows: true))
|
||||
{
|
||||
objectiveManager.AddObjective(new AIObjectiveCheckStolenItems(character, target, objectiveManager));
|
||||
lastInspectionTimes[target] = Timing.TotalTime;
|
||||
if (HumanAIController.CalculateObjectiveHullSafety(target) >= HumanAIController.HULL_SAFETY_THRESHOLD)
|
||||
{
|
||||
// Don't do inspections in unsafe hulls, because under a threat, bots are allowed to wear diving gear or hold fire extinguishers etc. Even if they are "stolen".
|
||||
AIObjectiveCheckStolenItems? existingObjective = objectiveManager.GetActiveObjectives<AIObjectiveCheckStolenItems>().FirstOrDefault(o => o.Target == target);
|
||||
if (existingObjective == null)
|
||||
{
|
||||
objectiveManager.AddObjective(new AIObjectiveCheckStolenItems(character, target, objectiveManager));
|
||||
lastInspectionTimes[target] = Timing.TotalTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
checkVisibleStolenItemsTimer = CheckVisibleStolenItemsInterval;
|
||||
}
|
||||
|
||||
private bool IsValidTarget(Character target, Character character)
|
||||
private bool CheckTarget(Character target)
|
||||
{
|
||||
if (target == null || target.Removed) { return false; }
|
||||
if (target.IsIncapacitated) { return false; }
|
||||
@@ -176,6 +181,8 @@ namespace Barotrauma
|
||||
//only player's crew can steal, ignore other teams
|
||||
if (!target.IsOnPlayerTeam) { return false; }
|
||||
if (target.IsHandcuffed) { return false; }
|
||||
//ignore thieves in the same team
|
||||
if (character.OriginalTeamID == target.TeamID || character.TeamID == target.TeamID) { return false; }
|
||||
// Ignore targets that are climbing, because might need to use ladders to get to them.
|
||||
if (target.IsClimbing) { return false; }
|
||||
if (HumanAIController.IsTrueForAnyBotInTheCrew(bot =>
|
||||
@@ -190,6 +197,16 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
protected override void OnObjectiveCompleted(AIObjective objective, Character target)
|
||||
{
|
||||
MarkTargetAsInspected(target);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks the targets as being inspected for stolen items (e.g. while arresting the character),
|
||||
/// meaning characters with this objective won't attempt to trigger an inspection in a while.
|
||||
/// </summary>
|
||||
/// <param name="target"></param>
|
||||
public static void MarkTargetAsInspected(Character target)
|
||||
{
|
||||
lastInspectionTimes[target] = Timing.TotalTime;
|
||||
}
|
||||
|
||||
+6
-6
@@ -31,7 +31,7 @@ namespace Barotrauma
|
||||
this.isPriority = isPriority;
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific() => Leak.Open <= 0 || Leak.Removed;
|
||||
protected override bool CheckObjectiveState() => Leak.Open <= 0 || Leak.Removed;
|
||||
|
||||
protected override float GetPriority()
|
||||
{
|
||||
@@ -166,7 +166,7 @@ namespace Barotrauma
|
||||
// TODO: use the collider size/reach?
|
||||
if (!character.AnimController.InWater && Math.Abs(toLeak.X) < 100 && toLeak.Y < 0.0f && toLeak.Y > -150)
|
||||
{
|
||||
HumanAIController.AnimController.Crouching = true;
|
||||
HumanAIController.AnimController.Crouch();
|
||||
}
|
||||
float reach = CalculateReach(repairTool, character);
|
||||
bool canOperate = toLeak.LengthSquared() < reach * reach;
|
||||
@@ -180,7 +180,7 @@ namespace Barotrauma
|
||||
onAbandon: () => Abandon = true,
|
||||
onCompleted: () =>
|
||||
{
|
||||
if (CheckObjectiveSpecific()) { IsCompleted = true; }
|
||||
if (CheckObjectiveState()) { IsCompleted = true; }
|
||||
else
|
||||
{
|
||||
// Failed to operate. Probably too far.
|
||||
@@ -194,7 +194,7 @@ namespace Barotrauma
|
||||
{
|
||||
UseDistanceRelativeToAimSourcePos = true,
|
||||
CloseEnough = reach,
|
||||
DialogueIdentifier = Leak.FlowTargetHull != null ? "dialogcannotreachleak".ToIdentifier() : Identifier.Empty,
|
||||
DialogueIdentifier = Leak.FlowTargetHull != null ? AIObjectiveGoTo.DialogCannotReachLeak : Identifier.Empty,
|
||||
TargetName = Leak.FlowTargetHull?.DisplayName,
|
||||
requiredCondition = () =>
|
||||
Leak.Submarine == character.Submarine &&
|
||||
@@ -202,11 +202,11 @@ namespace Barotrauma
|
||||
endNodeFilter = IsSuitableEndNode,
|
||||
// The Go To objective can be abandoned if the leak is fixed (in which case we don't want to use the dialogue)
|
||||
// Only report about contextual targets.
|
||||
SpeakCannotReachCondition = () => isPriority && !CheckObjectiveSpecific()
|
||||
SpeakCannotReachCondition = () => isPriority && !CheckObjectiveState()
|
||||
},
|
||||
onAbandon: () =>
|
||||
{
|
||||
if (CheckObjectiveSpecific()) { IsCompleted = true; }
|
||||
if (CheckObjectiveState()) { IsCompleted = true; }
|
||||
else if ((Leak.WorldPosition - character.AnimController.AimSourceWorldPos).LengthSquared() > MathUtils.Pow(reach * 2, 2))
|
||||
{
|
||||
// Too far
|
||||
|
||||
+27
-3
@@ -31,6 +31,10 @@ namespace Barotrauma
|
||||
|
||||
private Item targetItem;
|
||||
private readonly Item originalTarget;
|
||||
/// <summary>
|
||||
/// ItemContainer the bot is trying to put the <see cref="TargetItem"/> into. Only set when the objective is a subobjective of a <see cref="AIObjectiveContainItem"/>.
|
||||
/// </summary>
|
||||
public ItemContainer ContainTarget;
|
||||
private ISpatialEntity moveToTarget;
|
||||
private bool isDoneSeeking;
|
||||
public Item TargetItem => targetItem;
|
||||
@@ -76,6 +80,12 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
public InvSlotType? EquipSlotType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Tags of items that bots are allowed to take from outposts, when needed. For example when there's not enough oxygen in the room, or if they need to extinguish a fire.
|
||||
/// The guards won't react if these items are taken by the bots.
|
||||
/// </summary>
|
||||
public static readonly Identifier[] AllowedItemsToTake = { Tags.OxygenSource, Tags.FireExtinguisher, Tags.LightDivingGear, Tags.HeavyDivingGear };
|
||||
|
||||
public AIObjectiveGetItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, bool equip = true, float priorityModifier = 1)
|
||||
: base(character, objectiveManager, priorityModifier)
|
||||
@@ -477,7 +487,17 @@ namespace Barotrauma
|
||||
//the item is inside an item inside an item (e.g. fuel tank in a welding tool in a cabinet -> reduce priority to prefer items that aren't inside a tool)
|
||||
if (ownerItem != item.Container)
|
||||
{
|
||||
itemPriority *= 0.1f;
|
||||
if (ContainTarget != null && ContainTarget.Item.Prefab.Identifier == item.Container.Prefab.Identifier)
|
||||
{
|
||||
// The item is identical to the item we are trying to contain the item to (e.g. trying to find an oxygen source to a mask -> allow to take oxygen sources from other masks)
|
||||
// Reduce the priority just a tiny bit, so that we choose items that are not inside the items first.
|
||||
// TODO: Doesn't solve the issue for items that are not the same type but that should be treated the same. E.g. diving mask and clown diving mask.
|
||||
itemPriority = 0.95f;
|
||||
}
|
||||
else
|
||||
{
|
||||
itemPriority *= 0.1f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -645,6 +665,11 @@ namespace Barotrauma
|
||||
if (prefab is not ItemPrefab itemPrefab) { continue; }
|
||||
if (IdentifiersOrTags.Any(id => id == prefab.Identifier || prefab.Tags.Contains(id)))
|
||||
{
|
||||
if (character.AIController.HasInfiniteItemSpawns(prefab.Identifier))
|
||||
{
|
||||
// If an item with infinite spawns is defined, let's use it.
|
||||
return itemPrefab;
|
||||
}
|
||||
float cost = itemPrefab.DefaultPrice != null && itemPrefab.CanBeBought ?
|
||||
itemPrefab.DefaultPrice.Price :
|
||||
float.MaxValue;
|
||||
@@ -658,9 +683,8 @@ namespace Barotrauma
|
||||
return bestItem;
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific()
|
||||
protected override bool CheckObjectiveState()
|
||||
{
|
||||
if (IsCompleted) { return true; }
|
||||
if (targetItem == null)
|
||||
{
|
||||
// Not yet ready
|
||||
|
||||
+2
-2
@@ -44,7 +44,7 @@ namespace Barotrauma
|
||||
ignoredTags = AIObjectiveGetItem.ParseIgnoredTags(identifiersOrTags).ToImmutableHashSet();
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific() => subObjectivesCreated && subObjectives.None();
|
||||
protected override bool CheckObjectiveState() => subObjectivesCreated && subObjectives.None();
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
@@ -56,7 +56,7 @@ namespace Barotrauma
|
||||
AIObjectiveGetItem? getItem = null;
|
||||
TryAddSubObjective(ref getItem, () =>
|
||||
{
|
||||
var getItem = new AIObjectiveGetItem(character, tag, objectiveManager, Equip, CheckInventory && count <= 1)
|
||||
getItem = new AIObjectiveGetItem(character, tag, objectiveManager, Equip, CheckInventory && count <= 1)
|
||||
{
|
||||
AllowVariants = AllowVariants,
|
||||
Wear = Wear,
|
||||
|
||||
+167
-44
@@ -3,6 +3,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -94,7 +95,35 @@ namespace Barotrauma
|
||||
protected override bool AllowOutsideSubmarine => AllowGoingOutside;
|
||||
protected override bool AllowInAnySub => true;
|
||||
|
||||
public Identifier DialogueIdentifier { get; set; } = "dialogcannotreachtarget".ToIdentifier();
|
||||
/// <summary>
|
||||
/// NPC line for when the NPC fails to find a path to a target.
|
||||
/// Note that this line includes the tag [name], which needs to be replaced with the name of the target.
|
||||
/// </summary>
|
||||
public static readonly Identifier DialogCannotReachTarget = "dialogcannotreachtarget".ToIdentifier();
|
||||
/// <summary>
|
||||
/// Generic NPC line for when the NPC fails to find a path to some place/target.
|
||||
/// </summary>
|
||||
public static readonly Identifier DialogCannotReachPlace = "dialogcannotreachplace".ToIdentifier();
|
||||
/// <summary>
|
||||
/// NPC line for when the NPC fails to find a path to a patient they're trying to treat.
|
||||
/// Note that this line includes the tag [name], which needs to be replaced with the name of the target.
|
||||
/// </summary>
|
||||
public static readonly Identifier DialogCannotReachPatient = "dialogcannotreachpatient".ToIdentifier();
|
||||
/// <summary>
|
||||
/// NPC line for when the NPC fails to find a path to a fire they're trying to extinguish.
|
||||
/// Note that this line includes the tag [name], which needs to be replaced with the name of the room the NPC is trying to get to.
|
||||
/// </summary>
|
||||
public static readonly Identifier DialogCannotReachFire = "dialogcannotreachfire".ToIdentifier();
|
||||
/// <summary>
|
||||
/// NPC line for when the NPC fails to find a path to a leak they're trying to fix.
|
||||
/// Note that this line includes the tag [name], which needs to be replaced with the name of the room the NPC is trying to get to.
|
||||
/// </summary>
|
||||
public static readonly Identifier DialogCannotReachLeak = "dialogcannotreachleak".ToIdentifier();
|
||||
|
||||
public Identifier DialogueIdentifier { get; set; } = DialogCannotReachPlace;
|
||||
private readonly Identifier ExoSuitRefuel = "dialog.exosuit.refuel".ToIdentifier();
|
||||
private readonly Identifier ExoSuitOutOfFuel = "dialog.exosuit.outoffuel".ToIdentifier();
|
||||
|
||||
public LocalizedString TargetName { get; set; }
|
||||
|
||||
public ISpatialEntity Target { get; private set; }
|
||||
@@ -112,12 +141,12 @@ namespace Barotrauma
|
||||
Abandon = !isOrder;
|
||||
return Priority;
|
||||
}
|
||||
if (Target == null || Target is Entity e && e.Removed)
|
||||
if (Target is null or Entity { Removed: true })
|
||||
{
|
||||
Priority = 0;
|
||||
Abandon = !isOrder;
|
||||
}
|
||||
if (IgnoreIfTargetDead && Target is Character character && character.IsDead)
|
||||
if (IgnoreIfTargetDead && Target is Character { IsDead: true })
|
||||
{
|
||||
Priority = 0;
|
||||
Abandon = !isOrder;
|
||||
@@ -178,6 +207,17 @@ namespace Barotrauma
|
||||
if (DialogueIdentifier == null) { return; }
|
||||
if (!SpeakIfFails) { return; }
|
||||
if (SpeakCannotReachCondition != null && !SpeakCannotReachCondition()) { return; }
|
||||
|
||||
if (TargetName == null && DialogueIdentifier == DialogCannotReachTarget)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.ThrowError(
|
||||
$"Error in {nameof(SpeakCannotReach)}: "+
|
||||
$"attempted to use a dialog line that mentions the target (dialogue identifier: {DialogueIdentifier}), but the name of the target ({(Target?.ToString() ?? "null")}) isn't set.");
|
||||
#endif
|
||||
DialogueIdentifier = DialogCannotReachPlace;
|
||||
}
|
||||
|
||||
LocalizedString msg = TargetName == null ?
|
||||
TextManager.Get(DialogueIdentifier) :
|
||||
TextManager.GetWithVariable(DialogueIdentifier, "[name]".ToIdentifier(), TargetName, formatCapitals: Target is Character ? FormatCapitals.No : FormatCapitals.Yes);
|
||||
@@ -194,6 +234,43 @@ namespace Barotrauma
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
if (checkExoSuitTimer <= 0)
|
||||
{
|
||||
checkExoSuitTimer = CheckExoSuitTime * Rand.Range(0.9f, 1.1f);
|
||||
if (character.GetEquippedItem(Tags.PoweredDivingSuit, InvSlotType.OuterClothes) is { OwnInventory: Inventory exoSuitInventory } exoSuit &&
|
||||
exoSuit.GetComponent<Powered>() is not { HasPower: true })
|
||||
{
|
||||
if (HumanAIController.HasItem(character, Tags.DivingSuitFuel, out IEnumerable<Item> fuelRods, conditionPercentage: 1, recursive: true))
|
||||
{
|
||||
// Try to switch the fuel sources
|
||||
if (character.IsOnPlayerTeam)
|
||||
{
|
||||
character.Speak(TextManager.Get(ExoSuitRefuel).Value, minDurationBetweenSimilar: 10f, identifier: ExoSuitRefuel);
|
||||
}
|
||||
// Have to copy the list, because it's modified when we unequip the item.
|
||||
foreach (Item containedItem in exoSuit.ContainedItems.ToList())
|
||||
{
|
||||
if (containedItem.HasTag(Tags.DivingSuitFuel) && containedItem.Condition <= 0)
|
||||
{
|
||||
character.Unequip(containedItem);
|
||||
}
|
||||
}
|
||||
// Refuel
|
||||
// The information about the target slot is defined in a status effect. We could parse it, but let's keep it simple and just presume that the target slot is the second slot, as it the case with the vanilla exosuits.
|
||||
const int targetSlot = 1;
|
||||
Item fuelRod = fuelRods.MaxBy(b => b.Condition);
|
||||
exoSuitInventory.TryPutItem(fuelRod, targetSlot, allowSwapping: true, allowCombine: true, user: character);
|
||||
}
|
||||
else if (character.IsOnPlayerTeam)
|
||||
{
|
||||
character.Speak(TextManager.Get(ExoSuitOutOfFuel).Value, minDurationBetweenSimilar: 30.0f, identifier: ExoSuitOutOfFuel);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
checkExoSuitTimer -= deltaTime;
|
||||
}
|
||||
if (Target == character || character.SelectedBy != null && HumanAIController.IsFriendly(character.SelectedBy))
|
||||
{
|
||||
// Wait
|
||||
@@ -301,34 +378,43 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
if (Abandon) { return; }
|
||||
if (getDivingGearIfNeeded)
|
||||
bool needsDivingSuit = (!isInside || hasOutdoorNodes) && !character.IsImmuneToPressure;
|
||||
bool tryToGetDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit);
|
||||
bool tryToGetDivingSuit = needsDivingSuit;
|
||||
Character followTarget = Target as Character;
|
||||
if (Mimic && !character.IsImmuneToPressure)
|
||||
{
|
||||
Character followTarget = Target as Character;
|
||||
bool needsDivingSuit = (!isInside || hasOutdoorNodes) && !character.IsImmuneToPressure;
|
||||
bool tryToGetDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit);
|
||||
bool tryToGetDivingSuit = needsDivingSuit;
|
||||
if (Mimic && !character.IsImmuneToPressure)
|
||||
if (HumanAIController.HasDivingSuit(followTarget))
|
||||
{
|
||||
if (HumanAIController.HasDivingSuit(followTarget))
|
||||
{
|
||||
tryToGetDivingGear = true;
|
||||
tryToGetDivingSuit = true;
|
||||
}
|
||||
else if (HumanAIController.HasDivingMask(followTarget) && character.CharacterHealth.OxygenLowResistance < 1)
|
||||
{
|
||||
tryToGetDivingGear = true;
|
||||
}
|
||||
tryToGetDivingGear = true;
|
||||
tryToGetDivingSuit = true;
|
||||
}
|
||||
bool needsEquipment = false;
|
||||
float minOxygen = AIObjectiveFindDivingGear.GetMinOxygen(character);
|
||||
if (tryToGetDivingSuit)
|
||||
else if (HumanAIController.HasDivingMask(followTarget) && character.CharacterHealth.OxygenLowResistance < 1)
|
||||
{
|
||||
needsEquipment = !HumanAIController.HasDivingSuit(character, minOxygen, requireSuitablePressureProtection: !objectiveManager.FailedToFindDivingGearForDepth);
|
||||
tryToGetDivingGear = true;
|
||||
}
|
||||
else if (tryToGetDivingGear)
|
||||
}
|
||||
bool needsEquipment = false;
|
||||
float minOxygen = AIObjectiveFindDivingGear.GetMinOxygen(character);
|
||||
if (tryToGetDivingSuit)
|
||||
{
|
||||
needsEquipment = !HumanAIController.HasDivingSuit(character, minOxygen, requireSuitablePressureProtection: !objectiveManager.FailedToFindDivingGearForDepth);
|
||||
}
|
||||
else if (tryToGetDivingGear)
|
||||
{
|
||||
needsEquipment = !HumanAIController.HasDivingGear(character, minOxygen);
|
||||
}
|
||||
if (!getDivingGearIfNeeded)
|
||||
{
|
||||
if (needsEquipment)
|
||||
{
|
||||
needsEquipment = !HumanAIController.HasDivingGear(character, minOxygen);
|
||||
// Don't try to reach the target without proper equipment.
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (character.LockHands)
|
||||
{
|
||||
cantFindDivingGear = true;
|
||||
@@ -353,9 +439,9 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
// Try again without requiring the diving suit
|
||||
// Try again without requiring the diving suit (or mask)
|
||||
RemoveSubObjective(ref findDivingGear);
|
||||
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager),
|
||||
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: !tryToGetDivingSuit, objectiveManager),
|
||||
onAbandon: () =>
|
||||
{
|
||||
Abandon = character.CurrentHull != null && (objectiveManager.CurrentOrder != this || Target.Submarine == null);
|
||||
@@ -442,7 +528,7 @@ namespace Barotrauma
|
||||
if (checkScooterTimer <= 0)
|
||||
{
|
||||
useScooter = false;
|
||||
checkScooterTimer = checkScooterTime * Rand.Range(0.75f, 1.25f);
|
||||
checkScooterTimer = CheckScooterTime * Rand.Range(0.9f, 1.1f);
|
||||
Item scooter = null;
|
||||
bool shouldUseScooter = Mimic && targetCharacter != null && targetCharacter.HasEquippedItem(Tags.Scooter, allowBroken: false);
|
||||
if (!shouldUseScooter)
|
||||
@@ -465,24 +551,25 @@ namespace Barotrauma
|
||||
}
|
||||
else if (shouldUseScooter)
|
||||
{
|
||||
var leftHandItem = character.GetEquippedItem(slotType: InvSlotType.LeftHand);
|
||||
var rightHandItem = character.GetEquippedItem(slotType: InvSlotType.RightHand);
|
||||
bool handsFull =
|
||||
(leftHandItem != null && !character.Inventory.IsAnySlotAvailable(leftHandItem) && !character.Inventory.TryPutItem(leftHandItem, character, InvSlotType.Bag.ToEnumerable())) ||
|
||||
(rightHandItem != null && !character.Inventory.IsAnySlotAvailable(rightHandItem) && !character.Inventory.TryPutItem(rightHandItem, character, InvSlotType.Bag.ToEnumerable()));
|
||||
if (!handsFull)
|
||||
bool hasHandsFull = character.HasHandsFull(out (Item leftHandItem, Item rightHandItem) items);
|
||||
if (hasHandsFull)
|
||||
{
|
||||
hasHandsFull = !character.TryPutItemInAnySlot(items.leftHandItem) &&
|
||||
!character.TryPutItemInAnySlot(items.rightHandItem) &&
|
||||
!character.TryPutItemInBag(items.leftHandItem) &&
|
||||
!character.TryPutItemInBag(items.rightHandItem);
|
||||
}
|
||||
if (!hasHandsFull)
|
||||
{
|
||||
bool hasBattery = false;
|
||||
if (HumanAIController.HasItem(character, Tags.Scooter, out IEnumerable<Item> nonEquippedScooters, containedTag: Tags.MobileBattery, conditionPercentage: 1, requireEquipped: false))
|
||||
if (HumanAIController.HasItem(character, Tags.Scooter, out IEnumerable<Item> nonEquippedScootersWithBattery, containedTag: Tags.MobileBattery, conditionPercentage: 1, requireEquipped: false))
|
||||
{
|
||||
// Non-equipped scooter with a battery
|
||||
scooter = nonEquippedScooters.FirstOrDefault();
|
||||
scooter = nonEquippedScootersWithBattery.FirstOrDefault();
|
||||
hasBattery = true;
|
||||
}
|
||||
else if (HumanAIController.HasItem(character, Tags.Scooter, out IEnumerable<Item> _nonEquippedScooters, requireEquipped: false))
|
||||
else if (HumanAIController.HasItem(character, Tags.Scooter, out IEnumerable<Item> nonEquippedScootersWithoutBattery, requireEquipped: false))
|
||||
{
|
||||
// Non-equipped scooter without a battery
|
||||
scooter = _nonEquippedScooters.FirstOrDefault();
|
||||
scooter = nonEquippedScootersWithoutBattery.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, Tags.MobileBattery, out _, requireEquipped: false, conditionPercentage: 1, recursive: false);
|
||||
}
|
||||
@@ -518,8 +605,7 @@ namespace Barotrauma
|
||||
}
|
||||
if (!useScooter)
|
||||
{
|
||||
// Unequip
|
||||
character.Inventory.TryPutItem(scooter, character, CharacterInventory.AnySlot);
|
||||
character.TryPutItemInAnySlot(scooter);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -663,7 +749,10 @@ namespace Barotrauma
|
||||
|
||||
private bool useScooter;
|
||||
private float checkScooterTimer;
|
||||
private readonly float checkScooterTime = 0.5f;
|
||||
private const float CheckScooterTime = 0.5f;
|
||||
|
||||
private float checkExoSuitTimer;
|
||||
private const float CheckExoSuitTime = 2.0f;
|
||||
|
||||
public Hull GetTargetHull() => GetTargetHull(Target);
|
||||
|
||||
@@ -750,6 +839,11 @@ namespace Barotrauma
|
||||
// Going through a hatch
|
||||
return false;
|
||||
}
|
||||
if (Target is Item targetItem && targetItem.GetComponent<Pickable>() == null)
|
||||
{
|
||||
// Targeting a static item, such as a reactor or a controller -> Don't complete, until we are no longer climbing.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!AlwaysUseEuclideanDistance && !character.AnimController.InWater)
|
||||
@@ -764,9 +858,8 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific()
|
||||
protected override bool CheckObjectiveState()
|
||||
{
|
||||
if (IsCompleted) { return true; }
|
||||
// First check the distance and then if can interact (heaviest)
|
||||
if (Target == null)
|
||||
{
|
||||
@@ -850,5 +943,35 @@ namespace Barotrauma
|
||||
pathSteering.ResetPath();
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShouldRun(bool run)
|
||||
{
|
||||
if (run && objectiveManager.ForcedOrder == this && IsWaitOrder && !character.IsOnPlayerTeam)
|
||||
{
|
||||
// NPCs with a wait order don't run.
|
||||
run = false;
|
||||
}
|
||||
else if (Target != null)
|
||||
{
|
||||
if (character.CurrentHull == null)
|
||||
{
|
||||
run = Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition) > 300 * 300;
|
||||
}
|
||||
else
|
||||
{
|
||||
float yDiff = Target.WorldPosition.Y - character.WorldPosition.Y;
|
||||
if (Math.Abs(yDiff) > 100)
|
||||
{
|
||||
run = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
float xDiff = Target.WorldPosition.X - character.WorldPosition.X;
|
||||
run = Math.Abs(xDiff) > 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
return run;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+167
-130
@@ -88,7 +88,7 @@ namespace Barotrauma
|
||||
CalculatePriority();
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific() => false;
|
||||
protected override bool CheckObjectiveState() => false;
|
||||
public override bool CanBeCompleted => true;
|
||||
|
||||
public readonly HashSet<Identifier> PreferredOutpostModuleTypes = new HashSet<Identifier>();
|
||||
@@ -158,8 +158,17 @@ namespace Barotrauma
|
||||
{
|
||||
character.DeselectCharacter();
|
||||
}
|
||||
|
||||
character.SelectedItem = null;
|
||||
if (character.SelectedItem != null)
|
||||
{
|
||||
if (character.SelectedItem.Prefab.AllowDeselectWhenIdling)
|
||||
{
|
||||
character.SelectedItem = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!character.IsClimbing)
|
||||
{
|
||||
@@ -176,135 +185,162 @@ namespace Barotrauma
|
||||
IsForbidden(currentTarget) ||
|
||||
(PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull)));
|
||||
|
||||
if (behavior == BehaviorType.StayInHull && TargetHull != null && !IsForbidden(TargetHull) && !currentTargetIsInvalid && !HumanAIController.UnsafeHulls.Contains(TargetHull))
|
||||
if (behavior == BehaviorType.StayInHull && TargetHull != null && !currentTargetIsInvalid && !IsForbidden(TargetHull))
|
||||
{
|
||||
currentTarget = TargetHull;
|
||||
bool stayInHull = character.CurrentHull == currentTarget && IsSteeringFinished() && !character.IsClimbing;
|
||||
if (stayInHull)
|
||||
if (HumanAIController.UnsafeHulls.Contains(TargetHull))
|
||||
{
|
||||
Wander(deltaTime);
|
||||
}
|
||||
else if (currentTarget != null)
|
||||
{
|
||||
PathSteering.SteeringSeek(character.GetRelativeSimPosition(currentTarget), weight: 1, nodeFilter: node => node.Waypoint.CurrentHull != null);
|
||||
// Ask to refresh, because otherwise we can't get back to the hull.
|
||||
HumanAIController.AskToRecalculateHullSafety(TargetHull);
|
||||
}
|
||||
else
|
||||
{
|
||||
PathSteering.ResetPath();
|
||||
PathSteering.Reset();
|
||||
currentTarget = TargetHull;
|
||||
NavigateTo(currentTarget);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (currentTarget != null && !currentTargetIsInvalid)
|
||||
{
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted)
|
||||
{
|
||||
if (currentTarget.Submarine.TeamID != character.TeamID)
|
||||
{
|
||||
currentTargetIsInvalid = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (currentTarget.Submarine != character.Submarine)
|
||||
{
|
||||
currentTargetIsInvalid = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (currentTargetIsInvalid || currentTarget == null || IsForbidden(character.CurrentHull) && IsSteeringFinished())
|
||||
{
|
||||
if (newTargetTimer > timerMargin)
|
||||
{
|
||||
//don't reset to zero, otherwise the character will keep calling FindTargetHulls
|
||||
//almost constantly when there's a small number of potential hulls to move to
|
||||
SetTargetTimerLow();
|
||||
}
|
||||
}
|
||||
else if (character.IsClimbing)
|
||||
{
|
||||
if (currentTarget == null)
|
||||
{
|
||||
SetTargetTimerLow();
|
||||
}
|
||||
else if (Math.Abs(character.AnimController.TargetMovement.Y) > 0.9f)
|
||||
{
|
||||
// Don't allow new targets when climbing straight up or down
|
||||
SetTargetTimerHigh();
|
||||
}
|
||||
}
|
||||
else if (character.AnimController.InWater)
|
||||
{
|
||||
if (currentTarget == null)
|
||||
{
|
||||
SetTargetTimerLow();
|
||||
}
|
||||
}
|
||||
if (newTargetTimer <= 0.0f)
|
||||
{
|
||||
if (!searchingNewHull)
|
||||
{
|
||||
//find all available hulls first
|
||||
searchingNewHull = true;
|
||||
FindTargetHulls();
|
||||
}
|
||||
else if (targetHulls.Any())
|
||||
{
|
||||
//choose a random available hull
|
||||
currentTarget = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced);
|
||||
bool isInWrongSub = (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted) && character.Submarine.TeamID != character.TeamID;
|
||||
bool isCurrentHullAllowed = !isInWrongSub && !IsForbidden(character.CurrentHull);
|
||||
Vector2 targetPos = character.GetRelativeSimPosition(currentTarget);
|
||||
var path = PathSteering.PathFinder.FindPath(character.SimPosition, targetPos, character.Submarine, nodeFilter: node =>
|
||||
{
|
||||
if (node.Waypoint.CurrentHull == null) { return false; }
|
||||
// Check that there is no unsafe hulls on the way to the target
|
||||
if (node.Waypoint.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(node.Waypoint.CurrentHull)) { return false; }
|
||||
return true;
|
||||
//don't stop at ladders when idling
|
||||
}, endNodeFilter: node => node.Waypoint.Stairs == null && node.Waypoint.Ladders == null && (!isCurrentHullAllowed || !IsForbidden(node.Waypoint.CurrentHull)));
|
||||
if (path.Unreachable)
|
||||
{
|
||||
//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;
|
||||
}
|
||||
character.AIController.SelectTarget(currentTarget.AiTarget);
|
||||
PathSteering.SetPath(targetPos, path);
|
||||
SetTargetTimerNormal();
|
||||
searchingNewHull = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Couldn't find a valid hull
|
||||
SetTargetTimerHigh();
|
||||
searchingNewHull = false;
|
||||
}
|
||||
}
|
||||
newTargetTimer -= deltaTime;
|
||||
if (currentTarget == null || PathSteering.CurrentPath == null)
|
||||
{
|
||||
Wander(deltaTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (currentTarget != null && !currentTargetIsInvalid)
|
||||
NavigateTo(currentTarget);
|
||||
}
|
||||
|
||||
void NavigateTo(Hull target)
|
||||
{
|
||||
bool isAtTarget = character.CurrentHull == target && IsSteeringFinished();
|
||||
if (isAtTarget)
|
||||
{
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted)
|
||||
if (character.IsClimbing)
|
||||
{
|
||||
if (currentTarget.Submarine.TeamID != character.TeamID)
|
||||
StopMoving();
|
||||
if (character.AnimController.GetHeightFromFloor() < character.AnimController.ImpactTolerance / 2)
|
||||
{
|
||||
currentTargetIsInvalid = true;
|
||||
character.StopClimbing();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (currentTarget.Submarine != character.Submarine)
|
||||
{
|
||||
currentTargetIsInvalid = true;
|
||||
}
|
||||
Wander(deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentTargetIsInvalid || currentTarget == null || IsForbidden(character.CurrentHull) && IsSteeringFinished())
|
||||
else if (target != null)
|
||||
{
|
||||
if (newTargetTimer > timerMargin)
|
||||
{
|
||||
//don't reset to zero, otherwise the character will keep calling FindTargetHulls
|
||||
//almost constantly when there's a small number of potential hulls to move to
|
||||
SetTargetTimerLow();
|
||||
}
|
||||
}
|
||||
else if (character.IsClimbing)
|
||||
{
|
||||
if (currentTarget == null)
|
||||
{
|
||||
SetTargetTimerLow();
|
||||
}
|
||||
else if (Math.Abs(character.AnimController.TargetMovement.Y) > 0.9f)
|
||||
{
|
||||
// Don't allow new targets when climbing straight up or down
|
||||
SetTargetTimerHigh();
|
||||
}
|
||||
}
|
||||
else if (character.AnimController.InWater)
|
||||
{
|
||||
if (currentTarget == null)
|
||||
{
|
||||
SetTargetTimerLow();
|
||||
}
|
||||
}
|
||||
if (newTargetTimer <= 0.0f)
|
||||
{
|
||||
if (!searchingNewHull)
|
||||
{
|
||||
//find all available hulls first
|
||||
searchingNewHull = true;
|
||||
FindTargetHulls();
|
||||
}
|
||||
else if (targetHulls.Any())
|
||||
{
|
||||
//choose a random available hull
|
||||
currentTarget = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced);
|
||||
bool isInWrongSub = (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted) && character.Submarine.TeamID != character.TeamID;
|
||||
bool isCurrentHullAllowed = !isInWrongSub && !IsForbidden(character.CurrentHull);
|
||||
Vector2 targetPos = character.GetRelativeSimPosition(currentTarget);
|
||||
var path = PathSteering.PathFinder.FindPath(character.SimPosition, targetPos, character.Submarine, nodeFilter: node =>
|
||||
{
|
||||
if (node.Waypoint.CurrentHull == null) { return false; }
|
||||
// Check that there is no unsafe hulls on the way to the target
|
||||
if (node.Waypoint.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(node.Waypoint.CurrentHull)) { return false; }
|
||||
return true;
|
||||
//don't stop at ladders when idling
|
||||
}, endNodeFilter: node => node.Waypoint.Stairs == null && node.Waypoint.Ladders == null && (!isCurrentHullAllowed || !IsForbidden(node.Waypoint.CurrentHull)));
|
||||
if (path.Unreachable)
|
||||
{
|
||||
//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;
|
||||
}
|
||||
character.AIController.SelectTarget(currentTarget.AiTarget);
|
||||
PathSteering.SetPath(targetPos, path);
|
||||
SetTargetTimerNormal();
|
||||
searchingNewHull = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Couldn't find a valid hull
|
||||
SetTargetTimerHigh();
|
||||
searchingNewHull = false;
|
||||
}
|
||||
}
|
||||
newTargetTimer -= deltaTime;
|
||||
if (!character.IsClimbing && (PathSteering == null || PathSteering.CurrentPath == null || IsSteeringFinished()))
|
||||
{
|
||||
Wander(deltaTime);
|
||||
}
|
||||
else if (currentTarget != null)
|
||||
{
|
||||
PathSteering.SteeringSeek(character.GetRelativeSimPosition(currentTarget), weight: 1,
|
||||
nodeFilter: node => node.Waypoint.CurrentHull != null,
|
||||
endNodeFilter: node => node.Waypoint.Ladders == null && node.Waypoint.Stairs == null);
|
||||
PathTo(target);
|
||||
}
|
||||
else
|
||||
{
|
||||
PathSteering.ResetPath();
|
||||
PathSteering.Reset();
|
||||
StopMoving();
|
||||
}
|
||||
}
|
||||
|
||||
void StopMoving()
|
||||
{
|
||||
SteeringManager.Reset();
|
||||
PathSteering.ResetPath();
|
||||
}
|
||||
|
||||
void PathTo(ISpatialEntity target)
|
||||
{
|
||||
PathSteering.SteeringSeek(character.GetRelativeSimPosition(target), weight: 1,
|
||||
nodeFilter: node => node.Waypoint.CurrentHull != null,
|
||||
endNodeFilter: node => node.Waypoint.Ladders == null && node.Waypoint.Stairs == null);
|
||||
}
|
||||
}
|
||||
|
||||
public void Wander(float deltaTime)
|
||||
@@ -378,14 +414,14 @@ namespace Barotrauma
|
||||
chairCheckTimer -= deltaTime;
|
||||
if (chairCheckTimer <= 0.0f && character.SelectedSecondaryItem == null)
|
||||
{
|
||||
foreach (Item item in Item.ItemList)
|
||||
foreach (Item chair in Item.ChairItems)
|
||||
{
|
||||
if (item.CurrentHull != currentHull || !item.HasTag(Tags.ChairItem)) { continue; }
|
||||
if (chair.CurrentHull != currentHull) { continue; }
|
||||
//not possible in vanilla game, but a mod might have holdable/attachable chairs
|
||||
if (item.ParentInventory != null || item.body is { Enabled: true }) { continue; }
|
||||
var controller = item.GetComponent<Controller>();
|
||||
if (chair.ParentInventory != null || chair.body is { Enabled: true }) { continue; }
|
||||
var controller = chair.GetComponent<Controller>();
|
||||
if (controller == null || controller.User != null) { continue; }
|
||||
item.TryInteract(character, forceSelectKey: true);
|
||||
chair.TryInteract(character, forceSelectKey: true);
|
||||
}
|
||||
chairCheckTimer = chairCheckInterval;
|
||||
}
|
||||
@@ -489,27 +525,26 @@ namespace Barotrauma
|
||||
if (checkItemsTimer <= 0)
|
||||
{
|
||||
checkItemsTimer = checkItemsInterval * Rand.Range(0.9f, 1.1f);
|
||||
var hull = character.CurrentHull;
|
||||
if (hull != null)
|
||||
if (character.Submarine is not Submarine sub) { return; }
|
||||
if (sub.TeamID != character.TeamID) { return; }
|
||||
if (character.CurrentHull is not Hull currentHull) { return; }
|
||||
itemsToClean.Clear();
|
||||
foreach (Item item in Item.CleanableItems)
|
||||
{
|
||||
itemsToClean.Clear();
|
||||
foreach (Item item in Item.CleanableItems)
|
||||
if (item.CurrentHull != currentHull) { continue; }
|
||||
if (AIObjectiveCleanupItems.IsValidTarget(item, character, checkInventory: true, allowUnloading: false) && !ignoredItems.Contains(item))
|
||||
{
|
||||
if (item.CurrentHull != hull) { continue; }
|
||||
if (AIObjectiveCleanupItems.IsValidTarget(item, character, checkInventory: true, allowUnloading: false) && !ignoredItems.Contains(item))
|
||||
{
|
||||
itemsToClean.Add(item);
|
||||
}
|
||||
itemsToClean.Add(item);
|
||||
}
|
||||
if (itemsToClean.Any())
|
||||
}
|
||||
if (itemsToClean.Any())
|
||||
{
|
||||
var targetItem = itemsToClean.MinBy(i => Math.Abs(character.WorldPosition.X - i.WorldPosition.X));
|
||||
if (targetItem != null)
|
||||
{
|
||||
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);
|
||||
}
|
||||
var cleanupObjective = new AIObjectiveCleanupItem(targetItem, character, objectiveManager, PriorityModifier);
|
||||
cleanupObjective.Abandoned += () => ignoredItems.Add(targetItem);
|
||||
subObjectives.Add(cleanupObjective);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -534,6 +569,8 @@ namespace Barotrauma
|
||||
itemsToClean.Clear();
|
||||
ignoredItems.Clear();
|
||||
autonomousObjectiveRetryTimer = 10;
|
||||
timerMargin = 0;
|
||||
newTargetTimer = 0;
|
||||
}
|
||||
|
||||
public override void OnDeselected()
|
||||
|
||||
+1
-2
@@ -120,7 +120,6 @@ namespace Barotrauma
|
||||
{
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific() => false;
|
||||
|
||||
protected override bool CheckObjectiveState() => false;
|
||||
}
|
||||
}
|
||||
|
||||
+12
-11
@@ -22,7 +22,7 @@ namespace Barotrauma
|
||||
private static Dictionary<ItemPrefab, ImmutableHashSet<Identifier>> AllValidContainableItemIdentifiers { get; } = new Dictionary<ItemPrefab, ImmutableHashSet<Identifier>>();
|
||||
|
||||
private int itemIndex;
|
||||
private AIObjectiveDecontainItem decontainObjective;
|
||||
private AIObjectiveMoveItem moveItemObjective;
|
||||
private readonly HashSet<Item> ignoredItems = new HashSet<Item>();
|
||||
private Item targetItem;
|
||||
private readonly string abandonGetItemDialogueIdentifier = "dialogcannotfindloadable";
|
||||
@@ -196,17 +196,17 @@ namespace Barotrauma
|
||||
float devotion = (CumulatedDevotion + (hasContainable ? 100 - MaxDevotion : 0)) / 100;
|
||||
float max = AIObjectiveManager.LowestOrderPriority - (hasContainable ? 1 : 2);
|
||||
Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (distanceFactor * PriorityModifier), 0, 1));
|
||||
if (decontainObjective != null && targetItem.Container != Container)
|
||||
if (moveItemObjective != null && targetItem.Container != Container)
|
||||
{
|
||||
if (!IsValidContainable(targetItem))
|
||||
{
|
||||
// Target is not valid anymore, abandon the objective
|
||||
decontainObjective.Abandon = true;
|
||||
moveItemObjective.Abandon = true;
|
||||
}
|
||||
else if (!ItemContainer.Inventory.CanBePut(targetItem) && ItemContainer.Inventory.AllItems.None(i => AIObjectiveLoadItems.ItemMatchesTargetCondition(i, TargetItemCondition)))
|
||||
{
|
||||
// The container is full and there's no item that should be removed, abandon the objective
|
||||
decontainObjective.Abandon = true;
|
||||
moveItemObjective.Abandon = true;
|
||||
}
|
||||
}
|
||||
if (ItemContainer.Inventory.IsFull())
|
||||
@@ -257,26 +257,27 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
if(decontainObjective == null && !IsValidContainable(targetItem))
|
||||
if(moveItemObjective == null && !IsValidContainable(targetItem))
|
||||
{
|
||||
IgnoreTargetItem();
|
||||
Reset();
|
||||
return;
|
||||
}
|
||||
TryAddSubObjective(ref decontainObjective,
|
||||
constructor: () => new AIObjectiveDecontainItem(character, targetItem, objectiveManager, targetContainer: ItemContainer, priorityModifier: PriorityModifier)
|
||||
TryAddSubObjective(ref moveItemObjective,
|
||||
constructor: () => new AIObjectiveMoveItem(character, targetItem, objectiveManager, targetContainer: ItemContainer, priorityModifier: PriorityModifier)
|
||||
{
|
||||
AbandonGetItemDialogueCondition = () => IsValidContainable(targetItem),
|
||||
AbandonGetItemDialogueIdentifier = abandonGetItemDialogueIdentifier,
|
||||
Equip = true,
|
||||
RemoveExistingWhenNecessary = true,
|
||||
RemoveExistingPredicate = (i) => !ValidContainableItemIdentifiers.Contains(i.Prefab.Identifier) || AIObjectiveLoadItems.ItemMatchesTargetCondition(i, TargetItemCondition),
|
||||
RemoveExistingMax = 1
|
||||
RemoveExistingMax = 1,
|
||||
AllowToFindDivingGear = objectiveManager.HasOrder<AIObjectiveLoadItems>()
|
||||
},
|
||||
onCompleted: () =>
|
||||
{
|
||||
IsCompleted = true;
|
||||
RemoveSubObjective(ref decontainObjective);
|
||||
RemoveSubObjective(ref moveItemObjective);
|
||||
},
|
||||
onAbandon: () =>
|
||||
{
|
||||
@@ -318,13 +319,13 @@ namespace Barotrauma
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific() => IsCompleted;
|
||||
protected override bool CheckObjectiveState() => IsCompleted;
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
base.Reset();
|
||||
// Don't reset the target item when resetting the objective because it affects priority calculations
|
||||
decontainObjective = null;
|
||||
moveItemObjective = null;
|
||||
itemIndex = 0;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -71,7 +71,7 @@ namespace Barotrauma
|
||||
if (item.IsClaimedByBallastFlora) { return false; }
|
||||
if (!item.HasAccess(character)) { return false; }
|
||||
// Ignore items that require power but don't have it
|
||||
if (item.GetComponent<Powered>() is Powered powered && powered.PowerConsumption > 0 && powered.Voltage < powered.MinVoltage) { return false; }
|
||||
if (item.GetComponent<Powered>() is { PowerConsumption: > 0, HasPower: false }) { return false; }
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@ namespace Barotrauma
|
||||
: base(character, objectiveManager, priorityModifier, option) { }
|
||||
|
||||
protected override void Act(float deltaTime) { }
|
||||
protected override bool CheckObjectiveSpecific() => false;
|
||||
protected override bool CheckObjectiveState() => false;
|
||||
public override bool CanBeCompleted => true;
|
||||
public override bool AbandonWhenCannotCompleteSubObjectives => false;
|
||||
public override bool AllowSubObjectiveSorting => true;
|
||||
|
||||
+16
-9
@@ -35,7 +35,7 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public const float HighestOrderPriority = 70;
|
||||
/// <summary>
|
||||
/// Maximum priority of an order given to the character (rightmost order in the crew list)
|
||||
/// Minimum priority of an order given to the character (rightmost order in the crew list)
|
||||
/// </summary>
|
||||
public const float LowestOrderPriority = 60;
|
||||
/// <summary>
|
||||
@@ -228,11 +228,7 @@ namespace Barotrauma
|
||||
coroutine = CoroutineManager.Invoke(() =>
|
||||
{
|
||||
//round ended before the coroutine finished
|
||||
#if CLIENT
|
||||
if (GameMain.GameSession == null || Level.Loaded == null && !(GameMain.GameSession.GameMode is TestGameMode)) { return; }
|
||||
#else
|
||||
if (GameMain.GameSession == null || Level.Loaded == null) { return; }
|
||||
#endif
|
||||
if (GameMain.GameSession == null || Level.Loaded == null && GameMain.GameSession.GameMode is not TestGameMode) { return; }
|
||||
DelayedObjectives.Remove(objective);
|
||||
AddObjective(objective);
|
||||
callback?.Invoke();
|
||||
@@ -480,7 +476,7 @@ namespace Barotrauma
|
||||
IgnoreIfTargetDead = true,
|
||||
IsFollowOrder = true,
|
||||
Mimic = character.IsOnPlayerTeam,
|
||||
DialogueIdentifier = "dialogcannotreachplace".ToIdentifier()
|
||||
DialogueIdentifier = AIObjectiveGoTo.DialogCannotReachPlace
|
||||
};
|
||||
break;
|
||||
case "wait":
|
||||
@@ -719,6 +715,11 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public bool HasObjectiveOrOrder<T>() where T : AIObjective => Objectives.Any(o => o is T) || HasOrder<T>();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current objective or its currently active subobjective (first in chain), regadless of the type.
|
||||
/// Note: Not recursive, and thus doesn't work for deeper hierarchy!
|
||||
/// For seeking objectives of specific type and in a deep hierarchy, use <see cref="GetLastActiveObjective{T}"/> or with looping objectives <see cref="GetFirstActiveObjective{T}"/>
|
||||
/// </summary>
|
||||
public AIObjective GetActiveObjective() => CurrentObjective?.GetActiveObjective();
|
||||
|
||||
/// <summary>
|
||||
@@ -735,7 +736,8 @@ namespace Barotrauma
|
||||
/// Returns the last active objective of the specified objective type.
|
||||
/// Should generally be used to get the active objective (or subobjective) of objectives that don't sort their subobjectives by priority (see <see cref="AIObjective.AllowSubObjectiveSorting"/>.
|
||||
/// </summary>
|
||||
/// <returns>The last active objective of the specified type if found.
|
||||
/// <returns>
|
||||
/// The last active objective of the specified type if found.
|
||||
/// </returns>
|
||||
public T GetLastActiveObjective<T>() where T : AIObjective
|
||||
=> CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).LastOrDefault(so => so is T) as T;
|
||||
@@ -758,7 +760,12 @@ namespace Barotrauma
|
||||
if (CurrentObjective == null) { return Enumerable.Empty<T>(); }
|
||||
return CurrentObjective.GetSubObjectivesRecursive(includingSelf: true).OfType<T>();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Is the current objective or any of its subobjectives of the given type?
|
||||
/// Useful for checking whether the bot has a certain type of objective active in the hierarchy.
|
||||
/// </summary>
|
||||
/// <returns>False for objectives and orders that are not currently active.</returns>
|
||||
public bool HasActiveObjective<T>() where T : AIObjective => CurrentObjective is T || CurrentObjective != null && CurrentObjective.GetSubObjectivesRecursive().Any(so => so is T);
|
||||
|
||||
public bool IsOrder(AIObjective objective)
|
||||
|
||||
+26
-17
@@ -6,9 +6,9 @@ using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveDecontainItem : AIObjective
|
||||
class AIObjectiveMoveItem : AIObjective
|
||||
{
|
||||
public override Identifier Identifier { get; set; } = "decontain item".ToIdentifier();
|
||||
public override Identifier Identifier { get; set; } = "move item".ToIdentifier();
|
||||
protected override bool AllowWhileHandcuffed => false;
|
||||
|
||||
public Func<Item, float> GetItemPriority;
|
||||
@@ -47,8 +47,15 @@ namespace Barotrauma
|
||||
public int? RemoveExistingMax { get; set; }
|
||||
public string AbandonGetItemDialogueIdentifier { get; set; }
|
||||
public Func<bool> AbandonGetItemDialogueCondition { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// By default, finding diving gear is not allowed here, because it can cause unexpected behavior in most use cases.
|
||||
/// E.g. bots equipping diving suits to clean up some items in flooded rooms.
|
||||
/// Sometimes, at least when used in orders, we might want to allow this. See <see cref="AIObjectiveLoadItem"/>.
|
||||
/// </summary>
|
||||
public bool AllowToFindDivingGear { get; set; }
|
||||
|
||||
public AIObjectiveDecontainItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, ItemContainer sourceContainer = null, ItemContainer targetContainer = null, float priorityModifier = 1)
|
||||
public AIObjectiveMoveItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, ItemContainer sourceContainer = null, ItemContainer targetContainer = null, float priorityModifier = 1)
|
||||
: base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
this.targetItem = targetItem;
|
||||
@@ -56,10 +63,10 @@ namespace Barotrauma
|
||||
this.targetContainer = targetContainer;
|
||||
}
|
||||
|
||||
public AIObjectiveDecontainItem(Character character, Identifier itemIdentifier, AIObjectiveManager objectiveManager, ItemContainer sourceContainer, ItemContainer targetContainer = null, float priorityModifier = 1)
|
||||
public AIObjectiveMoveItem(Character character, Identifier itemIdentifier, AIObjectiveManager objectiveManager, ItemContainer sourceContainer, ItemContainer targetContainer = null, float priorityModifier = 1)
|
||||
: this(character, new Identifier[] { itemIdentifier }, objectiveManager, sourceContainer, targetContainer, priorityModifier) { }
|
||||
|
||||
public AIObjectiveDecontainItem(Character character, Identifier[] itemIdentifiers, AIObjectiveManager objectiveManager, ItemContainer sourceContainer, ItemContainer targetContainer = null, float priorityModifier = 1)
|
||||
public AIObjectiveMoveItem(Character character, Identifier[] itemIdentifiers, AIObjectiveManager objectiveManager, ItemContainer sourceContainer, ItemContainer targetContainer = null, float priorityModifier = 1)
|
||||
: base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
this.itemIdentifiers = itemIdentifiers;
|
||||
@@ -71,20 +78,20 @@ namespace Barotrauma
|
||||
this.targetContainer = targetContainer;
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific() => IsCompleted;
|
||||
protected override bool CheckObjectiveState() => IsCompleted;
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
Item itemToDecontain =
|
||||
Item itemToMove =
|
||||
targetItem ??
|
||||
sourceContainer.Inventory.FindItem(i => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id) && !i.IgnoreByAI(character)), recursive: false);
|
||||
|
||||
if (itemToDecontain == null)
|
||||
if (itemToMove == null)
|
||||
{
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
if (itemToDecontain.IgnoreByAI(character))
|
||||
if (itemToMove.IgnoreByAI(character))
|
||||
{
|
||||
Abandon = true;
|
||||
return;
|
||||
@@ -96,19 +103,19 @@ namespace Barotrauma
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
if (itemToDecontain.Container != sourceContainer.Item)
|
||||
if (itemToMove.Container != sourceContainer.Item)
|
||||
{
|
||||
itemToDecontain.Drop(character);
|
||||
itemToMove.Drop(character);
|
||||
IsCompleted = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (targetContainer.Inventory.Contains(itemToDecontain))
|
||||
else if (targetContainer.Inventory.Contains(itemToMove))
|
||||
{
|
||||
IsCompleted = true;
|
||||
return;
|
||||
}
|
||||
if (getItemObjective == null && !itemToDecontain.IsOwnedBy(character))
|
||||
if (getItemObjective == null && !itemToMove.IsOwnedBy(character))
|
||||
{
|
||||
TryAddSubObjective(ref getItemObjective,
|
||||
constructor: () => new AIObjectiveGetItem(character, targetItem, objectiveManager, Equip)
|
||||
@@ -116,7 +123,8 @@ namespace Barotrauma
|
||||
CannotFindDialogueCondition = AbandonGetItemDialogueCondition,
|
||||
CannotFindDialogueIdentifierOverride = AbandonGetItemDialogueIdentifier,
|
||||
SpeakIfFails = AbandonGetItemDialogueIdentifier != null,
|
||||
TakeWholeStack = this.TakeWholeStack
|
||||
TakeWholeStack = TakeWholeStack,
|
||||
AllowToFindDivingGear = AllowToFindDivingGear
|
||||
},
|
||||
onAbandon: () => Abandon = true);
|
||||
return;
|
||||
@@ -124,7 +132,7 @@ namespace Barotrauma
|
||||
if (targetContainer != null)
|
||||
{
|
||||
TryAddSubObjective(ref containObjective,
|
||||
constructor: () => new AIObjectiveContainItem(character, itemToDecontain, targetContainer, objectiveManager)
|
||||
constructor: () => new AIObjectiveContainItem(character, itemToMove, targetContainer, objectiveManager)
|
||||
{
|
||||
MoveWholeStack = TakeWholeStack,
|
||||
Equip = Equip,
|
||||
@@ -133,14 +141,15 @@ namespace Barotrauma
|
||||
RemoveExistingPredicate = RemoveExistingPredicate,
|
||||
RemoveMax = RemoveExistingMax,
|
||||
GetItemPriority = GetItemPriority,
|
||||
ignoredContainerIdentifiers = sourceContainer?.Item.Prefab.Identifier.ToEnumerable().ToImmutableHashSet()
|
||||
ignoredContainerIdentifiers = sourceContainer?.Item.Prefab.Identifier.ToEnumerable().ToImmutableHashSet(),
|
||||
AllowToFindDivingGear = AllowToFindDivingGear
|
||||
},
|
||||
onCompleted: () => IsCompleted = true,
|
||||
onAbandon: () => Abandon = true);
|
||||
}
|
||||
else
|
||||
{
|
||||
itemToDecontain.Drop(character);
|
||||
itemToMove.Drop(character);
|
||||
IsCompleted = true;
|
||||
}
|
||||
}
|
||||
+88
-14
@@ -1,4 +1,5 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.Extensions;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -15,7 +16,7 @@ namespace Barotrauma
|
||||
public override bool AllowMultipleInstances => true;
|
||||
protected override bool AllowInAnySub => true;
|
||||
protected override bool AllowWhileHandcuffed => false;
|
||||
public override bool PrioritizeIfSubObjectivesActive => component != null && (component is Reactor || component is Turret);
|
||||
public override bool PrioritizeIfSubObjectivesActive => component is Reactor or Turret;
|
||||
|
||||
private readonly ItemComponent component, controller;
|
||||
private readonly Entity operateTarget;
|
||||
@@ -88,12 +89,23 @@ namespace Barotrauma
|
||||
Priority = 0;
|
||||
return Priority;
|
||||
}
|
||||
var reactor = component?.Item.GetComponent<Reactor>();
|
||||
Hull targetHull = targetItem.CurrentHull;
|
||||
if (HumanAIController.UnsafeHulls.Contains(targetHull))
|
||||
{
|
||||
// Ignore the objective, if the target hull is dangerous.
|
||||
Priority = 0;
|
||||
if (isOrder && this == objectiveManager.CurrentObjective && character.IsOnPlayerTeam)
|
||||
{
|
||||
character.Speak(TextManager.GetWithVariable("dialogoperatetargetroomisunsafe", "[item]", targetItem.Name).Value, delay: 1.0f, identifier: "dialogoperatetargetroomisunsafe".ToIdentifier(), minDurationBetweenSimilar: 5.0f);
|
||||
}
|
||||
return Priority;
|
||||
}
|
||||
var reactor = component.Item.GetComponent<Reactor>();
|
||||
if (reactor != null)
|
||||
{
|
||||
if (!isOrder)
|
||||
{
|
||||
if (reactor.LastUserWasPlayer && character.TeamID != CharacterTeamType.FriendlyNPC)
|
||||
if (reactor.LastUserWasPlayer && character.IsOnPlayerTeam)
|
||||
{
|
||||
// The reactor was previously operated by a player -> ignore.
|
||||
Priority = 0;
|
||||
@@ -126,7 +138,7 @@ namespace Barotrauma
|
||||
}
|
||||
else if (!isOrder)
|
||||
{
|
||||
var steering = component?.Item.GetComponent<Steering>();
|
||||
var steering = component.Item.GetComponent<Steering>();
|
||||
if (steering != null && (steering.AutoPilot || HumanAIController.IsTrueForAnyCrewMember(c => c != character && c.IsCaptain, onlyActive: true, onlyConnectedSubs: true)))
|
||||
{
|
||||
// Ignore if already set to autopilot or if there's a captain onboard
|
||||
@@ -136,10 +148,8 @@ namespace Barotrauma
|
||||
}
|
||||
if (targetItem.CurrentHull == null ||
|
||||
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))
|
||||
|| component.Item.IgnoreByAI(character) || useController && controller.Item.IgnoreByAI(character))
|
||||
IsItemOperatedByAnother(target) ||
|
||||
component.Item.IgnoreByAI(character) || useController && controller.Item.IgnoreByAI(character))
|
||||
{
|
||||
Priority = 0;
|
||||
}
|
||||
@@ -154,8 +164,8 @@ namespace Barotrauma
|
||||
else if (!OverridePriority.HasValue)
|
||||
{
|
||||
float value = CumulatedDevotion + (AIObjectiveManager.LowestOrderPriority * PriorityModifier);
|
||||
float max = AIObjectiveManager.LowestOrderPriority - 1;
|
||||
if (reactor != null && reactor.PowerOn && reactor.FissionRate > 1 && reactor.AutoTemp && Option == "powerup")
|
||||
const float max = AIObjectiveManager.LowestOrderPriority - 1;
|
||||
if (reactor is { PowerOn: true, FissionRate: > 1, AutoTemp: true } && Option == "powerup")
|
||||
{
|
||||
// Already on, no need to operate.
|
||||
value = 0;
|
||||
@@ -171,12 +181,12 @@ namespace Barotrauma
|
||||
Entity operateTarget = null, bool useController = false, ItemComponent controller = null, float priorityModifier = 1)
|
||||
: base(character, objectiveManager, priorityModifier, option)
|
||||
{
|
||||
component = item ?? throw new ArgumentNullException("item", "Attempted to create an AIObjectiveOperateItem with a null target.");
|
||||
component = item ?? throw new ArgumentNullException(nameof(item), "Attempted to create an AIObjectiveOperateItem with a null target.");
|
||||
this.requireEquip = requireEquip;
|
||||
this.operateTarget = operateTarget;
|
||||
this.useController = useController;
|
||||
if (useController) { this.controller = controller ?? component?.Item?.FindController(); }
|
||||
var target = GetTarget();
|
||||
if (useController) { this.controller = controller ?? component.Item?.FindController(); }
|
||||
ItemComponent target = GetTarget();
|
||||
if (target == null)
|
||||
{
|
||||
Abandon = true;
|
||||
@@ -245,6 +255,7 @@ namespace Barotrauma
|
||||
{
|
||||
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(target.Item, character, objectiveManager, closeEnough: 50)
|
||||
{
|
||||
DialogueIdentifier = AIObjectiveGoTo.DialogCannotReachTarget,
|
||||
TargetName = target.Item.Name,
|
||||
endNodeFilter = EndNodeFilter ?? AIObjectiveGetItem.CreateEndNodeFilter(target.Item)
|
||||
},
|
||||
@@ -312,7 +323,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific() => isDoneOperating && !Repeat;
|
||||
protected override bool CheckObjectiveState() => isDoneOperating && !Repeat;
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
@@ -320,5 +331,68 @@ namespace Barotrauma
|
||||
goToObjective = null;
|
||||
getItemObjective = null;
|
||||
}
|
||||
|
||||
private bool IsItemOperatedByAnother(ItemComponent target)
|
||||
{
|
||||
if (target?.Item == null) { return false; }
|
||||
bool isOrdered = IsOrderedToOperateTarget(HumanAIController);
|
||||
foreach (Character c in Character.CharacterList)
|
||||
{
|
||||
if (!HumanAIController.IsActive(c)) { continue; }
|
||||
if (c == character) { continue; }
|
||||
if (c.TeamID != character.TeamID) { continue; }
|
||||
if (c.IsPlayer)
|
||||
{
|
||||
if (c.SelectedItem == target.Item)
|
||||
{
|
||||
// If the other character is player, don't try to operate
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (c.AIController is HumanAIController otherAI)
|
||||
{
|
||||
if (otherAI.ObjectiveManager.Objectives.None(o => o is AIObjectiveOperateItem operateObjective && operateObjective.Component.Item == target.Item))
|
||||
{
|
||||
// Not targeting the same item.
|
||||
continue;
|
||||
}
|
||||
bool isOtherCharacterOrdered = IsOrderedToOperateTarget(otherAI);
|
||||
switch (isOrdered)
|
||||
{
|
||||
case false when isOtherCharacterOrdered:
|
||||
// We are not ordered and the target is ordered -> let the other character operate the target item.
|
||||
return true;
|
||||
case true when !isOtherCharacterOrdered:
|
||||
// We are ordered and the other character is not -> allow to us to operate the target item.
|
||||
continue;
|
||||
default:
|
||||
{
|
||||
// Neither or both are ordered to operate this item.
|
||||
if (!IsOperatingTarget(otherAI))
|
||||
{
|
||||
// The other bot is doing something else -> stick to the target.
|
||||
continue;
|
||||
}
|
||||
if (target is Steering)
|
||||
{
|
||||
// Steering is hard-coded -> cannot use the required skills collection defined in the xml
|
||||
if (character.GetSkillLevel(Tags.HelmSkill) <= c.GetSkillLevel(Tags.HelmSkill))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (target.DegreeOfSuccess(character) <= target.DegreeOfSuccess(c))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
bool IsOrderedToOperateTarget(HumanAIController ai) => ai.ObjectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder.Component.Item == target.Item;
|
||||
bool IsOperatingTarget(HumanAIController ai) => ai.ObjectiveManager.CurrentObjective is AIObjectiveOperateItem operateObjective && operateObjective.Component.Item == target.Item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific() => IsCompleted;
|
||||
protected override bool CheckObjectiveState() => IsCompleted;
|
||||
|
||||
protected override float GetPriority()
|
||||
{
|
||||
|
||||
+3
-2
@@ -50,7 +50,7 @@ namespace Barotrauma
|
||||
}
|
||||
return Priority;
|
||||
}
|
||||
if (HumanAIController.IsItemRepairedByAnother(Item, out _))
|
||||
if (AIObjectiveRepairItems.IsItemRepairedByAnother(character, Item))
|
||||
{
|
||||
Priority = 0;
|
||||
IsCompleted = true;
|
||||
@@ -91,7 +91,7 @@ namespace Barotrauma
|
||||
return Priority;
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific()
|
||||
protected override bool CheckObjectiveState()
|
||||
{
|
||||
IsCompleted = Item.IsFullCondition;
|
||||
if (character.IsOnPlayerTeam && IsCompleted && IsRepairing())
|
||||
@@ -234,6 +234,7 @@ namespace Barotrauma
|
||||
{
|
||||
var objective = new AIObjectiveGoTo(Item, character, objectiveManager)
|
||||
{
|
||||
DialogueIdentifier = AIObjectiveGoTo.DialogCannotReachTarget,
|
||||
TargetName = Item.Name,
|
||||
SpeakCannotReachCondition = () => isPriority
|
||||
};
|
||||
|
||||
+53
-1
@@ -76,7 +76,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (item.Repairables.None(r => r.RequiredSkills.Any(s => s.Identifier == RelevantSkill))) { return false; }
|
||||
}
|
||||
return !HumanAIController.IsItemRepairedByAnother(item, out _);
|
||||
return !IsItemRepairedByAnother(character, item);
|
||||
}
|
||||
|
||||
public static bool ViableForRepair(Item item, Character character, HumanAIController humanAIController)
|
||||
@@ -161,5 +161,57 @@ namespace Barotrauma
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool IsItemRepairedByAnother(Character character, Item target)
|
||||
{
|
||||
if (target == null) { return false; }
|
||||
bool isOrder = IsOrderedToPrioritizeTarget(character.AIController as HumanAIController);
|
||||
foreach (Character c in Character.CharacterList)
|
||||
{
|
||||
if (!HumanAIController.IsActive(c)) { continue; }
|
||||
if (c == character) { continue; }
|
||||
if (c.TeamID != character.TeamID) { continue; }
|
||||
if (c.IsPlayer)
|
||||
{
|
||||
if (target.Repairables.Any(r => r.CurrentFixer == c))
|
||||
{
|
||||
// If the other character is player, don't try to repair
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (c.AIController is HumanAIController otherAI)
|
||||
{
|
||||
var repairItemsObjective = otherAI.ObjectiveManager.GetObjective<AIObjectiveRepairItems>();
|
||||
if (repairItemsObjective == null) { continue; }
|
||||
if (repairItemsObjective.SubObjectives.FirstOrDefault(o => o is AIObjectiveRepairItem) is not AIObjectiveRepairItem activeObjective || activeObjective.Item != target)
|
||||
{
|
||||
// Not targeting the same item.
|
||||
continue;
|
||||
}
|
||||
bool isTargetOrdered = IsOrderedToPrioritizeTarget(otherAI);
|
||||
switch (isOrder)
|
||||
{
|
||||
case false when isTargetOrdered:
|
||||
// We are not ordered and the target is ordered -> let the other character repair the target.
|
||||
return true;
|
||||
case true when !isTargetOrdered:
|
||||
// We are ordered and the target is not -> allow us to repair the target.
|
||||
continue;
|
||||
default:
|
||||
{
|
||||
// Neither or both are ordered to repair this item.
|
||||
if (otherAI.ObjectiveManager.CurrentObjective is not AIObjectiveRepairItems)
|
||||
{
|
||||
// The other bot is doing something else -> stick to the target.
|
||||
continue;
|
||||
}
|
||||
return target.Repairables.Max(r => r.DegreeOfSuccess(character)) <= target.Repairables.Max(r => r.DegreeOfSuccess(c));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
bool IsOrderedToPrioritizeTarget(HumanAIController ai) => ai.ObjectiveManager.CurrentOrder is AIObjectiveRepairItems repairOrder && repairOrder.PrioritizedItem == target;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+37
-20
@@ -70,7 +70,7 @@ namespace Barotrauma
|
||||
if (otherRescuer != null && otherRescuer != character)
|
||||
{
|
||||
// Someone else is rescuing/holding the target.
|
||||
Abandon = otherRescuer.IsPlayer || character.GetSkillLevel("medical") < otherRescuer.GetSkillLevel("medical");
|
||||
Abandon = otherRescuer.IsPlayer || character.GetSkillLevel(Tags.MedicalSkill) < otherRescuer.GetSkillLevel(Tags.MedicalSkill);
|
||||
return;
|
||||
}
|
||||
if (Target != character)
|
||||
@@ -149,7 +149,7 @@ namespace Barotrauma
|
||||
if (HumanAIController.VisibleHulls.Contains(Target.CurrentHull) && Target.CurrentHull.DisplayName != null)
|
||||
{
|
||||
character.Speak(TextManager.GetWithVariables("DialogFoundUnconsciousTarget",
|
||||
("[targetname]", Target.Name, FormatCapitals.No),
|
||||
("[targetname]", Target.DisplayName, FormatCapitals.No),
|
||||
("[roomname]", Target.CurrentHull.DisplayName, FormatCapitals.Yes)).Value,
|
||||
null, 1.0f, $"foundunconscioustarget{Target.Name}".ToIdentifier(), 60.0f);
|
||||
}
|
||||
@@ -161,7 +161,7 @@ namespace Barotrauma
|
||||
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(Target, character, objectiveManager)
|
||||
{
|
||||
CloseEnough = CloseEnoughToTreat,
|
||||
DialogueIdentifier = "dialogcannotreachpatient".ToIdentifier(),
|
||||
DialogueIdentifier = AIObjectiveGoTo.DialogCannotReachPatient,
|
||||
TargetName = Target.DisplayName
|
||||
},
|
||||
onCompleted: () => RemoveSubObjective(ref goToObjective),
|
||||
@@ -197,13 +197,16 @@ namespace Barotrauma
|
||||
{
|
||||
RemoveSubObjective(ref replaceOxygenObjective);
|
||||
RemoveSubObjective(ref goToObjective);
|
||||
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(safeHull, character, objectiveManager),
|
||||
onCompleted: () => RemoveSubObjective(ref goToObjective),
|
||||
onAbandon: () =>
|
||||
{
|
||||
RemoveSubObjective(ref goToObjective);
|
||||
safeHull = character.CurrentHull;
|
||||
});
|
||||
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(safeHull, character, objectiveManager)
|
||||
{
|
||||
DialogueIdentifier = AIObjectiveGoTo.DialogCannotReachPlace
|
||||
},
|
||||
onCompleted: () => RemoveSubObjective(ref goToObjective),
|
||||
onAbandon: () =>
|
||||
{
|
||||
RemoveSubObjective(ref goToObjective);
|
||||
safeHull = character.CurrentHull;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -221,7 +224,7 @@ namespace Barotrauma
|
||||
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(Target, character, objectiveManager)
|
||||
{
|
||||
CloseEnough = CloseEnoughToTreat,
|
||||
DialogueIdentifier = "dialogcannotreachpatient".ToIdentifier(),
|
||||
DialogueIdentifier = AIObjectiveGoTo.DialogCannotReachPatient,
|
||||
TargetName = Target.DisplayName
|
||||
},
|
||||
onCompleted: () => RemoveSubObjective(ref goToObjective),
|
||||
@@ -239,7 +242,7 @@ namespace Barotrauma
|
||||
if (Target.CurrentHull?.DisplayName != null)
|
||||
{
|
||||
character.Speak(TextManager.GetWithVariables("DialogFoundWoundedTarget",
|
||||
("[targetname]", Target.Name, FormatCapitals.No),
|
||||
("[targetname]", Target.DisplayName, FormatCapitals.No),
|
||||
("[roomname]", Target.CurrentHull.DisplayName, FormatCapitals.Yes)).Value,
|
||||
null, 1.0f, $"foundwoundedtarget{Target.Name}".ToIdentifier(), 60.0f);
|
||||
}
|
||||
@@ -287,6 +290,8 @@ namespace Barotrauma
|
||||
currentTreatmentSuitabilities,
|
||||
limb: Target.CharacterHealth.GetAfflictionLimb(affliction),
|
||||
user: character,
|
||||
checkTreatmentThreshold: true,
|
||||
checkTreatmentSuggestionThreshold: false,
|
||||
predictFutureDuration: 10.0f);
|
||||
|
||||
foreach (KeyValuePair<Identifier, float> treatmentSuitability in currentTreatmentSuitabilities)
|
||||
@@ -330,7 +335,10 @@ namespace Barotrauma
|
||||
{
|
||||
//get "overall" suitability for no specific limb at this point
|
||||
Target.CharacterHealth.GetSuitableTreatments(
|
||||
currentTreatmentSuitabilities, user: character, predictFutureDuration: 10.0f);
|
||||
currentTreatmentSuitabilities, user: character,
|
||||
checkTreatmentThreshold: true,
|
||||
checkTreatmentSuggestionThreshold: false,
|
||||
predictFutureDuration: 10.0f);
|
||||
//didn't have any suitable treatments available, try to find some medical items
|
||||
if (currentTreatmentSuitabilities.Any(s => s.Value > cprSuitability))
|
||||
{
|
||||
@@ -387,13 +395,22 @@ namespace Barotrauma
|
||||
if (Target != character && character.IsOnPlayerTeam)
|
||||
{
|
||||
character.Speak(TextManager.GetWithVariables("DialogListRequiredTreatments",
|
||||
("[targetname]", Target.Name, FormatCapitals.No),
|
||||
("[targetname]", Target.DisplayName, FormatCapitals.No),
|
||||
("[treatmentlist]", itemListStr, FormatCapitals.Yes)).Value,
|
||||
null, 2.0f, $"listrequiredtreatments{Target.Name}".ToIdentifier(), 60.0f);
|
||||
}
|
||||
|
||||
var itemsToFind = currentTreatmentSuitabilities
|
||||
//items that have a positive effect and that the bot doesn't yet have
|
||||
.Where(kvp => kvp.Value > 0.0f && character.Inventory.AllItems.None(it => it.Prefab.Identifier == kvp.Key))
|
||||
.Select(kvp => kvp.Key);
|
||||
|
||||
RemoveSubObjective(ref getItemObjective);
|
||||
TryAddSubObjective(ref getItemObjective,
|
||||
constructor: () => new AIObjectiveGetItem(character, suitableItemIdentifiers.ToArray(), objectiveManager, equip: true, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC),
|
||||
constructor: () => new AIObjectiveGetItem(character, itemsToFind, objectiveManager, equip: true, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
GetItemPriority = it => currentTreatmentSuitabilities.GetValueOrDefault(it.Prefab.Identifier)
|
||||
},
|
||||
onCompleted: () => RemoveSubObjective(ref getItemObjective),
|
||||
onAbandon: () =>
|
||||
{
|
||||
@@ -468,16 +485,16 @@ namespace Barotrauma
|
||||
item.ApplyTreatment(character, Target, Target.CharacterHealth.GetAfflictionLimb(affliction));
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific()
|
||||
protected override bool CheckObjectiveState()
|
||||
{
|
||||
bool isCompleted = AIObjectiveRescueAll.GetVitalityFactor(Target) >= AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager, character, Target);
|
||||
if (isCompleted && Target != character && character.IsOnPlayerTeam)
|
||||
IsCompleted = AIObjectiveRescueAll.GetVitalityFactor(Target) >= AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager, character, Target);
|
||||
if (IsCompleted && Target != character && character.IsOnPlayerTeam)
|
||||
{
|
||||
string textTag = performedCpr ? "DialogTargetResuscitated" : "DialogTargetHealed";
|
||||
string message = TextManager.GetWithVariable(textTag, "[targetname]", Target.Name)?.Value;
|
||||
string message = TextManager.GetWithVariable(textTag, "[targetname]", Target.DisplayName)?.Value;
|
||||
character.Speak(message, delay: 1.0f, identifier: $"targethealed{Target.Name}".ToIdentifier(), minDurationBetweenSimilar: 60.0f);
|
||||
}
|
||||
return isCompleted;
|
||||
return IsCompleted;
|
||||
}
|
||||
|
||||
protected override float GetPriority()
|
||||
|
||||
+15
-8
@@ -47,9 +47,9 @@ namespace Barotrauma
|
||||
if (objectiveManager.GetFirstActiveObjective<AIObjectiveRescue>() == null)
|
||||
{
|
||||
charactersWithMinorInjuries.Add(target);
|
||||
character.Speak(TextManager.GetWithVariable("dialogignoreminorinjuries", "[targetname]", target.Name).Value,
|
||||
character.Speak(TextManager.GetWithVariable("dialogignoreminorinjuries", "[targetname]", target.DisplayName).Value,
|
||||
delay: 1.0f,
|
||||
identifier: $"notreatableafflictions{target.Name}".ToIdentifier(),
|
||||
identifier: $"notreatableafflictions{target.DisplayName}".ToIdentifier(),
|
||||
minDurationBetweenSimilar: 10.0f);
|
||||
}
|
||||
}
|
||||
@@ -96,13 +96,20 @@ namespace Barotrauma
|
||||
{
|
||||
float strength = character.CharacterHealth.GetPredictedStrength(affliction, predictFutureDuration: 10.0f);
|
||||
vitality -= affliction.GetVitalityDecrease(character.CharacterHealth, strength) / character.MaxVitality * 100;
|
||||
if (affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType)
|
||||
if (affliction.Strength > affliction.Prefab.TreatmentThreshold)
|
||||
{
|
||||
vitality -= affliction.Strength;
|
||||
}
|
||||
else if (affliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType)
|
||||
{
|
||||
vitality -= affliction.Strength;
|
||||
if (affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType)
|
||||
{
|
||||
vitality -= affliction.Strength;
|
||||
}
|
||||
else if (affliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType)
|
||||
{
|
||||
vitality -= affliction.Strength;
|
||||
}
|
||||
else if (affliction.Prefab == AfflictionPrefab.HuskInfection)
|
||||
{
|
||||
vitality -= affliction.Strength;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Math.Clamp(vitality, 0, 100);
|
||||
|
||||
+16
-17
@@ -1,4 +1,4 @@
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Extensions;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Collections.Generic;
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace Barotrauma
|
||||
class AIObjectiveReturn : AIObjective
|
||||
{
|
||||
public override Identifier Identifier { get; set; } = "return".ToIdentifier();
|
||||
public Submarine ReturnTarget { get; }
|
||||
public Submarine Target { get; }
|
||||
|
||||
private AIObjectiveGoTo moveInsideObjective, moveOutsideObjective;
|
||||
private bool usingEscapeBehavior, isSteeringThroughGap;
|
||||
@@ -17,10 +17,13 @@ namespace Barotrauma
|
||||
|
||||
public AIObjectiveReturn(Character character, Character orderGiver, AIObjectiveManager objectiveManager, float priorityModifier = 1.0f) : base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
ReturnTarget = GetReturnTarget(Submarine.MainSubs) ?? GetReturnTarget(Submarine.Loaded);
|
||||
if (ReturnTarget == null)
|
||||
Target = GetReturnTarget(Submarine.MainSubs) ?? GetReturnTarget(Submarine.Loaded);
|
||||
if (Target == null)
|
||||
{
|
||||
DebugConsole.AddSafeError("Error with a Return objective: no suitable return target found");
|
||||
if (GameMain.GameSession?.GameMode is not TestGameMode)
|
||||
{
|
||||
DebugConsole.AddWarning($"({character.DisplayName}) No suitable return target found. Cannot return back to the main sub.");
|
||||
}
|
||||
Abandon = true;
|
||||
}
|
||||
|
||||
@@ -54,7 +57,7 @@ namespace Barotrauma
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
if (ReturnTarget == null)
|
||||
if (Target == null)
|
||||
{
|
||||
Abandon = true;
|
||||
return;
|
||||
@@ -62,7 +65,7 @@ namespace Barotrauma
|
||||
bool shouldUseEscapeBehavior = false;
|
||||
if (character.CurrentHull != null || isSteeringThroughGap)
|
||||
{
|
||||
if (character.Submarine == null || !character.Submarine.IsConnectedTo(ReturnTarget))
|
||||
if (character.Submarine == null || !character.Submarine.IsConnectedTo(Target))
|
||||
{
|
||||
// Character is on another sub that is not connected to the target sub, use the escape behavior to get them out
|
||||
shouldUseEscapeBehavior = true;
|
||||
@@ -76,13 +79,13 @@ namespace Barotrauma
|
||||
Abandon = true;
|
||||
}
|
||||
}
|
||||
else if (character.Submarine != ReturnTarget)
|
||||
else if (character.Submarine != Target)
|
||||
{
|
||||
// Character is on another sub that is connected to the target sub, create a Go To objective to reach the target sub
|
||||
if (moveInsideObjective == null)
|
||||
{
|
||||
Hull targetHull = null;
|
||||
foreach (var d in ReturnTarget.ConnectedDockingPorts.Values)
|
||||
foreach (var d in Target.ConnectedDockingPorts.Values)
|
||||
{
|
||||
if (!d.Docked) { continue; }
|
||||
if (d.DockingTarget == null) { continue; }
|
||||
@@ -143,7 +146,7 @@ namespace Barotrauma
|
||||
Hull targetHull = null;
|
||||
float targetDistanceSquared = float.MaxValue;
|
||||
bool targetIsAirlock = false;
|
||||
foreach (var hull in ReturnTarget.GetHulls(false))
|
||||
foreach (var hull in Target.GetHulls(false))
|
||||
{
|
||||
bool hullIsAirlock = hull.IsAirlock;
|
||||
if(hullIsAirlock || (!targetIsAirlock && hull.LeadsOutside(character)))
|
||||
@@ -178,18 +181,14 @@ namespace Barotrauma
|
||||
usingEscapeBehavior = shouldUseEscapeBehavior;
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific()
|
||||
protected override bool CheckObjectiveState()
|
||||
{
|
||||
if (IsCompleted)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (ReturnTarget == null)
|
||||
if (Target == null)
|
||||
{
|
||||
Abandon = true;
|
||||
return false;
|
||||
}
|
||||
if (character.Submarine == ReturnTarget)
|
||||
if (character.Submarine == Target)
|
||||
{
|
||||
IsCompleted = true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user