v1.6.17.0 (Unto the Breach update)

This commit is contained in:
Regalis11
2024-10-22 17:29:04 +03:00
parent e74b3cdb17
commit 6e6c17e100
417 changed files with 17166 additions and 5870 deletions
@@ -191,7 +191,7 @@ namespace Barotrauma
{
if (InDetectable) { return true; }
if (Entity == null) { return true; }
if (Level.Loaded != null && WorldPosition.Y > Level.Loaded.Size.Y)
if (Level.IsPositionAboveLevel(WorldPosition))
{
return true;
}
File diff suppressed because it is too large Load Diff
@@ -317,18 +317,21 @@ namespace Barotrauma
{
obstacleRaycastTimer = obstacleRaycastIntervalShort;
// Swimming outside and using the path finder -> check that the path is not blocked with anything (the path finder doesn't know about other subs).
foreach (var connectedSub in Submarine.MainSub.GetConnectedSubs())
if (Submarine.MainSub != null)
{
if (connectedSub == Submarine.MainSub) { continue; }
Vector2 rayStart = SimPosition - connectedSub.SimPosition;
Vector2 dir = PathSteering.CurrentPath.CurrentNode.WorldPosition - WorldPosition;
Vector2 rayEnd = rayStart + dir.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 5);
if (Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true) != null)
foreach (var connectedSub in Submarine.MainSub.GetConnectedSubs())
{
PathSteering.CurrentPath.Unreachable = true;
break;
if (connectedSub == Submarine.MainSub) { continue; }
Vector2 rayStart = SimPosition - connectedSub.SimPosition;
Vector2 dir = PathSteering.CurrentPath.CurrentNode.WorldPosition - WorldPosition;
Vector2 rayEnd = rayStart + dir.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 5);
if (Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true) != null)
{
PathSteering.CurrentPath.Unreachable = true;
break;
}
}
}
}
}
}
}
@@ -801,36 +804,41 @@ namespace Barotrauma
if (isCarrying) { return; }
if (!ObjectiveManager.CurrentObjective.AllowAutomaticItemUnequipping || !ObjectiveManager.GetActiveObjective().AllowAutomaticItemUnequipping) { return; }
if (findItemState == FindItemState.None || findItemState == FindItemState.OtherItem)
if (Character.Submarine?.TeamID == Character.TeamID && findItemState is FindItemState.None or FindItemState.OtherItem)
{
// Only unequip other items inside a friendly sub.
foreach (Item item in Character.HeldItems)
{
if (item == null || !item.IsInteractable(Character)) { continue; }
if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, CharacterInventory.AnySlot) && Character.Submarine?.TeamID == Character.TeamID)
if (Character.TryPutItemInAnySlot(item)) { continue; }
if (Character.TryPutItemInBag(item)) { continue; }
if (item.HasTag(Tags.Weapon))
{
if (item.AllowedSlots.Contains(InvSlotType.Bag) && Character.Inventory.TryPutItem(item, Character, new List<InvSlotType>() { InvSlotType.Bag })) { continue; }
findItemState = FindItemState.OtherItem;
if (FindSuitableContainer(item, out Item targetContainer))
// Don't decontain weapons, because it could be that we are holding a weapon that cannot be placed on back (if we have a toolbelt) nor in the any slot, such as an HMG.
// Could check that we only ignore weapons when we've had an order to find a weapon, but it could also be that we picked the weapon for self-defence, on ad-hoc basis.
// And I don't think it would make sense to decontain those weapons either.
continue;
}
findItemState = FindItemState.OtherItem;
if (FindSuitableContainer(item, out Item targetContainer))
{
findItemState = FindItemState.None;
itemIndex = 0;
if (targetContainer != null)
{
findItemState = FindItemState.None;
itemIndex = 0;
if (targetContainer != null)
var decontainObjective = new AIObjectiveDecontainItem(Character, item, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
decontainObjective.Abandoned += () =>
{
var decontainObjective = new AIObjectiveDecontainItem(Character, item, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
decontainObjective.Abandoned += () =>
{
ReequipUnequipped();
IgnoredItems.Add(targetContainer);
};
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
return;
}
else
{
item.Drop(Character);
HandleRelocation(item);
}
ReequipUnequipped();
IgnoredItems.Add(targetContainer);
};
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
return;
}
else
{
item.Drop(Character);
HandleRelocation(item);
}
}
}
@@ -842,7 +850,7 @@ namespace Barotrauma
public void HandleRelocation(Item item)
{
if (item.SpawnedInCurrentOutpost) { return; }
if (item.Submarine == null) { return; }
if (item.Submarine == null || Submarine.MainSub == null) { return; }
// Only affects bots in the player team
if (!Character.IsOnPlayerTeam) { return; }
// Don't relocate if the item is on a sub of the same team
@@ -869,6 +877,7 @@ namespace Barotrauma
if (item == null || item.Removed) { return; }
if (!itemsToRelocate.Contains(item)) { return; }
var mainSub = Submarine.MainSub;
if (mainSub == null) { return; }
Entity owner = item.GetRootInventoryOwner();
if (owner != null)
{
@@ -1295,7 +1304,13 @@ namespace Barotrauma
//if (Character.LastDamageSource == null) { return; }
//AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, Rand.Range(0.5f, 1f, Rand.RandSync.Unsynced));
}
if (realDamage <= 0 && (attacker.IsBot || attacker.TeamID == Character.TeamID))
bool sameTeam =
attacker.TeamID == Character.TeamID ||
// consider escorted characters to be in the same team (otherwise accidental damage or side-effects from healing trigger them too easily)
(attacker.TeamID == CharacterTeamType.Team1 && Character.IsEscorted);
if (realDamage <= 0 && (attacker.IsBot || sameTeam))
{
// Don't react to damage that is entirely based on karma penalties (medics, poisons etc), unless applier is player
return;
@@ -1307,9 +1322,9 @@ namespace Barotrauma
}
bool isAttackerInfected = false;
bool isAttackerFightingEnemy = false;
float minorDamageThreshold = 1;
float minorDamageThreshold = 5;
float majorDamageThreshold = 20;
if (attacker.TeamID == Character.TeamID && !attacker.IsInstigator)
if (sameTeam && !attacker.IsInstigator)
{
minorDamageThreshold = 10;
majorDamageThreshold = 40;
@@ -2168,15 +2183,15 @@ namespace Barotrauma
float fireFactor = 1;
if (!ignoreFire)
{
static float calculateFire(Hull h) => h.FireSources.Count * 0.5f + h.FireSources.Sum(fs => fs.DamageRange) / h.Size.X;
static float CalculateFire(Hull h) => h.FireSources.Count * 0.5f + h.FireSources.Sum(fs => fs.DamageRange) / h.Size.X;
// Even the smallest fire reduces the safety by 50%
float fire = visibleHulls == null ? calculateFire(hull) : visibleHulls.Sum(h => calculateFire(h));
float fire = visibleHulls?.Sum(CalculateFire) ?? CalculateFire(hull);
fireFactor = MathHelper.Lerp(1, 0, MathHelper.Clamp(fire, 0, 1));
}
float enemyFactor = 1;
if (!ignoreEnemies)
{
int enemyCount = 0;
int enemyCount = 0;
foreach (Character c in Character.CharacterList)
{
if (visibleHulls == null)
@@ -2476,7 +2491,7 @@ namespace Barotrauma
{
other = null;
if (target?.Item == null) { return false; }
bool isOrder = IsOrderedToOperateThis(Character.AIController);
bool isOrder = IsOrderedToOperateTarget(this);
foreach (Character c in Character.CharacterList)
{
if (!IsActive(c)) { continue; }
@@ -2491,14 +2506,14 @@ namespace Barotrauma
break;
}
}
else if (c.AIController is HumanAIController operatingAI)
else if (c.AIController is HumanAIController otherAI)
{
if (operatingAI.ObjectiveManager.Objectives.None(o => o is AIObjectiveOperateItem operateObjective && operateObjective.Component.Item == target.Item))
if (otherAI.ObjectiveManager.Objectives.None(o => o is AIObjectiveOperateItem operateObjective && operateObjective.Component.Item == target.Item))
{
// Not targeting the same item.
continue;
}
bool isTargetOrdered = IsOrderedToOperateThis(c.AIController);
bool isTargetOrdered = IsOrderedToOperateTarget(otherAI);
if (!isOrder && isTargetOrdered)
{
// If the other bot is ordered to operate the item, let him do it, unless we are ordered too
@@ -2514,15 +2529,15 @@ namespace Barotrauma
}
else
{
if (!isTargetOrdered && operatingAI.ObjectiveManager.CurrentOrder != operatingAI.ObjectiveManager.CurrentObjective)
if (!IsOperatingTarget(otherAI))
{
// The other bot is ordered to do something else
// 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("helm") <= c.GetSkillLevel("helm"))
if (Character.GetSkillLevel(Tags.HelmSkill) <= c.GetSkillLevel(Tags.HelmSkill))
{
other = c;
break;
@@ -2538,7 +2553,8 @@ namespace Barotrauma
}
}
return other != null;
bool IsOrderedToOperateThis(AIController ai) => ai is HumanAIController humanAI && humanAI.ObjectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder.Component.Item == target.Item;
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;
}
public bool IsItemRepairedByAnother(Item target, out Character other)
@@ -320,8 +320,7 @@ namespace Barotrauma
Vector2 pos = host.WorldPosition;
Vector2 diff = currentPath.CurrentNode.WorldPosition - pos;
bool isDiving = character.AnimController.InWater && character.AnimController.HeadInWater;
// Only humanoids can climb ladders
bool canClimb = character.AnimController is HumanoidAnimController && !character.LockHands;
bool canClimb = character.CanClimb;
Ladder currentLadder = GetCurrentLadder();
Ladder nextLadder = GetNextLadder();
var ladders = currentLadder ?? nextLadder;
@@ -559,26 +558,41 @@ namespace Barotrauma
}
else
{
// We'll want this to run each time, because the delegate is used to find a valid button component.
bool canAccessButtons = false;
foreach (var button in door.Item.GetConnectedComponents<Controller>(true, connectionFilter: c => c.Name == "toggle" || c.Name == "set_state"))
bool buttonsFound = false;
// Check wired controllers (e.g. buttons)
// Always run the buttonFilter delegate (inside CanAccessButton method), if defined, because it's used for find a valid controller component that can be used for closing the door, when needed.
foreach (Controller button in door.Item.GetConnectedComponents<Controller>(recursive: true, connectionFilter: c => c.Name is "toggle" or "set_state"))
{
if (button.HasAccess(character) && (buttonFilter == null || buttonFilter(button)))
buttonsFound = true;
if (CanAccessButton(button))
{
canAccessButtons = true;
}
}
foreach (var linked in door.Item.linkedTo)
if (!canAccessButtons)
{
if (linked is not Item linkedItem) { continue; }
var button = linkedItem.GetComponent<Controller>();
if (button == null) { continue; }
if (button.HasAccess(character) && (buttonFilter == null || buttonFilter(button)))
// Check linked controllers (more complex circuits)
foreach (MapEntity linked in door.Item.linkedTo)
{
canAccessButtons = true;
}
}
return canAccessButtons || door.IsOpen || ShouldBreakDoor(door);
if (linked is not Item linkedItem) { continue; }
var button = linkedItem.GetComponent<Controller>();
if (button == null) { continue; }
buttonsFound = true;
if (CanAccessButton(button))
{
canAccessButtons = true;
}
}
}
if (door.IsOpen || ShouldBreakDoor(door))
{
return true;
}
// If no buttons were found, just trust it if we should have the access to the door. Could be there's some other mechanism controlling the door.
return buttonsFound ? canAccessButtons : door.HasAccess(character);
bool CanAccessButton(Controller button) => button.HasAccess(character) && (buttonFilter == null || buttonFilter(button));
}
}
@@ -796,10 +810,9 @@ namespace Barotrauma
float? penalty = GetSingleNodePenalty(nextNode);
if (penalty == null) { return null; }
bool nextNodeAboveWaterLevel = nextNode.Waypoint.CurrentHull != null && nextNode.Waypoint.CurrentHull.Surface < nextNode.Waypoint.Position.Y;
//non-humanoids can't climb up ladders
if (!(character.AnimController is HumanoidAnimController))
if (!character.CanClimb)
{
if (node.Waypoint.Ladders != null && nextNode.Waypoint.Ladders != null && (!nextNode.Waypoint.Ladders.Item.IsInteractable(character) || character.LockHands)||
if (node.Waypoint.Ladders != null && nextNode.Waypoint.Ladders != null && (!nextNode.Waypoint.Ladders.Item.IsInteractable(character) || character.LockHands) ||
(nextNode.Position.Y - node.Position.Y > 1.0f && //more than one sim unit to climb up
nextNodeAboveWaterLevel)) //upper node not underwater
{
@@ -847,7 +860,7 @@ namespace Barotrauma
if (!node.Waypoint.IsTraversable) { return null; }
if (node.IsBlocked()) { return null; }
float penalty = 0.0f;
if (node.Waypoint.ConnectedGap != null && node.Waypoint.ConnectedGap.Open < 0.9f)
if (node.Waypoint.ConnectedGap is { Open: < 0.9f })
{
var door = node.Waypoint.ConnectedDoor;
if (door == null)
@@ -858,19 +871,19 @@ namespace Barotrauma
{
if (!CanAccessDoor(door, button =>
{
// Ignore buttons that are on the wrong side of the door
// Ignore buttons that are on the wrong side of the door, unless there's a motion sensor connected to the door, which can be triggered by the character.
if (door.IsHorizontal)
{
if (Math.Sign(button.Item.WorldPosition.Y - door.Item.WorldPosition.Y) != Math.Sign(character.WorldPosition.Y - door.Item.WorldPosition.Y))
{
return false;
return door.Item.GetDirectlyConnectedComponent<MotionSensor>() is MotionSensor ms && ms.TriggersOn(character);
}
}
else
{
if (Math.Sign(button.Item.WorldPosition.X - door.Item.WorldPosition.X) != Math.Sign(character.WorldPosition.X - door.Item.WorldPosition.X))
{
return false;
return door.Item.GetDirectlyConnectedComponent<MotionSensor>() is MotionSensor ms && ms.TriggersOn(character);
}
}
return true;
@@ -308,7 +308,7 @@ namespace Barotrauma
if (enemyAI.AttackLimb == null) { break; }
if (targetBody == null) { break; }
if (IsAttached && AttachJoints[0].BodyB == targetBody) { break; }
Vector2 referencePos = TargetCharacter != null ? TargetCharacter.WorldPosition : ConvertUnits.ToDisplayUnits(transformedAttachPos);
Vector2 referencePos = TargetCharacter?.WorldPosition ?? ConvertUnits.ToDisplayUnits(transformedAttachPos);
if (Vector2.DistanceSquared(referencePos, enemyAI.AttackLimb.WorldPosition) < enemyAI.AttackLimb.attack.DamageRange * enemyAI.AttackLimb.attack.DamageRange)
{
AttachToBody(transformedAttachPos);
@@ -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; }
@@ -45,7 +45,7 @@ namespace Barotrauma
InitTimers();
}
protected override bool CheckObjectiveSpecific() => false;
protected override bool CheckObjectiveState() => false;
protected override float GetPriority()
{
@@ -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))
{
@@ -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; }
@@ -257,7 +257,7 @@ namespace Barotrauma
}
}
protected override bool CheckObjectiveSpecific()
protected override bool CheckObjectiveState()
{
if (character.Submarine is { TeamID: CharacterTeamType.FriendlyNPC } && character.Submarine == Enemy.Submarine)
{
@@ -898,23 +898,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 +919,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);
@@ -1322,8 +1320,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 +1349,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,7 +1369,8 @@ 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)
@@ -1420,11 +1419,8 @@ namespace Barotrauma
break;
}
case MeleeWeapon mw:
{
if (character.AnimController is HumanoidAnimController { Crouching: false })
{
reloadTime = mw.Reload;
}
{
reloadTime = mw.Reload;
break;
}
}
@@ -1485,7 +1481,7 @@ namespace Barotrauma
}
if (ShouldUnequipWeapon)
{
Unequip();
UnequipWeapon();
}
SteeringManager?.Reset();
}
@@ -1495,7 +1491,7 @@ namespace Barotrauma
base.OnAbandon();
if (ShouldUnequipWeapon)
{
Unequip();
UnequipWeapon();
}
SteeringManager?.Reset();
}
@@ -77,9 +77,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;
@@ -15,6 +15,7 @@ namespace Barotrauma
private Deconstructor deconstructor;
private AIObjectiveDecontainItem decontainObjective;
private AIObjectiveGoTo gotoObjective;
public AIObjectiveDeconstructItem(Item item, Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
@@ -45,14 +46,24 @@ 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();
}
else
{
TryAddSubObjective(ref gotoObjective,
constructor: () => new AIObjectiveGoTo(Item, character, objectiveManager, priorityModifier: PriorityModifier),
onCompleted: () =>
{
StartDeconstruction();
RemoveSubObjective(ref gotoObjective);
},
onAbandon: () =>
{
Abandon = true;
});
}
IsCompleted = true;
RemoveSubObjective(ref decontainObjective);
},
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))
{
@@ -59,12 +59,14 @@ 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);
}
//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,
@@ -71,7 +71,7 @@ namespace Barotrauma
this.targetContainer = targetContainer;
}
protected override bool CheckObjectiveSpecific() => IsCompleted;
protected override bool CheckObjectiveState() => IsCompleted;
protected override void Act(float deltaTime)
{
@@ -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;
@@ -76,7 +76,7 @@ namespace Barotrauma
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)
@@ -23,7 +23,7 @@ namespace Barotrauma
public const float MIN_OXYGEN = 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, () =>
{
@@ -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()
@@ -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;
@@ -339,6 +340,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 +358,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 +371,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 +439,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.NewMessage($"({character.DisplayName}) Sorted hulls by suitability in {stopWatch.ElapsedMilliseconds} ms", debugOnly: true);
#endif
}
Hull potentialHull = hulls[hullSearchIndex];
@@ -420,7 +460,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);
@@ -176,6 +176,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 =>
@@ -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.
@@ -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
@@ -658,9 +658,8 @@ namespace Barotrauma
return bestItem;
}
protected override bool CheckObjectiveSpecific()
protected override bool CheckObjectiveState()
{
if (IsCompleted) { return true; }
if (targetItem == null)
{
// Not yet ready
@@ -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,
@@ -3,6 +3,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
namespace Barotrauma
{
@@ -95,6 +96,9 @@ namespace Barotrauma
protected override bool AllowInAnySub => true;
public Identifier DialogueIdentifier { get; set; } = "dialogcannotreachtarget".ToIdentifier();
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; }
@@ -194,6 +198,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
@@ -353,9 +394,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 +483,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 +506,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 +560,7 @@ namespace Barotrauma
}
if (!useScooter)
{
// Unequip
character.Inventory.TryPutItem(scooter, character, CharacterInventory.AnySlot);
character.TryPutItemInAnySlot(scooter);
}
}
}
@@ -663,7 +704,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);
@@ -764,9 +808,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)
{
@@ -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)
{
@@ -489,27 +498,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 +542,8 @@ namespace Barotrauma
itemsToClean.Clear();
ignoredItems.Clear();
autonomousObjectiveRetryTimer = 10;
timerMargin = 0;
newTargetTimer = 0;
}
public override void OnDeselected()
@@ -120,7 +120,6 @@ namespace Barotrauma
{
}
protected override bool CheckObjectiveSpecific() => false;
protected override bool CheckObjectiveState() => false;
}
}
@@ -318,7 +318,7 @@ namespace Barotrauma
return true;
}
protected override bool CheckObjectiveSpecific() => IsCompleted;
protected override bool CheckObjectiveState() => IsCompleted;
public override void Reset()
{
@@ -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;
}
@@ -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;
@@ -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();
@@ -312,7 +312,7 @@ namespace Barotrauma
}
}
protected override bool CheckObjectiveSpecific() => isDoneOperating && !Repeat;
protected override bool CheckObjectiveState() => isDoneOperating && !Repeat;
public override void Reset()
{
@@ -54,7 +54,7 @@ namespace Barotrauma
}
}
protected override bool CheckObjectiveSpecific() => IsCompleted;
protected override bool CheckObjectiveState() => IsCompleted;
protected override float GetPriority()
{
@@ -91,7 +91,7 @@ namespace Barotrauma
return Priority;
}
protected override bool CheckObjectiveSpecific()
protected override bool CheckObjectiveState()
{
IsCompleted = Item.IsFullCondition;
if (character.IsOnPlayerTeam && IsCompleted && IsRepairing())
@@ -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)
@@ -391,9 +391,18 @@ namespace Barotrauma
("[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 +477,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;
character.Speak(message, delay: 1.0f, identifier: $"targethealed{Target.Name}".ToIdentifier(), minDurationBetweenSimilar: 60.0f);
}
return isCompleted;
return IsCompleted;
}
protected override float GetPriority()
@@ -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);
@@ -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;
}
@@ -204,12 +204,8 @@ namespace Barotrauma
var allTargetItems = new List<Identifier>();
for (int i = 0; i < AllOptions.Length; i++)
{
Identifier[] optionTargetItemsSplit = i < splitTargetItems.Length ? splitTargetItems[i].Split(',', '').ToIdentifiers() : Array.Empty<Identifier>();
for (int j = 0; j < optionTargetItemsSplit.Length; j++)
{
optionTargetItemsSplit[j] = optionTargetItemsSplit[j].Value.Trim().ToIdentifier();
allTargetItems.Add(optionTargetItemsSplit[j]);
}
Identifier[] optionTargetItemsSplit = i < splitTargetItems.Length ? splitTargetItems[i].ToIdentifiers().ToArray() : Array.Empty<Identifier>();
allTargetItems.AddRange(optionTargetItemsSplit);
optionTargetItems.Add(AllOptions[i], optionTargetItemsSplit.ToImmutableArray());
}
TargetItems = allTargetItems.ToImmutableArray();
@@ -45,7 +45,8 @@ namespace Barotrauma
public float HappyThreshold { get; set; }
public float MaxHappiness { get; set; }
public bool HideStatusIndicators { get; set; }
/// <summary>
/// At which point is the pet considered "hungry" (playing unhappy sounds and showing the icon)
@@ -59,6 +60,14 @@ namespace Barotrauma
public float PlayForce { get; set; }
public float PlayTimer { get; set; }
public float PlayCooldown { get; set; }
/// <summary>
/// Should the pet lose ownership (and stop following) when the same character interacts with it twice? Unlike with other pets, if another character interacts with the pet, they will become the owner.
/// </summary>
public bool ToggleOwner { get; set; }
private float? UnstunY { get; set; }
public EnemyAIController AIController { get; private set; } = null;
@@ -162,7 +171,7 @@ namespace Barotrauma
private class Food
{
public string Tag;
public Identifier Tag;
public Vector2 HungerRange;
public float Hunger;
public float Happiness;
@@ -182,6 +191,7 @@ namespace Barotrauma
MaxHappiness = element.GetAttributeFloat(nameof(MaxHappiness), 100.0f);
UnhappyThreshold = element.GetAttributeFloat(nameof(UnhappyThreshold), MaxHappiness * 0.25f);
HappyThreshold = element.GetAttributeFloat(nameof(HappyThreshold), MaxHappiness * 0.8f);
HideStatusIndicators = element.GetAttributeBool(nameof(HideStatusIndicators), false);
MaxHunger = element.GetAttributeFloat(nameof(MaxHunger), 100.0f);
HungryThreshold = element.GetAttributeFloat(nameof(HungryThreshold), MaxHunger * 0.5f);
@@ -192,7 +202,9 @@ namespace Barotrauma
HappinessDecreaseRate = element.GetAttributeFloat(nameof(HappinessDecreaseRate), 0.1f);
HungerIncreaseRate = element.GetAttributeFloat(nameof(HungerIncreaseRate), 0.25f);
PlayForce = element.GetAttributeFloat("playforce", 15.0f);
PlayForce = element.GetAttributeFloat(nameof(PlayForce), 15.0f);
PlayCooldown = element.GetAttributeFloat(nameof(PlayCooldown), 5.0f);
ToggleOwner = element.GetAttributeBool(nameof(ToggleOwner), false);
foreach (var subElement in element.Elements())
{
@@ -204,7 +216,7 @@ namespace Barotrauma
case "eat":
Food food = new Food
{
Tag = subElement.GetAttributeString("tag", ""),
Tag = subElement.GetAttributeIdentifier("tag", Identifier.Empty),
Hunger = subElement.GetAttributeFloat("hunger", -1),
Happiness = subElement.GetAttributeFloat("happiness", 1),
Priority = subElement.GetAttributeFloat("priority", 100),
@@ -227,6 +239,7 @@ namespace Barotrauma
public StatusIndicatorType GetCurrentStatusIndicatorType()
{
if (HideStatusIndicators) { return StatusIndicatorType.None; }
if (Hunger > HungryThreshold) { return StatusIndicatorType.Hungry; }
if (Happiness > HappyThreshold) { return StatusIndicatorType.Happy; }
if (Happiness < UnhappyThreshold) { return StatusIndicatorType.Sad; }
@@ -283,14 +296,22 @@ namespace Barotrauma
public void Play(Character player)
{
if (PlayTimer > 0.0f) { return; }
Owner ??= player;
PlayTimer = 5.0f;
if (!AIController.Character.IsFriendly(player)) { return; }
if (ToggleOwner)
{
Owner = Owner == player ? null : player;
}
else
{
Owner ??= player;
}
PlayTimer = PlayCooldown;
AIController.Character.IsRagdolled = true;
Happiness += 10.0f;
AIController.Character.AnimController.MainLimb.body.LinearVelocity += new Vector2(0, PlayForce);
UnstunY = AIController.Character.SimPosition.Y;
#if CLIENT
AIController.Character.PlaySound(CharacterSound.SoundType.Happy, 0.9f);
AIController.Character.PlaySound(Owner == null ? CharacterSound.SoundType.Unhappy : CharacterSound.SoundType.Happy);
#endif
}
@@ -318,7 +339,7 @@ namespace Barotrauma
if (UnstunY.HasValue)
{
if (PlayTimer > 4.0f)
if (PlayTimer > PlayCooldown - 1.0f)
{
float extent = character.AnimController.MainLimb.body.GetMaxExtent();
if (character.SimPosition.Y < (UnstunY.Value + extent * 3.0f) &&
@@ -354,9 +375,12 @@ namespace Barotrauma
{
if (food.TargetParams == null)
{
if (AIController.AIParams.TryGetTarget(food.Tag, out TargetParams target))
if (AIController.AIParams.TryGetTargets(food.Tag, out IEnumerable<TargetParams> existingTargetParams))
{
food.TargetParams = target;
foreach (var targetParams in existingTargetParams)
{
food.TargetParams = targetParams;
}
}
else if (AIController.AIParams.TryAddNewTarget(food.Tag, AIState.Eat, food.Priority, out TargetParams targetParams))
{
@@ -444,11 +468,15 @@ namespace Barotrauma
}
else
{
WayPoint spawnPoint = null;
//try to find a spawnpoint in the main sub
var spawnPoint = WayPoint.WayPointList.Where(wp => wp.SpawnType == SpawnType.Human && wp.Submarine == Submarine.MainSub).GetRandomUnsynced();
if (Submarine.MainSub != null)
{
spawnPoint = WayPoint.WayPointList.Where(wp => wp.SpawnType == SpawnType.Human && wp.Submarine == Submarine.MainSub).GetRandomUnsynced();
}
//if not found, try any player sub (shuttle/drone etc)
spawnPoint ??= WayPoint.WayPointList.Where(wp => wp.SpawnType == SpawnType.Human && wp.Submarine?.Info.Type == SubmarineType.Player).GetRandomUnsynced();
spawnPos = spawnPoint?.WorldPosition ?? Submarine.MainSub.WorldPosition;
spawnPos = spawnPoint?.WorldPosition ?? Submarine.MainSub?.WorldPosition ?? Vector2.Zero;
}
var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName.ToIdentifier());
@@ -52,7 +52,9 @@ namespace Barotrauma
{
if (orderedCharacter != CommandingCharacter)
{
CommandingCharacter.Speak(SuggestedOrder.GetChatMessage(OrderedCharacter.Name, "", givingOrderToSelf: false), minDurationBetweenSimilar: 5);
CommandingCharacter.Speak(SuggestedOrder.GetChatMessage(OrderedCharacter.Name, "", givingOrderToSelf: false),
minDurationBetweenSimilar: 5,
identifier: ("GiveOrder." + SuggestedOrder.Prefab.Identifier).ToIdentifier());
}
CurrentOrder = SuggestedOrder
.WithOption(Option)
@@ -60,7 +62,9 @@ namespace Barotrauma
.WithOrderGiver(CommandingCharacter)
.WithManualPriority(CharacterInfo.HighestManualOrderPriority);
OrderedCharacter.SetOrder(CurrentOrder, CommandingCharacter != OrderedCharacter);
OrderedCharacter.Speak(TextManager.Get("DialogAffirmative").Value, delay: 1.0f, minDurationBetweenSimilar: 5);
OrderedCharacter.Speak(TextManager.Get("DialogAffirmative").Value, delay: 1.0f,
minDurationBetweenSimilar: 5,
identifier: ("ReceiveOrder." + SuggestedOrder.Prefab.Identifier).ToIdentifier());
}
TimeSinceLastAttempt = 0f;
}
@@ -11,7 +11,7 @@ namespace Barotrauma
public override void CalculateImportanceSpecific()
{
if (shipCommandManager.NavigationState == ShipCommandManager.NavigationStates.Inactive) { return; }
if (TargetItemComponent is Powered powered && powered.Voltage <= powered.MinVoltage) { return; }
if (TargetItemComponent is Powered { HasPower: false }) { return; }
if (TargetItem.Condition <= 0f) { return; }
Importance = 70f;