Merge branch 'master' of https://github.com/Regalis11/Barotrauma.git into Regalis11-master

This commit is contained in:
Evil Factory
2021-12-15 14:45:31 -03:00
388 changed files with 12646 additions and 8136 deletions
@@ -1,8 +1,6 @@
using Microsoft.Xna.Framework;
using NLog.Targets;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
@@ -64,7 +62,7 @@ namespace Barotrauma
#endif
}
private IEnumerable<object> Update(ISpatialEntity targetEntity, Camera cam)
private IEnumerable<CoroutineStatus> Update(ISpatialEntity targetEntity, Camera cam)
{
if (targetEntity == null || (targetEntity is Entity e && e.Removed)) { yield return CoroutineStatus.Success; }
@@ -1,5 +1,4 @@
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using FarseerPhysics;
using Microsoft.Xna.Framework;
@@ -125,6 +124,8 @@ namespace Barotrauma
minGapSize = ConvertUnits.ToDisplayUnits(Math.Min(colliderWidth, colliderLength));
}
public virtual void OnHealed(Character healer, float healAmount) { }
public virtual void OnAttacked(Character attacker, AttackResult attackResult) { }
public virtual void SelectTarget(AITarget target) { }
@@ -306,14 +307,15 @@ namespace Barotrauma
public void UnequipEmptyItems(Item parentItem, bool avoidDroppingInSea = true) => UnequipEmptyItems(Character, parentItem, avoidDroppingInSea);
public void UnequipContainedItems(Item parentItem, Func<Item, bool> predicate = null, bool avoidDroppingInSea = true) => UnequipContainedItems(Character, parentItem, predicate, avoidDroppingInSea);
public void UnequipContainedItems(Item parentItem, Func<Item, bool> predicate = null, bool avoidDroppingInSea = true, int? unequipMax = null) => UnequipContainedItems(Character, parentItem, predicate, avoidDroppingInSea, unequipMax);
public static void UnequipEmptyItems(Character character, Item parentItem, bool avoidDroppingInSea = true) => UnequipContainedItems(character, parentItem, it => it.Condition <= 0, avoidDroppingInSea);
public static void UnequipContainedItems(Character character, Item parentItem, Func<Item, bool> predicate, bool avoidDroppingInSea = true)
public static void UnequipContainedItems(Character character, Item parentItem, Func<Item, bool> predicate, bool avoidDroppingInSea = true, int? unequipMax = null)
{
var inventory = parentItem.OwnInventory;
if (inventory == null) { return; }
int removed = 0;
if (predicate == null || inventory.AllItems.Any(predicate))
{
foreach (Item containedItem in inventory.AllItemsMod)
@@ -326,10 +328,12 @@ namespace Barotrauma
// If we are not inside a friendly sub (= same team), try to put the item in the inventory instead dropping it.
if (character.Inventory.TryPutItem(containedItem, character, CharacterInventory.anySlot))
{
if (unequipMax.HasValue && ++removed >= unequipMax) { return; }
continue;
}
}
containedItem.Drop(character);
if (unequipMax.HasValue && ++removed >= unequipMax) { return; }
}
}
}
@@ -76,7 +76,7 @@ namespace Barotrauma
{
string errorMsg = "Invalid AITarget sector direction (" + value + ")\n" + Environment.StackTrace.CleanupStackTrace();
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("AITarget.SectorDir:" + entity?.ToString(), GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
GameAnalyticsManager.AddErrorEventOnce("AITarget.SectorDir:" + entity?.ToString(), GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
return;
}
sectorDir = value;
@@ -125,7 +125,7 @@ namespace Barotrauma
DebugConsole.ThrowError("Attempted to access a removed AITarget\n" + Environment.StackTrace.CleanupStackTrace());
#endif
GameAnalyticsManager.AddErrorEventOnce("AITarget.WorldPosition:EntityRemoved",
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
GameAnalyticsManager.ErrorSeverity.Error,
"Attempted to access a removed AITarget\n" + Environment.StackTrace.CleanupStackTrace());
return Vector2.Zero;
}
@@ -144,7 +144,7 @@ namespace Barotrauma
DebugConsole.ThrowError("Attempted to access a removed AITarget\n" + Environment.StackTrace.CleanupStackTrace());
#endif
GameAnalyticsManager.AddErrorEventOnce("AITarget.WorldPosition:EntityRemoved",
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
GameAnalyticsManager.ErrorSeverity.Error,
"Attempted to access a removed AITarget\n" + Environment.StackTrace.CleanupStackTrace());
return Vector2.Zero;
}
@@ -229,7 +229,7 @@ namespace Barotrauma
{
if (sectorRad >= MathHelper.TwoPi) { return true; }
Vector2 diff = worldPosition - WorldPosition;
return MathUtils.GetShortestAngle(MathUtils.VectorToAngle(diff), MathUtils.VectorToAngle(sectorDir)) <= sectorRad * 0.5f;
return Math.Abs(MathUtils.GetShortestAngle(MathUtils.VectorToAngle(diff), MathUtils.VectorToAngle(sectorDir))) <= sectorRad * 0.5f;
}
public void Remove()
@@ -339,11 +339,15 @@ namespace Barotrauma
{
targetingTag = "dead";
}
else if (AIParams.TryGetTarget(targetCharacter.CharacterHealth.GetActiveAfflictionTags(), out CharacterParams.TargetParams tp) && tp.Threshold > Character.GetDamageDoneByAttacker(targetCharacter))
{
targetingTag = tp.Tag;
}
else if (PetBehavior != null && aiTarget.Entity == PetBehavior.Owner)
{
targetingTag = "owner";
targetingTag = "owner";
}
else if (AIParams.TryGetTarget(targetCharacter.SpeciesName, out CharacterParams.TargetParams tP))
else if (AIParams.TryGetTarget(targetCharacter, out CharacterParams.TargetParams tP))
{
targetingTag = tP.Tag;
}
@@ -353,7 +357,7 @@ namespace Barotrauma
{
targetingTag = "husk";
}
else
else if (!Character.IsFriendly(targetCharacter))
{
if (enemy.CombatStrength > CombatStrength)
{
@@ -386,6 +390,10 @@ namespace Barotrauma
{
targetingTag = "sonar";
}
if (targetItem.GetComponent<Door>() != null)
{
targetingTag = "door";
}
}
}
else if (aiTarget.Entity is Structure)
@@ -511,8 +519,7 @@ namespace Barotrauma
}
else if (avoidTimer <= 0 || activeTriggers.Any() && returnTimer <= 0)
{
CharacterParams.TargetParams targetingParams = null;
UpdateTargets(Character, out targetingParams);
UpdateTargets(out CharacterParams.TargetParams targetingParams);
updateTargetsTimer = updateTargetsInterval * Rand.Range(0.75f, 1.25f);
if (SelectedAiTarget == null)
{
@@ -1973,7 +1980,7 @@ namespace Barotrauma
ChangeTargetState(attacker, canAttack ? AIState.Attack : AIState.Escape, 100);
}
}
else if (canAttack && attacker.IsHuman && AIParams.TryGetTarget(attacker.SpeciesName, out CharacterParams.TargetParams targetingParams))
else if (canAttack && attacker.IsHuman && AIParams.TryGetTarget(attacker, out CharacterParams.TargetParams targetingParams))
{
if (targetingParams.State == AIState.Aggressive || targetingParams.State == AIState.PassiveAggressive)
{
@@ -2362,7 +2369,7 @@ namespace Barotrauma
//goes through all the AItargets, evaluates how preferable it is to attack the target,
//whether the Character can see/hear the target and chooses the most preferable target within
//sight/hearing range
public AITarget UpdateTargets(Character character, out CharacterParams.TargetParams targetingParams)
public AITarget UpdateTargets(out CharacterParams.TargetParams targetingParams)
{
AITarget newTarget = null;
targetValue = 0;
@@ -2386,70 +2393,35 @@ namespace Barotrauma
}
Character targetCharacter = aiTarget.Entity as Character;
//ignore the aitarget if it is the Character itself
if (targetCharacter == character) { continue; }
if (targetCharacter == Character) { continue; }
float valueModifier = 1;
string targetingTag = null;
string targetingTag = GetTargetingTag(aiTarget);
if (targetCharacter != null)
{
// ignore if target is tagged to be explicitly ignored (Feign Death)
if (targetCharacter.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI)) { continue; }
if (targetCharacter.IsDead)
if (AIParams.Targets.None() && Character.IsFriendly(targetCharacter))
{
targetingTag = "dead";
continue;
}
else if (PetBehavior != null && aiTarget.Entity == PetBehavior.Owner)
if (targetCharacter.AIController is EnemyAIController enemy)
{
targetingTag = "owner";
}
else if (AIParams.TryGetTarget(targetCharacter.SpeciesName, out CharacterParams.TargetParams tP))
{
targetingTag = tP.Tag;
}
else
{
if (Character.IsFriendly(targetCharacter))
if (targetingTag == "stronger" && (State == AIState.Avoid || State == AIState.Escape || State == AIState.Flee))
{
continue;
}
if (targetCharacter.AIController is EnemyAIController enemy)
{
if (targetCharacter.IsHusk && AIParams.HasTag("husk"))
if (SelectedAiTarget == aiTarget)
{
targetingTag = "husk";
// Freightened -> hold on to the target
valueModifier *= 2;
}
else
if (IsBeingChasedBy(targetCharacter))
{
if (enemy.CombatStrength > CombatStrength)
{
targetingTag = "stronger";
}
else if (enemy.CombatStrength < CombatStrength)
{
targetingTag = "weaker";
}
else
{
targetingTag = "equal";
}
if (targetingTag == "stronger" && (State == AIState.Avoid || State == AIState.Escape || State == AIState.Flee))
{
if (SelectedAiTarget == aiTarget)
{
// Freightened -> hold on to the target
valueModifier *= 2;
}
if (IsBeingChasedBy(targetCharacter))
{
valueModifier *= 2;
}
if (Character.CurrentHull != null && !VisibleHulls.Contains(targetCharacter.CurrentHull))
{
// Inside but in a different room
valueModifier /= 2;
}
}
valueModifier *= 2;
}
if (Character.CurrentHull != null && !VisibleHulls.Contains(targetCharacter.CurrentHull))
{
// Inside but in a different room
valueModifier /= 2;
}
}
}
@@ -2469,7 +2441,7 @@ namespace Barotrauma
if (aiTarget.Entity is Hull hull)
{
// Ignore the target if it's a room and the character is already inside a sub
if (character.CurrentHull != null) { continue; }
if (Character.CurrentHull != null) { continue; }
// Ignore ruins
if (hull.Submarine == null) { continue; }
if (hull.Submarine.Info.IsRuin) { continue; }
@@ -2479,7 +2451,7 @@ namespace Barotrauma
if (aiTarget.Entity is Item item)
{
door = item.GetComponent<Door>();
bool targetingFromOutsideToInside = item.CurrentHull != null && character.CurrentHull == null;
bool targetingFromOutsideToInside = item.CurrentHull != null && Character.CurrentHull == null;
if (targetingFromOutsideToInside)
{
if (door != null && (!canAttackDoors && !AIParams.CanOpenDoors) || !canAttackWalls)
@@ -2488,28 +2460,12 @@ namespace Barotrauma
continue;
}
}
foreach (var prio in AIParams.Targets)
if (door == null && targetingFromOutsideToInside)
{
if (item.HasTag(prio.Tag))
if (item.Submarine?.Info is { IsRuin: true })
{
targetingTag = prio.Tag;
break;
}
}
if (door == null && targetingTag == null)
{
if (item.GetComponent<Sonar>() != null)
{
targetingTag = "sonar";
}
else if (targetingFromOutsideToInside)
{
targetingTag = "room";
if (item.Submarine?.Info.IsRuin != null)
{
// Ignore ruin items when the creature is outside.
continue;
}
// Ignore ruin items when the creature is outside.
continue;
}
}
else if (targetingTag == "nasonov")
@@ -2521,14 +2477,13 @@ namespace Barotrauma
}
}
// Ignore the target if it's a decoy and the character is already inside a sub
if (character.CurrentHull != null && targetingTag == "decoy")
if (Character.CurrentHull != null && targetingTag == "decoy")
{
continue;
}
}
else if (aiTarget.Entity is Structure s)
{
targetingTag = "wall";
if (!s.HasBody)
{
// Ignore structures that doesn't have a body (not walls)
@@ -2537,7 +2492,7 @@ namespace Barotrauma
if (s.IsPlatform) { continue; }
if (s.Submarine == null) { continue; }
if (s.Submarine.Info.IsRuin) { continue; }
bool isCharacterInside = character.CurrentHull != null;
bool isCharacterInside = Character.CurrentHull != null;
bool isInnerWall = s.prefab.Tags.Contains("inner");
if (isInnerWall && !isCharacterInside)
{
@@ -2624,21 +2579,12 @@ namespace Barotrauma
}
}
}
else
{
targetingTag = "room";
}
if (door != null)
{
// If there's not a more specific tag for the door
if (string.IsNullOrEmpty(targetingTag) || targetingTag == "room")
{
targetingTag = "door";
}
if (door.Item.Submarine == null) { continue; }
bool isOutdoor = door.LinkedGap?.FlowTargetHull != null && !door.LinkedGap.IsRoomToRoom;
// Ignore inner doors when outside
if (character.CurrentHull == null && !isOutdoor) { continue; }
if (Character.CurrentHull == null && !isOutdoor) { continue; }
bool isOpen = door.CanBeTraversed;
if (!isOpen)
{
@@ -2651,7 +2597,7 @@ namespace Barotrauma
}
if (IsAggressiveBoarder)
{
if (character.CurrentHull == null)
if (Character.CurrentHull == null)
{
// Increase the priority if the character is outside and the door is from outside to inside
if (door.CanBeTraversed)
@@ -2679,14 +2625,14 @@ namespace Barotrauma
if (targetingTag == null) { continue; }
var targetParams = GetTargetParams(targetingTag);
if (targetParams == null) { continue; }
if (targetParams.IgnoreInside && character.CurrentHull != null) { continue; }
if (targetParams.IgnoreOutside && character.CurrentHull == null) { continue; }
if (targetParams.IgnoreInside && Character.CurrentHull != null) { continue; }
if (targetParams.IgnoreOutside && Character.CurrentHull == null) { continue; }
if (targetParams.IgnoreIncapacitated && targetCharacter != null && targetCharacter.IsIncapacitated) { continue; }
if (targetParams.IgnoreIfNotInSameSub)
{
if (aiTarget.Entity.Submarine != Character.Submarine) { continue; }
var targetHull = targetCharacter != null ? targetCharacter.CurrentHull : aiTarget.Entity is Item it ? it.CurrentHull : null;
if ((targetHull == null) != (character.CurrentHull == null)) { continue; }
if ((targetHull == null) != (Character.CurrentHull == null)) { continue; }
}
if (targetParams.State == AIState.Observe || targetParams.State == AIState.Eat)
{
@@ -2706,7 +2652,7 @@ namespace Barotrauma
{
target = selectedTargetingParams == targetParams ? targetParams.ThresholdMax : targetParams.ThresholdMin;
}
if (character.HealthPercentage > target)
if (Character.HealthPercentage > target)
{
continue;
}
@@ -2721,7 +2667,7 @@ namespace Barotrauma
// Halve the priority for each swarm mate targeting the same target -> reduces stacking
foreach (Character otherCharacter in SwarmBehavior.Members)
{
if (otherCharacter == character) { continue; }
if (otherCharacter == Character) { continue; }
if (otherCharacter.AIController?.SelectedAiTarget != aiTarget) { continue; }
valueModifier /= 2;
}
@@ -2731,15 +2677,15 @@ namespace Barotrauma
// The same as above, but using all the friendly characters in the level.
foreach (Character otherCharacter in Character.CharacterList)
{
if (otherCharacter == character) { continue; }
if (otherCharacter == Character) { continue; }
if (otherCharacter.AIController?.SelectedAiTarget != aiTarget) { continue; }
if (!character.IsFriendly(otherCharacter)) { continue; }
if (!Character.IsFriendly(otherCharacter)) { continue; }
valueModifier /= 2;
}
}
}
if (!aiTarget.IsWithinSector(WorldPosition)) { continue; }
Vector2 toTarget = aiTarget.WorldPosition - character.WorldPosition;
Vector2 toTarget = aiTarget.WorldPosition - Character.WorldPosition;
float dist = toTarget.Length();
float nonModifiedDist = dist;
//if the target has been within range earlier, the character will notice it more easily
@@ -2829,7 +2775,7 @@ namespace Barotrauma
Character owner = GetOwner(i);
// Don't target items that we own.
// This is a rare case, and almost entirely related to Humanhusks, so let's check it last to reduce unnecessary checks (although the check shouldn't be expensive)
if (owner == character) { continue; }
if (owner == Character) { continue; }
if (owner != null && (Character.IsFriendly(owner) || owner.AiTarget != null && ignoredTargets.Contains(owner.AiTarget)))
{
continue;
@@ -85,6 +85,7 @@ namespace Barotrauma
/// List of previous attacks done to this character
/// </summary>
private readonly Dictionary<Character, AttackResult> previousAttackResults = new Dictionary<Character, AttackResult>();
private readonly Dictionary<Character, float> previousHealAmounts = new Dictionary<Character, float>();
private readonly SteeringManager outsideSteering, insideSteering;
@@ -187,6 +188,15 @@ namespace Barotrauma
foreach (var previousAttackResult in previousAttackResults)
{
RespondToAttack(previousAttackResult.Key, previousAttackResult.Value);
if (previousHealAmounts.ContainsKey(previousAttackResult.Key))
{
//gradually forget past heals
previousHealAmounts[previousAttackResult.Key] = Math.Min(previousHealAmounts[previousAttackResult.Key] - 5.0f, 100.0f);
if (previousHealAmounts[previousAttackResult.Key] <= 0.0f)
{
previousHealAmounts.Remove(previousAttackResult.Key);
}
}
}
previousAttackResults.Clear();
respondToAttackTimer = RespondToAttackInterval;
@@ -237,39 +247,41 @@ namespace Barotrauma
if (Character.Submarine == null)
{
// When the character is outside, far enough from the target, and the direct route is blocked,
// use the indoor steering with the main and side path waypoints to help avoid getting stuck in level walls
if (SelectedAiTarget?.Entity != null && !IsCloseEnoughToTarget(2000, useTargetSub: false))
obstacleRaycastTimer -= deltaTime;
if (obstacleRaycastTimer <= 0)
{
obstacleRaycastTimer -= deltaTime;
if (obstacleRaycastTimer <= 0)
obstacleRaycastTimer = obstacleRaycastIntervalLong;
if (SelectedAiTarget?.Entity == null || SelectedAiTarget.Entity is ISpatialEntity target && target.Submarine == null || !IsCloseEnoughToTarget(2000, useTargetSub: false))
{
obstacleRaycastTimer = obstacleRaycastIntervalLong;
Vector2 rayEnd = SelectedAiTarget.Entity.SimPosition;
if (SelectedAiTarget.Entity.Submarine != null)
// If the target is behind a level wall, switch to the pathing to get around the obstacles.
ISpatialEntity spatialTarget = SelectedAiTarget?.Entity;
if (spatialTarget == null)
{
rayEnd += SelectedAiTarget.Entity.Submarine.SimPosition;
var gotoObjective = ObjectiveManager.GetActiveObjective<AIObjectiveGoTo>();
spatialTarget = gotoObjective?.Target;
}
IEnumerable<FarseerPhysics.Dynamics.Body> ignoredBodies = null;
if (SelectedAiTarget.Entity is ISpatialEntity spatialTarget)
if (spatialTarget == null)
{
UseIndoorSteeringOutside = false;
}
else
{
IEnumerable<FarseerPhysics.Dynamics.Body> ignoredBodies = null;
Vector2 rayEnd = spatialTarget.SimPosition;
Submarine targetSub = spatialTarget.Submarine;
if (targetSub != null)
{
rayEnd += targetSub.SimPosition;
ignoredBodies = targetSub.PhysicsBody.FarseerBody.ToEnumerable();
}
var obstacle = Submarine.PickBody(SimPosition, rayEnd, ignoredBodies, collisionCategory: Physics.CollisionLevel | Physics.CollisionWall);
UseIndoorSteeringOutside = obstacle != null;
}
var obstacle = Submarine.PickBody(SimPosition, rayEnd, ignoredBodies, collisionCategory: Physics.CollisionLevel | Physics.CollisionWall);
UseIndoorSteeringOutside = obstacle != null;
}
}
else
{
UseIndoorSteeringOutside = false;
if (hasValidPath)
else
{
obstacleRaycastTimer -= deltaTime;
if (obstacleRaycastTimer <= 0)
UseIndoorSteeringOutside = false;
if (hasValidPath)
{
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).
@@ -332,25 +344,10 @@ namespace Barotrauma
}
}
}
IsInsideCave = Character.CurrentHull == null && Level.Loaded?.Caves.FirstOrDefault(c => c.Area.Contains(Character.WorldPosition)) is Level.Cave;
// Check whether the character is inside a cave
if (IsInsideCave)
{
// If the character was inside a cave, require them to move a bit further from the area to set the field back to false
// This is to avoid any twitchy behavior with the steering managers
IsInsideCave = Character.CurrentHull == null && Level.Loaded?.Caves.FirstOrDefault(c =>
{
var area = c.Area;
area.Inflate(new Vector2(100));
return area.Contains(Character.WorldPosition);
}) is Level.Cave;
}
else
{
IsInsideCave = Character.CurrentHull == null && Level.Loaded?.Caves.FirstOrDefault(c => c.Area.Contains(Character.WorldPosition)) is Level.Cave;
}
if (UseIndoorSteeringOutside || IsInsideCave || Character.CurrentHull?.Submarine != null || hasValidPath || IsCloseEnoughToTarget(steeringBuffer))
if (UseIndoorSteeringOutside || Character.CurrentHull?.Submarine != null || hasValidPath || IsCloseEnoughToTarget(steeringBuffer))
{
if (steeringManager != insideSteering)
{
@@ -524,11 +521,12 @@ namespace Barotrauma
if (Character.LockHands) { return; }
if (ObjectiveManager.CurrentObjective == null) { return; }
if (Character.CurrentHull == null) { return; }
bool oxygenLow = !Character.AnimController.HeadInWater && Character.OxygenAvailable < CharacterHealth.LowOxygenThreshold;
bool oxygenLow = !Character.AnimController.HeadInWater && Character.OxygenAvailable < CharacterHealth.LowOxygenThreshold && Character.NeedsOxygen;
bool isCarrying = ObjectiveManager.HasActiveObjective<AIObjectiveContainItem>() || ObjectiveManager.HasActiveObjective<AIObjectiveDecontainItem>();
bool NeedsDivingGearOnPath(AIObjectiveGoTo gotoObjective)
{
if (!Character.NeedsAir) { return false; }
bool insideSteering = SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.IsPathDirty;
Hull targetHull = gotoObjective.GetTargetHull();
return gotoObjective.Target != null && targetHull == null ||
@@ -567,6 +565,7 @@ namespace Barotrauma
Character.AnimController.HeadInWater ||
Character.Submarine == null ||
(Character.Submarine.TeamID != Character.TeamID && !Character.IsEscorted) ||
ObjectiveManager.CurrentOrders.Any(o => o.Objective.KeepDivingGearOn) ||
ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn) ||
Character.CurrentHull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 10;
bool IsOrderedToWait() => Character.IsOnPlayerTeam && ObjectiveManager.CurrentOrder is AIObjectiveGoTo goTo && goTo.Target == Character;
@@ -599,7 +598,7 @@ namespace Barotrauma
takeMaskOff = false;
break;
}
else if (gotoObjective.mimic)
else if (gotoObjective.Mimic)
{
if (!removeSuit)
{
@@ -625,7 +624,7 @@ namespace Barotrauma
if (removeDivingSuit)
{
var divingSuit = Character.Inventory.FindItemByTag(AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR);
if (divingSuit != null)
if (divingSuit != null && !divingSuit.HasTag(AIObjectiveFindDivingGear.DIVING_GEAR_WEARABLE_INDOORS))
{
if (oxygenLow || Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
{
@@ -727,6 +726,7 @@ namespace Barotrauma
if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, new List<InvSlotType>() { InvSlotType.Any }) && Character.Submarine?.TeamID == Character.TeamID )
{
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))
{
@@ -878,7 +878,7 @@ namespace Barotrauma
if (target.CurrentHull != hull || !target.Enabled) { continue; }
if (AIObjectiveFightIntruders.IsValidTarget(target, Character))
{
if (AddTargets<AIObjectiveFightIntruders, Character>(Character, target) && newOrder == null)
if (!target.IsArrested && AddTargets<AIObjectiveFightIntruders, Character>(Character, target) && newOrder == null)
{
var orderPrefab = Order.GetPrefab("reportintruders");
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
@@ -950,7 +950,7 @@ namespace Barotrauma
if (item.CurrentHull != hull) { continue; }
if (AIObjectiveRepairItems.IsValidTarget(item, Character))
{
if (item.Repairables.All(r => item.ConditionPercentage > r.RepairIconThreshold)) { continue; }
if (!item.Repairables.Any(r => r.IsBelowRepairIconThreshold)) { continue; }
if (AddTargets<AIObjectiveRepairItems, Item>(Character, item) && newOrder == null && !ObjectiveManager.HasActiveObjective<AIObjectiveRepairItem>())
{
var orderPrefab = Order.GetPrefab("reportbrokendevices");
@@ -1031,6 +1031,19 @@ namespace Barotrauma
}
}
public override void OnHealed(Character healer, float healAmount)
{
if (healer == null || healAmount <= 0.0f) { return; }
if (previousHealAmounts.ContainsKey(healer))
{
previousHealAmounts[healer] += healAmount;
}
else
{
previousHealAmounts.Add(healer, healAmount);
}
}
public override void OnAttacked(Character attacker, AttackResult attackResult)
{
// The attack incapacitated/killed the character: respond immediately to trigger nearby characters because the update loop no longer runs
@@ -1074,11 +1087,16 @@ namespace Barotrauma
}
private void RespondToAttack(Character attacker, AttackResult attackResult)
{
{
float healAmount = 0.0f;
if (attacker != null)
{
previousHealAmounts.TryGetValue(attacker, out healAmount);
}
// excluding poisons etc
float realDamage = attackResult.Damage;
float realDamage = attackResult.Damage - healAmount;
// including poisons etc
float totalDamage = realDamage;
float totalDamage = realDamage - healAmount;
if (attackResult.Afflictions != null)
{
foreach (Affliction affliction in attackResult.Afflictions)
@@ -1117,6 +1135,7 @@ namespace Barotrauma
// Don't react to attackers that are outside of the sub (e.g. AoE attacks)
return;
}
bool isAttackerFightingEnemy = false;
if (IsFriendly(attacker))
{
if (attacker.AnimController.Anim == Barotrauma.AnimController.Animation.CPR && attacker.SelectedCharacter == Character)
@@ -1125,7 +1144,7 @@ namespace Barotrauma
// Should not cancel any existing ai objectives (so that if the character attacked you and then helped, we still would want to retaliate).
return;
}
float cumulativeDamage = GetDamageDoneByAttacker(attacker);
float cumulativeDamage = Character.GetDamageDoneByAttacker(attacker);
bool isAccidental = attacker.IsBot && !IsMentallyUnstable && !attacker.AIController.IsMentallyUnstable && Character.CombatAction == null;
if (isAccidental)
{
@@ -1136,7 +1155,6 @@ namespace Barotrauma
}
else
{
(GameMain.GameSession?.GameMode as CampaignMode)?.OutpostNPCAttacked(Character, attacker, attackResult);
// Inform other NPCs
if (cumulativeDamage > 1 || totalDamage >= 10)
{
@@ -1184,6 +1202,10 @@ namespace Barotrauma
}
}
}
if (!isAttackerFightingEnemy)
{
(GameMain.GameSession?.GameMode as CampaignMode)?.OutpostNPCAttacked(Character, attacker, attackResult);
}
}
}
else
@@ -1191,7 +1213,7 @@ namespace Barotrauma
if (Character.Submarine != null && Character.Submarine.GetConnectedSubs().Contains(attacker.Submarine))
{
// Non-friendly
InformOtherNPCs(GetDamageDoneByAttacker(attacker));
InformOtherNPCs(Character.GetDamageDoneByAttacker(attacker));
}
if (Character.IsBot)
{
@@ -1211,7 +1233,15 @@ namespace Barotrauma
if (!(otherCharacter.AIController is HumanAIController otherHumanAI)) { continue; }
if (!otherHumanAI.IsFriendly(Character)) { continue; }
bool isWitnessing = otherHumanAI.VisibleHulls.Contains(Character.CurrentHull) || otherHumanAI.VisibleHulls.Contains(attacker.CurrentHull);
if (!isWitnessing && !CheckReportRange(Character, otherCharacter, ReportRange)) { continue; }
if (!isWitnessing)
{
//if the other character did not witness the attack, and the character is not within report range (or capable of reporting)
//don't react to the attack
if (Character.IsDead || Character.IsUnconscious || !CheckReportRange(Character, otherCharacter, ReportRange))
{
continue;
}
}
var combatMode = DetermineCombatMode(otherCharacter, cumulativeDamage, isWitnessing, dmgThreshold: attacker.TeamID == Character.TeamID ? 50 : 10);
float delay = isWitnessing ? GetReactionTime() : Rand.Range(2.0f, 5.0f, Rand.RandSync.Unsynced);
otherHumanAI.AddCombatObjective(combatMode, attacker, delay);
@@ -1244,18 +1274,20 @@ namespace Barotrauma
return AIObjectiveCombat.CombatMode.None;
}
// If there are any enemies around, just ignore the friendly fire
if (Character.CharacterList.Any(ch => ch.Submarine == Character.Submarine && !ch.Removed && !ch.IsDead && !ch.IsIncapacitated && !IsFriendly(ch) && VisibleHulls.Contains(ch.CurrentHull)))
if (Character.CharacterList.Any(ch => ch.Submarine == Character.Submarine && !ch.Removed && !ch.IsIncapacitated && !IsFriendly(ch) && VisibleHulls.Contains(ch.CurrentHull)))
{
isAttackerFightingEnemy = true;
return AIObjectiveCombat.CombatMode.None;
}
else if (isWitnessing && Character.CombatAction != null && !c.IsSecurity)
{
return Character.CombatAction.WitnessReaction;
}
else if (Character.IsInstigator && attacker.IsPlayer)
else if (attacker.IsPlayer && FindInstigator() is Character instigator)
{
// The guards don't react when the player attacks instigators.
return c.IsSecurity ? AIObjectiveCombat.CombatMode.None : (Character.CombatAction != null ? Character.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.Retreat);
// The guards don't react when the player there's an instigator around
isAttackerFightingEnemy = true;
return c.IsSecurity ? AIObjectiveCombat.CombatMode.None : (instigator.CombatAction != null ? instigator.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.Retreat);
}
else if (attacker.TeamID == CharacterTeamType.FriendlyNPC && attacker.AIController != null && !(attacker.AIController.IsMentallyUnstable || attacker.AIController.IsMentallyUnstable))
{
@@ -1295,6 +1327,22 @@ namespace Barotrauma
return c.IsSecurity ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Retreat;
}
}
Character FindInstigator()
{
if (Character.IsInstigator)
{
return Character;
}
else if (c.AIController is HumanAIController humanAi)
{
return Character.CharacterList.FirstOrDefault(ch => ch.Submarine == c.Submarine && !ch.Removed && !ch.IsIncapacitated && ch.IsInstigator && humanAi.VisibleHulls.Contains(ch.CurrentHull));
}
else
{
return null;
}
}
}
}
}
@@ -1416,15 +1464,20 @@ namespace Barotrauma
return true;
}
public static bool NeedsDivingGear(Hull hull, out bool needsSuit)
public bool NeedsDivingGear(Hull hull, out bool needsSuit)
{
if (!Character.NeedsAir)
{
needsSuit = false;
return false;
}
needsSuit = false;
if (hull == null ||
hull.WaterPercentage > 90 ||
hull.LethalPressure > 0 ||
hull.ConnectedGaps.Any(gap => !gap.IsRoomToRoom && gap.Open > 0.5f))
{
needsSuit = true;
needsSuit = !Character.HasAbilityFlag(AbilityFlags.ImmuneToPressure);
return true;
}
if (hull.WaterPercentage > 60 || hull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 1)
@@ -1564,21 +1617,19 @@ namespace Barotrauma
}
}
public static void ItemTaken(Item item, Character character)
public static void ItemTaken(Item item, Character thief)
{
if (item == null || character == null || item.GetComponent<LevelResource>() != null) { return; }
Character thief = character;
if (item == null || thief == null || item.GetComponent<LevelResource>() != null) { return; }
bool someoneSpoke = false;
bool stolenItemsInside = item.OwnInventory?.FindAllItems(it => it.SpawnedInCurrentOutpost && !it.AllowStealing, recursive: true).Any() ?? false;
bool stolenItemsInside = item.OwnInventory?.FindAllItems(it => it.SpawnedInOutpost && !it.AllowStealing, recursive: true).Any() ?? false;
if ((item.SpawnedInOutpost && !item.AllowStealing || stolenItemsInside) && thief.TeamID != CharacterTeamType.FriendlyNPC && !item.HasTag("handlocker"))
if ((item.SpawnedInCurrentOutpost && !item.AllowStealing || stolenItemsInside) && thief.TeamID != CharacterTeamType.FriendlyNPC && !item.HasTag("handlocker"))
{
foreach (Character otherCharacter in Character.CharacterList)
{
if (otherCharacter == thief || otherCharacter.TeamID == thief.TeamID || otherCharacter.IsDead ||
otherCharacter.Info?.Job == null ||
!(otherCharacter.AIController is HumanAIController otherHumanAI) ||
if (otherCharacter == thief || otherCharacter.TeamID == thief.TeamID || otherCharacter.IsIncapacitated || otherCharacter.Stun > 0.0f ||
otherCharacter.Info?.Job == null || !(otherCharacter.AIController is HumanAIController otherHumanAI) ||
!otherHumanAI.VisibleHulls.Contains(thief.CurrentHull))
{
continue;
@@ -1587,13 +1638,13 @@ namespace Barotrauma
if (!otherCharacter.CanSeeCharacter(thief)) { continue; }
// Don't react if the player is taking an extinguisher and there's any fires on the sub, or diving gear when the sub is flooding
// -> allow them to use the emergency items
if (character.Submarine != null)
if (thief.Submarine != null)
{
var connectedHulls = character.Submarine.GetHulls(alsoFromConnectedSubs: true);
var connectedHulls = thief.Submarine.GetHulls(alsoFromConnectedSubs: true);
if (item.HasTag("fireextinguisher") && connectedHulls.Any(h => h.FireSources.Any())) { continue; }
if (item.HasTag("diving") && connectedHulls.Any(h => h.ConnectedGaps.Any(g => AIObjectiveFixLeaks.IsValidTarget(g, thief)))) { continue; }
}
if (!someoneSpoke && !character.IsIncapacitated && character.Stun <= 0.0f)
if (!someoneSpoke)
{
if (!item.StolenDuringRound && GameMain.GameSession?.Campaign?.Map?.CurrentLocation != null)
{
@@ -1624,9 +1675,9 @@ namespace Barotrauma
}
}
}
else if (item.OwnInventory?.FindItem(it => it.SpawnedInOutpost && !item.AllowStealing, true) is { } foundItem)
else if (item.OwnInventory?.FindItem(it => it.SpawnedInCurrentOutpost && !item.AllowStealing, true) is { } foundItem)
{
ItemTaken(foundItem, character);
ItemTaken(foundItem, thief);
}
bool TriggerSecurity(HumanAIController humanAI)
@@ -1698,7 +1749,7 @@ namespace Barotrauma
if (item.CurrentHull != hull) { continue; }
if (AIObjectiveRepairItems.IsValidTarget(item, character))
{
if (item.Repairables.All(r => item.ConditionPercentage >= r.RepairThreshold)) { continue; }
if (item.Repairables.All(r => r.IsBelowRepairThreshold)) { continue; }
AddTargets<AIObjectiveRepairItems, Item>(character, item);
}
}
@@ -1754,17 +1805,6 @@ namespace Barotrauma
humanAI.ObjectiveManager.GetObjective<T1>()?.ReportedTargets.Remove(target));
}
public float GetDamageDoneByAttacker(Character otherCharacter)
{
float dmg = 0;
Character.Attacker attacker = Character.LastAttackers.LastOrDefault(a => a.Character == otherCharacter);
if (attacker != null)
{
dmg = attacker.Damage;
}
return dmg;
}
private void StoreHullSafety(Hull hull, HullSafety safety)
{
if (knownHulls.ContainsKey(hull))
@@ -1786,7 +1826,7 @@ namespace Barotrauma
{
if (isCurrentHull)
{
CurrentHullSafety = 0;
CurrentHullSafety = character.NeedsAir ? 0 : 100;
}
return CurrentHullSafety;
}
@@ -1809,8 +1849,8 @@ namespace Barotrauma
private static float CalculateHullSafety(Hull hull, IEnumerable<Hull> visibleHulls, Character character, bool ignoreWater = false, bool ignoreOxygen = false, bool ignoreFire = false, bool ignoreEnemies = false)
{
if (hull == null) { return 0; }
if (hull.LethalPressure > 0 && character.PressureProtection <= 0) { return 0; }
if (hull == null) { return character.NeedsAir ? 0 : 100; }
if (hull.LethalPressure > 0 && character.PressureProtection <= 0 && !character.HasAbilityFlag(AbilityFlags.ImmuneToPressure)) { return 0; }
// Oxygen factor should be 1 with 70% oxygen or more and 0.1 when the oxygen level is 30% or lower.
// With insufficient oxygen, the safety of the hull should be 39, all the other factors aside. So, just below the HULL_SAFETY_THRESHOLD.
float oxygenFactor = ignoreOxygen ? 1 : MathHelper.Lerp((HULL_SAFETY_THRESHOLD - 1) / 100, 1, MathUtils.InverseLerp(HULL_LOW_OXYGEN_PERCENTAGE, 100 - HULL_LOW_OXYGEN_PERCENTAGE, hull.OxygenPercentage));
@@ -1831,7 +1871,7 @@ namespace Barotrauma
float enemyFactor = 1;
if (!ignoreEnemies)
{
bool isValidTarget(Character e) => IsActive(e) && !IsFriendly(character, e);
bool isValidTarget(Character e) => IsActive(e) && !IsFriendly(character, e) && !e.IsArrested;
int enemyCount = visibleHulls == null ?
Character.CharacterList.Count(e => isValidTarget(e) && e.CurrentHull == hull) :
Character.CharacterList.Count(e => isValidTarget(e) && visibleHulls.Contains(e.CurrentHull));
@@ -26,9 +26,9 @@ namespace Barotrauma
private float findPathTimer;
private float buttonPressCooldown;
const float ButtonPressInterval = 0.25f;
private const float buttonPressCooldown = 3;
private float checkDoorsTimer;
private float buttonPressTimer;
public SteeringPath CurrentPath
{
@@ -97,9 +97,10 @@ namespace Barotrauma
public override void Update(float speed)
{
base.Update(speed);
buttonPressCooldown -= 1.0f / 60.0f;
findPathTimer -= 1.0f / 60.0f;
float step = 1.0f / 60.0f;
checkDoorsTimer -= step;
buttonPressTimer -= step;
findPathTimer -= step;
}
public void SetPath(SteeringPath path)
@@ -120,10 +121,18 @@ namespace Barotrauma
{
steering += base.DoSteeringSeek(targetSimPos, weight);
}
public void SteeringSeek(Vector2 target, float weight, float minGapWidth = 0, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null, bool checkVisiblity = true)
{
steering += CalculateSteeringSeek(target, weight, minGapWidth, startNodeFilter, endNodeFilter, nodeFilter, checkVisiblity);
if (buttonPressTimer > 0 && lastDoor.door != null && lastDoor.state && !lastDoor.door.IsOpen)
{
// We have pressed the button and are waiting for the door to open -> Hold still until we can press the button again.
Reset();
}
else
{
steering += CalculateSteeringSeek(target, weight, minGapWidth, startNodeFilter, endNodeFilter, nodeFilter, checkVisiblity);
}
}
/// <summary>
@@ -204,28 +213,23 @@ namespace Barotrauma
pathFinder.ApplyPenaltyToOutsideNodes = character.Submarine != null && character.PressureProtection <= 0;
var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", minGapSize, startNodeFilter, endNodeFilter, nodeFilter, checkVisibility: checkVisibility);
bool useNewPath = needsNewPath || currentPath == null || currentPath.CurrentNode == null || character.Submarine != null && findPathTimer < -1 && Math.Abs(character.AnimController.TargetMovement.Combine()) <= 0;
if (newPath.Unreachable || newPath.Nodes.None())
{
useNewPath = false;
}
else if (!useNewPath && currentPath != null && currentPath.CurrentNode != null)
if (!useNewPath && currentPath?.CurrentNode != null && newPath.Nodes.Any() && !newPath.Unreachable)
{
// Check if the new path is the same as the old, in which case we just ignore it and continue using the old path (or the progress would reset).
if (IsIdenticalPath())
{
useNewPath = false;
}
else
else if (!character.IsClimbing)
{
// Use the new path if it has significantly lower cost (don't change the path if it has marginally smaller cost. This reduces navigating backwards due to new path that is calculated from the node just behind us).
float t = (float)currentPath.CurrentIndex / (currentPath.Nodes.Count - 1);
useNewPath = newPath.Cost < currentPath.Cost * MathHelper.Lerp(0.95f, 0, t);
if (!useNewPath && character.Submarine != null && !character.IsClimbing)
if (!useNewPath && character.Submarine != null)
{
// It's possible that the current path was calculated from a start point that is no longer valid.
// Therefore, let's accept also paths with a greater cost than the current, if the current node is much farther than the new start node.
// This is a special case for cases e.g. where the character falls and thus needs a new path.
// Don't do this outside or when climbing ladders, because both cause issues.
useNewPath = Vector2.DistanceSquared(character.WorldPosition, currentPath.CurrentNode.WorldPosition) > Math.Pow(Vector2.Distance(character.WorldPosition, newPath.Nodes.First().WorldPosition) * 3, 2);
}
}
@@ -319,7 +323,8 @@ namespace Barotrauma
return currentTarget - pos2;
}
bool doorsChecked = false;
if (!character.LockHands && buttonPressCooldown <= 0.0f)
checkDoorsTimer = Math.Min(checkDoorsTimer, GetDoorCheckTime());
if (!character.LockHands && checkDoorsTimer <= 0.0f)
{
CheckDoorsInPath();
doorsChecked = true;
@@ -340,7 +345,7 @@ namespace Barotrauma
{
if (character.CanInteractWith(ladders.Item))
{
ladders.Item.TryInteract(character, false, true);
ladders.Item.TryInteract(character, forceSelectKey: true);
}
else
{
@@ -351,7 +356,7 @@ namespace Barotrauma
if (previousLadders != null && previousLadders != ladders && character.SelectedConstruction != previousLadders.Item &&
character.CanInteractWith(previousLadders.Item) && Math.Abs(previousLadders.Item.WorldPosition.X - ladders.Item.WorldPosition.X) < 5)
{
previousLadders.Item.TryInteract(character, false, true);
previousLadders.Item.TryInteract(character, forceSelectKey: true);
}
}
}
@@ -391,7 +396,7 @@ namespace Barotrauma
// Try to change the ladder (hatches between two submarines)
if (character.SelectedConstruction != nextLadder.Item && nextLadder.Item.IsInsideTrigger(character.WorldPosition))
{
nextLadder.Item.TryInteract(character, false, true);
nextLadder.Item.TryInteract(character, forceSelectKey: true);
}
}
if (isAboveFloor || nextLadderSameAsCurrent)
@@ -491,7 +496,14 @@ namespace Barotrauma
else
{
// We'll want this to run each time, because the delegate is used to find a valid button component.
bool canAccessButtons = door.Item.GetConnectedComponents<Controller>(true).Any(b => b.HasAccess(character) && (buttonFilter == null || buttonFilter(b)));
bool canAccessButtons = false;
foreach (var button in door.Item.GetConnectedComponents<Controller>(true))
{
if (button.HasAccess(character) && (buttonFilter == null || buttonFilter(button)))
{
canAccessButtons = true;
}
}
return canAccessButtons || door.IsOpen || ShouldBreakDoor(door);
}
}
@@ -504,8 +516,22 @@ namespace Barotrauma
return ConvertUnits.ToDisplayUnits(Math.Max(colliderSize.X, colliderSize.Y));
}
private (Door door, bool state) lastDoor;
private float GetDoorCheckTime()
{
if (steering.LengthSquared() > 0)
{
return character.AnimController.IsMovingFast ? 0.1f : 0.3f;
}
else
{
return float.PositiveInfinity;
}
}
private void CheckDoorsInPath()
{
checkDoorsTimer = GetDoorCheckTime();
if (!canOpenDoors) { return; }
for (int i = 0; i < 5; i++)
{
@@ -522,8 +548,7 @@ namespace Barotrauma
}
else
{
bool closeDoors = character.IsBot && character.IsInFriendlySub || character.Params.AI != null && character.Params.AI.KeepDoorsClosed;
if (i == 0 || !closeDoors)
if (i == 0)
{
currentWaypoint = currentPath.CurrentNode;
nextWaypoint = currentPath.NextNode;
@@ -544,7 +569,7 @@ namespace Barotrauma
if (currentWaypoint.ConnectedDoor.LinkedGap != null)
{
// Keep the airlock doors closed, but not in ruins/wrecks
if (currentWaypoint.ConnectedDoor.LinkedGap.IsRoomToRoom || currentWaypoint.Submarine?.Info.IsRuin != null || currentWaypoint.Submarine?.Info.IsWreck != null)
if (currentWaypoint.ConnectedDoor.LinkedGap.IsRoomToRoom && currentWaypoint.CurrentHull is { IsWetRoom: false } || currentWaypoint.Submarine == null || currentWaypoint.Submarine.Info.IsRuin || currentWaypoint.Submarine.Info.IsWreck)
{
shouldBeOpen = true;
door = currentWaypoint.ConnectedDoor;
@@ -570,28 +595,48 @@ namespace Barotrauma
}
if (door == null) { return; }
if (door.BotsShouldKeepOpen) { shouldBeOpen = true; }
//toggle the door if it's the previous node and open, or if it's current node and closed
if ((door.IsOpen || door.IsBroken) != shouldBeOpen)
{
if (!shouldBeOpen)
{
if (character.AIController is HumanAIController humanAI)
{
bool keepDoorsClosed = character.IsBot && door.Item.Submarine?.TeamID == character.TeamID || character.Params.AI != null && character.Params.AI.KeepDoorsClosed;
if (!keepDoorsClosed) { return; }
bool isInAirlock = door.Item.CurrentHull is { IsWetRoom: true } || character.CurrentHull is { IsWetRoom: true };
if (!isInAirlock)
{
// Don't slam the door at anyones face
if (Character.CharacterList.Any(c => c != character && humanAI.IsFriendly(c) && humanAI.VisibleHulls.Contains(c.CurrentHull) && !c.IsUnconscious))
{
return;
}
}
}
}
Controller closestButton = null;
float closestDist = 0;
bool canAccess = CanAccessDoor(door, button =>
{
if (currentWaypoint == null) { return true; }
// Check that the button is on the right side of the door.
if (door.LinkedGap.IsHorizontal)
if (nextWaypoint != null)
{
int dir = Math.Sign((nextWaypoint ?? currentWaypoint).WorldPosition.X - door.Item.WorldPosition.X);
if (button.Item.WorldPosition.X * dir > door.Item.WorldPosition.X * dir) { return false; }
}
else
{
int dir = Math.Sign((nextWaypoint ?? currentWaypoint).WorldPosition.Y - door.Item.WorldPosition.Y);
if (button.Item.WorldPosition.Y * dir > door.Item.WorldPosition.Y * dir) { return false; }
if (door.LinkedGap.IsHorizontal)
{
int dir = Math.Sign((nextWaypoint).WorldPosition.X - door.Item.WorldPosition.X);
if (button.Item.WorldPosition.X * dir > door.Item.WorldPosition.X * dir) { return false; }
}
else
{
int dir = Math.Sign((nextWaypoint).WorldPosition.Y - door.Item.WorldPosition.Y);
if (button.Item.WorldPosition.Y * dir > door.Item.WorldPosition.Y * dir) { return false; }
}
}
float distance = Vector2.DistanceSquared(button.Item.WorldPosition, character.WorldPosition);
if (closestButton == null || distance < closestDist)
if (closestButton == null || distance < closestDist && character.CanSeeTarget(button.Item))
{
closestButton = button;
closestDist = distance;
@@ -600,18 +645,39 @@ namespace Barotrauma
});
if (canAccess)
{
bool pressButton = buttonPressTimer <= 0 || lastDoor.door != door || lastDoor.state != shouldBeOpen;
if (door.HasIntegratedButtons)
{
door.Item.TryInteract(character, false, true);
buttonPressCooldown = ButtonPressInterval;
if (pressButton && character.CanSeeTarget(door.Item))
{
if (door.Item.TryInteract(character, forceSelectKey: true))
{
lastDoor = (door, shouldBeOpen);
buttonPressTimer = buttonPressCooldown;
}
else
{
buttonPressTimer = 0;
}
}
break;
}
else if (closestButton != null)
{
if (Vector2.DistanceSquared(closestButton.Item.WorldPosition, character.WorldPosition) < MathUtils.Pow(closestButton.Item.InteractDistance + GetColliderLength(), 2))
if (closestDist < MathUtils.Pow2(closestButton.Item.InteractDistance + GetColliderLength()))
{
closestButton.Item.TryInteract(character, false, true);
buttonPressCooldown = ButtonPressInterval;
if (pressButton)
{
if (closestButton.Item.TryInteract(character, forceSelectKey: true))
{
lastDoor = (door, shouldBeOpen);
buttonPressTimer = buttonPressCooldown;
}
else
{
buttonPressTimer = 0;
}
}
break;
}
else
@@ -631,6 +697,7 @@ namespace Barotrauma
// The button is on the wrong side of the door or a wall
currentPath.Unreachable = true;
}
lastDoor = (null, false);
return;
}
}
@@ -12,20 +12,19 @@ namespace Barotrauma
class LatchOntoAI
{
const float RaycastInterval = 5.0f;
private float raycastTimer;
private Structure targetWall;
private Body targetBody;
private Vector2 attachSurfaceNormal;
private Submarine targetSubmarine;
private Character targetCharacter;
private readonly Character character;
public bool AttachToSub { get; private set; }
public bool AttachToWalls { get; private set; }
public bool AttachToCharacters { get; private set; }
public Submarine TargetSubmarine { get; private set; }
public Structure TargetWall { get; private set; }
public Character TargetCharacter { get; private set; }
private readonly float minDeattachSpeed, maxDeattachSpeed, maxAttachDuration, coolDown;
private readonly float damageOnDetach, detachStun;
private readonly bool weld;
@@ -51,7 +50,7 @@ namespace Barotrauma
public bool IsAttached => AttachJoints.Count > 0;
public bool IsAttachedToSub => IsAttached && targetSubmarine != null && targetCharacter == null;
public bool IsAttachedToSub => IsAttached && TargetSubmarine != null && TargetCharacter == null;
public LatchOntoAI(XElement element, EnemyAIController enemyAI)
{
@@ -93,9 +92,9 @@ namespace Barotrauma
var sub = wall.Submarine;
if (sub == null) { return; }
Reset();
targetWall = wall;
targetSubmarine = sub;
targetBody = targetSubmarine.PhysicsBody.FarseerBody;
TargetWall = wall;
TargetSubmarine = sub;
targetBody = TargetSubmarine.PhysicsBody.FarseerBody;
this.attachSurfaceNormal = attachSurfaceNormal;
_attachPos = attachPos;
}
@@ -103,23 +102,20 @@ namespace Barotrauma
public void SetAttachTarget(Character target)
{
if (!AttachToCharacters) { return; }
if (target.Submarine != character.Submarine) { return; }
Reset();
targetCharacter = target;
targetSubmarine = target.Submarine;
TargetCharacter = target;
targetBody = target.AnimController.Collider.FarseerBody;
attachSurfaceNormal = Vector2.Normalize(character.WorldPosition - target.WorldPosition);
}
public void Update(EnemyAIController enemyAI, float deltaTime)
{
if (character.Submarine != null)
if (TargetCharacter != null && character.Submarine != TargetCharacter.Submarine ||
character.Submarine != null && TargetSubmarine != null && TargetCharacter == null)
{
if (targetCharacter != null && targetCharacter.Submarine != targetSubmarine ||
character.Submarine != null && targetSubmarine != null && targetCharacter == null)
{
DeattachFromBody(reset: true);
return;
}
DeattachFromBody(reset: true);
return;
}
if (IsAttached)
{
@@ -150,7 +146,7 @@ namespace Barotrauma
return;
}
}
if (targetCharacter != null)
if (TargetCharacter != null)
{
if (enemyAI.AttackingLimb?.attack == null)
{
@@ -159,10 +155,14 @@ namespace Barotrauma
else
{
float range = enemyAI.AttackingLimb.attack.DamageRange * 2f;
if (Vector2.DistanceSquared(targetCharacter.WorldPosition, enemyAI.AttackingLimb.WorldPosition) > range * range)
if (Vector2.DistanceSquared(TargetCharacter.WorldPosition, enemyAI.AttackingLimb.WorldPosition) > range * range)
{
DeattachFromBody(reset: true, cooldown: 1);
}
else
{
TargetCharacter.Latchers.Add(this);
}
}
}
}
@@ -176,15 +176,15 @@ namespace Barotrauma
deattachCheckTimer -= deltaTime;
}
if (targetCharacter != null)
if (TargetCharacter != null)
{
// Own sim pos -> target where we are
_attachPos = character.SimPosition;
}
Vector2 transformedAttachPos = _attachPos;
if (character.Submarine == null && targetSubmarine != null)
if (character.Submarine == null && TargetSubmarine != null)
{
transformedAttachPos += ConvertUnits.ToSimUnits(targetSubmarine.Position);
transformedAttachPos += ConvertUnits.ToSimUnits(TargetSubmarine.Position);
}
if (transformedAttachPos != Vector2.Zero)
{
@@ -207,7 +207,8 @@ namespace Barotrauma
var cells = Level.Loaded.GetCells(character.WorldPosition, 1);
if (cells.Count > 0)
{
float closestDist = float.PositiveInfinity;
//ignore walls more than 200 meters away
float closestDist = 200.0f * 200.0f;
foreach (Voronoi2.VoronoiCell cell in cells)
{
foreach (Voronoi2.GraphEdge edge in cell.Edges)
@@ -267,7 +268,7 @@ namespace Barotrauma
if (enemyAI.AttackingLimb == 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 != null ? TargetCharacter.WorldPosition : ConvertUnits.ToDisplayUnits(transformedAttachPos);
if (Vector2.DistanceSquared(referencePos, enemyAI.AttackingLimb.WorldPosition) < enemyAI.AttackingLimb.attack.DamageRange * enemyAI.AttackingLimb.attack.DamageRange)
{
AttachToBody(transformedAttachPos);
@@ -286,11 +287,11 @@ namespace Barotrauma
deattach = true;
attachCooldown = coolDown;
}
if (!deattach && targetWall != null && targetSubmarine != null)
if (!deattach && TargetWall != null && TargetSubmarine != null)
{
// Deattach if the wall is broken enough where we are attached to
int targetSection = targetWall.FindSectionIndex(attachLimb.WorldPosition, world: true, clamp: true);
if (enemyAI.CanPassThroughHole(targetWall, targetSection))
int targetSection = TargetWall.FindSectionIndex(attachLimb.WorldPosition, world: true, clamp: true);
if (enemyAI.CanPassThroughHole(TargetWall, targetSection))
{
deattach = true;
attachCooldown = coolDown;
@@ -298,7 +299,7 @@ namespace Barotrauma
if (!deattach)
{
// Deattach if the velocity is high
float velocity = targetSubmarine.Velocity == Vector2.Zero ? 0.0f : targetSubmarine.Velocity.Length();
float velocity = TargetSubmarine.Velocity == Vector2.Zero ? 0.0f : TargetSubmarine.Velocity.Length();
deattach = velocity > maxDeattachSpeed;
if (!deattach)
{
@@ -385,11 +386,8 @@ namespace Barotrauma
} as Joint;
GameMain.World.Add(colliderJoint);
AttachJoints.Add(colliderJoint);
if (targetCharacter != null)
{
targetCharacter.Latchers.Add(this);
}
AttachJoints.Add(colliderJoint);
TargetCharacter?.Latchers.Add(this);
if (maxAttachDuration > 0)
{
deattachCheckTimer = maxAttachDuration;
@@ -407,25 +405,19 @@ namespace Barotrauma
{
attachCooldown = cooldown;
}
TargetCharacter?.Latchers.Remove(this);
if (reset)
{
Reset();
}
if (targetCharacter != null)
{
targetCharacter.Latchers.Remove(this);
}
}
private void Reset()
{
if (targetCharacter != null)
{
targetCharacter.Latchers.Remove(this);
}
targetCharacter = null;
targetWall = null;
targetSubmarine = null;
TargetCharacter?.Latchers.Remove(this);
TargetCharacter = null;
TargetWall = null;
TargetSubmarine = null;
targetBody = null;
AttachPos = null;
}
@@ -93,13 +93,6 @@ namespace Barotrauma
_abandon = value;
if (_abandon)
{
#if DEBUG
if (HumanAIController.debugai && objectiveManager.IsOrder(this) && !objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() && !objectiveManager.IsCurrentOrder<AIObjectiveReturn>())
{
// TODO: dismiss
throw new Exception("Order abandoned!");
}
#endif
OnAbandon();
}
}
@@ -247,7 +240,7 @@ namespace Barotrauma
}
}
protected bool IsAllowed
public bool IsAllowed
{
get
{
@@ -271,7 +264,7 @@ namespace Barotrauma
if (!IsAllowed)
{
Priority = 0;
Abandon = !isOrder;
Abandon = true;
return Priority;
}
if (isOrder)
@@ -290,9 +283,9 @@ namespace Barotrauma
/// </summary>
public float CalculatePriority()
{
ForceWalk = false;
Priority = GetPriority();
ForceHighestPriority = false;
ForceWalk = false;
return Priority;
}
@@ -508,5 +501,34 @@ namespace Barotrauma
}
}
}
protected static bool CanEquip(Character character, Item item)
{
bool canEquip = item != null;
if (canEquip && !item.AllowedSlots.Contains(InvSlotType.Any))
{
canEquip = false;
var inv = character.Inventory;
foreach (var allowedSlot in item.AllowedSlots)
{
foreach (var slotType in inv.SlotTypes)
{
if (!allowedSlot.HasFlag(slotType)) { continue; }
for (int i = 0; i < inv.Capacity; i++)
{
canEquip = true;
if (allowedSlot.HasFlag(inv.SlotTypes[i]) && inv.GetItemAt(i) != null)
{
canEquip = false;
break;
}
}
}
}
}
return canEquip;
}
protected bool CanEquip(Item item) => CanEquip(character, item);
}
}
@@ -68,15 +68,12 @@ namespace Barotrauma
protected override void OnObjectiveCompleted(AIObjective objective, Item target)
=> HumanAIController.RemoveTargets<AIObjectiveCleanupItems, Item>(character, target);
private static bool IsItemInsideValidSubmarine(Item item, Character character)
public static bool IsItemInsideValidSubmarine(Item item, Character character)
{
if (item.CurrentHull == null) { return false; }
if (item.Submarine == null) { return false; }
if (item.Submarine.TeamID != character.TeamID) { return false; }
if (character.Submarine != null)
{
if (!character.Submarine.IsConnectedTo(item.Submarine)) { return false; }
}
if (character.Submarine != null && !character.Submarine.IsConnectedTo(item.Submarine)) { return false; }
return true;
}
@@ -94,7 +91,7 @@ namespace Barotrauma
if (item == null) { return false; }
if (item.IgnoreByAI(character)) { return false; }
if (!item.IsInteractable(character)) { return false; }
if (item.SpawnedInOutpost) { return false; }
if ((item.SpawnedInCurrentOutpost && !item.AllowStealing) == character.IsOnPlayerTeam) { return false; }
if (item.ParentInventory != null)
{
if (item.Container == null)
@@ -129,29 +126,7 @@ namespace Barotrauma
{
return true;
}
bool canEquip = true;
if (!item.AllowedSlots.Contains(InvSlotType.Any))
{
canEquip = false;
var inv = character.Inventory;
foreach (var allowedSlot in item.AllowedSlots)
{
foreach (var slotType in inv.SlotTypes)
{
if (!allowedSlot.HasFlag(slotType)) { continue; }
for (int i = 0; i < inv.Capacity; i++)
{
canEquip = true;
if (allowedSlot.HasFlag(inv.SlotTypes[i]) && inv.GetItemAt(i) != null)
{
canEquip = false;
break;
}
}
}
}
}
return canEquip;
return CanEquip(character, item);
}
public override void OnDeselected()
@@ -111,7 +111,7 @@ namespace Barotrauma
public CombatMode Mode { get; private set; }
private bool IsOffensiveOrArrest => initialMode == CombatMode.Offensive || initialMode == CombatMode.Arrest;
private bool TargetEliminated => IsEnemyDisabled || (Enemy.IsUnconscious && Enemy.Params.Health.ConstantHealthRegeneration <= 0.0f);
private bool TargetEliminated => IsEnemyDisabled || Enemy.IsUnconscious && Enemy.Params.Health.ConstantHealthRegeneration <= 0.0f || Enemy.IsArrested && !character.IsInstigator;
private bool IsEnemyDisabled => Enemy == null || Enemy.Removed || Enemy.IsDead;
private float AimSpeed => HumanAIController.AimSpeed;
@@ -158,7 +158,7 @@ namespace Barotrauma
return Priority;
}
}
float damageFactor = MathUtils.InverseLerp(0.0f, 5.0f, HumanAIController.GetDamageDoneByAttacker(Enemy) / 100.0f);
float damageFactor = MathUtils.InverseLerp(0.0f, 5.0f, character.GetDamageDoneByAttacker(Enemy) / 100.0f);
Priority = TargetEliminated ? 0 : Math.Min((95 + damageFactor) * PriorityModifier, 100);
return Priority;
}
@@ -177,11 +177,12 @@ namespace Barotrauma
ignoredWeapons.Clear();
ignoreWeaponTimer = ignoredWeaponsClearTime;
}
if (findSafety != null)
bool isCurrentObjective = objectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>();
if (findSafety != null && isCurrentObjective)
{
findSafety.Priority = 0;
}
if (!AllowCoolDown && !character.IsOnPlayerTeam && !objectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>())
if (!AllowCoolDown && !character.IsOnPlayerTeam && !isCurrentObjective)
{
distanceTimer -= deltaTime;
if (distanceTimer < 0)
@@ -204,13 +205,18 @@ namespace Barotrauma
protected override void Act(float deltaTime)
{
if (IsEnemyDisabled)
{
IsCompleted = true;
return;
}
if (AllowCoolDown)
{
coolDownTimer -= deltaTime;
}
if (seekAmmunitionObjective == null && seekWeaponObjective == null)
{
if (Mode != CombatMode.Retreat && TryArm() && !IsEnemyDisabled)
if (Mode != CombatMode.Retreat && TryArm())
{
OperateWeapon(deltaTime);
}
@@ -283,10 +289,12 @@ namespace Barotrauma
}
else
{
AskHelp();
Retreat(deltaTime);
}
break;
case CombatMode.Retreat:
AskHelp();
Retreat(deltaTime);
break;
default:
@@ -294,10 +302,6 @@ namespace Barotrauma
}
}
private bool IsLoaded(ItemComponent weapon, bool checkContainedItems = true) =>
weapon.HasRequiredContainedItems(character, addMessage: false) &&
(!checkContainedItems || weapon.Item.OwnInventory == null || weapon.Item.OwnInventory.AllItems.Any(i => i.Condition > 0));
private bool TryArm()
{
if (character.LockHands || Enemy == null)
@@ -325,7 +329,7 @@ namespace Barotrauma
Weapon = null;
continue;
}
if (IsLoaded(WeaponComponent, checkContainedItems: true))
if (WeaponComponent.IsLoaded(character))
{
// All good, the weapon is loaded
break;
@@ -367,6 +371,7 @@ namespace Barotrauma
{
if (WeaponComponent == null)
{
SpeakNoWeapons();
Mode = CombatMode.Retreat;
}
}
@@ -380,6 +385,7 @@ namespace Barotrauma
constructor: () => new AIObjectiveGetItem(character, "weapon", objectiveManager, equip: true, checkInventory: false)
{
AllowStealing = HumanAIController.IsMentallyUnstable,
EvaluateCombatPriority = false, // Use a custom formula instead
GetItemPriority = i =>
{
if (Weapon != null && (i == Weapon || i.Prefab.Identifier == Weapon.Prefab.Identifier)) { return 0; }
@@ -412,6 +418,7 @@ namespace Barotrauma
onCompleted: () => RemoveSubObjective(ref seekWeaponObjective),
onAbandon: () =>
{
SpeakNoWeapons();
RemoveSubObjective(ref seekWeaponObjective);
Mode = CombatMode.Retreat;
});
@@ -433,7 +440,7 @@ namespace Barotrauma
// Not in the inventory anymore or cannot find the weapon component
return false;
}
if (!IsLoaded(WeaponComponent))
if (!WeaponComponent.IsLoaded(character))
{
// Try reloading (and seek ammo)
if (!Reload(seekAmmo))
@@ -475,7 +482,7 @@ namespace Barotrauma
foreach (var weapon in weaponList)
{
float priority = weapon.CombatPriority;
if (!IsLoaded(weapon))
if (!weapon.IsLoaded(character))
{
if (weapon is RangedWeapon && enemyIsClose)
{
@@ -564,31 +571,6 @@ namespace Barotrauma
}
return weaponComponent.Item;
static Attack GetAttackDefinition(ItemComponent weapon)
{
Attack attack = null;
if (weapon is MeleeWeapon meleeWeapon)
{
attack = meleeWeapon.Attack;
}
else if (weapon is RangedWeapon rangedWeapon)
{
attack = rangedWeapon.FindProjectile(triggerOnUseOnContainers: false)?.Attack;
}
return attack;
}
static float GetLethalDamage(ItemComponent weapon)
{
float lethalDmg = 0;
Attack attack = GetAttackDefinition(weapon);
if (attack != null)
{
lethalDmg = attack.GetTotalDamage();
}
return lethalDmg;
}
float ApproximateStunDamage(ItemComponent weapon, Attack attack)
{
// Try to reduce the priority using the actual damage values and status effects.
@@ -628,6 +610,31 @@ namespace Barotrauma
}
}
public static float GetLethalDamage(ItemComponent weapon)
{
float lethalDmg = 0;
Attack attack = GetAttackDefinition(weapon);
if (attack != null)
{
lethalDmg = attack.GetTotalDamage();
}
return lethalDmg;
}
private static Attack GetAttackDefinition(ItemComponent weapon)
{
Attack attack = null;
if (weapon is MeleeWeapon meleeWeapon)
{
attack = meleeWeapon.Attack;
}
else if (weapon is RangedWeapon rangedWeapon)
{
attack = rangedWeapon.FindProjectile(triggerOnUseOnContainers: false)?.Attack;
}
return attack;
}
private HashSet<ItemComponent> FindWeaponsFromInventory()
{
weapons.Clear();
@@ -661,6 +668,13 @@ namespace Barotrauma
{
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);
}
}
@@ -673,22 +687,25 @@ namespace Barotrauma
{
return false;
}
if (!character.HasEquippedItem(Weapon))
if (!character.HasEquippedItem(Weapon, predicate: IsHandSlotType))
{
Weapon.TryInteract(character, forceSelectKey: true);
var slots = Weapon.AllowedSlots.Where(s => s == InvSlotType.LeftHand || s == InvSlotType.RightHand || s == (InvSlotType.LeftHand | InvSlotType.RightHand));
var slots = Weapon.AllowedSlots.Where(s => IsHandSlotType(s));
if (character.Inventory.TryPutItem(Weapon, character, slots))
{
aimTimer = Rand.Range(0.2f, 0.4f) / AimSpeed;
}
else
{
SpeakNoWeapons();
Weapon = null;
Mode = CombatMode.Retreat;
return false;
}
}
return true;
bool IsHandSlotType(InvSlotType s) => s == InvSlotType.LeftHand || s == InvSlotType.RightHand || s == (InvSlotType.LeftHand | InvSlotType.RightHand);
}
private float findHullTimer;
@@ -788,7 +805,6 @@ namespace Barotrauma
{
UsePathingOutside = false,
IgnoreIfTargetDead = true,
DialogueIdentifier = "dialogcannotreachtarget",
TargetName = Enemy.DisplayName,
AlwaysUseEuclideanDistance = false
},
@@ -812,7 +828,7 @@ namespace Barotrauma
ItemPrefab prefab = ItemPrefab.Find(null, "handcuffs");
if (prefab != null)
{
Entity.Spawner.AddToSpawnQueue(prefab, character.Inventory, onSpawned: (Item i) => i.SpawnedInOutpost = true);
Entity.Spawner.AddToSpawnQueue(prefab, character.Inventory, onSpawned: (Item i) => i.SpawnedInCurrentOutpost = true);
}
}
RemoveFollowTarget();
@@ -896,8 +912,9 @@ namespace Barotrauma
TryAddSubObjective(ref seekAmmunitionObjective,
constructor: () => new AIObjectiveContainItem(character, ammunitionIdentifiers, Weapon.GetComponent<ItemContainer>(), objectiveManager)
{
targetItemCount = Weapon.GetComponent<ItemContainer>().Capacity,
checkInventory = false
ItemCount = Weapon.GetComponent<ItemContainer>().Capacity * Weapon.GetComponent<ItemContainer>().MaxStackSize,
checkInventory = false,
MoveWholeStack = true
},
onCompleted: () => RemoveSubObjective(ref seekAmmunitionObjective),
onAbandon: () =>
@@ -1103,7 +1120,7 @@ namespace Barotrauma
if (WeaponComponent is RangedWeapon rangedWeapon)
{
// If the weapon is just equipped, we can't shoot just yet.
if (rangedWeapon.ReloadTimer <= 0)
if (rangedWeapon.ReloadTimer <= 0 && !rangedWeapon.HoldTrigger)
{
reloadTime = rangedWeapon.Reload;
}
@@ -1159,6 +1176,21 @@ namespace Barotrauma
retreatTarget = null;
}
private void SpeakNoWeapons() => Speak("dialogcombatnoweapons", delay: 0, minDuration: 30);
private void AskHelp() => Speak("dialogcombatretreating", delay: Rand.Range(0, 1), minDuration: 20);
private void Speak(string textIdentifier, float delay, float minDuration)
{
if (character.IsOnPlayerTeam && !character.IsInFriendlySub)
{
string msg = TextManager.Get(textIdentifier, true);
if (msg != null)
{
character.Speak(msg, identifier: textIdentifier, delay: delay, minDurationBetweenSimilar: minDuration);
}
}
}
//private float CalculateEnemyStrength()
//{
// float enemyStrength = 0;
@@ -11,12 +11,11 @@ namespace Barotrauma
public Func<Item, float> GetItemPriority;
public int targetItemCount = 1;
public string[] ignoredContainerIdentifiers;
public bool checkInventory = true;
//if the item can't be found, spawn it in the character's inventory (used by outpost NPCs)
private bool spawnItemIfNotFound = false;
//if the item can't be found, spawn it in the character's inventory (used by outpost NPCs and in some cases also enemy NPCs, like pirates)
private readonly bool spawnItemIfNotFound;
//can either be a tag or an identifier
public readonly string[] itemIdentifiers;
@@ -35,9 +34,24 @@ namespace Barotrauma
public bool Equip { get; set; }
public bool RemoveEmpty { get; set; } = true;
public bool RemoveExisting { get; set; }
/// <summary>
/// Only remove existing items when the contain target can't be put in the inventory
/// </summary>
public bool RemoveExistingWhenNecessary { get; set; }
public Func<Item, bool> RemoveExistingPredicate { get; set; }
public int? RemoveMax { get; set; }
public bool MoveWholeStack { get; set; }
private int _itemCount = 1;
public int ItemCount
{
get { return _itemCount; }
set
{
_itemCount = Math.Max(value, 1);
}
}
public AIObjectiveContainItem(Character character, Item item, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
@@ -83,7 +97,7 @@ namespace Barotrauma
containedItemCount++;
}
}
return containedItemCount >= targetItemCount;
return containedItemCount >= ItemCount;
}
}
@@ -106,9 +120,9 @@ namespace Barotrauma
}
if (character.CanInteractWith(container.Item, checkLinked: false))
{
if (RemoveExisting)
if (RemoveExisting || (RemoveExistingWhenNecessary && !container.Inventory.CanBePut(item)))
{
HumanAIController.UnequipContainedItems(container.Item);
HumanAIController.UnequipContainedItems(container.Item, predicate: RemoveExistingPredicate, unequipMax: RemoveMax);
}
else if (RemoveEmpty)
{
@@ -127,7 +141,6 @@ namespace Barotrauma
container.Inventory.TryPutItem(item, null);
}
}
IsCompleted = true;
}
}
@@ -144,7 +157,6 @@ namespace Barotrauma
{
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(container.Item, character, objectiveManager, getDivingGearIfNeeded: AllowToFindDivingGear)
{
DialogueIdentifier = "dialogcannotreachtarget",
TargetName = container.Item.Name,
AbortCondition = obj =>
container?.Item == null || container.Item.Removed || container.Item.IsThisOrAnyContainerIgnoredByAI(character) ||
@@ -174,7 +186,9 @@ namespace Barotrauma
AllowToFindDivingGear = AllowToFindDivingGear,
AllowDangerousPressure = AllowDangerousPressure,
TargetCondition = ConditionLevel,
ItemFilter = (Item potentialItem) => RemoveEmpty ? container.CanBeContained(potentialItem) : container.Inventory.CanBePut(potentialItem)
ItemFilter = (Item potentialItem) => RemoveEmpty ? container.CanBeContained(potentialItem) : container.Inventory.CanBePut(potentialItem),
ItemCount = ItemCount,
TakeWholeStack = MoveWholeStack
}, onAbandon: () =>
{
Abandon = true;
@@ -36,6 +36,11 @@ namespace Barotrauma
/// </summary>
public bool DropIfFails { get; set; } = true;
public bool RemoveExistingWhenNecessary { get; set; }
public Func<Item, bool> RemoveExistingPredicate { get; set; }
public int? RemoveExistingMax { get; set; }
public string AbandonGetItemDialogueIdentifier { get; set; }
public AIObjectiveDecontainItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, ItemContainer sourceContainer = null, ItemContainer targetContainer = null, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
@@ -86,6 +91,7 @@ namespace Barotrauma
}
if (itemToDecontain.Container != sourceContainer.Item)
{
itemToDecontain.Drop(character);
IsCompleted = true;
return;
}
@@ -98,7 +104,12 @@ namespace Barotrauma
if (getItemObjective == null && !itemToDecontain.IsOwnedBy(character))
{
TryAddSubObjective(ref getItemObjective,
constructor: () => new AIObjectiveGetItem(character, targetItem, objectiveManager, Equip) { TakeWholeStack = this.TakeWholeStack },
constructor: () => new AIObjectiveGetItem(character, targetItem, objectiveManager, Equip)
{
CannotFindDialogueIdentifierOverride = AbandonGetItemDialogueIdentifier,
SpeakIfFails = AbandonGetItemDialogueIdentifier != null,
TakeWholeStack = this.TakeWholeStack
},
onAbandon: () => Abandon = true);
return;
}
@@ -110,6 +121,9 @@ namespace Barotrauma
MoveWholeStack = TakeWholeStack,
Equip = Equip,
RemoveEmpty = false,
RemoveExistingWhenNecessary = RemoveExistingWhenNecessary,
RemoveExistingPredicate = RemoveExistingPredicate,
RemoveMax = RemoveExistingMax,
GetItemPriority = GetItemPriority,
ignoredContainerIdentifiers = sourceContainer != null ? new string[] { sourceContainer.Item.Prefab.Identifier } : null
},
@@ -31,7 +31,7 @@ namespace Barotrauma
protected override AIObjective ObjectiveConstructor(Character target)
{
AIObjectiveCombat.CombatMode combatMode = target.IsEscorted && character.TeamID == CharacterTeamType.Team1 ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Offensive;
AIObjectiveCombat.CombatMode combatMode = ShouldArrest(target, character) ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Offensive;
var combatObjective = new AIObjectiveCombat(character, target, combatMode, objectiveManager, PriorityModifier);
if (character.TeamID == CharacterTeamType.FriendlyNPC && target.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign)
{
@@ -41,7 +41,7 @@ namespace Barotrauma
combatObjective.holdFireCondition = () =>
{
//hold fire while the enemy is in the airlock (except if they've attacked us)
if (HumanAIController.GetDamageDoneByAttacker(target) > 0.0f) { return false; }
if (character.GetDamageDoneByAttacker(target) > 0.0f) { return false; }
return target.CurrentHull == null || target.CurrentHull.OutpostModuleTags.Any(t => t.Equals("airlock", System.StringComparison.OrdinalIgnoreCase));
};
character.Speak(TextManager.Get("dialogenteroutpostwarning"), null, Rand.Range(0.5f, 1.0f), "leaveoutpostwarning", 30.0f);
@@ -65,7 +65,13 @@ namespace Barotrauma
if (HumanAIController.IsFriendly(character, target)) { return false; }
if (!character.Submarine.IsConnectedTo(target.Submarine)) { return false; }
if (target.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI)) { return false; }
if (target.IsArrested) { return false; }
return true;
}
public static bool ShouldArrest(Character target, Character character)
{
return target != null && target.IsEscorted && character.TeamID == CharacterTeamType.Team1;
}
}
}
@@ -19,10 +19,15 @@ namespace Barotrauma
private AIObjectiveContainItem getOxygen;
private Item targetItem;
public static float MIN_OXYGEN = 10;
public static string HEAVY_DIVING_GEAR = "deepdiving";
public static string LIGHT_DIVING_GEAR = "lightdiving";
public static string OXYGEN_SOURCE = "oxygensource";
public const float MIN_OXYGEN = 10;
public const string HEAVY_DIVING_GEAR = "deepdiving";
public const string LIGHT_DIVING_GEAR = "lightdiving";
/// <summary>
/// Diving gear that's suitable for wearing indoors (-> the bots don't try to unequip it when they don't need diving gear)
/// </summary>
public const string DIVING_GEAR_WEARABLE_INDOORS = "divinggear_wearableindoors";
public const string OXYGEN_SOURCE = "oxygensource";
protected override bool CheckObjectiveSpecific() => targetItem != null && character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.Head);
@@ -46,10 +46,19 @@ namespace Barotrauma
}
if (character.CurrentHull == null)
{
Priority = (objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() ||
objectiveManager.IsCurrentOrder<AIObjectiveReturn>() ||
objectiveManager.Objectives.Any(o => o.Priority > 0 && o is AIObjectiveCombat))
&& HumanAIController.HasDivingSuit(character) ? 0 : 100;
if (!character.NeedsAir)
{
Priority = 0;
}
else
{
Priority = (
objectiveManager.HasOrder<AIObjectiveGoTo>(o => o.Priority > 0) ||
objectiveManager.HasOrder<AIObjectiveReturn>(o => o.Priority > 0) ||
objectiveManager.HasActiveObjective<AIObjectiveRescue>() ||
objectiveManager.Objectives.Any(o => o is AIObjectiveCombat && o.Priority > 0))
&& HumanAIController.HasDivingSuit(character) ? 0 : 100;
}
}
else
{
@@ -253,7 +262,7 @@ namespace Barotrauma
}
foreach (Character enemy in Character.CharacterList)
{
if (!HumanAIController.IsActive(enemy) || HumanAIController.IsFriendly(enemy)) { continue; }
if (!HumanAIController.IsActive(enemy) || HumanAIController.IsFriendly(enemy) || enemy.IsArrested) { continue; }
if (HumanAIController.VisibleHulls.Contains(enemy.CurrentHull))
{
Vector2 dir = character.Position - enemy.Position;
@@ -1,6 +1,7 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Immutable;
using System.Collections.Generic;
using System.Linq;
@@ -11,6 +12,7 @@ namespace Barotrauma
public override string Identifier { get; set; } = "get item";
public override bool AbandonWhenCannotCompleteSubjectives => false;
public override bool AllowMultipleInstances => true;
public HashSet<Item> ignoredItems = new HashSet<Item>();
@@ -19,7 +21,7 @@ namespace Barotrauma
public float TargetCondition { get; set; } = 1;
public bool AllowDangerousPressure { get; set; }
private readonly string[] identifiersOrTags;
public readonly ImmutableArray<string> IdentifiersOrTags;
//if the item can't be found, spawn it in the character's inventory (used by outpost NPCs)
private bool spawnItemIfNotFound = false;
@@ -31,6 +33,7 @@ namespace Barotrauma
public Item TargetItem => targetItem;
private int currSearchIndex;
public string[] ignoredContainerIdentifiers;
public string[] ignoredIdentifiersOrTags;
private AIObjectiveGoTo goToObjective;
private float currItemPriority;
private readonly bool checkInventory;
@@ -51,6 +54,21 @@ namespace Barotrauma
public bool AllowVariants { get; set; }
public bool Equip { get; set; }
public bool Wear { get; set; }
public bool RequireLoaded { get; set; }
public bool EvaluateCombatPriority { get; set; }
public bool CheckPathForEachItem { get; set; }
public bool SpeakIfFails { get; set; }
public string CannotFindDialogueIdentifierOverride { get; set; }
private int _itemCount = 1;
public int ItemCount
{
get { return _itemCount; }
set
{
_itemCount = Math.Max(value, 1);
}
}
public InvSlotType? EquipSlotType { get; set; }
@@ -67,23 +85,46 @@ namespace Barotrauma
public AIObjectiveGetItem(Character character, string identifierOrTag, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1, bool spawnItemIfNotFound = false)
: this(character, new string[] { identifierOrTag }, objectiveManager, equip, checkInventory, priorityModifier, spawnItemIfNotFound) { }
public AIObjectiveGetItem(Character character, string[] identifiersOrTags, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1, bool spawnItemIfNotFound = false)
public AIObjectiveGetItem(Character character, IEnumerable<string> identifiersOrTags, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1, bool spawnItemIfNotFound = false)
: base(character, objectiveManager, priorityModifier)
{
currSearchIndex = -1;
Equip = equip;
this.identifiersOrTags = identifiersOrTags;
this.spawnItemIfNotFound = spawnItemIfNotFound;
for (int i = 0; i < identifiersOrTags.Length; i++)
{
identifiersOrTags[i] = identifiersOrTags[i].ToLowerInvariant();
}
this.checkInventory = checkInventory;
IdentifiersOrTags = ParseGearTags(identifiersOrTags).ToImmutableArray();
ignoredIdentifiersOrTags = ParseIgnoredTags(identifiersOrTags).ToArray();
}
public static IEnumerable<string> ParseGearTags(IEnumerable<string> identifiersOrTags)
{
var tags = new List<string>();
foreach (string tag in identifiersOrTags)
{
if (!tag.Contains('!'))
{
tags.Add(tag.ToLowerInvariant());
}
}
return tags;
}
public static IEnumerable<string> ParseIgnoredTags(IEnumerable<string> identifiersOrTags)
{
var ignoredTags = new List<string>();
foreach (string tag in identifiersOrTags)
{
if (tag.Contains('!'))
{
ignoredTags.Add(tag.Remove("!").ToLowerInvariant());
}
}
return ignoredTags;
}
private bool CheckInventory()
{
if (identifiersOrTags == null) { return false; }
if (IdentifiersOrTags == null) { return false; }
var item = character.Inventory.FindItem(i => CheckItem(i), recursive: true);
if (item != null)
{
@@ -93,6 +134,19 @@ namespace Barotrauma
return item != null;
}
private bool CountItems()
{
int itemCount = 0;
foreach (Item it in character.Inventory.AllItems)
{
if (CheckItem(it))
{
itemCount++;
}
}
return itemCount >= ItemCount;
}
protected override void Act(float deltaTime)
{
if (character.LockHands)
@@ -105,7 +159,7 @@ namespace Barotrauma
Abandon = true;
return;
}
if (identifiersOrTags != null && !isDoneSeeking)
if (IdentifiersOrTags != null && !isDoneSeeking)
{
if (checkInventory)
{
@@ -122,7 +176,7 @@ namespace Barotrauma
if (dangerousPressure)
{
#if DEBUG
string itemName = targetItem != null ? targetItem.Name : identifiersOrTags.FirstOrDefault();
string itemName = targetItem != null ? targetItem.Name : IdentifiersOrTags.FirstOrDefault();
DebugConsole.NewMessage($"{character.Name}: Seeking item ({itemName}) aborted, because the pressure is dangerous.", Color.Yellow);
#endif
Abandon = true;
@@ -215,10 +269,28 @@ namespace Barotrauma
}
}
}
IsCompleted = true;
if (IdentifiersOrTags == null)
{
IsCompleted = true;
}
else
{
IsCompleted = CountItems();
if (!IsCompleted)
{
ResetInternal();
}
}
}
else
{
if (!Equip)
{
// Try equipping and wearing the item
Wear = true;
Equip = true;
return;
}
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Failed to equip/move the item '{targetItem.Name}' into the character inventory. Aborting.", Color.Red);
#endif
@@ -243,6 +315,10 @@ namespace Barotrauma
{
// Try again
ignoredItems.Add(targetItem);
if (targetItem != moveToTarget && moveToTarget is Item item)
{
ignoredItems.Add(item);
}
ResetInternal();
}
else
@@ -256,7 +332,7 @@ namespace Barotrauma
private void FindTargetItem()
{
if (identifiersOrTags == null)
if (IdentifiersOrTags == null)
{
if (targetItem == null)
{
@@ -269,7 +345,15 @@ namespace Barotrauma
}
float priority = Math.Clamp(objectiveManager.GetCurrentPriority(), 10, 100);
bool checkPath = priority >= AIObjectiveManager.LowestOrderPriority && (objectiveManager.IsCurrentOrder<AIObjectiveFixLeaks>() || objectiveManager.CurrentOrder is AIObjectiveGoTo gotoOrder && gotoOrder.isFollowOrderObjective);
if (!CheckPathForEachItem)
{
// While following the player, let's ensure that there's a valid path to the target before accepting it.
// Otherwise it will take some time for us to find a valid item when there are multiple items that we can't reach and some that we can.
// This is relatively expensive, so let's do this only when it significantly improves the behavior.
// Only allow one path find call per frame.
CheckPathForEachItem = priority >= AIObjectiveManager.LowestOrderPriority && (objectiveManager.IsCurrentOrder<AIObjectiveFixLeaks>() || objectiveManager.CurrentOrder is AIObjectiveGoTo gotoOrder && gotoOrder.IsFollowOrderObjective);
}
bool checkPath = CheckPathForEachItem;
bool hasCalledPathFinder = false;
int itemsPerFrame = (int)priority;
for (int i = 0; i < itemsPerFrame && currSearchIndex < Item.ItemList.Count - 1; i++)
@@ -280,14 +364,20 @@ namespace Barotrauma
if (itemSub == null) { continue; }
Submarine mySub = character.Submarine;
if (mySub == null) { continue; }
if (!checkInventory)
{
// Ignore items in the inventory when defined not to check it.
if (item.IsOwnedBy(character)) { continue; }
}
if (!AllowStealing)
{
if (character.TeamID == CharacterTeamType.FriendlyNPC != item.SpawnedInOutpost) { continue; }
if (character.TeamID == CharacterTeamType.FriendlyNPC != item.SpawnedInCurrentOutpost) { continue; }
}
if (!CheckItem(item)) { continue; }
if (item.Container != null)
{
if (item.Container.HasTag("donttakeitems")) { continue; }
if (ignoredItems.Contains(item.Container)) { continue; }
if (ignoredContainerIdentifiers != null)
{
if (ignoredContainerIdentifiers.Contains(item.ContainerIdentifier)) { continue; }
@@ -315,17 +405,51 @@ namespace Barotrauma
float yDist = Math.Abs(character.WorldPosition.Y - itemPos.Y);
yDist = yDist > 100 ? yDist * 5 : 0;
float dist = Math.Abs(character.WorldPosition.X - itemPos.X) + yDist;
float distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, 10000, dist));
float minDistFactor = EvaluateCombatPriority ? 0.1f : 0;
float distanceFactor = MathHelper.Lerp(1, minDistFactor, MathUtils.InverseLerp(100, 10000, dist));
itemPriority *= distanceFactor;
itemPriority *= item.Condition / item.MaxCondition;
if (EvaluateCombatPriority)
{
var mw = item.GetComponent<MeleeWeapon>();
var rw = item.GetComponent<RangedWeapon>();
float combatFactor = 0;
if (mw != null)
{
if (mw.CombatPriority > 0)
{
combatFactor = mw.CombatPriority / 100;
}
else
{
// The combat factor of items with zero combat priority is not allowed to be greater than 0.1f
combatFactor = Math.Min(AIObjectiveCombat.GetLethalDamage(mw) / 1000, 0.1f);
}
}
else if (rw != null)
{
if (rw.CombatPriority > 0)
{
combatFactor = rw.CombatPriority / 100;
}
else
{
combatFactor = Math.Min(AIObjectiveCombat.GetLethalDamage(rw) / 1000, 0.1f);
}
}
else
{
combatFactor = Math.Min(item.Components.Sum(ic => AIObjectiveCombat.GetLethalDamage(ic)) / 1000, 0.1f);
}
itemPriority *= combatFactor;
}
else
{
itemPriority *= item.Condition / item.MaxCondition;
}
// Ignore if the item has a lower priority than the currently selected one
if (itemPriority < currItemPriority) { continue; }
if (!hasCalledPathFinder && PathSteering != null && checkPath)
{
// While following the player, let's ensure that there's a valid path to the target before accepting it.
// Otherwise it will take some time for us to find a valid item when there are multiple items that we can't reach and some that we can.
// This is relatively expensive, so let's do this only when it significantly improves the behavior.
// Only allow one path find call per frame.
hasCalledPathFinder = true;
var path = PathSteering.PathFinder.FindPath(character.SimPosition, item.SimPosition, character.Submarine, errorMsgStr: $"AIObjectiveGetItem {character.DisplayName}", nodeFilter: node => node.Waypoint.CurrentHull != null);
if (path.Unreachable) { continue; }
@@ -341,10 +465,10 @@ namespace Barotrauma
{
if (spawnItemIfNotFound)
{
if (!(MapEntityPrefab.List.FirstOrDefault(me => me is ItemPrefab ip && identifiersOrTags.Any(id => id == ip.Identifier || ip.Tags.Contains(id))) is ItemPrefab prefab))
if (!(MapEntityPrefab.List.FirstOrDefault(me => me is ItemPrefab ip && IdentifiersOrTags.Any(id => id == ip.Identifier || ip.Tags.Contains(id))) is ItemPrefab prefab))
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot find an item with the following identifier(s) or tag(s): {string.Join(", ", identifiersOrTags)}, tried to spawn the item but no matching item prefabs were found.", Color.Yellow);
DebugConsole.NewMessage($"{character.Name}: Cannot find an item with the following identifier(s) or tag(s): {string.Join(", ", IdentifiersOrTags)}, tried to spawn the item but no matching item prefabs were found.", Color.Yellow);
#endif
Abandon = true;
}
@@ -355,7 +479,7 @@ namespace Barotrauma
targetItem = spawnedItem;
if (character.TeamID == CharacterTeamType.FriendlyNPC && (character.Submarine?.Info.IsOutpost ?? false))
{
spawnedItem.SpawnedInOutpost = true;
spawnedItem.SpawnedInCurrentOutpost = true;
}
});
}
@@ -363,9 +487,8 @@ namespace Barotrauma
else
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot find an item with the following identifier(s) or tag(s): {string.Join(", ", identifiersOrTags)}", Color.Yellow);
DebugConsole.NewMessage($"{character.Name}: Cannot find an item with the following identifier(s) or tag(s): {string.Join(", ", IdentifiersOrTags)}", Color.Yellow);
#endif
SpeakCannotFind();
Abandon = true;
}
}
@@ -375,7 +498,16 @@ namespace Barotrauma
protected override bool CheckObjectiveSpecific()
{
if (IsCompleted) { return true; }
if (targetItem != null)
if (targetItem == null)
{
// Not yet ready
return false;
}
if (IdentifiersOrTags != null && ItemCount > 1)
{
return CountItems();
}
else
{
if (Equip && EquipSlotType.HasValue)
{
@@ -386,23 +518,6 @@ namespace Barotrauma
return character.HasItem(targetItem, Equip);
}
}
else if (identifiersOrTags != null)
{
var matchingItem = character.Inventory.FindItem(i => CheckItem(i), recursive: true);
if (matchingItem != null)
{
if (Equip && EquipSlotType.HasValue)
{
return character.HasEquippedItem(matchingItem, EquipSlotType.Value);
}
else
{
return !Equip || character.HasEquippedItem(matchingItem);
}
}
return false;
}
return false;
}
private bool CheckItem(Item item)
@@ -410,9 +525,11 @@ namespace Barotrauma
if (!item.IsInteractable(character)) { return false; }
if (item.IsThisOrAnyContainerIgnoredByAI(character)) { return false; }
if (ignoredItems.Contains(item)) { return false; };
if (ignoredIdentifiersOrTags != null && ignoredIdentifiersOrTags.Any(id => item.prefab.Identifier == id || item.HasTag(id))) { return false; }
if (item.Condition < TargetCondition) { return false; }
if (ItemFilter != null && !ItemFilter(item)) { return false; }
return identifiersOrTags.Any(id => id == item.Prefab.Identifier || item.HasTag(id) || (AllowVariants && item.Prefab.VariantOf?.Identifier == id));
if (RequireLoaded && item.Components.Any(i => !i.IsLoaded(character))) { return false; }
return IdentifiersOrTags.Any(id => id == item.Prefab.Identifier || item.HasTag(id) || (AllowVariants && item.Prefab.VariantOf?.Identifier == id));
}
public override void Reset()
@@ -437,37 +554,28 @@ namespace Barotrauma
protected override void OnAbandon()
{
base.OnAbandon();
if (moveToTarget == null) { return; }
if (moveToTarget != null)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Get item failed to reach {moveToTarget}", Color.Yellow);
DebugConsole.NewMessage($"{character.Name}: Get item failed to reach {moveToTarget}", Color.Yellow);
#endif
}
if (SpeakIfFails)
{
SpeakCannotFind();
}
}
private void SpeakCannotFind()
{
// TODO: Use the item name as the variable here.
if (character.IsOnPlayerTeam && objectiveManager.CurrentOrder == objectiveManager.CurrentObjective)
{
string msg = TextManager.Get("dialogcannotfinditem", true);
string msg = TextManager.Get(CannotFindDialogueIdentifierOverride, returnNull: true) ?? TextManager.Get("dialogcannotfinditem", returnNull: true);
if (msg != null)
{
character.Speak(msg, identifier: "dialogcannotfinditem", minDurationBetweenSimilar: 20.0f);
}
}
}
// TODO: remove?
private void SpeakCannotReach()
{
if (character.IsOnPlayerTeam && objectiveManager.CurrentOrder == objectiveManager.CurrentObjective)
{
string TargetName = (moveToTarget as MapEntity)?.Name ?? (moveToTarget as Character)?.Name ?? moveToTarget.ToString();
string msg = TargetName == null ? TextManager.Get("dialogcannotreachtarget", true) : TextManager.GetWithVariable("dialogcannotreachtarget", "[name]", TargetName, formatCapitals: !(moveToTarget is Character));
if (msg != null)
{
character.Speak(msg, identifier: "dialogcannotreachtarget", minDurationBetweenSimilar: 20.0f);
}
}
}
}
}
@@ -0,0 +1,101 @@
#nullable enable
using Barotrauma.Extensions;
using System.Linq;
using System.Collections.Generic;
using System.Collections.Immutable;
namespace Barotrauma
{
class AIObjectiveGetItems : AIObjective
{
public override string Identifier { get; set; } = "get items";
public override string DebugTag => $"{Identifier}";
public override bool KeepDivingGearOn => true;
public override bool AllowMultipleInstances => true;
public bool AllowStealing { get; set; }
public bool TakeWholeStack { get; set; }
public bool AllowVariants { get; set; }
public bool Equip { get; set; }
public bool Wear { get; set; }
public bool CheckInventory { get; set; }
public bool EvaluateCombatPriority { get; set; }
public bool CheckPathForEachItem { get; set; }
public bool RequireLoaded { get; set; }
public bool RequireAllItems { get; set; }
private readonly ImmutableArray<string> gearTags;
private readonly string[] ignoredTags;
private bool subObjectivesCreated;
public readonly HashSet<Item> achievedItems = new HashSet<Item>();
public AIObjectiveGetItems(Character character, AIObjectiveManager objectiveManager, IEnumerable<string> identifiersOrTags, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier)
{
gearTags = AIObjectiveGetItem.ParseGearTags(identifiersOrTags).ToImmutableArray();
ignoredTags = AIObjectiveGetItem.ParseIgnoredTags(identifiersOrTags).ToArray();
}
protected override bool CheckObjectiveSpecific() => subObjectivesCreated && subObjectives.None();
protected override void Act(float deltaTime)
{
if (character.LockHands)
{
Abandon = true;
return;
}
if (!subObjectivesCreated)
{
foreach (string tag in gearTags)
{
if (subObjectives.Any(so => so is AIObjectiveGetItem getItem && getItem.IdentifiersOrTags.Contains(tag))) { continue; }
int count = gearTags.Count(t => t == tag);
AIObjectiveGetItem? getItem = null;
TryAddSubObjective(ref getItem, () =>
new AIObjectiveGetItem(character, tag, objectiveManager, Equip, CheckInventory && count <= 1)
{
AllowVariants = AllowVariants,
Wear = Wear,
TakeWholeStack = TakeWholeStack,
AllowStealing = AllowStealing,
ignoredIdentifiersOrTags = ignoredTags,
CheckPathForEachItem = CheckPathForEachItem,
RequireLoaded = RequireLoaded,
ItemCount = count,
SpeakIfFails = RequireAllItems
},
onCompleted: () =>
{
var item = getItem?.TargetItem;
if (item?.IsOwnedBy(character) != null)
{
achievedItems.Add(item);
}
},
onAbandon: () =>
{
var item = getItem?.TargetItem;
if (item != null)
{
achievedItems.Remove(item);
}
RemoveSubObjective(ref getItem);
if (RequireAllItems)
{
Abandon = true;
}
});
}
subObjectivesCreated = true;
}
}
public override void Reset()
{
base.Reset();
subObjectivesCreated = false;
achievedItems.Clear();
}
}
}
@@ -22,15 +22,23 @@ namespace Barotrauma
public Func<bool> requiredCondition;
public Func<PathNode, bool> endNodeFilter;
public Func<float> priorityGetter;
public Func<float> PriorityGetter;
public bool IsFollowOrderObjective;
public bool Mimic;
public bool isFollowOrderObjective;
public bool mimic;
public bool SpeakIfFails { get; set; } = true;
public bool DebugLogWhenFails { get; set; } = true;
public bool UsePathingOutside { get; set; } = true;
public float extraDistanceWhileSwimming;
public float extraDistanceOutsideSub;
public float ExtraDistanceWhileSwimming;
public float ExtraDistanceOutsideSub;
private float _closeEnoughMultiplier = 1;
public float CloseEnoughMultiplier
{
get { return _closeEnoughMultiplier; }
set { _closeEnoughMultiplier = Math.Max(value, 1); }
}
private float _closeEnough = 50;
private readonly float minDistance = 50;
private readonly float seekGapsInterval = 1;
@@ -44,14 +52,15 @@ namespace Barotrauma
{
get
{
float dist = _closeEnough;
float dist = _closeEnough * CloseEnoughMultiplier;
float extraMultiplier = Math.Clamp(CloseEnoughMultiplier * 0.6f, 1, 3);
if (character.AnimController.InWater)
{
dist += extraDistanceWhileSwimming;
dist += ExtraDistanceWhileSwimming * extraMultiplier;
}
if (character.CurrentHull == null)
{
dist += extraDistanceOutsideSub;
dist += ExtraDistanceOutsideSub * extraMultiplier;
}
return dist;
}
@@ -61,6 +70,9 @@ namespace Barotrauma
}
}
// TODO: Currently we never check the visibility (to the end node), which is actually unintentional.
// I don't think it has caused any issues so far, so let's keep defaulting to false for now, because the less we do raycasts the better.
// However, if there are cases where the bots attempt to go through walls (select the end node that is behind an obstacle), we should set this true.
public bool CheckVisibility { get; set; }
public bool IgnoreIfTargetDead { get; set; }
public bool AllowGoingOutside { get; set; }
@@ -77,7 +89,7 @@ namespace Barotrauma
public override bool AllowOutsideSubmarine => AllowGoingOutside;
public override bool AllowInAnySub => true;
public string DialogueIdentifier { get; set; }
public string DialogueIdentifier { get; set; } = "dialogcannotreachtarget";
public string TargetName { get; set; }
public ISpatialEntity Target { get; private set; }
@@ -105,9 +117,9 @@ namespace Barotrauma
}
else
{
if (priorityGetter != null)
if (PriorityGetter != null)
{
Priority = priorityGetter();
Priority = PriorityGetter();
}
else if (OverridePriority.HasValue)
{
@@ -149,7 +161,10 @@ namespace Barotrauma
private void SpeakCannotReach()
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot reach the target: {Target}", Color.Yellow);
if (DebugLogWhenFails)
{
DebugConsole.NewMessage($"{character.Name}: Cannot reach the target: {Target}", Color.Yellow);
}
#endif
if (character.IsOnPlayerTeam && objectiveManager.CurrentOrder == objectiveManager.CurrentObjective && DialogueIdentifier != null && SpeakIfFails)
{
@@ -176,11 +191,6 @@ namespace Barotrauma
character.AIController.SteeringManager.Reset();
return;
}
if (cannotFollow)
{
// Wait
character.AIController.SteeringManager.Reset();
}
if (!character.IsClimbing)
{
character.SelectedConstruction = null;
@@ -198,7 +208,7 @@ namespace Barotrauma
}
}
Hull targetHull = GetTargetHull();
if (!isFollowOrderObjective)
if (!IsFollowOrderObjective)
{
// Abandon if going through unsafe paths. Note ignores unsafe nodes when following an order or when the objective is set to ignore unsafe hulls.
bool containsUnsafeNodes = character.IsDismissed && !HumanAIController.ObjectiveManager.CurrentObjective.IgnoreUnsafeHulls
@@ -211,26 +221,33 @@ namespace Barotrauma
}
bool insideSteering = SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.IsPathDirty;
bool isInside = character.CurrentHull != null;
bool targetIsOutside = (Target != null && targetHull == null) || (insideSteering && PathSteering.CurrentPath.HasOutdoorsNodes);
if (isInside && targetIsOutside && !AllowGoingOutside)
bool hasOutdoorNodes = insideSteering && PathSteering.CurrentPath.HasOutdoorsNodes;
if (isInside && hasOutdoorNodes && !AllowGoingOutside)
{
Abandon = true;
}
else if (HumanAIController.IsCurrentPathNullOrUnreachable)
else if (HumanAIController.SteeringManager == PathSteering)
{
waitUntilPathUnreachable -= deltaTime;
SteeringManager.Reset();
if (waitUntilPathUnreachable < 0)
if (HumanAIController.IsCurrentPathNullOrUnreachable)
{
SteeringManager.Reset();
if (waitUntilPathUnreachable < 0)
{
waitUntilPathUnreachable = pathWaitingTime;
if (repeat)
{
SpeakCannotReach();
}
else
{
Abandon = true;
}
}
}
else if (HumanAIController.HasValidPath(requireNonDirty: true, requireUnfinished: false))
{
waitUntilPathUnreachable = pathWaitingTime;
if (repeat)
{
SpeakCannotReach();
}
else
{
Abandon = true;
}
}
}
if (!Abandon)
@@ -238,16 +255,16 @@ namespace Barotrauma
if (getDivingGearIfNeeded && !character.LockHands)
{
Character followTarget = Target as Character;
bool needsDivingSuit = !isInside || targetIsOutside;
bool needsDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit);
if (mimic)
bool needsDivingSuit = (!isInside || hasOutdoorNodes) && character.NeedsAir && !character.HasAbilityFlag(AbilityFlags.ImmuneToPressure);
bool needsDivingGear = (needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit)) && character.NeedsAir;
if (Mimic)
{
if (HumanAIController.HasDivingSuit(followTarget))
if (HumanAIController.HasDivingSuit(followTarget) && character.NeedsAir)
{
needsDivingGear = true;
needsDivingSuit = true;
}
else if (HumanAIController.HasDivingMask(followTarget))
else if (HumanAIController.HasDivingMask(followTarget) && character.NeedsAir)
{
needsDivingGear = true;
}
@@ -268,7 +285,7 @@ namespace Barotrauma
if (findDivingGear != null && !findDivingGear.CanBeCompleted)
{
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager),
onAbandon: () => Abort(),
onAbandon: () => Abandon = true,
onCompleted: () =>
{
cannotFollow = false;
@@ -278,7 +295,7 @@ namespace Barotrauma
else
{
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager),
onAbandon: () => Abort(),
onAbandon: () => Abandon = true,
onCompleted: () =>
{
cannotFollow = false;
@@ -311,7 +328,7 @@ namespace Barotrauma
if (character.AnimController.InWater)
{
if (character.CurrentHull == null ||
isFollowOrderObjective &&
IsFollowOrderObjective &&
targetCharacter != null && (targetCharacter.CurrentHull == null) != (character.CurrentHull == null) &&
Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition) < maxGapDistance * maxGapDistance)
{
@@ -354,7 +371,7 @@ namespace Barotrauma
}
if (TargetGap != null)
{
if (TargetGap.FlowTargetHull != null && HumanAIController.SteerThroughGap(TargetGap, isFollowOrderObjective ? Target.WorldPosition : TargetGap.FlowTargetHull.WorldPosition, deltaTime))
if (TargetGap.FlowTargetHull != null && HumanAIController.SteerThroughGap(TargetGap, IsFollowOrderObjective ? Target.WorldPosition : TargetGap.FlowTargetHull.WorldPosition, deltaTime))
{
SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 1);
return;
@@ -373,7 +390,7 @@ namespace Barotrauma
Item scooter = null;
float closeEnough = 250;
float squaredDistance = Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition);
bool shouldUseScooter = squaredDistance > closeEnough * closeEnough && (!mimic ||
bool shouldUseScooter = squaredDistance > closeEnough * closeEnough && (!Mimic ||
(targetCharacter != null && targetCharacter.HasEquippedItem(scooterTag, allowBroken: false)) || squaredDistance > Math.Pow(closeEnough * 2, 2));
if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> equippedScooters, recursive: false, requireEquipped: true))
{
@@ -382,24 +399,32 @@ namespace Barotrauma
}
else if (shouldUseScooter)
{
bool hasBattery = false;
if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> nonEquippedScooters, containedTag: batteryTag, conditionPercentage: 1, requireEquipped: false))
var leftHandItem = character.GetEquippedItem(slotType: InvSlotType.LeftHand);
var rightHandItem = character.GetEquippedItem(slotType: InvSlotType.RightHand);
bool handsFull =
(leftHandItem != null && character.Inventory.CheckIfAnySlotAvailable(leftHandItem, inWrongSlot: false) == -1) ||
(rightHandItem != null && character.Inventory.CheckIfAnySlotAvailable(rightHandItem, inWrongSlot: false) == -1);
if (!handsFull)
{
// Non-equipped scooter with a battery
scooter = nonEquippedScooters.FirstOrDefault();
hasBattery = true;
}
else if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> _nonEquippedScooters, requireEquipped: false))
{
// Non-equipped scooter without a battery
scooter = _nonEquippedScooters.FirstOrDefault();
// Non-recursive so that the bots won't take batteries from other items. Also means that they can't find batteries inside containers. Not sure how to solve this.
hasBattery = HumanAIController.HasItem(character, batteryTag, out _, requireEquipped: false, conditionPercentage: 1, recursive: false);
}
if (scooter != null && hasBattery)
{
// Equip only if we have a battery available
HumanAIController.TakeItem(scooter, character.Inventory, equip: true, dropOtherIfCannotMove: false, allowSwapping: true, storeUnequipped: false);
bool hasBattery = false;
if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> nonEquippedScooters, containedTag: batteryTag, conditionPercentage: 1, requireEquipped: false))
{
// Non-equipped scooter with a battery
scooter = nonEquippedScooters.FirstOrDefault();
hasBattery = true;
}
else if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> _nonEquippedScooters, requireEquipped: false))
{
// Non-equipped scooter without a battery
scooter = _nonEquippedScooters.FirstOrDefault();
// Non-recursive so that the bots won't take batteries from other items. Also means that they can't find batteries inside containers. Not sure how to solve this.
hasBattery = HumanAIController.HasItem(character, batteryTag, out _, requireEquipped: false, conditionPercentage: 1, recursive: false);
}
if (scooter != null && hasBattery)
{
// Equip only if we have a battery available
HumanAIController.TakeItem(scooter, character.Inventory, equip: true, dropOtherIfCannotMove: false, allowSwapping: true, storeUnequipped: false);
}
}
}
bool isScooterEquipped = scooter != null && character.HasEquippedItem(scooter);
@@ -588,7 +613,7 @@ namespace Barotrauma
{
if (gap.Open < 1) { continue; }
if (gap.Submarine == null) { continue; }
if (!isFollowOrderObjective)
if (!IsFollowOrderObjective)
{
if (gap.FlowTargetHull == null) { continue; }
if (gap.Submarine != Target.Submarine) { continue; }
@@ -669,18 +694,6 @@ namespace Barotrauma
return IsCompleted;
}
private void Abort()
{
if (!objectiveManager.IsOrder(this))
{
Abandon = true;
}
else
{
cannotFollow = true;
}
}
protected override void OnAbandon()
{
StopMovement();
@@ -0,0 +1,235 @@
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
namespace Barotrauma
{
class AIObjectiveLoadItem : AIObjective
{
public override string Identifier { get; set; } = "load item";
public override bool IsLoop
{
get => true;
set => throw new Exception("Trying to set the value for AIObjectiveLoadItem.IsLoop from: " + Environment.StackTrace.CleanupStackTrace());
}
private AIObjectiveLoadItems.ItemCondition TargetItemCondition { get; }
private Item Container { get; }
private ItemContainer ItemContainer { get; }
private ImmutableArray<string> TargetContainerTags { get; }
private int itemIndex = 0;
private AIObjectiveDecontainItem decontainObjective;
private readonly HashSet<Item> ignoredItems = new HashSet<Item>();
private Item targetItem;
private readonly string abandonGetItemDialogueIdentifier = "dialogcannotfindloadable";
public AIObjectiveLoadItem(Item container, ImmutableArray<string> targetTags, AIObjectiveLoadItems.ItemCondition targetCondition, string option, Character character, AIObjectiveManager objectiveManager, float priorityModifier)
: base(character, objectiveManager, priorityModifier)
{
Container = container;
ItemContainer = container?.GetComponent<ItemContainer>();
if (ItemContainer?.Inventory == null)
{
Abandon = true;
return;
}
TargetContainerTags = targetTags;
TargetItemCondition = targetCondition;
if (!string.IsNullOrEmpty(option))
{
string optionSpecificDialogueIdentifier = $"{abandonGetItemDialogueIdentifier}.{option}";
if (TextManager.ContainsTag(optionSpecificDialogueIdentifier))
{
abandonGetItemDialogueIdentifier = optionSpecificDialogueIdentifier;
}
}
}
protected override float GetPriority()
{
if (!IsAllowed)
{
Priority = 0;
Abandon = true;
return Priority;
}
else if (!AIObjectiveLoadItems.IsValidTarget(Container, character, targetCondition: TargetItemCondition))
{
// Reduce priority to 0 if the this isn't a valid container right now
Priority = 0;
}
else if (targetItem == null)
{
Priority = 0;
}
else
{
float dist = 0.0f;
if (character.CurrentHull != targetItem.CurrentHull)
{
AddDistance(character.WorldPosition, targetItem.WorldPosition);
}
if (targetItem.CurrentHull != Container.CurrentHull)
{
AddDistance(targetItem.WorldPosition, Container.WorldPosition);
}
void AddDistance(Vector2 startPos, Vector2 targetPos)
{
float yDist = Math.Abs(startPos.Y - targetPos.Y);
// If we're on the same level with the target, we'll disregard the vertical distance
if (yDist > 100) { dist += yDist * 5; }
dist += Math.Abs(character.WorldPosition.X - targetPos.X);
}
float distanceFactor = dist > 0.0f ? MathHelper.Lerp(0.9f, 0, MathUtils.InverseLerp(0, 5000, dist)) : 0.9f;
bool hasContainable = character.HasItem(targetItem);
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 (!IsValidContainable(targetItem))
{
// Target is not valid anymore, abandon the objective
decontainObjective.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;
}
}
if (ItemContainer.Inventory.IsFull())
{
// Prioritize containers that still have empty space by lowering the priority of objectives with a full target container
Priority /= 4;
}
}
return Priority;
}
public override void Update(float deltaTime)
{
base.Update(deltaTime);
if (targetItem == null)
{
if (character.FindItem(ref itemIndex, out Item item, identifiers: ItemContainer.ContainableItemIdentifiers, ignoreBroken: false, customPredicate: IsValidContainable, customPriorityFunction: GetConditionBasedPriority))
{
if (item == null)
{
// No possible containables found, abandon the objective
Abandon = true;
}
targetItem = item;
}
// Prefer items closer to full condition when target condition is Empty, and vice versa
float GetConditionBasedPriority(Item item)
{
try
{
return TargetItemCondition switch
{
AIObjectiveLoadItems.ItemCondition.Full => MathUtils.InverseLerp(100.0f, 0.0f, item.ConditionPercentage),
AIObjectiveLoadItems.ItemCondition.Empty => MathUtils.InverseLerp(0.0f, 100.0f, item.ConditionPercentage),
_ => throw new NotImplementedException()
};
}
catch (NotImplementedException)
{
#if DEBUG
DebugConsole.ShowError($"Unexpected target condition \"{TargetItemCondition}\" in local function GetConditionBasedProperty");
#endif
return 0.0f;
}
}
}
}
protected override void Act(float deltaTime)
{
if (targetItem != null)
{
if(decontainObjective == null && !IsValidContainable(targetItem))
{
IgnoreTargetItem();
Reset();
return;
}
TryAddSubObjective(ref decontainObjective,
constructor: () => new AIObjectiveDecontainItem(character, targetItem, objectiveManager, targetContainer: ItemContainer, priorityModifier: PriorityModifier)
{
AbandonGetItemDialogueIdentifier = abandonGetItemDialogueIdentifier,
Equip = true,
RemoveExistingWhenNecessary = true,
RemoveExistingPredicate = (i) => AIObjectiveLoadItems.ItemMatchesTargetCondition(i, TargetItemCondition),
RemoveExistingMax = 1
},
onCompleted: () =>
{
IsCompleted = true;
RemoveSubObjective(ref decontainObjective);
},
onAbandon: () =>
{
// Try again
IgnoreTargetItem();
Reset();
});
}
else
{
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
}
}
private bool IsValidContainable(Item item)
{
if (item == null) { return false; }
if (item.Removed) { return false; }
if (ignoredItems.Contains(item)) { return false; }
if ((item.SpawnedInCurrentOutpost && !item.AllowStealing) == character.IsOnPlayerTeam) { return false; }
var rootInventoryOwner = item.GetRootInventoryOwner();
if (rootInventoryOwner is Character owner && owner != character) { return false; }
if (rootInventoryOwner is Item parentItem)
{
if (parentItem.HasTag("donttakeitems")) { return false; }
if (!(parentItem.GetComponent<ItemContainer>()?.HasAccess(character) ?? true)) { return false; }
}
if (item.IsThisOrAnyContainerIgnoredByAI(character)) { return false; }
if (!character.HasItem(item) && !CanEquip(item)) { return false; }
if (!ItemContainer.HasAccess(character)) { return false; }
if (!ItemContainer.CanBeContained(item)) { return false; }
if (AIObjectiveLoadItems.ItemMatchesTargetCondition(item, TargetItemCondition)) { return false; }
if (TargetItemCondition == AIObjectiveLoadItems.ItemCondition.Full)
{
// Ignore items that have had their condition increase recently
if (TargetItemCondition == AIObjectiveLoadItems.ItemCondition.Full && item.ConditionIncreasedRecently) { return false; }
// Ignore items inside their (condition-restricted) primary containers
if (item.ParentInventory is ItemInventory itemInventory && item.IsContainerPreferred(itemInventory.Container, out bool _, out bool isSecondary, requireConditionRestriction: true) && !isSecondary) { return false; }
}
// Ignore items inside another valid container
if (AIObjectiveLoadItems.IsValidTarget(item.Container, character, TargetContainerTags)) { return false; }
return true;
}
protected override bool CheckObjectiveSpecific() => IsCompleted;
public override void Reset()
{
base.Reset();
// Don't reset the target item when resetting the objective because it affects priority calculations
decontainObjective = null;
itemIndex = 0;
}
private void IgnoreTargetItem()
{
if(targetItem == null) { return; }
ignoredItems.Add(targetItem);
targetItem = null;
}
}
}
@@ -0,0 +1,115 @@
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
namespace Barotrauma
{
class AIObjectiveLoadItems : AIObjectiveLoop<Item>
{
public override string Identifier { get; set; } = "load items";
protected override float IgnoreListClearInterval => 20.0f;
protected override bool ResetWhenClearingIgnoreList => false;
private ImmutableArray<string> TargetContainerTags { get; }
private List<Item> TargetContainers { get; } = new List<Item>();
private ItemCondition TargetCondition { get; }
public enum ItemCondition
{
Empty,
Full
}
public AIObjectiveLoadItems(Character character, AIObjectiveManager objectiveManager, string option, ImmutableArray<string> containerTags, Item targetContainer = null, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier, option)
{
if ((containerTags == null || containerTags.None()) && targetContainer == null)
{
Abandon = true;
return;
}
else
{
TargetContainerTags = containerTags.ToImmutableArray();
}
if (targetContainer != null)
{
TargetContainers.Add(targetContainer);
}
TargetCondition = option == "turretammo" ? ItemCondition.Empty : ItemCondition.Full;
}
protected override bool Filter(Item target)
{
if (!IsValidTarget(target, character, TargetContainerTags, TargetCondition)) { return false; }
if (target.CurrentHull == null || target.CurrentHull.FireSources.Count > 0) { return false; }
if (Character.CharacterList.Any(c => c.CurrentHull == target.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; }
return true;
}
public static bool IsValidTarget(Item item, Character character, ImmutableArray<string>? targetContainerTags = null, ItemCondition? targetCondition = null)
{
if (item == null) { return false; }
if (item.Removed) { return false; }
if (targetContainerTags.HasValue && !Order.TargetItemsMatchItem(targetContainerTags.Value, item)) { return false; }
if (!(item.GetComponent<ItemContainer>() is ItemContainer container)) { return false; }
if (container.Inventory == null) { return false; }
if (targetCondition.HasValue && container.Inventory.IsFull() && container.Inventory.AllItems.None(i => ItemMatchesTargetCondition(i, targetCondition.Value))) { return false; }
if (!AIObjectiveCleanupItems.IsItemInsideValidSubmarine(item, character)) { return false; }
if (item.GetRootInventoryOwner() is Character owner && owner != character) { return false; }
if (!item.IsInteractable(character)) { return false; }
if (item.IsThisOrAnyContainerIgnoredByAI(character)) { return false; }
if (!container.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; }
return true;
}
public static bool ItemMatchesTargetCondition(Item item, ItemCondition targetCondition)
{
if(item == null) { return false; }
try
{
return targetCondition switch
{
ItemCondition.Empty => item.Condition <= 0.1f,
ItemCondition.Full => item.IsFullCondition,
_ => throw new NotImplementedException(),
};
}
catch (NotImplementedException)
{
#if DEBUG
DebugConsole.ShowError($"Unexpected target condition \"{targetCondition}\" in AIObjectiveLoadItems.ItemMatchesTargetCondition");
#endif
return false;
}
}
protected override IEnumerable<Item> GetList() => TargetContainers.Any() ? TargetContainers : Item.ItemList;
protected override AIObjective ObjectiveConstructor(Item target)
=> new AIObjectiveLoadItem(target, TargetContainerTags, TargetCondition, Option, character, objectiveManager, PriorityModifier);
protected override void OnObjectiveCompleted(AIObjective objective, Item target)
=> HumanAIController.RemoveTargets<AIObjectiveLoadItems, Item>(character, target);
protected override float TargetEvaluation()
{
if (Targets.None()) { return 0; }
if (objectiveManager.IsOrder(this))
{
float prio = objectiveManager.GetOrderPriority(this);
if (subObjectives.All(so => so.SubObjectives.None() || so.Priority <= 0))
{
ForceWalk = true;
}
return prio;
}
return AIObjectiveManager.RunPriority - 0.5f;
}
}
}
@@ -45,6 +45,7 @@ namespace Barotrauma
public override bool AbandonWhenCannotCompleteSubjectives => false;
public override bool AllowSubObjectiveSorting => true;
public virtual bool InverseTargetEvaluation => false;
protected virtual bool ResetWhenClearingIgnoreList => true;
public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace.CleanupStackTrace()); }
@@ -55,7 +56,15 @@ namespace Barotrauma
{
if (ignoreListTimer > IgnoreListClearInterval)
{
Reset();
if (ResetWhenClearingIgnoreList)
{
Reset();
}
else
{
ignoreList.Clear();
ignoreListTimer = 0;
}
}
else
{
@@ -113,7 +122,7 @@ namespace Barotrauma
Priority = 0;
return Priority;
}
if (character.LockHands || character.Submarine == null)
if (character.LockHands)
{
Priority = 0;
}
@@ -131,7 +140,7 @@ namespace Barotrauma
// If the priority is higher than the target value, let's just use it.
// The priority calculation is more precise, but it takes into account things like distances,
// so it's better not to use it if it's lower than the rougher targetValue.
targetValue = Priority;
targetValue = currentSubObjective.Priority;
}
// If the target value is less than 1% of the max value, let's just treat it as zero.
if (targetValue < 1)
@@ -137,7 +137,7 @@ namespace Barotrauma
Item item = null;
if (orderPrefab.MustSetTarget)
{
item = orderPrefab.GetMatchingItems(character.Submarine, mustBelongToPlayerSub: false, requiredTeam: character.Info.TeamID, interactableFor: character)?.GetRandom();
item = orderPrefab.GetMatchingItems(character.Submarine, mustBelongToPlayerSub: false, requiredTeam: character.Info.TeamID, interactableFor: character, orderOption: autonomousObjective.option)?.GetRandom();
}
var order = new Order(orderPrefab, item ?? character.CurrentHull as Entity, orderPrefab.GetTargetItemComponent(item), orderGiver: character);
if (order == null) { continue; }
@@ -288,6 +288,7 @@ namespace Barotrauma
float highestPriority = 0;
for (int i = CurrentOrders.Count - 1; i >= 0; i--)
{
if (CurrentOrders.Count <= i) { break; }
var orderObjective = CurrentOrders[i].Objective;
if (orderObjective == null) { continue; }
orderObjective.CalculatePriority();
@@ -332,7 +333,6 @@ namespace Barotrauma
SortObjectives();
}
private CoroutineHandle speakRoutine;
public void SetOrder(Order order, string option, int priority, Character orderGiver, bool speak)
{
if (character.IsDead)
@@ -364,6 +364,7 @@ namespace Barotrauma
// Make sure the order priorities reflect those set by the player
for (int i = CurrentOrders.Count - 1; i >= 0; i--)
{
if (CurrentOrders.Count <= i) { break; }
var currentOrder = CurrentOrders[i];
if (currentOrder.Objective == null || currentOrder.MatchesOrder(order, option))
{
@@ -388,6 +389,7 @@ namespace Barotrauma
var newCurrentOrder = CreateObjective(order, option, orderGiver);
if (newCurrentOrder != null)
{
newCurrentOrder.Abandoned += () => DismissSelf(order, option);
CurrentOrders.Add(new OrderInfo(order, option, priority, newCurrentOrder));
}
if (!HasOrders())
@@ -395,53 +397,12 @@ namespace Barotrauma
// Recreate objectives, because some of them may be removed, if impossible to complete (e.g. due to path finding)
CreateAutonomousObjectives();
}
else
else if (newCurrentOrder != null)
{
// This should be redundant, because all the objectives are reset when they are selected as active.
newCurrentOrder?.Reset();
if (speak && character.IsOnPlayerTeam)
{
character.Speak(TextManager.Get("DialogAffirmative"), null, 1.0f);
//if (speakRoutine != null)
//{
// CoroutineManager.StopCoroutines(speakRoutine);
//}
//speakRoutine = CoroutineManager.InvokeAfter(() =>
//{
// if (GameMain.GameSession == null || Level.Loaded == null) { return; }
// if (newCurrentOrder != null && character.SpeechImpediment < 100.0f)
// {
// if (newCurrentOrder is AIObjectiveRepairItems repairItems && repairItems.Targets.None())
// {
// character.Speak(TextManager.Get("DialogNoRepairTargets"), null, 3.0f, "norepairtargets");
// }
// else if (newCurrentOrder is AIObjectiveChargeBatteries chargeBatteries && chargeBatteries.Targets.None())
// {
// character.Speak(TextManager.Get("DialogNoBatteries"), null, 3.0f, "nobatteries");
// }
// else if (newCurrentOrder is AIObjectiveExtinguishFires extinguishFires && extinguishFires.Targets.None())
// {
// character.Speak(TextManager.Get("DialogNoFire"), null, 3.0f, "nofire");
// }
// else if (newCurrentOrder is AIObjectiveFixLeaks fixLeaks && fixLeaks.Targets.None())
// {
// character.Speak(TextManager.Get("DialogNoLeaks"), null, 3.0f, "noleaks");
// }
// else if (newCurrentOrder is AIObjectiveFightIntruders fightIntruders && fightIntruders.Targets.None())
// {
// character.Speak(TextManager.Get("DialogNoEnemies"), null, 3.0f, "noenemies");
// }
// else if (newCurrentOrder is AIObjectiveRescueAll rescueAll && rescueAll.Targets.None())
// {
// character.Speak(TextManager.Get("DialogNoRescueTargets"), null, 3.0f, "norescuetargets");
// }
// else if (newCurrentOrder is AIObjectivePumpWater pumpWater && pumpWater.Targets.None())
// {
// character.Speak(TextManager.Get("DialogNoPumps"), null, 3.0f, "nopumps");
// }
// }
//}, 3);
string msg = newCurrentOrder.IsAllowed ? TextManager.Get("DialogAffirmative") : TextManager.Get("DialogNegative");
character.Speak(msg, delay: 1.0f);
}
}
}
@@ -456,13 +417,14 @@ namespace Barotrauma
if (orderGiver == null) { return null; }
newObjective = new AIObjectiveGoTo(orderGiver, character, this, repeat: true, priorityModifier: priorityModifier)
{
CloseEnough = Rand.Range(90, 100) + Rand.Range(50, 70) * Math.Min(HumanAIController.CountCrew(c => c.ObjectiveManager.CurrentOrder is AIObjectiveGoTo gotoOrder && gotoOrder.Target == orderGiver, onlyBots: true), 4),
extraDistanceOutsideSub = 100,
extraDistanceWhileSwimming = 100,
CloseEnough = Rand.Range(80, 100),
CloseEnoughMultiplier = Math.Min(1 + HumanAIController.CountCrew(c => c.ObjectiveManager.HasOrder<AIObjectiveGoTo>(o => o.Target == orderGiver), onlyBots: true) * Rand.Range(0.8f, 1f), 4),
ExtraDistanceOutsideSub = 100,
ExtraDistanceWhileSwimming = 100,
AllowGoingOutside = true,
IgnoreIfTargetDead = true,
isFollowOrderObjective = true,
mimic = true,
IsFollowOrderObjective = true,
Mimic = character.IsOnPlayerTeam,
DialogueIdentifier = "dialogcannotreachplace"
};
break;
@@ -474,7 +436,6 @@ namespace Barotrauma
break;
case "return":
newObjective = new AIObjectiveReturn(character, orderGiver, this, priorityModifier: priorityModifier);
newObjective.Abandoned += () => DismissSelf(order, option);
newObjective.Completed += () => DismissSelf(order, option);
break;
case "fixleaks":
@@ -500,12 +461,10 @@ namespace Barotrauma
if (!order.TargetItemComponent.Item.IsInteractable(character)) { return null; }
newObjective = new AIObjectiveOperateItem(targetPump, character, this, option, false, priorityModifier: priorityModifier)
{
IsLoop = true,
IsLoop = false,
Override = orderGiver != null && orderGiver.IsCommanding
};
// ItemComponent.AIOperate() returns false by default -> We'd have to set IsLoop = false and implement a custom override of AIOperate for the Pump.cs,
// if we want that the bot just switches the pump on/off and continues doing something else.
// If we want that the bot does the objective and then forgets about it, I think we could do the same plus dismiss when the bot is done.
newObjective.Completed += () => DismissSelf(order, option);
}
else
{
@@ -575,6 +534,39 @@ namespace Barotrauma
case "escapehandcuffs":
newObjective = new AIObjectiveEscapeHandcuffs(character, this, priorityModifier: priorityModifier);
break;
case "prepareforexpedition":
newObjective = new AIObjectivePrepare(character, this, order.GetTargetItems(option), order.RequireItems)
{
KeepActiveWhenReady = true,
CheckInventory = true,
Equip = false,
FindAllItems = true
};
break;
case "findweapon":
AIObjectivePrepare prepareObjective;
if (order.TargetEntity is Item tItem)
{
prepareObjective = new AIObjectivePrepare(character, this, targetItem: tItem);
}
else
{
prepareObjective = new AIObjectivePrepare(character, this, order.GetTargetItems(option), order.RequireItems)
{
KeepActiveWhenReady = false,
CheckInventory = false,
EvaluateCombatPriority = true,
FindAllItems = false
};
}
prepareObjective.KeepActiveWhenReady = false;
prepareObjective.Equip = true;
newObjective = prepareObjective;
newObjective.Completed += () => DismissSelf(order, option);
break;
case "loaditems":
newObjective = new AIObjectiveLoadItems(character, this, option, order.GetTargetItems(option), order.TargetEntity as Item, priorityModifier);
break;
default:
if (order.TargetItemComponent == null) { return null; }
if (!order.TargetItemComponent.Item.IsInteractable(character)) { return null; }
@@ -606,17 +598,20 @@ namespace Barotrauma
#endif
return;
}
Order dismissOrder = Order.GetPrefab("dismissed");
var orderOption = Order.GetDismissOrderOption(currentOrder);
int priority = currentOrder.ManualPriority;
#if CLIENT
if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.IsSinglePlayer)
{
GameMain.GameSession?.CrewManager?.SetCharacterOrder(character, Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(currentOrder), currentOrder.ManualPriority, character);
GameMain.GameSession.CrewManager.SetCharacterOrder(character, dismissOrder, orderOption, priority, character);
}
#else
GameMain.Server?.SendOrderChatMessage(new OrderChatMessage(Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(currentOrder), currentOrder.ManualPriority, currentOrder.Order?.TargetSpatialEntity, character, character));
GameMain.Server?.SendOrderChatMessage(new OrderChatMessage(dismissOrder, orderOption, priority, currentOrder.Order.TargetSpatialEntity, character, character));
SetOrder(dismissOrder, orderOption, priority, character, speak: false);
#endif
}
private bool IsAllowedToWait()
{
if (!character.IsOnPlayerTeam) { return false; }
@@ -665,10 +660,9 @@ namespace Barotrauma
return ForcedOrder != null || CurrentOrders.Any();
}
public bool HasOrder<T>() where T : AIObjective
{
return ForcedOrder is T || CurrentOrders.Any(o => o.Objective is T);
}
public bool HasOrder<T>(Func<T, bool> predicate = null) where T : AIObjective =>
ForcedOrder is T forcedOrder && (predicate == null || predicate(forcedOrder)) ||
CurrentOrders.Any(o => o.Objective is T order && (predicate == null || predicate(order)));
public float GetOrderPriority(AIObjective objective)
{
@@ -202,7 +202,7 @@ namespace Barotrauma
HumanAIController.FaceTarget(target.Item);
if (character.SelectedConstruction != target.Item)
{
target.Item.TryInteract(character, false, true);
target.Item.TryInteract(character, forceSelectKey: true);
}
if (component.AIOperate(deltaTime, character, this))
{
@@ -213,7 +213,6 @@ namespace Barotrauma
{
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(target.Item, character, objectiveManager, closeEnough: 50)
{
DialogueIdentifier = "dialogcannotreachtarget",
TargetName = target.Item.Name,
endNodeFilter = node => node.Waypoint.Ladders == null
},
@@ -0,0 +1,200 @@
using Barotrauma.Extensions;
using System;
using System.Linq;
using System.Collections.Generic;
using System.Collections.Immutable;
namespace Barotrauma
{
class AIObjectivePrepare : AIObjective
{
public override string Identifier { get; set; } = "prepare";
public override string DebugTag => $"{Identifier}";
public override bool KeepDivingGearOn => true;
public override bool PrioritizeIfSubObjectivesActive => true;
private AIObjectiveGetItem getSingleItemObjective;
private AIObjectiveGetItems getAllItemsObjective;
private AIObjectiveGetItems getMultipleItemsObjective;
private bool subObjectivesCreated;
private readonly Item targetItem;
private readonly ImmutableArray<string> requiredItems;
private readonly ImmutableArray<string> optionalItems;
private readonly HashSet<Item> items = new HashSet<Item>();
public bool KeepActiveWhenReady { get; set; }
public bool CheckInventory { get; set; }
public bool FindAllItems { get; set; }
public bool Equip { get; set; }
public bool EvaluateCombatPriority { get; set; }
private AIObjective GetSubObjective()
{
if (getSingleItemObjective != null) { return getSingleItemObjective; }
if (getAllItemsObjective == null || getAllItemsObjective.IsCompleted)
{
return getMultipleItemsObjective;
}
return getAllItemsObjective;
}
public AIObjectivePrepare(Character character, AIObjectiveManager objectiveManager, Item targetItem, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
this.targetItem = targetItem;
}
public AIObjectivePrepare(Character character, AIObjectiveManager objectiveManager, IEnumerable<string> optionalItems, IEnumerable<string> requiredItems = null, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
this.optionalItems = optionalItems.ToImmutableArray();
if (requiredItems != null)
{
this.requiredItems = requiredItems.ToImmutableArray();
}
}
protected override bool CheckObjectiveSpecific() => IsCompleted;
protected override float GetPriority()
{
if (!IsAllowed)
{
Priority = 0;
Abandon = true;
return Priority;
}
Priority = objectiveManager.GetOrderPriority(this);
var subObjective = GetSubObjective();
if (subObjective != null && subObjective.IsCompleted)
{
Priority = 0;
items.RemoveWhere(i => i == null || i.Removed || !i.IsOwnedBy(character));
if (items.None())
{
Abandon = true;
}
else if (items.Any(i => i.Components.Any(i => !i.IsLoaded(character))))
{
Reset();
}
}
return Priority;
}
protected override void Act(float deltaTime)
{
if (character.LockHands)
{
Abandon = true;
return;
}
if (!subObjectivesCreated)
{
if (FindAllItems && targetItem == null)
{
getMultipleItemsObjective = CreateObjectives(optionalItems, requireAll: false);
if (requiredItems != null && requiredItems.Any())
{
getAllItemsObjective = CreateObjectives(requiredItems, requireAll: true);
}
AIObjectiveGetItems CreateObjectives(IEnumerable<string> itemTags, bool requireAll)
{
AIObjectiveGetItems objectiveReference = null;
if (!TryAddSubObjective(ref objectiveReference, () => new AIObjectiveGetItems(character, objectiveManager, itemTags)
{
CheckInventory = CheckInventory,
Equip = Equip,
EvaluateCombatPriority = EvaluateCombatPriority,
RequireLoaded = true,
RequireAllItems = requireAll
},
onCompleted: () =>
{
if (KeepActiveWhenReady)
{
if (objectiveReference != null)
{
foreach (var item in objectiveReference.achievedItems)
{
if (item?.IsOwnedBy(character) != null)
{
items.Add(item);
}
}
}
}
else
{
IsCompleted = true;
}
},
onAbandon: () => Abandon = true))
{
Abandon = true;
}
return objectiveReference;
}
}
else
{
Func<AIObjectiveGetItem> getItemConstructor;
if (targetItem != null)
{
getItemConstructor = () => new AIObjectiveGetItem(character, targetItem, objectiveManager, equip: Equip)
{
SpeakIfFails = true
};
}
else
{
IEnumerable<string> allItems = optionalItems;
if (requiredItems != null && requiredItems.Any())
{
allItems = requiredItems;
}
getItemConstructor = () => new AIObjectiveGetItem(character, allItems, objectiveManager, equip: Equip, checkInventory: CheckInventory)
{
EvaluateCombatPriority = EvaluateCombatPriority,
SpeakIfFails = true,
RequireLoaded = true
};
}
if (!TryAddSubObjective(ref getSingleItemObjective, getItemConstructor,
onCompleted: () =>
{
if (KeepActiveWhenReady)
{
if (getSingleItemObjective != null)
{
var item = getSingleItemObjective?.TargetItem;
if (item?.IsOwnedBy(character) != null)
{
items.Add(item);
}
}
}
else
{
IsCompleted = true;
}
},
onAbandon: () => Abandon = true))
{
Abandon = true;
}
}
subObjectivesCreated = true;
}
}
public override void Reset()
{
base.Reset();
items.Clear();
subObjectivesCreated = false;
getMultipleItemsObjective = null;
getSingleItemObjective = null;
getAllItemsObjective = null;
}
}
}
@@ -20,6 +20,9 @@ namespace Barotrauma
private float previousCondition = -1;
private RepairTool repairTool;
private const float WaitTimeBeforeRepair = 0.5f;
private float waitTimer;
private bool IsRepairing() => IsRepairing(character, Item);
private readonly bool isPriority;
@@ -107,7 +110,7 @@ namespace Barotrauma
{
foreach (RelatedItem requiredItem in kvp.Value)
{
var getItemObjective = new AIObjectiveGetItem(character, requiredItem.Identifiers, objectiveManager, true)
var getItemObjective = new AIObjectiveGetItem(character, requiredItem.Identifiers, objectiveManager, equip: true)
{
AllowVariants = requiredItem.AllowVariants
};
@@ -160,6 +163,9 @@ namespace Barotrauma
}
if (!character.IsClimbing && character.CanInteractWith(Item, out _, checkLinked: false))
{
waitTimer += deltaTime;
if (waitTimer < WaitTimeBeforeRepair) { return; }
HumanAIController.FaceTarget(Item);
if (repairTool != null)
{
@@ -176,8 +182,12 @@ namespace Barotrauma
{
if (character.SelectedConstruction != Item)
{
if (!Item.TryInteract(character, ignoreRequiredItems: true, forceSelectKey: true) &&
!Item.TryInteract(character, ignoreRequiredItems: true, forceActionKey: true))
if (Item.TryInteract(character, ignoreRequiredItems: true, forceSelectKey: true) ||
Item.TryInteract(character, ignoreRequiredItems: true, forceUseKey: true))
{
character.SelectedConstruction = Item;
}
else
{
Abandon = true;
}
@@ -209,6 +219,7 @@ namespace Barotrauma
}
else
{
waitTimer = 0.0f;
RemoveSubObjective(ref refuelObjective);
// If cannot reach the item, approach it.
TryAddSubObjective(ref goToObjective,
@@ -219,8 +230,7 @@ namespace Barotrauma
{
// Don't stop in ladders, because we can't interact with other items while holding the ladders.
endNodeFilter = node => node.Waypoint.Ladders == null,
// Allow repairing hatches and airlock doors.
AllowGoingOutside = HumanAIController.ObjectiveManager.IsCurrentOrder<AIObjectiveRepairItems>() && Item.GetComponent<Door>() != null
TargetName = Item.Name
};
if (repairTool != null)
{
@@ -91,8 +91,7 @@ namespace Barotrauma
public static bool NearlyFullCondition(Item item)
{
float condition = item.ConditionPercentage;
return item.Repairables.All(r => condition >= r.RepairThreshold);
return item.Repairables.All(r => !r.IsBelowRepairThreshold);
}
protected override float TargetEvaluation()
@@ -36,9 +36,9 @@ namespace Barotrauma
{
if (targetCharacter == null)
{
string errorMsg = $"{character.Name}: Attempted to create a Rescue objective with no target!\n" + Environment.StackTrace.CleanupStackTrace();
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("AIObjectiveRescue:ctor:targetnull", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
string errorMsg = $"Attempted to create a Rescue objective with no target!\n" + Environment.StackTrace.CleanupStackTrace();
DebugConsole.ThrowError(character.Name + ": " + errorMsg);
GameAnalyticsManager.AddErrorEventOnce("AIObjectiveRescue:ctor:targetnull", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
Abandon = true;
return;
}
@@ -59,7 +59,7 @@ namespace Barotrauma
protected override void Act(float deltaTime)
{
if (character.LockHands || targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead)
if (character.LockHands || targetCharacter == null || targetCharacter.Removed || targetCharacter.IsDead)
{
Abandon = true;
return;
@@ -137,66 +137,69 @@ namespace Barotrauma
recursive: true);
}
}
if (HumanAIController.GetHullSafety(targetCharacter.CurrentHull, targetCharacter) < HumanAIController.HULL_SAFETY_THRESHOLD)
if (character.Submarine != null)
{
// Incapacitated target is not in a safe place -> Move to a safe place first
if (character.SelectedCharacter != targetCharacter)
if (HumanAIController.GetHullSafety(targetCharacter.CurrentHull, targetCharacter) < HumanAIController.HULL_SAFETY_THRESHOLD)
{
if (targetCharacter.CurrentHull != null && HumanAIController.VisibleHulls.Contains(targetCharacter.CurrentHull) && targetCharacter.CurrentHull.DisplayName != null)
// Incapacitated target is not in a safe place -> Move to a safe place first
if (character.SelectedCharacter != targetCharacter)
{
character.Speak(TextManager.GetWithVariables("DialogFoundUnconsciousTarget", new string[2] { "[targetname]", "[roomname]" },
new string[2] { targetCharacter.Name, targetCharacter.CurrentHull.DisplayName }, new bool[2] { false, true }),
null, 1.0f, "foundunconscioustarget" + targetCharacter.Name, 60.0f);
}
// Go to the target and select it
if (!character.CanInteractWith(targetCharacter))
{
RemoveSubObjective(ref replaceOxygenObjective);
RemoveSubObjective(ref goToObjective);
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager)
if (targetCharacter.CurrentHull != null && HumanAIController.VisibleHulls.Contains(targetCharacter.CurrentHull) && targetCharacter.CurrentHull.DisplayName != null)
{
CloseEnough = CloseEnoughToTreat,
DialogueIdentifier = "dialogcannotreachpatient",
TargetName = targetCharacter.DisplayName
},
onCompleted: () => RemoveSubObjective(ref goToObjective),
onAbandon: () =>
character.Speak(TextManager.GetWithVariables("DialogFoundUnconsciousTarget", new string[2] { "[targetname]", "[roomname]" },
new string[2] { targetCharacter.Name, targetCharacter.CurrentHull.DisplayName }, new bool[2] { false, true }),
null, 1.0f, "foundunconscioustarget" + targetCharacter.Name, 60.0f);
}
// Go to the target and select it
if (!character.CanInteractWith(targetCharacter))
{
RemoveSubObjective(ref replaceOxygenObjective);
RemoveSubObjective(ref goToObjective);
Abandon = true;
});
}
else
{
character.SelectCharacter(targetCharacter);
}
}
else
{
// Drag the character into safety
if (safeHull == null)
{
if (findHullTimer > 0)
{
findHullTimer -= deltaTime;
}
else
{
safeHull = objectiveManager.GetObjective<AIObjectiveFindSafety>().FindBestHull(HumanAIController.VisibleHulls);
findHullTimer = findHullInterval * Rand.Range(0.9f, 1.1f);
}
}
if (safeHull != null && character.CurrentHull != safeHull)
{
RemoveSubObjective(ref replaceOxygenObjective);
RemoveSubObjective(ref goToObjective);
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(safeHull, character, objectiveManager),
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager)
{
CloseEnough = CloseEnoughToTreat,
DialogueIdentifier = "dialogcannotreachpatient",
TargetName = targetCharacter.DisplayName
},
onCompleted: () => RemoveSubObjective(ref goToObjective),
onAbandon: () =>
{
RemoveSubObjective(ref goToObjective);
safeHull = character.CurrentHull;
Abandon = true;
});
}
else
{
character.SelectCharacter(targetCharacter);
}
}
else
{
// Drag the character into safety
if (safeHull == null)
{
if (findHullTimer > 0)
{
findHullTimer -= deltaTime;
}
else
{
safeHull = objectiveManager.GetObjective<AIObjectiveFindSafety>().FindBestHull(HumanAIController.VisibleHulls);
findHullTimer = findHullInterval * Rand.Range(0.9f, 1.1f);
}
}
if (safeHull != null && character.CurrentHull != safeHull)
{
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;
});
}
}
}
}
@@ -228,7 +231,7 @@ namespace Barotrauma
// We can start applying treatment
if (character != targetCharacter && character.SelectedCharacter != targetCharacter)
{
if (targetCharacter.CurrentHull.DisplayName != null)
if (targetCharacter.CurrentHull?.DisplayName != null)
{
character.Speak(TextManager.GetWithVariables("DialogFoundWoundedTarget", new string[2] { "[targetname]", "[roomname]" },
new string[2] { targetCharacter.Name, targetCharacter.CurrentHull.DisplayName }, new bool[2] { false, true }),
@@ -252,7 +255,7 @@ namespace Barotrauma
return;
}
SteeringManager?.Reset();
SteeringManager.Reset();
if (!targetCharacter.IsPlayer)
{
@@ -269,26 +272,36 @@ namespace Barotrauma
float cprSuitability = targetCharacter.Oxygen < 0.0f ? -targetCharacter.Oxygen * 100.0f : 0.0f;
//find which treatments are the most suitable to treat the character's current condition
targetCharacter.CharacterHealth.GetSuitableTreatments(currentTreatmentSuitabilities, normalize: false);
targetCharacter.CharacterHealth.GetSuitableTreatments(currentTreatmentSuitabilities, normalize: false, predictFutureDuration: 10.0f);
//check if we already have a suitable treatment for any of the afflictions
foreach (Affliction affliction in GetSortedAfflictions(targetCharacter))
{
if (affliction == null) { throw new Exception("Affliction was null"); }
if (affliction.Prefab == null) { throw new Exception("Affliction prefab was null"); }
float bestSuitability = 0.0f;
Item bestItem = null;
foreach (KeyValuePair<string, float> treatmentSuitability in affliction.Prefab.TreatmentSuitability)
{
if (currentTreatmentSuitabilities.ContainsKey(treatmentSuitability.Key) && currentTreatmentSuitabilities[treatmentSuitability.Key] > 0.0f)
if (currentTreatmentSuitabilities.ContainsKey(treatmentSuitability.Key) &&
currentTreatmentSuitabilities[treatmentSuitability.Key] > bestSuitability)
{
Item matchingItem = character.Inventory.FindItemByIdentifier(treatmentSuitability.Key, true);
if (matchingItem == null) { continue; }
if (targetCharacter != character) { character.SelectCharacter(targetCharacter); }
ApplyTreatment(affliction, matchingItem);
//wait a bit longer after applying a treatment to wait for potential side-effects to manifest
treatmentTimer = TreatmentDelay * 4;
return;
if (matchingItem != null)
{
bestItem = matchingItem;
bestSuitability = currentTreatmentSuitabilities[treatmentSuitability.Key];
}
}
}
if (bestItem != null)
{
if (targetCharacter != character) { character.SelectCharacter(targetCharacter); }
ApplyTreatment(affliction, bestItem);
//wait a bit longer after applying a treatment to wait for potential side-effects to manifest
treatmentTimer = TreatmentDelay * 4;
return;
}
}
// Find treatments outside of own inventory only if inside the own sub.
if (character.Submarine != null && character.Submarine.TeamID == character.TeamID)
@@ -319,9 +332,32 @@ namespace Barotrauma
{
itemListStr = itemNameList[0];
}
else if (itemNameList.Count == 2)
{
//[treatment1] or [treatment2]
itemListStr = TextManager.GetWithVariables(
"DialogRequiredTreatmentOptionsLast",
new string[] { "[treatment1]", "[treatment2]" },
new string[] { itemNameList[0], itemNameList[1] });
}
else
{
itemListStr = string.Join(" or ", string.Join(", ", itemNameList.Take(itemNameList.Count - 1)), itemNameList.Last());
//[treatment1], [treatment2], [treatment3] ... or [treatmentx]
itemListStr = TextManager.GetWithVariables(
"DialogRequiredTreatmentOptionsFirst",
new string[] { "[treatment1]", "[treatment2]" },
new string[] { itemNameList[0], itemNameList[1] });
for (int i = 2; i < itemNameList.Count - 1; i++)
{
itemListStr = TextManager.GetWithVariables(
"DialogRequiredTreatmentOptionsFirst",
new string[] { "[treatment1]", "[treatment2]" },
new string[] { itemListStr, itemNameList[i] });
}
itemListStr = TextManager.GetWithVariables(
"DialogRequiredTreatmentOptionsLast",
new string[] { "[treatment1]", "[treatment2]" },
new string[] { itemListStr, itemNameList.Last() });
}
if (targetCharacter != character && character.IsOnPlayerTeam)
{
@@ -395,21 +431,7 @@ namespace Barotrauma
protected override bool CheckObjectiveSpecific()
{
if (character.LockHands || targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead)
{
Abandon = true;
return false;
}
// Don't go into rooms that have enemies
if (Character.CharacterList.Any(c => c.CurrentHull == targetCharacter.CurrentHull && !HumanAIController.IsFriendly(character, c) && HumanAIController.IsActive(c)))
{
Abandon = true;
return false;
}
bool isCompleted =
AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) >= AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager, character, targetCharacter) ||
targetCharacter.CharacterHealth.GetAllAfflictions().All(a => a.Prefab.IsBuff || a.Strength <= a.Prefab.TreatmentThreshold);
bool isCompleted = AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) >= AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager, character, targetCharacter);
if (isCompleted && targetCharacter != character && character.IsOnPlayerTeam)
{
character.Speak(TextManager.GetWithVariable("DialogTargetHealed", "[targetname]", targetCharacter.Name),
@@ -426,16 +448,36 @@ namespace Barotrauma
Abandon = true;
return Priority;
}
if (character.LockHands || targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead)
if (character.CurrentHull == null)
{
if (!objectiveManager.HasOrder<AIObjectiveRescueAll>())
{
Priority = 0;
Abandon = true;
return Priority;
}
}
else if (Character.CharacterList.Any(c => c.CurrentHull == targetCharacter.CurrentHull && !HumanAIController.IsFriendly(character, c) && HumanAIController.IsActive(c)))
{
// Don't go into rooms that have enemies
Priority = 0;
Abandon = true;
return Priority;
}
if (targetCharacter == null)
{
Priority = 0;
Abandon = true;
}
else
{
// Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally)
float dist = Math.Abs(character.WorldPosition.X - targetCharacter.WorldPosition.X) + Math.Abs(character.WorldPosition.Y - targetCharacter.WorldPosition.Y) * 2.0f;
float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, dist));
float horizontalDistance = Math.Abs(character.WorldPosition.X - targetCharacter.WorldPosition.X);
float verticalDistance = Math.Abs(character.WorldPosition.Y - targetCharacter.WorldPosition.Y);
if (character.Submarine?.Info is { IsRuin: false })
{
verticalDistance *= 2;
}
float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, horizontalDistance + verticalDistance));
if (targetCharacter.CurrentHull == character.CurrentHull)
{
distanceFactor = 1;
@@ -449,6 +491,16 @@ namespace Barotrauma
public static IEnumerable<Affliction> GetSortedAfflictions(Character character) => CharacterHealth.SortAfflictionsBySeverity(character.CharacterHealth.GetAllAfflictions());
public static IEnumerable<Affliction> GetTreatableAfflictions(Character character)
{
foreach (Affliction affliction in character.CharacterHealth.GetAllAfflictions())
{
if (affliction.Prefab.IsBuff || affliction.Strength < affliction.Prefab.TreatmentThreshold) { continue; }
if (!affliction.Prefab.TreatmentSuitability.Any(kvp => kvp.Value > 0)) { continue; }
yield return affliction;
}
}
public override void Reset()
{
base.Reset();
@@ -64,8 +64,15 @@ namespace Barotrauma
public static float GetVitalityFactor(Character character)
{
float vitality = character.HealthPercentage - (character.Bleeding * 2) - character.Bloodloss + Math.Min(character.Oxygen, 0);
float vitality = 100;
vitality -= character.Bleeding * 2;
vitality += Math.Min(character.Oxygen, 0);
vitality -= character.CharacterHealth.GetAfflictionStrength("paralysis");
foreach (Affliction affliction in AIObjectiveRescue.GetTreatableAfflictions(character))
{
float strength = character.CharacterHealth.GetPredictedStrength(affliction, predictFutureDuration: 10.0f);
vitality -= affliction.GetVitalityDecrease(character.CharacterHealth, strength) / character.MaxVitality * 100;
}
return Math.Clamp(vitality, 0, 100);
}
@@ -79,11 +86,11 @@ namespace Barotrauma
{
if (target == null || target.IsDead || target.Removed) { return false; }
if (target.IsInstigator) { return false; }
if (target.IsPet) { return false; }
if (!HumanAIController.IsFriendly(character, target, onlySameTeam: true)) { return false; }
if (character.AIController is HumanAIController humanAI)
{
if (GetVitalityFactor(target) >= GetVitalityThreshold(humanAI.ObjectiveManager, character, target) ||
target.CharacterHealth.GetAllAfflictions().All(a => a.Prefab.IsBuff || a.Strength <= a.Prefab.TreatmentThreshold))
if (GetVitalityFactor(target) >= GetVitalityThreshold(humanAI.ObjectiveManager, character, target))
{
return false;
}
@@ -105,10 +112,13 @@ namespace Barotrauma
{
if (GetVitalityFactor(target) >= vitalityThreshold) { return false; }
}
if (target.Submarine == null || character.Submarine == null) { return false; }
// Don't allow going into another sub, unless it's connected and of the same team and type.
if (!character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, includingConnectedSubs: true)) { return false; }
if (target != character &&!target.IsPlayer && HumanAIController.IsActive(target) && target.AIController is HumanAIController targetAI)
if (target.Submarine != character.Submarine) { return false; }
if (character.Submarine != null)
{
// Don't allow going into another sub, unless it's connected and of the same team and type.
if (!character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, includingConnectedSubs: true)) { return false; }
}
if (target != character && target.IsBot && HumanAIController.IsActive(target) && target.AIController is HumanAIController targetAI)
{
// Ignore all concious targets that are currently fighting, fleeing, fixing, or treating characters
if (targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveCombat>() ||
@@ -119,9 +129,12 @@ namespace Barotrauma
return false;
}
}
// Don't go into rooms that have enemies
if (Character.CharacterList.Any(c => c.CurrentHull == target.CurrentHull && !HumanAIController.IsFriendly(character, c) && HumanAIController.IsActive(c))) { return false; }
return true;
if (target.CurrentHull != null)
{
// Don't go into rooms that have enemies
if (Character.CharacterList.Any(c => c.CurrentHull == target.CurrentHull && !HumanAIController.IsFriendly(character, c) && HumanAIController.IsActive(c))) { return false; }
}
return character.GetDamageDoneByAttacker(target) <= 0;
}
}
}
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Xml.Linq;
using System.Linq;
using System.Collections.Immutable;
namespace Barotrauma
{
@@ -19,11 +20,11 @@ namespace Barotrauma
struct OrderInfo
{
public Order Order { get; }
public string OrderOption { get; }
public int ManualPriority { get; }
public OrderType Type { get; }
public AIObjective Objective { get; }
public readonly Order Order;
public readonly string OrderOption;
public readonly int ManualPriority;
public readonly OrderType Type;
public readonly AIObjective Objective;
public bool IsCurrentOrder => Type == OrderType.Current;
public enum OrderType
@@ -41,6 +42,8 @@ namespace Barotrauma
Objective = objective;
}
public OrderInfo(Order order, string orderOption) : this(order, orderOption, CharacterInfo.HighestManualOrderPriority, null) { }
public OrderInfo(Order order, string orderOption, int manualPriority) : this(order, orderOption, manualPriority, OrderType.Current, null) { }
public OrderInfo(Order order, string orderOption, int manualPriority, AIObjective objective) : this(order, orderOption, manualPriority, OrderType.Current, objective) { }
@@ -103,7 +106,10 @@ namespace Barotrauma
public readonly Type ItemComponentType;
public readonly bool CanTypeBeSubclass;
public readonly string[] TargetItems;
public readonly ImmutableArray<string> TargetItems;
public readonly ImmutableArray<string> RequireItems;
private readonly Dictionary<string, ImmutableArray<string>> OptionTargetItems;
public bool HasOptionSpecificTargetItems => OptionTargetItems != null && OptionTargetItems.Any();
public readonly string Identifier;
@@ -170,6 +176,7 @@ namespace Barotrauma
public bool HasOptions => (IsPrefab ? Options : Prefab.Options).Length > 1;
public bool IsPrefab { get; private set; }
public readonly bool MustManuallyAssign;
public readonly bool AutoDismiss;
public readonly OrderTarget TargetPosition;
@@ -208,6 +215,12 @@ namespace Barotrauma
/// </summary>
public bool DrawIconWhenContained { get; }
/// <summary>
/// Affects how high on the order list the order will be placed (i.e. the manual priority order when it's given) when it's first given.
/// Manually rearranging orders will override this priority.
/// </summary>
public int AssignmentPriority { get; }
public static void Init()
{
Prefabs = new Dictionary<string, Order>();
@@ -305,8 +318,6 @@ namespace Barotrauma
}
}
CanTypeBeSubclass = orderElement.GetAttributeBool("cantypebesubclass", false);
TargetItems = orderElement.GetAttributeStringArray("targetitems", new string[0], trim: true, convertToLowerInvariant: true);
color = orderElement.GetAttributeColor("color");
FadeOutTime = orderElement.GetAttributeFloat("fadeouttime", 0.0f);
UseController = orderElement.GetAttributeBool("usecontroller", false);
@@ -316,6 +327,36 @@ namespace Barotrauma
Options = orderElement.GetAttributeStringArray("options", new string[0]);
HiddenOptions = orderElement.GetAttributeStringArray("hiddenoptions", new string[0]);
AllOptions = Options.Concat(HiddenOptions).ToArray();
OptionTargetItems = new Dictionary<string, ImmutableArray<string>>();
if (orderElement.GetAttributeString("targetitems", "") is string targetItems && targetItems.Contains(';'))
{
string[] splitTargetItems = targetItems.Split(';');
#if DEBUG
if (splitTargetItems.Length != AllOptions.Length)
{
DebugConsole.ThrowError($"Order \"{Identifier}\" has option-specific target items, but the option count doesn't match the target item count");
}
#endif
var allTargetItems = new List<string>();
for (int i = 0; i < AllOptions.Length; i++)
{
string[] optionTargetItems = i < splitTargetItems.Length ? splitTargetItems[i].Split(',', '') : new string[0];
for (int j = 0; j < optionTargetItems.Length; j++)
{
optionTargetItems[j] = optionTargetItems[j].ToLowerInvariant().Trim();
allTargetItems.Add(optionTargetItems[j]);
}
OptionTargetItems.Add(AllOptions[i], optionTargetItems.ToImmutableArray());
}
TargetItems = allTargetItems.ToImmutableArray();
}
else
{
TargetItems = orderElement.GetAttributeStringArray("targetitems", new string[0], trim: true, convertToLowerInvariant: true).ToImmutableArray();
}
RequireItems = orderElement.GetAttributeStringArray("requireitems", new string[0], trim: true, convertToLowerInvariant: true).ToImmutableArray();
var category = orderElement.GetAttributeString("category", null);
if (!string.IsNullOrWhiteSpace(category)) { this.Category = (OrderCategory)Enum.Parse(typeof(OrderCategory), category, true); }
MustSetTarget = orderElement.GetAttributeBool("mustsettarget", false);
@@ -363,6 +404,8 @@ namespace Barotrauma
MustManuallyAssign = orderElement.GetAttributeBool("mustmanuallyassign", false);
IsIgnoreOrder = Identifier == "ignorethis" || Identifier == "unignorethis";
DrawIconWhenContained = orderElement.GetAttributeBool("displayiconwhencontained", false);
AutoDismiss = orderElement.GetAttributeBool("autodismiss", Category == OrderCategory.Movement);
AssignmentPriority = Math.Clamp(orderElement.GetAttributeInt("assignmentpriority", 100), 0, 100);
}
/// <summary>
@@ -378,6 +421,8 @@ namespace Barotrauma
ItemComponentType = prefab.ItemComponentType;
CanTypeBeSubclass = prefab.CanTypeBeSubclass;
TargetItems = prefab.TargetItems;
OptionTargetItems = prefab.OptionTargetItems;
RequireItems = prefab.RequireItems;
Options = prefab.Options;
SymbolSprite = prefab.SymbolSprite;
Color = prefab.Color;
@@ -395,6 +440,7 @@ namespace Barotrauma
DrawIconWhenContained = prefab.DrawIconWhenContained;
Hidden = prefab.Hidden;
IgnoreAtOutpost = prefab.IgnoreAtOutpost;
AssignmentPriority = prefab.AssignmentPriority;
OrderGiver = orderGiver;
TargetEntity = targetEntity;
@@ -510,16 +556,17 @@ namespace Barotrauma
}
/// <param name="interactableFor">Only returns items which are interactable for this character</param>
public List<Item> GetMatchingItems(Submarine submarine, bool mustBelongToPlayerSub, CharacterTeamType? requiredTeam = null, Character interactableFor = null)
public List<Item> GetMatchingItems(Submarine submarine, bool mustBelongToPlayerSub, CharacterTeamType? requiredTeam = null, Character interactableFor = null, string orderOption = null)
{
List<Item> matchingItems = new List<Item>();
if (submarine == null) { return matchingItems; }
if (ItemComponentType != null || TargetItems.Length > 0)
if (ItemComponentType != null || TargetItems.Any() || RequireItems.Any())
{
foreach (var item in Item.ItemList)
{
if (TargetItems.Length > 0 && !TargetItems.Contains(item.Prefab.Identifier) && !item.HasTag(TargetItems)) { continue; }
if (TargetItems.Length == 0 && !TryGetTargetItemComponent(item, out _)) { continue; }
if (RequireItems.Any() && !TargetItemsMatchItem(RequireItems, item)) { continue; }
if (TargetItems.Any() && !TargetItemsMatchItem(item, orderOption)) { continue; }
if (RequireItems.None() && TargetItems.None() && !TryGetTargetItemComponent(item, out _)) { continue; }
if (mustBelongToPlayerSub && item.Submarine?.Info != null && item.Submarine.Info.Type != SubmarineType.Player) { continue; }
if (item.Submarine != submarine && !submarine.DockedTo.Contains(item.Submarine)) { continue; }
if (requiredTeam.HasValue && (item.Submarine == null || item.Submarine.TeamID != requiredTeam.Value)) { continue; }
@@ -534,14 +581,13 @@ namespace Barotrauma
return matchingItems;
}
/// <param name="interactableFor">Only returns items which are interactable for this character</param>
public List<Item> GetMatchingItems(bool mustBelongToPlayerSub, Character interactableFor = null)
public List<Item> GetMatchingItems(bool mustBelongToPlayerSub, Character interactableFor = null, string orderOption = null)
{
Submarine submarine = Character.Controlled != null && Character.Controlled.TeamID == CharacterTeamType.Team2 && Submarine.MainSubs.Length > 1 ?
Submarine.MainSubs[1] :
Submarine.MainSub;
return GetMatchingItems(submarine, mustBelongToPlayerSub, interactableFor: interactableFor);
return GetMatchingItems(submarine, mustBelongToPlayerSub, interactableFor: interactableFor, orderOption: orderOption);
}
public string GetOptionName(string id)
@@ -580,5 +626,34 @@ namespace Barotrauma
}
return "";
}
public override string ToString()
{
return $"Order ({Name})";
}
public ImmutableArray<string> GetTargetItems(string option = null)
{
if (string.IsNullOrEmpty(option) || !OptionTargetItems.TryGetValue(option, out ImmutableArray<string> optionTargetItems))
{
return TargetItems;
}
else
{
return optionTargetItems;
}
}
public bool TargetItemsMatchItem(Item item, string option = null)
{
if (item == null) { return false; }
ImmutableArray<string> targetItems = GetTargetItems(option);
return TargetItemsMatchItem(targetItems, item);
}
public static bool TargetItemsMatchItem(ImmutableArray<string> targetItems, Item item)
{
return item != null && targetItems != null && targetItems.Length > 0 && (targetItems.Contains(item.Prefab.Identifier) || item.HasTag(targetItems));
}
}
}
@@ -84,18 +84,17 @@ namespace Barotrauma
public bool IsBlocked()
{
if (blocked.HasValue) { return blocked.Value; }
blocked = false;
if (Waypoint.Submarine != null) { return blocked.Value; }
if (Waypoint.Tunnel?.Type != Level.TunnelType.Cave) { return blocked.Value; }
foreach (var w in Level.Loaded.ExtraWalls)
{
if (!(w is DestructibleLevelWall d)) { return blocked.Value; }
if (d.Destroyed) { return blocked.Value; }
if (!d.IsPointInside(Waypoint.Position)) { return blocked.Value; }
blocked = true;
break;
if (!w.IsPointInside(Waypoint.Position)) { continue; }
if (w is DestructibleLevelWall d)
{
blocked = !d.Destroyed;
}
if (blocked.Value) { break; }
}
return blocked.Value;
}
@@ -125,6 +124,7 @@ namespace Barotrauma
{
wp.OnLinksChanged += WaypointLinksChanged;
}
sortedNodes = new List<PathNode>(nodes.Count);
this.isCharacter = isCharacter;
}
@@ -166,7 +166,7 @@ namespace Barotrauma
}
}
private readonly List<PathNode> sortedNodes = new List<PathNode>();
private readonly List<PathNode> sortedNodes;
public SteeringPath FindPath(Vector2 start, Vector2 end, Submarine hostSub = null, string errorMsgStr = null, float minGapSize = 0, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null, bool checkVisibility = true)
{
@@ -175,8 +175,7 @@ namespace Barotrauma
node.ResetBlocked();
}
//sort nodes roughly according to distance
sortedNodes.Clear();
// First calculate the temp positions for all nodes.
foreach (PathNode node in nodes)
{
node.TempPosition = node.Position;
@@ -194,8 +193,15 @@ namespace Barotrauma
else if (hostSub == null && wpSub != null)
{
// Outside and targeting inside
node.TempPosition += wpSub.SimPosition;
node.TempPosition += wpSub.SimPosition;
}
}
//sort nodes roughly according to distance
sortedNodes.Clear();
PathNode startNode = null;
foreach (PathNode node in nodes)
{
float xDiff = Math.Abs(start.X - node.TempPosition.X);
float yDiff = Math.Abs(start.Y - node.TempPosition.Y);
if (InsideSubmarine && !(node.Waypoint.Submarine?.Info?.IsRuin ?? false))
@@ -215,13 +221,20 @@ namespace Barotrauma
//much higher cost to waypoints that are outside
if (node.Waypoint.CurrentHull == null && ApplyPenaltyToOutsideNodes) { node.TempDistance *= 10.0f; }
//optimization:
//node extremely far, don't try to use it as a start node
if (node.TempDistance > 800.0f)
//optimization: node extremely far, don't try to use it as a start node
if (node.TempDistance > (InsideSubmarine ? 100.0f : 800.0f))
{
continue;
}
//optimization: node extremely close (< 1 m). If it's valid, choose it as the start node and skip the more exhaustive search for the closest one
if (node.TempDistance < 1.0f)
{
if (IsValidStartNode(node))
{
startNode = node;
break;
}
}
//prefer nodes that are closer to the end position
node.TempDistance += (Math.Abs(end.X - node.TempPosition.X) + Math.Abs(end.Y - node.TempPosition.Y)) / 100.0f;
@@ -233,6 +246,88 @@ namespace Barotrauma
sortedNodes.Insert(i, node);
}
//find the most suitable start node, starting from the ones that are the closest
if (startNode == null)
{
foreach (PathNode node in sortedNodes)
{
if (IsValidStartNode(node))
{
startNode = node;
break;
}
}
}
if (startNode == null)
{
#if DEBUG
DebugConsole.NewMessage("Pathfinding error, couldn't find a start node. "+ errorMsgStr, Color.DarkRed);
#endif
return new SteeringPath(true);
}
//sort nodes again, now based on distance from the end position
sortedNodes.Clear();
PathNode endNode = null;
foreach (PathNode node in nodes)
{
node.TempDistance = Vector2.DistanceSquared(end, node.TempPosition);
if (InsideSubmarine)
{
if (ApplyPenaltyToOutsideNodes)
{
//much higher cost to waypoints that are outside
if (node.Waypoint.CurrentHull == null) { node.TempDistance *= 10.0f; }
}
//avoid stopping at a doorway
if (node.Waypoint.ConnectedDoor != null) { node.TempDistance *= 10.0f; }
//avoid stopping at a ladder
if (node.Waypoint.Ladders != null) { node.TempDistance *= 10.0f; }
}
//optimization: node extremely far (> 100m / 800 m) from the end position, don't try to use it as an end node
if (node.TempDistance > (InsideSubmarine ? 100.0f * 100.0f : 800.0f * 800.0f))
{
continue;
}
//optimization: node extremely close (< 1 m). If it's valid, choose it as the end node and skip the more exhaustive search for the closest one
if (node.TempDistance < 1.0f)
{
if (IsValidEndNode(node))
{
endNode = node;
break;
}
}
int i = 0;
while (i < sortedNodes.Count && sortedNodes[i].TempDistance < node.TempDistance)
{
i++;
}
sortedNodes.Insert(i, node);
}
if (endNode == null)
{
//find the most suitable end node, starting from the ones closest to the end position
foreach (PathNode node in sortedNodes)
{
if (IsValidEndNode(node))
{
endNode = node;
break;
}
}
}
if (endNode == null)
{
#if DEBUG
DebugConsole.NewMessage("Pathfinding error, couldn't find an end node. " + errorMsgStr, Color.DarkRed);
#endif
return new SteeringPath(true);
}
var path = FindPath(startNode, endNode, nodeFilter, errorMsgStr, minGapSize);
return path;
bool IsWaypointVisible(PathNode node, Vector2 rayStart, bool checkVisibility = true)
{
//if searching for a path inside the sub, make sure the waypoint is visible
@@ -251,85 +346,33 @@ namespace Barotrauma
return true;
}
//find the most suitable start node, starting from the ones that are the closest
PathNode startNode = null;
foreach (PathNode node in sortedNodes)
bool IsValidStartNode(PathNode node)
{
if (nodeFilter != null && !nodeFilter(node)) { continue; }
if (startNodeFilter != null && !startNodeFilter(node)) { continue; }
if (nodeFilter != null && !nodeFilter(node)) { return false; }
if (startNodeFilter != null && !startNodeFilter(node)) { return false; }
// Always check the visibility for the start node
if (!IsWaypointVisible(node, start)) { continue; }
if (node.IsBlocked()) { continue; }
if (!IsWaypointVisible(node, start)) { return false; }
if (node.IsBlocked()) { return false; }
if (node.Waypoint.ConnectedGap != null)
{
if (!CanFitThroughGap(node.Waypoint.ConnectedGap, minGapSize)) { continue; }
if (!CanFitThroughGap(node.Waypoint.ConnectedGap, minGapSize)) { return false; }
}
startNode = node;
break;
return true;
}
if (startNode == null)
bool IsValidEndNode(PathNode node)
{
#if DEBUG
DebugConsole.NewMessage("Pathfinding error, couldn't find a start node. "+ errorMsgStr, Color.DarkRed);
#endif
return new SteeringPath(true);
}
//sort nodes again, now based on distance from the end position
sortedNodes.Clear();
foreach (PathNode node in nodes)
{
node.TempDistance = Vector2.DistanceSquared(end, node.TempPosition);
if (InsideSubmarine)
{
if (ApplyPenaltyToOutsideNodes)
{
//much higher cost to waypoints that are outside
if (node.Waypoint.CurrentHull == null) { node.TempDistance *= 10.0f; }
}
//avoid stopping at a doorway
if (node.Waypoint.ConnectedDoor != null) { node.TempDistance *= 10.0f; }
//avoid stopping at a ladder
if (node.Waypoint.Ladders != null) { node.TempDistance *= 10.0f; }
}
int i = 0;
while (i < sortedNodes.Count && sortedNodes[i].TempDistance < node.TempDistance)
{
i++;
}
sortedNodes.Insert(i, node);
}
//find the most suitable end node, starting from the ones closest to the end position
PathNode endNode = null;
foreach (PathNode node in sortedNodes)
{
if (nodeFilter != null && !nodeFilter(node)) { continue; }
if (endNodeFilter != null && !endNodeFilter(node)) { continue; }
if (nodeFilter != null && !nodeFilter(node)) { return false; }
if (endNodeFilter != null && !endNodeFilter(node)) { return false; }
// Only check the visibility for the end node when allowed (fix leaks)
if (!IsWaypointVisible(node, end, checkVisibility: checkVisibility)) { continue; }
if (node.IsBlocked()) { continue; }
if (!IsWaypointVisible(node, end, checkVisibility: checkVisibility)) { return false; }
if (node.IsBlocked()) { return false; }
if (node.Waypoint.ConnectedGap != null)
{
if (!CanFitThroughGap(node.Waypoint.ConnectedGap, minGapSize)) { continue; }
if (!CanFitThroughGap(node.Waypoint.ConnectedGap, minGapSize)) { return false; }
}
endNode = node;
break;
return true;
}
if (endNode == null)
{
#if DEBUG
DebugConsole.NewMessage("Pathfinding error, couldn't find an end node. " + errorMsgStr, Color.DarkRed);
#endif
return new SteeringPath(true);
}
var path = FindPath(startNode, endNode, nodeFilter, errorMsgStr, minGapSize);
return path;
}
private SteeringPath FindPath(PathNode start, PathNode end, Func<PathNode, bool> filter = null, string errorMsgStr = "", float minGapSize = 0)
@@ -1,4 +1,5 @@
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
namespace Barotrauma
@@ -47,14 +48,16 @@ namespace Barotrauma
public void SetOrder(Character orderedCharacter)
{
OrderedCharacter = orderedCharacter;
if (orderedCharacter != CommandingCharacter)
if (OrderedCharacter.AIController is HumanAIController humanAI && humanAI.ObjectiveManager.CurrentOrders.None(o => o.MatchesOrder(SuggestedOrderPrefab, Option)))
{
CommandingCharacter.Speak(SuggestedOrderPrefab.GetChatMessage(OrderedCharacter.Name, "", false));
if (orderedCharacter != CommandingCharacter)
{
CommandingCharacter.Speak(SuggestedOrderPrefab.GetChatMessage(OrderedCharacter.Name, "", false), minDurationBetweenSimilar: 5);
}
CurrentOrder = new Order(SuggestedOrderPrefab, TargetItem, TargetItemComponent, CommandingCharacter);
OrderedCharacter.SetOrder(CurrentOrder, Option, priority: 3, CommandingCharacter, CommandingCharacter != OrderedCharacter);
OrderedCharacter.Speak(TextManager.Get("DialogAffirmative"), delay: 1.0f, minDurationBetweenSimilar: 5);
}
// not sure if new orders are supposed to be created each time. TODO m61: check later
CurrentOrder = new Order(SuggestedOrderPrefab, TargetItem, TargetItemComponent, CommandingCharacter);
OrderedCharacter.SetOrder(CurrentOrder, Option, priority: 3, CommandingCharacter, CommandingCharacter != OrderedCharacter);
TimeSinceLastAttempt = 0f;
}
@@ -31,10 +31,10 @@ namespace Barotrauma
}
// there should maybe be additional logic for targeting and destroying spires, because they currently cause some issues with pathing
if (targetingImportances.Any())
if (targetingImportances.Any(i => i > 0))
{
targetingImportances.Sort();
Importance = targetingImportances.TakeLast(3).Average();
Importance = targetingImportances.TakeLast(3).Sum();
}
}
}
@@ -199,7 +199,7 @@ namespace Barotrauma
foreach (Character potentialCharacter in Character.CharacterList)
{
if (!HumanAIController.IsActive(character)) { continue; }
if (!HumanAIController.IsActive(potentialCharacter)) { continue; }
if (HumanAIController.IsFriendly(character, potentialCharacter, true) && potentialCharacter.AIController is HumanAIController)
{
@@ -313,7 +313,7 @@ namespace Barotrauma
ShipCommandLog("Dismissing " + shipIssueWorker + " for character " + shipIssueWorker.OrderedCharacter);
#endif
Order orderPrefab = Order.GetPrefab("dismissed");
character.Speak(orderPrefab.GetChatMessage(shipIssueWorker.OrderedCharacter.Name, "", givingOrderToSelf: false));
//character.Speak(orderPrefab.GetChatMessage(shipIssueWorker.OrderedCharacter.Name, "", givingOrderToSelf: false));
shipIssueWorker.OrderedCharacter.SetOrder(Order.GetPrefab("dismissed"), orderOption: null, priority: 3, character);
shipIssueWorker.RemoveOrder();
break;
@@ -462,7 +462,7 @@ namespace Barotrauma
DebugConsole.Log(errorMsg);
GameAnalyticsManager.AddErrorEventOnce(
"HumanoidAnimController.HoldItem:InvalidPos:" + character.Name + item.Name,
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
GameAnalyticsManager.ErrorSeverity.Error,
errorMsg);
return;
@@ -951,7 +951,7 @@ namespace Barotrauma
{
string errorMsg = "Creature death animation error: invalid limb mass on character \"" + character.SpeciesName + "\" (type: " + limb.type + ", mass: " + limb.Mass + ")";
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("FishAnimController.UpdateDying:InvalidMass" + character.ID, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
GameAnalyticsManager.AddErrorEventOnce("FishAnimController.UpdateDying:InvalidMass" + character.ID, GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
deathAnimTimer = deathAnimDuration;
return;
}
@@ -961,7 +961,7 @@ namespace Barotrauma
{
string errorMsg = "Creature death animation error: invalid diff (center of mass: " + centerOfMass + ", limb position: " + limb.SimPosition + ")";
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("FishAnimController.UpdateDying:InvalidDiff" + character.ID, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
GameAnalyticsManager.AddErrorEventOnce("FishAnimController.UpdateDying:InvalidDiff" + character.ID, GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
deathAnimTimer = deathAnimDuration;
return;
}
@@ -811,35 +811,41 @@ namespace Barotrauma
if (currentHull != null)
{
float surfacePos = currentHull.Surface;
float surfaceThreshold = ConvertUnits.ToDisplayUnits(Collider.SimPosition.Y + 1.0f);
//if the hull is almost full of water, check if there's a water-filled hull above it
//and use its water surface instead of the current hull's
if (currentHull.Rect.Y - currentHull.Surface < 5.0f)
{
foreach (Gap gap in currentHull.ConnectedGaps)
GetSurfacePos(CurrentHull, ref surfacePos);
void GetSurfacePos(Hull hull, ref float prevSurfacePos)
{
if (gap.IsHorizontal || gap.Open <= 0.0f) { continue; }
if (Collider.SimPosition.X < ConvertUnits.ToSimUnits(gap.Rect.X) || Collider.SimPosition.X > ConvertUnits.ToSimUnits(gap.Rect.Right)) { continue; }
//if the gap is above us and leads outside, there's no surface to limit the movement
if (!gap.IsRoomToRoom && gap.Position.Y > currentHull.Position.Y)
if (prevSurfacePos > surfaceThreshold) { return; }
foreach (Gap gap in hull.ConnectedGaps)
{
surfacePos += 100000.0f;
continue;
}
if (gap.IsHorizontal || gap.Open <= 0.0f || gap.WorldPosition.Y < hull.WorldPosition.Y) { continue; }
if (Collider.SimPosition.X < ConvertUnits.ToSimUnits(gap.Rect.X) || Collider.SimPosition.X > ConvertUnits.ToSimUnits(gap.Rect.Right)) { continue; }
foreach (var linkedTo in gap.linkedTo)
{
if (linkedTo is Hull hull && hull != currentHull)
//if the gap is above us and leads outside, there's no surface to limit the movement
if (!gap.IsRoomToRoom && gap.Position.Y > hull.Position.Y)
{
surfacePos = Math.Max(surfacePos, hull.Surface);
break;
prevSurfacePos += 100000.0f;
return;
}
foreach (var linkedTo in gap.linkedTo)
{
if (linkedTo is Hull otherHull && otherHull != hull)
{
prevSurfacePos = Math.Max(surfacePos, otherHull.Surface);
GetSurfacePos(otherHull, ref prevSurfacePos);
break;
}
}
}
}
}
surfaceLimiter = ConvertUnits.ToDisplayUnits(Collider.SimPosition.Y + 1.0f) - surfacePos;
surfaceLimiter = Math.Max(1.0f, surfaceLimiter);
surfaceLimiter = Math.Max(1.0f, surfaceThreshold - surfacePos);
if (surfaceLimiter > 50.0f) { return; }
}
@@ -1048,7 +1054,8 @@ namespace Barotrauma
void UpdateClimbing()
{
if (character.SelectedConstruction == null || character.SelectedConstruction.GetComponent<Ladder>() == null || character.IsIncapacitated)
var ladder = character.SelectedConstruction?.GetComponent<Ladder>();
if (ladder == null || character.IsIncapacitated)
{
Anim = Animation.None;
return;
@@ -1057,10 +1064,12 @@ namespace Barotrauma
onGround = false;
IgnorePlatforms = true;
Vector2 tempTargetMovement = TargetMovement;
tempTargetMovement.Y = Math.Min(tempTargetMovement.Y, 1.0f);
bool climbFast = targetMovement.Y > 3.0f;
bool slide = targetMovement.Y < -1.1f;
Vector2 tempTargetMovement = TargetMovement;
tempTargetMovement.Y = climbFast ?
Math.Min(tempTargetMovement.Y, 2.0f) :
Math.Min(tempTargetMovement.Y, 1.0f);
movement = MathUtils.SmoothStep(movement, tempTargetMovement, 0.3f);
@@ -1075,79 +1084,98 @@ namespace Barotrauma
if (leftHand == null || rightHand == null || head == null || torso == null) { return; }
Vector2 ladderSimPos = ConvertUnits.ToSimUnits(
character.SelectedConstruction.Rect.X + character.SelectedConstruction.Rect.Width / 2.0f,
character.SelectedConstruction.Rect.Y);
ladder.Item.Rect.X + ladder.Item.Rect.Width / 2.0f,
ladder.Item.Rect.Y);
Vector2 ladderSimSize = ConvertUnits.ToSimUnits(character.SelectedConstruction.Rect.Size.ToVector2());
Vector2 ladderSimSize = ConvertUnits.ToSimUnits(ladder.Item.Rect.Size.ToVector2());
float lowestLadderSimPos = ladderSimPos.Y - ladderSimPos.Y;
var lowestNearbyLadder = GetLowestNearbyLadder(ladder);
if (lowestNearbyLadder != null && lowestNearbyLadder != ladder)
{
ladderSimSize.Y = ConvertUnits.ToSimUnits(ladder.Item.WorldRect.Y - (lowestNearbyLadder.Item.WorldRect.Y - lowestNearbyLadder.Item.Rect.Size.Y));
}
float stepHeight = ConvertUnits.ToSimUnits(30.0f);
if (climbFast) { stepHeight *= 2; }
if (currentHull == null && character.SelectedConstruction.Submarine != null)
if (currentHull == null && ladder.Item.Submarine != null)
{
ladderSimPos += character.SelectedConstruction.Submarine.SimPosition;
ladderSimPos += ladder.Item.Submarine.SimPosition;
}
else if (currentHull?.Submarine != null && currentHull.Submarine != character.SelectedConstruction.Submarine && character.SelectedConstruction.Submarine != null)
else if (currentHull?.Submarine != null && currentHull.Submarine != ladder.Item.Submarine && ladder.Item.Submarine != null)
{
ladderSimPos += character.SelectedConstruction.Submarine.SimPosition - currentHull.Submarine.SimPosition;
ladderSimPos += ladder.Item.Submarine.SimPosition - currentHull.Submarine.SimPosition;
}
else if (currentHull?.Submarine != null && character.SelectedConstruction.Submarine == null)
else if (currentHull?.Submarine != null && ladder.Item.Submarine == null)
{
ladderSimPos -= currentHull.Submarine.SimPosition;
}
float bottomPos = Collider.SimPosition.Y - ColliderHeightFromFloor - Collider.radius - Collider.height / 2.0f;
float headPos = HeadPosition ?? 0;
float torsoPos = TorsoPosition ?? 0;
MoveLimb(head, new Vector2(ladderSimPos.X - 0.2f * Dir, bottomPos + headPos), 10.5f);
MoveLimb(torso, new Vector2(ladderSimPos.X - 0.35f * Dir, bottomPos + torsoPos), 10.5f);
float headPos = HeadPosition ?? 0;
MoveLimb(head, new Vector2(ladderSimPos.X - 0.2f * Dir, bottomPos + headPos), 10.5f);
Collider.MoveToPos(new Vector2(ladderSimPos.X - 0.1f * Dir, Collider.SimPosition.Y), 10.5f);
Vector2 handPos = new Vector2(
ladderSimPos.X,
bottomPos + torsoPos + movement.Y * 0.1f - ladderSimPos.Y);
if (climbFast) { handPos.Y -= stepHeight; }
bool aiming = this.aiming || aimingMelee;
//prevent the hands from going above the top of the ladders
handPos.Y = Math.Min(-0.5f, handPos.Y);
if (!character.IsKeyDown(InputType.Aim) || Math.Abs(movement.Y) > 0.01f)
if (!aiming || !(character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand)?.GetComponent<Holdable>()?.ControlPose ?? false) || Math.Abs(movement.Y) > 0.01f)
{
MoveLimb(rightHand,
new Vector2(slide ? handPos.X + ladderSimSize.X * 0.5f : handPos.X,
(slide ? handPos.Y : MathUtils.Round(handPos.Y, stepHeight * 2.0f)) + ladderSimPos.Y),
5.2f);
rightHand.body.ApplyTorque(Dir * 2.0f);
}
if (!aiming || !(character.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand)?.GetComponent<Holdable>()?.ControlPose ?? false) || Math.Abs(movement.Y) > 0.01f)
{
MoveLimb(leftHand,
new Vector2(handPos.X - ladderSimSize.X * 0.5f,
(slide ? handPos.Y : MathUtils.Round(handPos.Y - stepHeight, stepHeight * 2.0f) + stepHeight) + ladderSimPos.Y),
5.2f); ;
MoveLimb(rightHand,
new Vector2(slide ? handPos.X + ladderSimSize.X * 0.5f : handPos.X,
(slide ? handPos.Y : MathUtils.Round(handPos.Y, stepHeight * 2.0f)) + ladderSimPos.Y),
5.2f);
leftHand.body.ApplyTorque(Dir * 2.0f);
rightHand.body.ApplyTorque(Dir * 2.0f);
}
Vector2 footPos = new Vector2(
handPos.X - Dir * 0.05f,
bottomPos + ColliderHeightFromFloor - stepHeight * 2.7f - ladderSimPos.Y);
if (climbFast) { footPos.Y += stepHeight; }
//apply torque to the legs to make the knees bend
Limb leftLeg = GetLimb(LimbType.LeftLeg);
Limb rightLeg = GetLimb(LimbType.RightLeg);
//only move the feet if they're above the bottom of the ladders
//(if not, they'll just dangle in air, and the character holds itself up with it's arms)
if (footPos.Y > -ladderSimSize.Y && leftFoot != null && rightFoot != null)
if (footPos.Y > -ladderSimSize.Y - 0.2f && leftFoot != null && rightFoot != null)
{
Limb refLimb = GetLimb(LimbType.Waist) ?? GetLimb(LimbType.Torso);
bool leftLegBackwards = Math.Abs(leftLeg.body.Rotation - refLimb.body.Rotation) > MathHelper.Pi;
bool rightLegBackwards = Math.Abs(rightLeg.body.Rotation - refLimb.body.Rotation) > MathHelper.Pi;
if (slide)
{
MoveLimb(leftFoot, new Vector2(footPos.X - ladderSimSize.X * 0.5f, footPos.Y + ladderSimPos.Y), 15.5f, true);
MoveLimb(rightFoot, new Vector2(footPos.X, footPos.Y + ladderSimPos.Y), 15.5f, true);
if (!leftLegBackwards) { MoveLimb(leftFoot, new Vector2(footPos.X - ladderSimSize.X * 0.5f, footPos.Y + ladderSimPos.Y), 15.5f, true); }
if (!rightLegBackwards) { MoveLimb(rightFoot, new Vector2(footPos.X, footPos.Y + ladderSimPos.Y), 15.5f, true); }
}
else
{
float leftFootPos = MathUtils.Round(footPos.Y + stepHeight, stepHeight * 2.0f) - stepHeight;
float prevLeftFootPos = MathUtils.Round(prevFootPos + stepHeight, stepHeight * 2.0f) - stepHeight;
MoveLimb(leftFoot, new Vector2(footPos.X, leftFootPos + ladderSimPos.Y), 15.5f, true);
if (!leftLegBackwards) { MoveLimb(leftFoot, new Vector2(footPos.X, leftFootPos + ladderSimPos.Y), 15.5f, true); }
float rightFootPos = MathUtils.Round(footPos.Y, stepHeight * 2.0f);
float prevRightFootPos = MathUtils.Round(prevFootPos, stepHeight * 2.0f);
MoveLimb(rightFoot, new Vector2(footPos.X, rightFootPos + ladderSimPos.Y), 15.5f, true);
if (!rightLegBackwards) { MoveLimb(rightFoot, new Vector2(footPos.X, rightFootPos + ladderSimPos.Y), 15.5f, true); }
#if CLIENT
if (Math.Abs(leftFootPos - prevLeftFootPos) > stepHeight && leftFoot.LastImpactSoundTime < Timing.TotalTime - Limb.SoundInterval)
{
@@ -1163,36 +1191,48 @@ namespace Barotrauma
prevFootPos = footPos.Y;
}
//apply torque to the legs to make the knees bend
Limb leftLeg = GetLimb(LimbType.LeftLeg);
Limb rightLeg = GetLimb(LimbType.RightLeg);
leftLeg.body.ApplyTorque(Dir * -8.0f);
rightLeg.body.ApplyTorque(Dir * -8.0f);
if (!leftLegBackwards) { leftLeg.body.ApplyTorque(Dir * -8.0f); }
if (!rightLegBackwards) { rightLeg.body.ApplyTorque(Dir * -8.0f); }
}
float movementFactor = (handPos.Y / stepHeight) * (float)Math.PI;
movementFactor = 0.8f + (float)Math.Abs(Math.Sin(movementFactor));
Vector2 subSpeed = currentHull != null || character.SelectedConstruction.Submarine == null
? Vector2.Zero : character.SelectedConstruction.Submarine.Velocity;
Vector2 subSpeed = currentHull != null || ladder.Item.Submarine == null
? Vector2.Zero : ladder.Item.Submarine.Velocity;
//reached the top of the ladders -> can't go further up
Vector2 climbForce = new Vector2(0.0f, movement.Y) * movementFactor;
if (!InWater) { climbForce.Y += 0.3f * movementFactor; }
Vector2 climbForce = new Vector2(0.0f, movement.Y + 0.3f) * movementFactor;
if (character.SimPosition.Y > ladderSimPos.Y) { climbForce.Y = Math.Min(0.0f, climbForce.Y); }
//reached the bottom -> can't go further down
float minHeightFromFloor = ColliderHeightFromFloor / 2 + Collider.height;
if (floorFixture != null &&
!floorFixture.CollisionCategories.HasFlag(Physics.CollisionStairs) &&
!floorFixture.CollisionCategories.HasFlag(Physics.CollisionPlatform) &&
character.SimPosition.Y < standOnFloorY + minHeightFromFloor)
{
climbForce.Y = MathHelper.Clamp((standOnFloorY + minHeightFromFloor - character.SimPosition.Y) * 5.0f, climbForce.Y, 1.0f);
}
//apply forces to the collider to move the Character up/down
Collider.ApplyForce((climbForce * 20.0f + subSpeed * 50.0f) * Collider.Mass);
float movementMultiplier = targetMovement.Y < 0 ? 0 : 1;
head.body.SmoothRotate(MathHelper.PiOver4 * movementMultiplier * Dir, WalkParams.HeadTorque);
if (!aiming)
{
float movementMultiplier = targetMovement.Y < 0 ? 0 : 1;
head.body.SmoothRotate(MathHelper.PiOver4 * movementMultiplier * Dir, WalkParams.HeadTorque);
}
if (!character.SelectedConstruction.Prefab.Triggers.Any())
if (!ladder.Item.Prefab.Triggers.Any())
{
character.SelectedConstruction = null;
return;
}
Rectangle trigger = character.SelectedConstruction.Prefab.Triggers.FirstOrDefault();
trigger = character.SelectedConstruction.TransformTrigger(trigger);
Rectangle trigger = ladder.Item.Prefab.Triggers.FirstOrDefault();
trigger = ladder.Item.TransformTrigger(trigger);
bool isRemote = false;
bool isClimbing = true;
@@ -1221,6 +1261,19 @@ namespace Barotrauma
character.SelectedConstruction = null;
IgnorePlatforms = false;
}
Ladder GetLowestNearbyLadder(Ladder currentLadder, float threshold = 16.0f)
{
foreach (Ladder ladder in Ladder.List)
{
if (ladder == currentLadder || !ladder.Item.IsInteractable(character)) { continue; }
if (Math.Abs(ladder.Item.WorldPosition.X - currentLadder.Item.WorldPosition.X) > threshold) { continue; }
if (ladder.Item.WorldPosition.Y > currentLadder.Item.WorldPosition.Y) { continue; }
if ((currentLadder.Item.WorldRect.Y - currentLadder.Item.Rect.Height) - ladder.Item.WorldRect.Y > threshold) { continue; }
return ladder;
}
return null;
}
}
void UpdateDying(float deltaTime)
@@ -1576,6 +1629,7 @@ namespace Barotrauma
Vector2 shoulderPos = rightShoulder.WorldAnchorA;
Vector2 dragDir = inWater ? Vector2.Normalize(targetLimb.SimPosition - shoulderPos) : Vector2.UnitY;
if (!MathUtils.IsValid(dragDir)) { dragDir = Vector2.UnitY; }
targetAnchor = shoulderPos - dragDir * ConvertUnits.ToSimUnits(upperArmLength + forearmLength);
targetForce = 200.0f;
@@ -1656,7 +1710,7 @@ namespace Barotrauma
#if DEBUG
DebugConsole.ThrowError(errorMsg);
#endif
GameAnalyticsManager.AddErrorEventOnce("FootIK:InvalidPos", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
GameAnalyticsManager.AddErrorEventOnce("FootIK:InvalidPos", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
return;
}
@@ -1690,7 +1744,7 @@ namespace Barotrauma
#if DEBUG
DebugConsole.ThrowError(errorMsg);
#endif
GameAnalyticsManager.AddErrorEventOnce("FootIK:InvalidAngle", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
GameAnalyticsManager.AddErrorEventOnce("FootIK:InvalidAngle", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
return;
}
@@ -65,8 +65,8 @@ namespace Barotrauma
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce(
"Ragdoll.Limbs:AccessRemoved",
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Attempted to access a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this) + "\n" + Environment.StackTrace.CleanupStackTrace());
GameAnalyticsManager.ErrorSeverity.Error,
"Attempted to access a potentially removed ragdoll. Character: " + character.SpeciesName + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this) + "\n" + Environment.StackTrace.CleanupStackTrace());
accessRemovedCharacterErrorShown = true;
}
return new Limb[0];
@@ -122,6 +122,7 @@ namespace Barotrauma
protected Vector2 overrideTargetMovement;
protected float floorY, standOnFloorY;
protected Fixture floorFixture;
protected Vector2 floorNormal = Vector2.UnitY;
protected float surfaceY;
@@ -489,6 +490,7 @@ namespace Barotrauma
limbDictionary = new Dictionary<LimbType, Limb>();
limbs = new Limb[RagdollParams.Limbs.Count];
RagdollParams.Limbs.ForEach(l => AddLimb(l));
if (limbs.Contains(null)) { return; }
SetupDrawOrder();
}
@@ -549,19 +551,23 @@ namespace Barotrauma
byte ID = Convert.ToByte(limbParams.ID);
Limb limb = new Limb(this, character, limbParams);
limb.body.FarseerBody.OnCollision += OnLimbCollision;
if (ID >= Limbs.Length)
{
throw new Exception($"Failed to add a limb to the character \"{Character?.ConfigPath ?? "null"}\" (limb index {ID} out of bounds). The ragdoll file may be configured incorrectly.");
}
Limbs[ID] = limb;
Mass += limb.Mass;
if (!limbDictionary.ContainsKey(limb.type)) limbDictionary.Add(limb.type, limb);
if (!limbDictionary.ContainsKey(limb.type)) { limbDictionary.Add(limb.type, limb); }
}
public void AddLimb(Limb limb)
{
if (Limbs.Contains(limb)) return;
if (Limbs.Contains(limb)) { return; }
limb.body.FarseerBody.OnCollision += OnLimbCollision;
Array.Resize(ref limbs, Limbs.Length + 1);
Limbs[Limbs.Length - 1] = limb;
Mass += limb.Mass;
if (!limbDictionary.ContainsKey(limb.type)) limbDictionary.Add(limb.type, limb);
if (!limbDictionary.ContainsKey(limb.type)) { limbDictionary.Add(limb.type, limb); }
SetupDrawOrder();
}
@@ -887,7 +893,7 @@ namespace Barotrauma
string errorMsg = "Ragdoll.GetCenterOfMass returned an invalid value (" + centerOfMass + "). Limb positions: {"
+ string.Join(", ", limbs.Select(l => l.SimPosition)) + "}, total mass: " + totalMass + ".";
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("Ragdoll.GetCenterOfMass", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
GameAnalyticsManager.AddErrorEventOnce("Ragdoll.GetCenterOfMass", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
return Collider.SimPosition;
}
@@ -925,14 +931,14 @@ namespace Barotrauma
{
GameAnalyticsManager.AddErrorEventOnce(
"Ragdoll.FindHull:InvalidPosition",
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
GameAnalyticsManager.ErrorSeverity.Error,
"Attempted to find a hull at an invalid position (" + findPos + ")\n" + Environment.StackTrace.CleanupStackTrace());
return;
}
Hull newHull = Hull.FindHull(findPos, currentHull);
if (newHull == currentHull) return;
if (newHull == currentHull) { return; }
if (!CanEnterSubmarine || (character.AIController != null && !character.AIController.CanEnterSubmarine))
{
@@ -965,16 +971,16 @@ namespace Barotrauma
if (newHull?.Submarine == null && currentHull?.Submarine != null)
{
//don't teleport out yet if the character is going through a gap
if (Gap.FindAdjacent(currentHull.ConnectedGaps, findPos, 150.0f) != null) { return; }
if (Gap.FindAdjacent(Gap.GapList.Where(g => g.Submarine == currentHull.Submarine), findPos, 150.0f) != null) { return; }
if (Limbs.Any(l => Gap.FindAdjacent(currentHull.ConnectedGaps, l.WorldPosition, ConvertUnits.ToDisplayUnits(l.body.GetSize().Combine())) != null)) { return; }
character.MemLocalState?.Clear();
Teleport(ConvertUnits.ToSimUnits(currentHull.Submarine.Position), currentHull.Submarine.Velocity);
Teleport(ConvertUnits.ToSimUnits(currentHull.Submarine.Position), currentHull.Submarine.Velocity, detachProjectiles: false);
}
//out -> in
else if (currentHull == null && newHull.Submarine != null)
{
character.MemLocalState?.Clear();
Teleport(-ConvertUnits.ToSimUnits(newHull.Submarine.Position), -newHull.Submarine.Velocity);
Teleport(-ConvertUnits.ToSimUnits(newHull.Submarine.Position), -newHull.Submarine.Velocity, detachProjectiles: false);
}
//from one sub to another
else if (newHull != null && currentHull != null && newHull.Submarine != currentHull.Submarine)
@@ -982,13 +988,13 @@ namespace Barotrauma
character.MemLocalState?.Clear();
Vector2 newSubPos = newHull.Submarine == null ? Vector2.Zero : newHull.Submarine.Position;
Vector2 prevSubPos = currentHull.Submarine == null ? Vector2.Zero : currentHull.Submarine.Position;
Teleport(ConvertUnits.ToSimUnits(prevSubPos - newSubPos), Vector2.Zero);
Teleport(ConvertUnits.ToSimUnits(prevSubPos - newSubPos), Vector2.Zero, detachProjectiles: false);
}
}
CurrentHull = newHull;
character.Submarine = currentHull?.Submarine;
character.AttachedProjectiles.ForEach(p => p?.Item?.UpdateTransform());
}
private void PreventOutsideCollision()
@@ -1021,7 +1027,7 @@ namespace Barotrauma
}
}
public void Teleport(Vector2 moveAmount, Vector2 velocityChange)
public void Teleport(Vector2 moveAmount, Vector2 velocityChange, bool detachProjectiles = true)
{
foreach (Limb limb in Limbs)
{
@@ -1044,7 +1050,7 @@ namespace Barotrauma
character.DisableImpactDamageTimer = 0.25f;
SetPosition(Collider.SimPosition + moveAmount);
SetPosition(Collider.SimPosition + moveAmount, detachProjectiles: detachProjectiles);
character.CursorPosition += moveAmount;
Collider?.UpdateDrawPosition();
@@ -1356,19 +1362,19 @@ namespace Barotrauma
string errorMsg = null;
if (!MathUtils.IsValid(body.SimPosition) || Math.Abs(body.SimPosition.X) > 1e10f || Math.Abs(body.SimPosition.Y) > 1e10f)
{
errorMsg = GetBodyName() + " position invalid (" + body.SimPosition + ", character: " + character.Name + ").";
errorMsg = GetBodyName() + " position invalid (" + body.SimPosition + ", character: [name]).";
}
else if (!MathUtils.IsValid(body.LinearVelocity) || Math.Abs(body.LinearVelocity.X) > 1000f || Math.Abs(body.LinearVelocity.Y) > 1000f)
{
errorMsg = GetBodyName() + " velocity invalid (" + body.LinearVelocity + ", character: " + character.Name + ").";
errorMsg = GetBodyName() + " velocity invalid (" + body.LinearVelocity + ", character: [name]).";
}
else if (!MathUtils.IsValid(body.Rotation))
{
errorMsg = GetBodyName() + " rotation invalid (" + body.Rotation + ", character: " + character.Name + ").";
errorMsg = GetBodyName() + " rotation invalid (" + body.Rotation + ", character: [name]).";
}
else if (!MathUtils.IsValid(body.AngularVelocity) || Math.Abs(body.AngularVelocity) > 1000f)
{
errorMsg = GetBodyName() + " angular velocity invalid (" + body.AngularVelocity + ", character: " + character.Name + ").";
errorMsg = GetBodyName() + " angular velocity invalid (" + body.AngularVelocity + ", character: [name]).";
}
if (errorMsg != null)
{
@@ -1386,11 +1392,11 @@ namespace Barotrauma
}
#if DEBUG
DebugConsole.ThrowError(errorMsg);
DebugConsole.ThrowError(errorMsg.Replace("[name]", Character.Name));
#else
DebugConsole.NewMessage(errorMsg, Color.Red);
DebugConsole.NewMessage(errorMsg.Replace("[name]", Character.Name), Color.Red);
#endif
GameAnalyticsManager.AddErrorEventOnce("Ragdoll.CheckValidity:" + character.ID, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
GameAnalyticsManager.AddErrorEventOnce("Ragdoll.CheckValidity:" + character.ID, GameAnalyticsManager.ErrorSeverity.Error, errorMsg.Replace("[name]", Character.SpeciesName));
if (!MathUtils.IsValid(Collider.SimPosition) || Math.Abs(Collider.SimPosition.X) > 1e10f || Math.Abs(Collider.SimPosition.Y) > 1e10f)
{
@@ -1508,6 +1514,7 @@ namespace Barotrauma
{
onGround = false;
Stairs = null;
floorFixture = null;
Vector2 rayStart = simPosition;
float height = ColliderHeightFromFloor;
if (HeadPosition.HasValue && MathUtils.IsValid(HeadPosition.Value)) { height = Math.Max(height, HeadPosition.Value); }
@@ -1580,6 +1587,7 @@ namespace Barotrauma
if (standOnFloorFixture != null && !IsHanging)
{
floorFixture = standOnFloorFixture;
standOnFloorY = rayStart.Y + (rayEnd.Y - rayStart.Y) * standOnFloorFraction;
if (rayStart.Y - standOnFloorY < Collider.height * 0.5f + Collider.radius + ColliderHeightFromFloor * 1.2f)
{
@@ -1620,26 +1628,41 @@ namespace Barotrauma
}
}
public void SetPosition(Vector2 simPosition, bool lerp = false, bool ignorePlatforms = true, bool forceMainLimbToCollider = false)
public void SetPosition(Vector2 simPosition, bool lerp = false, bool ignorePlatforms = true, bool forceMainLimbToCollider = false, bool detachProjectiles = true)
{
if (!MathUtils.IsValid(simPosition))
{
DebugConsole.ThrowError("Attempted to move a ragdoll (" + character.Name + ") to an invalid position (" + simPosition + "). " + Environment.StackTrace.CleanupStackTrace());
GameAnalyticsManager.AddErrorEventOnce(
"Ragdoll.SetPosition:InvalidPosition",
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Attempted to move a ragdoll (" + character.Name + ") to an invalid position (" + simPosition + "). " + Environment.StackTrace.CleanupStackTrace());
GameAnalyticsManager.ErrorSeverity.Error,
"Attempted to move a ragdoll (" + character.SpeciesName + ") to an invalid position (" + simPosition + "). " + Environment.StackTrace.CleanupStackTrace());
return;
}
if (MainLimb == null) { return; }
// A Work-around for an issue with teleporting the characters:
// Detach every latcher when either one of the latchers or the target is teleported,
// because otherwise all the characters are teleported to invalid positions.
if (Character.AIController is EnemyAIController enemyAI && enemyAI.LatchOntoAI != null && enemyAI.LatchOntoAI.IsAttached)
{
var target = enemyAI.LatchOntoAI.TargetCharacter;
if (target != null)
{
target.Latchers.ForEachMod(l => l?.DeattachFromBody(reset: true));
target.Latchers.Clear();
}
enemyAI.LatchOntoAI.DeattachFromBody(reset: true);
}
Character.Latchers.ForEachMod(l => l.DeattachFromBody(reset: true));
Character.Latchers.ForEachMod(l => l?.DeattachFromBody(reset: true));
Character.Latchers.Clear();
if (detachProjectiles)
{
character.AttachedProjectiles.ForEachMod(p => p?.Unstick());
character.AttachedProjectiles.Clear();
}
Vector2 limbMoveAmount = forceMainLimbToCollider ? simPosition - MainLimb.SimPosition : simPosition - Collider.SimPosition;
if (lerp)
{
@@ -1720,7 +1743,7 @@ namespace Barotrauma
if (distSqrd > resetDist * resetDist)
{
//ragdoll way too far, reset position
SetPosition(Collider.SimPosition, true, forceMainLimbToCollider: true);
SetPosition(Collider.SimPosition, lerp: true, forceMainLimbToCollider: true);
}
if (distSqrd > allowedDist * allowedDist)
{
@@ -1740,7 +1763,7 @@ namespace Barotrauma
else if (collisionsDisabled)
{
//set the position of the ragdoll to make sure limbs don't get stuck inside walls when re-enabling collisions
SetPosition(Collider.SimPosition, true);
SetPosition(Collider.SimPosition, lerp: true);
collisionsDisabled = false;
//force collision categories to be updated
prevCollisionCategory = Category.None;
@@ -20,7 +20,7 @@ namespace Barotrauma
{
string errorMsg = "Invalid cause of death (the type of the cause of death was Affliction, but affliction was not specified).\n" + Environment.StackTrace.CleanupStackTrace();
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("InvalidCauseOfDeath", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
GameAnalyticsManager.AddErrorEventOnce("InvalidCauseOfDeath", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
type = CauseOfDeathType.Unknown;
}
@@ -130,6 +130,7 @@ namespace Barotrauma
}
public readonly HashSet<LatchOntoAI> Latchers = new HashSet<LatchOntoAI>();
public readonly HashSet<Projectile> AttachedProjectiles = new HashSet<Projectile>();
protected readonly Dictionary<string, ActiveTeamChange> activeTeamChanges = new Dictionary<string, ActiveTeamChange>();
protected ActiveTeamChange currentTeamChange;
@@ -265,7 +266,7 @@ namespace Barotrauma
private CharacterPrefab prefab;
public readonly CharacterParams Params;
public string SpeciesName => Params.SpeciesName;
public string SpeciesName => Params?.SpeciesName ?? "null";
public string Group => Params.Group;
public bool IsHumanoid => Params.Humanoid;
public bool IsHusk => Params.Husk;
@@ -611,8 +612,7 @@ namespace Barotrauma
get
{
if (IsUnconscious) { return true; }
if (IsDead) { return true; }
return CharacterHealth.Afflictions.Any(a => a.Prefab.AfflictionType == "paralysis" && a.Strength >= a.Prefab.MaxStrength);
return CharacterHealth.GetAllAfflictions().Any(a => a.Prefab.AfflictionType == "paralysis" && a.Strength >= a.Prefab.MaxStrength);
}
}
@@ -621,6 +621,11 @@ namespace Barotrauma
get { return CharacterHealth.IsUnconscious; }
}
public bool IsArrested
{
get { return IsHuman && HasEquippedItem("handlocker"); }
}
public bool IsPet
{
get { return AIController is EnemyAIController enemyController && enemyController.PetBehavior != null; }
@@ -642,6 +647,11 @@ namespace Barotrauma
set { oxygenAvailable = MathHelper.Clamp(value, 0.0f, 100.0f); }
}
public float HullOxygenPercentage
{
get { return CurrentHull?.OxygenPercentage ?? 0.0f; }
}
public bool UseHullOxygen { get; set; } = true;
public float Stun
@@ -690,12 +700,12 @@ namespace Barotrauma
{
get
{
if (!CanSpeak || IsUnconscious || Stun > 0.0f || IsDead) return 100.0f;
if (!CanSpeak || IsUnconscious || Stun > 0.0f || IsDead) { return 100.0f; }
return speechImpediment;
}
set
{
if (value < speechImpediment) return;
if (value < speechImpediment) { return; }
speechImpedimentSet = true;
speechImpediment = MathHelper.Clamp(value, 0.0f, 100.0f);
}
@@ -807,7 +817,7 @@ namespace Barotrauma
{
if (!canBeDragged) { return false; }
if (Removed || !AnimController.Draggable) { return false; }
return IsKnockedDown || LockHands || IsPet;
return IsKnockedDown || LockHands || IsPet || CanInventoryBeAccessed;
}
set { canBeDragged = value; }
}
@@ -825,7 +835,7 @@ namespace Barotrauma
}
else
{
return IsKnockedDown || LockHands;
return IsKnockedDown || LockHands || IsBot && TeamID != CharacterTeamType.FriendlyNPC;
}
}
set { canInventoryBeAccessed = value; }
@@ -854,7 +864,7 @@ namespace Barotrauma
{
if (!accessRemovedCharacterErrorShown)
{
string errorMsg = "Attempted to access a potentially removed character. Character: " + Name + ", id: " + ID + ", removed: " + Removed + ".";
string errorMsg = "Attempted to access a potentially removed character. Character: [name], id: " + ID + ", removed: " + Removed + ".";
if (AnimController == null)
{
errorMsg += " AnimController == null";
@@ -864,11 +874,11 @@ namespace Barotrauma
errorMsg += " AnimController.Collider == null";
}
errorMsg += '\n' + Environment.StackTrace.CleanupStackTrace();
DebugConsole.NewMessage(errorMsg, Color.Red);
DebugConsole.NewMessage(errorMsg.Replace("[name]", Name), Color.Red);
GameAnalyticsManager.AddErrorEventOnce(
"Character.SimPosition:AccessRemoved",
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace());
GameAnalyticsManager.ErrorSeverity.Error,
errorMsg.Replace("[name]", SpeciesName) + "\n" + Environment.StackTrace.CleanupStackTrace());
accessRemovedCharacterErrorShown = true;
}
return Vector2.Zero;
@@ -1362,7 +1372,11 @@ namespace Barotrauma
public override string ToString()
{
#if DEBUG
return (info != null && !string.IsNullOrWhiteSpace(info.Name)) ? info.Name : SpeciesName;
#else
return SpeciesName;
#endif
}
public void GiveJobItems(WayPoint spawnPoint = null)
@@ -1988,7 +2002,7 @@ namespace Barotrauma
public bool CanSeeTarget(ISpatialEntity target, ISpatialEntity seeingEntity = null)
{
seeingEntity ??= AnimController.SimplePhysicsEnabled ? this as ISpatialEntity : GetSeeingLimb() as ISpatialEntity;
seeingEntity ??= AnimController.SimplePhysicsEnabled ? this : GetSeeingLimb() as ISpatialEntity;
if (seeingEntity == null) { return false; }
ISpatialEntity sourceEntity = seeingEntity ;
// TODO: Could we just use the method below? If not, let's refactor it so that we can.
@@ -2023,16 +2037,11 @@ namespace Barotrauma
{
return wall != target;
}
else if (body.UserData is Item item && item != target)
else if (body.UserData is Item item)
{
// TODO: The door collider should be disabled, so this check is probably unnecessary.
var door = item.GetComponent<Door>();
if (door != null)
{
return !door.CanBeTraversed;
}
return item != target;
}
return false;
return true;
}
}
@@ -2041,18 +2050,23 @@ namespace Barotrauma
/// </summary>
public bool IsFacing(Vector2 targetWorldPos) => AnimController.Dir > 0 && targetWorldPos.X > WorldPosition.X || AnimController.Dir < 0 && targetWorldPos.X < WorldPosition.X;
public bool HasItem(Item item, bool requireEquipped = false, InvSlotType? slotType = null) => requireEquipped ? HasEquippedItem(item) : item.IsOwnedBy(this);
public bool HasItem(Item item, bool requireEquipped = false, InvSlotType? slotType = null) => requireEquipped ? HasEquippedItem(item, slotType) : item.IsOwnedBy(this);
public bool HasEquippedItem(Item item, InvSlotType? slotType = null)
public bool HasEquippedItem(Item item, InvSlotType? slotType = null, Func<InvSlotType, bool> predicate = null)
{
if (Inventory == null) { return false; }
for (int i = 0; i < Inventory.Capacity; i++)
{
InvSlotType slot = Inventory.SlotTypes[i];
if (predicate != null)
{
if (!predicate(slot)) { continue; }
}
if (slotType.HasValue)
{
if (!slotType.Value.HasFlag(Inventory.SlotTypes[i])) { continue; }
if (!slotType.Value.HasFlag(slot)) { continue; }
}
else if (Inventory.SlotTypes[i] == InvSlotType.Any)
else if (slot == InvSlotType.Any)
{
continue;
}
@@ -2239,6 +2253,10 @@ namespace Barotrauma
{
return wire.Connections[0] == null;
}
if (SelectedConstruction?.GetComponent<ConnectionPanel>()?.DisconnectedWires.Contains(wire) ?? false)
{
return wire.Connections[0] == null && wire.Connections[1] == null;
}
}
if (checkLinked && item.DisplaySideBySideWhenLinked)
@@ -2472,7 +2490,10 @@ namespace Barotrauma
{
minDist = dist;
nearbyLadder = ladder;
if (isControlled) ladder.Item.IsHighlighted = true;
if (isControlled)
{
ladder.Item.IsHighlighted = true;
}
break;
}
}
@@ -2480,7 +2501,10 @@ namespace Barotrauma
if (nearbyLadder != null && climbInput)
{
if (nearbyLadder.Select(this)) SelectedConstruction = nearbyLadder.Item;
if (nearbyLadder.Select(this))
{
SelectedConstruction = nearbyLadder.Item;
}
}
}
@@ -2502,14 +2526,20 @@ namespace Barotrauma
{
DeselectCharacter();
#if CLIENT
if (Controlled == this) CharacterHealth.OpenHealthWindow = null;
if (Controlled == this)
{
CharacterHealth.OpenHealthWindow = null;
}
#endif
}
else
{
SelectCharacter(FocusedCharacter);
#if CLIENT
if (Controlled == this) CharacterHealth.OpenHealthWindow = FocusedCharacter.CharacterHealth;
if (Controlled == this)
{
CharacterHealth.OpenHealthWindow = FocusedCharacter.CharacterHealth;
}
#endif
}
}
@@ -2644,7 +2674,7 @@ namespace Barotrauma
ApplyStatusEffects(ActionType.Always, deltaTime);
PreviousHull = CurrentHull;
CurrentHull = Hull.FindHull(WorldPosition, CurrentHull, true);
CurrentHull = Hull.FindHull(WorldPosition, CurrentHull, useWorldCoordinates: true);
speechBubbleTimer = Math.Max(0.0f, speechBubbleTimer - deltaTime);
@@ -2714,7 +2744,7 @@ namespace Barotrauma
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
{
Implode();
if (IsDead) { return; }
if (IsDead) { return; }
}
}
}
@@ -2723,7 +2753,9 @@ namespace Barotrauma
PressureTimer = 0.0f;
}
}
else if ((GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) && WorldPosition.Y < CharacterHealth.CrushDepth)
else if ((GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) &&
PressureProtection < (Level.Loaded?.GetRealWorldDepth(WorldPosition.Y) ?? 1.0f) &&
WorldPosition.Y < CharacterHealth.CrushDepth)
{
//implode if below crush depth, and either outside or in a high-pressure hull
if (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure >= 80.0f)
@@ -2874,6 +2906,18 @@ namespace Barotrauma
}
}
public float GetDamageDoneByAttacker(Character otherCharacter)
{
if (otherCharacter == null) { return 0; }
float dmg = 0;
Attacker attacker = LastAttackers.LastOrDefault(a => a.Character == otherCharacter);
if (attacker != null)
{
dmg = attacker.Damage;
}
return dmg;
}
private void UpdateAttackers(float deltaTime)
{
//slowly forget about damage done by attackers
@@ -3128,47 +3172,53 @@ namespace Barotrauma
//set the character order only if the character is close enough to hear the message
if (!force && orderGiver != null && !CanHearCharacter(orderGiver)) { return; }
if (order != null && order.OrderGiver != orderGiver)
if (order != null)
{
order.OrderGiver = orderGiver;
}
switch (order?.Category)
{
case OrderCategory.Operate when order?.TargetEntity != null:
// If there's another character operating the same device, make them dismiss themself
foreach (var character in CharacterList)
if (order.OrderGiver != orderGiver)
{
order.OrderGiver = orderGiver;
}
if (order.AutoDismiss)
{
switch (order.Category)
{
if (character == this) { continue; }
if (character.TeamID != TeamID) { continue; }
if (!(character.AIController is HumanAIController)) { continue; }
if (!HumanAIController.IsActive(character)) { continue; }
foreach (var currentOrder in character.CurrentOrders)
{
if (currentOrder.Order == null) { continue; }
if (currentOrder.Order.Category != OrderCategory.Operate) { continue; }
if (currentOrder.Order.Identifier != order.Identifier) { continue; }
if (currentOrder.Order.TargetEntity != order.TargetEntity) { continue; }
character.SetOrder(Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(currentOrder), currentOrder.ManualPriority, character, speak: speak, force: force);
case OrderCategory.Operate when order.TargetEntity != null:
// If there's another character operating the same device, make them dismiss themself
foreach (var character in CharacterList)
{
if (character == this) { continue; }
if (character.TeamID != TeamID) { continue; }
if (!(character.AIController is HumanAIController)) { continue; }
if (!HumanAIController.IsActive(character)) { continue; }
foreach (var currentOrder in character.CurrentOrders)
{
if (currentOrder.Order == null) { continue; }
if (currentOrder.Order.Category != OrderCategory.Operate) { continue; }
if (currentOrder.Order.Identifier != order.Identifier) { continue; }
if (currentOrder.Order.TargetEntity != order.TargetEntity) { continue; }
if (!currentOrder.Order.AutoDismiss) { continue; }
character.SetOrder(Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(currentOrder), currentOrder.ManualPriority, character, speak: speak, force: force);
break;
}
}
break;
case OrderCategory.Movement:
// If there character has another movement order, dismiss that order
OrderInfo? orderToReplace = null;
foreach (var currentOrder in CurrentOrders)
{
if (currentOrder.Order == null) { continue; }
if (currentOrder.Order.Category != OrderCategory.Movement) { continue; }
orderToReplace = currentOrder;
break;
}
if (orderToReplace.HasValue && orderToReplace.Value.Order.AutoDismiss)
{
SetOrder(Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(orderToReplace.Value), orderToReplace.Value.ManualPriority, this, speak: speak, force: force);
}
break;
}
}
break;
case OrderCategory.Movement:
// If there character has another movement order, dismiss that order
OrderInfo? orderToReplace = null;
foreach (var currentOrder in CurrentOrders)
{
if (currentOrder.Order == null) { continue; }
if (currentOrder.Order.Category != OrderCategory.Movement) { continue; }
orderToReplace = currentOrder;
break;
}
if (orderToReplace.HasValue)
{
SetOrder(Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(orderToReplace.Value), orderToReplace.Value.ManualPriority, this, speak: speak, force: force);
}
break;
}
}
// Prevent adding duplicate orders
@@ -3324,6 +3374,8 @@ namespace Barotrauma
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
if (string.IsNullOrEmpty(message)) { return; }
if (SpeechImpediment >= 100.0f) { return; }
if (prevAiChatMessages.ContainsKey(identifier) &&
prevAiChatMessages[identifier] < Timing.TotalTime - minDurationBetweenSimilar)
{
@@ -3341,13 +3393,13 @@ namespace Barotrauma
private void UpdateAIChatMessages(float deltaTime)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) return;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
List<AIChatMessage> sentMessages = new List<AIChatMessage>();
foreach (AIChatMessage message in aiChatMessageQueue)
{
message.SendDelay -= deltaTime;
if (message.SendDelay > 0.0f) continue;
if (message.SendDelay > 0.0f) { continue; }
if (message.MessageType == null)
{
@@ -3424,9 +3476,9 @@ namespace Barotrauma
{
if (Removed)
{
string errorMsg = "Tried to apply an attack to a removed character (" + Name + ").\n" + Environment.StackTrace.CleanupStackTrace();
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("Character.ApplyAttack:RemovedCharacter", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
string errorMsg = "Tried to apply an attack to a removed character ([name]).\n" + Environment.StackTrace.CleanupStackTrace();
DebugConsole.ThrowError(errorMsg.Replace("[name]", Name));
GameAnalyticsManager.AddErrorEventOnce("Character.ApplyAttack:RemovedCharacter", GameAnalyticsManager.ErrorSeverity.Error, errorMsg.Replace("[name]", SpeciesName));
return new AttackResult();
}
@@ -3615,7 +3667,7 @@ namespace Barotrauma
// {
// string errorMsg = $"Character {Name} received damage from outside the sub while inside (attacker: {attacker.Name})";
// GameAnalyticsManager.AddErrorEventOnce("Character.DamageLimb:DamageFromOutside" + Name + attacker.Name,
// GameAnalyticsSDK.Net.EGAErrorSeverity.Warning,
// GameAnalyticsManager.ErrorSeverity.Warning,
// errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace());
//#if DEBUG
// DebugConsole.ThrowError(errorMsg);
@@ -3628,7 +3680,7 @@ namespace Barotrauma
{
if (attacker.TeamID == TeamID)
{
afflictions = afflictions.Where(a => !a.Prefab.IsBuff);
afflictions = afflictions.Where(a => a.Prefab.IsBuff);
if (!afflictions.Any()) { return new AttackResult(); }
}
}
@@ -3845,7 +3897,7 @@ namespace Barotrauma
{
string errorMsg = "Attempted to apply an invalid impulse to a limb in Character.BreakJoints (" + diff + "). Limb position: " + limb.SimPosition + ", center of mass: " + centerOfMass + ".";
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("Ragdoll.GetCenterOfMass", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
GameAnalyticsManager.AddErrorEventOnce("Ragdoll.GetCenterOfMass", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
return;
}
@@ -3890,7 +3942,7 @@ namespace Barotrauma
AnimController.Frozen = false;
if (GameSettings.SendUserStatistics)
if (GameAnalyticsManager.SendUserStatistics)
{
string characterType = "Unknown";
@@ -3940,6 +3992,7 @@ namespace Barotrauma
}
SelectedConstruction = null;
SelectedCharacter = null;
AnimController.ResetPullJoints();
@@ -4068,10 +4121,15 @@ namespace Barotrauma
public void TeleportTo(Vector2 worldPos)
{
CurrentHull = null;
AnimController.CurrentHull = null;
Submarine = null;
AnimController.SetPosition(ConvertUnits.ToSimUnits(worldPos), false);
AnimController.FindHull(worldPos, true);
AnimController.SetPosition(ConvertUnits.ToSimUnits(worldPos), lerp: false);
AnimController.FindHull(worldPos, setSubmarine: true);
if (AIController is HumanAIController humanAI)
{
humanAI.PathSteering?.ResetPath();
}
}
public static void SaveInventory(Inventory inventory, XElement parentElement)
@@ -4442,6 +4500,10 @@ namespace Barotrauma
public static IEnumerable<Character> GetFriendlyCrew(Character character)
{
if (character is null)
{
return Enumerable.Empty<Character>();
}
return CharacterList.Where(c => HumanAIController.IsFriendly(character, c, onlySameTeam: true) && !c.IsDead);
}
@@ -365,6 +365,30 @@ namespace Barotrauma
public const int MaxCurrentOrders = 3;
public static int HighestManualOrderPriority => MaxCurrentOrders;
public int GetManualOrderPriority(Order order)
{
if (order != null && order.AssignmentPriority < 100 && CurrentOrders.Any())
{
int orderPriority = HighestManualOrderPriority;
for (int i = 0; i < CurrentOrders.Count; i++)
{
if (CurrentOrders[i].Order is Order currentOrder && order.AssignmentPriority >= currentOrder.AssignmentPriority)
{
break;
}
else
{
orderPriority--;
}
}
return Math.Max(orderPriority, 1);
}
else
{
return HighestManualOrderPriority;
}
}
public List<OrderInfo> CurrentOrders { get; } = new List<OrderInfo>();
//unique ID given to character infos in MP
@@ -1176,13 +1200,13 @@ namespace Barotrauma
int salary = 0;
foreach (Skill skill in Job.Skills)
{
salary += (int)(skill.Level * skill.Prefab.PriceMultiplier);
salary += (int)(skill.Level * skill.PriceMultiplier);
}
return (int)(salary * Job.Prefab.PriceMultiplier);
}
public void IncreaseSkillLevel(string skillIdentifier, float increase, bool gainedFromApprenticeship = false)
public void IncreaseSkillLevel(string skillIdentifier, float increase, bool gainedFromAbility = false)
{
if (Job == null || (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) || Character == null) { return; }
@@ -1202,7 +1226,7 @@ namespace Barotrauma
{
// assume we are getting at least 1 point in skill, since this logic only runs in such cases
float increaseSinceLastSkillPoint = MathHelper.Max(increase, 1f);
var abilitySkillGain = new AbilitySkillGain(increaseSinceLastSkillPoint, skillIdentifier, Character, gainedFromApprenticeship);
var abilitySkillGain = new AbilitySkillGain(increaseSinceLastSkillPoint, skillIdentifier, Character, gainedFromAbility);
Character.CheckTalents(AbilityEffectType.OnGainSkillPoint, abilitySkillGain);
foreach (Character character in Character.GetFriendlyCrew(Character))
{
@@ -1276,17 +1300,16 @@ namespace Barotrauma
public float GetProgressTowardsNextLevel()
{
float progress = (ExperiencePoints - GetExperienceRequiredForCurrentLevel()) / (GetExperienceRequiredToLevelUp() - GetExperienceRequiredForCurrentLevel());
return progress;
return (ExperiencePoints - GetExperienceRequiredForCurrentLevel()) / (float)(GetExperienceRequiredToLevelUp() - GetExperienceRequiredForCurrentLevel());
}
public float GetExperienceRequiredForCurrentLevel()
public int GetExperienceRequiredForCurrentLevel()
{
GetCurrentLevel(out int experienceRequired);
return experienceRequired;
}
public float GetExperienceRequiredToLevelUp()
public int GetExperienceRequiredToLevelUp()
{
int level = GetCurrentLevel(out int experienceRequired);
return experienceRequired + ExperienceRequiredPerLevel(level);
@@ -1388,7 +1411,6 @@ namespace Barotrauma
foreach (var savedStat in statValuePair.Value)
{
if (savedStat.StatValue == 0f) { continue; }
if (savedStat.RemoveAfterRound) { continue; }
savedStatElement.Add(new XElement("savedstatvalue",
new XAttribute("stattype", statValuePair.Key.ToString()),
@@ -1746,6 +1768,20 @@ namespace Barotrauma
OnPermanentStatChanged(statType);
}
public void RemoveSavedStatValuesOnDeath()
{
foreach (StatTypes statType in SavedStatValues.Keys)
{
foreach (SavedStatValue savedStatValue in SavedStatValues[statType])
{
if (!savedStatValue.RemoveOnDeath) { continue; }
if (MathUtils.NearlyEqual(savedStatValue.StatValue, 0.0f)) { continue; }
savedStatValue.StatValue = 0.0f;
// no need to make a network update, as this is only done after the character has died
}
}
}
public void ResetSavedStatValue(string statIdentifier)
{
foreach (StatTypes statType in SavedStatValues.Keys)
@@ -1785,7 +1821,7 @@ namespace Barotrauma
}
}
public void ChangeSavedStatValue(StatTypes statType, float value, string statIdentifier, bool removeOnDeath, bool removeAfterRound = false, float maxValue = float.MaxValue, bool setValue = false)
public void ChangeSavedStatValue(StatTypes statType, float value, string statIdentifier, bool removeOnDeath, float maxValue = float.MaxValue, bool setValue = false)
{
if (!SavedStatValues.ContainsKey(statType))
{
@@ -1801,7 +1837,7 @@ namespace Barotrauma
}
else
{
SavedStatValues[statType].Add(new SavedStatValue(statIdentifier, MathHelper.Min(value, maxValue), removeOnDeath, removeAfterRound));
SavedStatValues[statType].Add(new SavedStatValue(statIdentifier, MathHelper.Min(value, maxValue), removeOnDeath));
changed = true;
}
if (changed) { OnPermanentStatChanged(statType); }
@@ -1813,29 +1849,27 @@ namespace Barotrauma
public string StatIdentifier { get; set; }
public float StatValue { get; set; }
public bool RemoveOnDeath { get; set; }
public bool RemoveAfterRound { get; set; }
public SavedStatValue(string statIdentifier, float value, bool removeOnDeath, bool retainAfterRound)
public SavedStatValue(string statIdentifier, float value, bool removeOnDeath)
{
StatValue = value;
RemoveOnDeath = removeOnDeath;
StatIdentifier = statIdentifier;
RemoveAfterRound = retainAfterRound;
}
}
class AbilitySkillGain : AbilityObject, IAbilityValue, IAbilityString, IAbilityCharacter
{
public AbilitySkillGain(float value, string abilityString, Character character, bool gainedFromApprenticeship)
public AbilitySkillGain(float value, string abilityString, Character character, bool gainedFromAbility)
{
Value = value;
String = abilityString;
Character = character;
GainedFromApprenticeship = gainedFromApprenticeship;
GainedFromAbility = gainedFromAbility;
}
public Character Character { get; set; }
public float Value { get; set; }
public string String { get; set; }
public bool GainedFromApprenticeship { get; set; }
public bool GainedFromAbility { get; }
}
}
@@ -99,15 +99,21 @@ namespace Barotrauma
public float GetVitalityDecrease(CharacterHealth characterHealth)
{
if (Strength < Prefab.ActivationThreshold) { return 0.0f; }
AfflictionPrefab.Effect currentEffect = GetActiveEffect();
return GetVitalityDecrease(characterHealth, Strength);
}
public float GetVitalityDecrease(CharacterHealth characterHealth, float strength)
{
if (strength < Prefab.ActivationThreshold) { return 0.0f; }
strength = MathHelper.Clamp(strength, 0.0f, Prefab.MaxStrength);
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(strength);
if (currentEffect == null) { return 0.0f; }
if (currentEffect.MaxStrength - currentEffect.MinStrength <= 0.0f) { return 0.0f; }
float currVitalityDecrease = MathHelper.Lerp(
currentEffect.MinVitalityDecrease,
currentEffect.MaxVitalityDecrease,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
currentEffect.MinVitalityDecrease,
currentEffect.MaxVitalityDecrease,
(strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
if (currentEffect.MultiplyByMaxVitality)
{
@@ -116,7 +122,8 @@ namespace Barotrauma
return currVitalityDecrease;
}
public float GetScreenGrainStrength()
{
if (Strength < Prefab.ActivationThreshold) { return 0.0f; }
@@ -59,17 +59,25 @@ namespace Barotrauma
}
}
private float DormantThreshold => (Prefab as AfflictionPrefabHusk)?.DormantThreshold ?? Prefab.MaxStrength * 0.5f;
private float ActiveThreshold => (Prefab as AfflictionPrefabHusk)?.ActiveThreshold ?? Prefab.MaxStrength * 0.75f;
private readonly AfflictionPrefabHusk HuskPrefab;
private float TransitionThreshold => (Prefab as AfflictionPrefabHusk)?.TransitionThreshold ?? Prefab.MaxStrength * 0.75f;
private float DormantThreshold => HuskPrefab.DormantThreshold;
private float ActiveThreshold => HuskPrefab.ActiveThreshold;
private float TransitionThreshold => HuskPrefab.TransitionThreshold;
private float TransformThresholdOnDeath => HuskPrefab.TransformThresholdOnDeath;
private float TransformThresholdOnDeath => (Prefab as AfflictionPrefabHusk)?.TransformThresholdOnDeath ?? ActiveThreshold;
public AfflictionHusk(AfflictionPrefab prefab, float strength) : base(prefab, strength) { }
public AfflictionHusk(AfflictionPrefab prefab, float strength) : base(prefab, strength)
{
HuskPrefab = prefab as AfflictionPrefabHusk;
if (HuskPrefab == null)
{
DebugConsole.ThrowError("Error in husk affliction definition: the prefab is of wrong type!");
}
}
public override void Update(CharacterHealth characterHealth, Limb targetLimb, float deltaTime)
{
if (HuskPrefab == null) { return; }
base.Update(characterHealth, targetLimb, deltaTime);
character = characterHealth.Character;
if (character == null) { return; }
@@ -174,7 +182,8 @@ namespace Barotrauma
private void CharacterDead(Character character, CauseOfDeath causeOfDeath)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
if (Strength < TransformThresholdOnDeath || character.Removed)
if (Strength < TransformThresholdOnDeath || character.Removed ||
character.CharacterHealth.GetAllAfflictions().Any(a => a.GetActiveEffect()?.BlockTransformation.Contains(Prefab.Identifier) ?? false))
{
UnsubscribeFromDeathEvent();
return;
@@ -193,7 +202,7 @@ namespace Barotrauma
CoroutineManager.StartCoroutine(CreateAIHusk());
}
private IEnumerable<object> CreateAIHusk()
private IEnumerable<CoroutineStatus> CreateAIHusk()
{
//character already in remove queue (being removed by something else, for example a modded affliction that uses AfflictionHusk as the base)
// -> don't spawn the AI husk
@@ -272,11 +281,13 @@ namespace Barotrauma
if ((Prefab as AfflictionPrefabHusk)?.TransferBuffs ?? false)
{
foreach (Affliction affliction in character.CharacterHealth.Afflictions)
foreach (Affliction affliction in character.CharacterHealth.GetAllAfflictions())
{
if (affliction.Prefab.IsBuff)
{
husk.CharacterHealth.ApplyAffliction(null, affliction.Prefab.Instantiate(affliction.Strength));
husk.CharacterHealth.ApplyAffliction(
character.CharacterHealth.GetAfflictionLimb(affliction),
affliction.Prefab.Instantiate(affliction.Strength));
}
}
}
@@ -222,6 +222,9 @@ namespace Barotrauma
[Serialize("", false)]
public string DialogFlag { get; private set; }
[Serialize("", false)]
public string Tag { get; private set; }
[Serialize("0,0,0,0", false)]
public Color MinFaceTint { get; private set; }
@@ -234,6 +237,11 @@ namespace Barotrauma
[Serialize("0,0,0,0", false)]
public Color MaxBodyTint { get; private set; }
/// <summary>
/// Prevents AfflictionHusks with the specified identifier(s) from transforming the character into an AI-controlled character
/// </summary>
public string[] BlockTransformation { get; private set; }
public readonly Dictionary<StatTypes, (float minValue, float maxValue)> AfflictionStatValues = new Dictionary<StatTypes, (float minValue, float maxValue)>();
public readonly HashSet<AbilityFlags> AfflictionAbilityFlags = new HashSet<AbilityFlags>();
@@ -245,6 +253,7 @@ namespace Barotrauma
SerializableProperty.DeserializeProperties(this, element);
resistanceFor = element.GetAttributeStringArray("resistancefor", new string[0], convertToLowerInvariant: true);
BlockTransformation = element.GetAttributeStringArray("blocktransformation", new string[0], convertToLowerInvariant: true);
foreach (XElement subElement in element.Elements())
{
@@ -266,6 +275,9 @@ namespace Barotrauma
var flagType = CharacterAbilityGroup.ParseFlagType(subElement.GetAttributeString("flagtype", ""), parentDebugName);
AfflictionAbilityFlags.Add(flagType);
break;
case "affliction":
DebugConsole.AddWarning($"Error in affliction \"{parentDebugName}\" - additional afflictions caused by the affliction should be configured inside status effects.");
break;
}
}
}
File diff suppressed because it is too large Load Diff
@@ -153,7 +153,10 @@ namespace Barotrauma
(GameMain.GameSession.GameMode as CampaignMode)?.AssignNPCMenuInteraction(npc, CampaignInteractionType);
if (positionToStayIn != null && humanAI != null)
{
humanAI.ObjectiveManager.SetForcedOrder(new AIObjectiveGoTo(positionToStayIn, npc, humanAI.ObjectiveManager, repeat: true, getDivingGearIfNeeded: false, closeEnough: 200));
humanAI.ObjectiveManager.SetForcedOrder(new AIObjectiveGoTo(positionToStayIn, npc, humanAI.ObjectiveManager, repeat: true, getDivingGearIfNeeded: false, closeEnough: 200)
{
DebugLogWhenFails = false
});
}
}
}
@@ -191,7 +194,7 @@ namespace Barotrauma
{
string errorMsg = $"Error while spawning job items. Item {item.Name} created network events before the spawn event had been created.";
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("Job.InitializeJobItem:EventsBeforeSpawning", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
GameAnalyticsManager.AddErrorEventOnce("Job.InitializeJobItem:EventsBeforeSpawning", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
GameMain.Server.EntityEventManager.UniqueEvents.RemoveAll(ev => ev.Entity == item);
GameMain.Server.EntityEventManager.Events.RemoveAll(ev => ev.Entity == item);
}
@@ -9,7 +9,7 @@ namespace Barotrauma
{
private readonly JobPrefab prefab;
private Dictionary<string, Skill> skills;
private readonly Dictionary<string, Skill> skills;
public string Name
{
@@ -147,7 +147,7 @@ namespace Barotrauma
{
string errorMsg = $"Error while spawning job items. Item {item.Name} created network events before the spawn event had been created.";
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("Job.InitializeJobItem:EventsBeforeSpawning", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
GameAnalyticsManager.AddErrorEventOnce("Job.InitializeJobItem:EventsBeforeSpawning", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
GameMain.Server.EntityEventManager.UniqueEvents.RemoveAll(ev => ev.Entity == item);
GameMain.Server.EntityEventManager.Events.RemoveAll(ev => ev.Entity == item);
}
@@ -282,7 +282,7 @@ namespace Barotrauma
Variants = variant;
Skills.Sort((x,y) => y.LevelRange.X.CompareTo(x.LevelRange.X));
Skills.Sort((x,y) => y.LevelRange.Start.CompareTo(x.LevelRange.Start));
// Disabled on purpose, TODO: remove all references?
//ClothingElement = element.GetChildElement("PortraitClothing");
@@ -9,7 +9,7 @@ namespace Barotrauma
public string Identifier { get; }
public const float MaximumSkill = 100.0f;
public float Level
{
get { return level; }
@@ -18,7 +18,7 @@ namespace Barotrauma
public void IncreaseSkill(float value, bool increasePastMax)
{
level = MathHelper.Clamp(level + value, 0.0f, increasePastMax ? float.MaxValue : MaximumSkill);
level = MathHelper.Clamp(level + value, 0.0f, increasePastMax ? SkillSettings.Current.MaximumOlympianSkill : MaximumSkill);
}
private Sprite icon;
@@ -34,14 +34,14 @@ namespace Barotrauma
}
}
internal SkillPrefab Prefab { get; private set; }
public readonly float PriceMultiplier = 1.0f;
public Skill(SkillPrefab prefab)
{
this.Prefab = prefab;
Identifier = prefab.Identifier;
level = Rand.Range(prefab.LevelRange.X, prefab.LevelRange.Y, Rand.RandSync.Server);
level = Rand.Range(prefab.LevelRange.Start, prefab.LevelRange.End, Rand.RandSync.Server);
icon = GetIcon();
PriceMultiplier = prefab.PriceMultiplier;
}
public Skill(string identifier, float level)
@@ -7,7 +7,7 @@ namespace Barotrauma
{
public readonly string Identifier;
public Vector2 LevelRange { get; private set; }
public Range<float> LevelRange { get; private set; }
/// <summary>
/// How much this skill affects characters' hiring cost
@@ -23,12 +23,13 @@ namespace Barotrauma
var levelString = element.GetAttributeString("level", "");
if (levelString.Contains(","))
{
LevelRange = XMLExtensions.ParseVector2(levelString, false);
var rangeVector2 = XMLExtensions.ParseVector2(levelString, false);
LevelRange = new Range<float>(rangeVector2.X, rangeVector2.Y);
}
else
{
float skillLevel = float.Parse(levelString, System.Globalization.CultureInfo.InvariantCulture);
LevelRange = new Vector2(skillLevel, skillLevel);
LevelRange = new Range<float>(skillLevel, skillLevel);
}
IsPrimarySkill = element.GetAttributeBool("primary", false);
@@ -364,7 +364,7 @@ namespace Barotrauma
#if DEBUG
DebugConsole.ThrowError("Attempted to access a removed limb.\n" + Environment.StackTrace.CleanupStackTrace());
#endif
GameAnalyticsManager.AddErrorEventOnce("Limb.LinearVelocity:SimPosition", GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
GameAnalyticsManager.AddErrorEventOnce("Limb.LinearVelocity:SimPosition", GameAnalyticsManager.ErrorSeverity.Error,
"Attempted to access a removed limb.\n" + Environment.StackTrace.CleanupStackTrace());
return Vector2.Zero;
}
@@ -381,7 +381,7 @@ namespace Barotrauma
#if DEBUG
DebugConsole.ThrowError("Attempted to access a removed limb.\n" + Environment.StackTrace.CleanupStackTrace());
#endif
GameAnalyticsManager.AddErrorEventOnce("Limb.LinearVelocity:SimPosition", GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
GameAnalyticsManager.AddErrorEventOnce("Limb.LinearVelocity:SimPosition", GameAnalyticsManager.ErrorSeverity.Error,
"Attempted to access a removed limb.\n" + Environment.StackTrace.CleanupStackTrace());
return 0.0f;
}
@@ -401,7 +401,7 @@ namespace Barotrauma
#if DEBUG
DebugConsole.ThrowError("Attempted to access a removed limb.\n" + Environment.StackTrace.CleanupStackTrace());
#endif
GameAnalyticsManager.AddErrorEventOnce("Limb.Mass:AccessRemoved", GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
GameAnalyticsManager.AddErrorEventOnce("Limb.Mass:AccessRemoved", GameAnalyticsManager.ErrorSeverity.Error,
"Attempted to access a removed limb.\n" + Environment.StackTrace.CleanupStackTrace());
return 1.0f;
}
@@ -420,7 +420,7 @@ namespace Barotrauma
#if DEBUG
DebugConsole.ThrowError("Attempted to access a removed limb.\n" + Environment.StackTrace.CleanupStackTrace());
#endif
GameAnalyticsManager.AddErrorEventOnce("Limb.LinearVelocity:AccessRemoved", GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
GameAnalyticsManager.AddErrorEventOnce("Limb.LinearVelocity:AccessRemoved", GameAnalyticsManager.ErrorSeverity.Error,
"Attempted to access a removed limb.\n" + Environment.StackTrace.CleanupStackTrace());
return Vector2.Zero;
}
@@ -430,8 +430,15 @@ namespace Barotrauma
public float Dir
{
get { return ((dir == Direction.Left) ? -1.0f : 1.0f); }
set { dir = (value == -1.0f) ? Direction.Left : Direction.Right; }
get { return (dir == Direction.Left) ? -1.0f : 1.0f; }
set
{
dir = (value == -1.0f) ? Direction.Left : Direction.Right;
if (body != null)
{
body.Dir = Dir;
}
}
}
public int RefJointIndex => Params.RefJoint;
@@ -464,7 +471,7 @@ namespace Barotrauma
if (!MathUtils.IsValid(value))
{
string errorMsg = "Attempted to set the anchor A of a limb's pull joint to an invalid value (" + value + ")\n" + Environment.StackTrace.CleanupStackTrace();
GameAnalyticsManager.AddErrorEventOnce("Limb.SetPullJointAnchorA:InvalidValue", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
GameAnalyticsManager.AddErrorEventOnce("Limb.SetPullJointAnchorA:InvalidValue", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
#if DEBUG
DebugConsole.ThrowError(errorMsg);
#endif
@@ -478,7 +485,7 @@ namespace Barotrauma
", limb enabled: " + body.Enabled +
", simple physics enabled: " + character.AnimController.SimplePhysicsEnabled + ")\n"
+ Environment.StackTrace.CleanupStackTrace();
GameAnalyticsManager.AddErrorEventOnce("Limb.SetPullJointAnchorA:ExcessiveValue", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
GameAnalyticsManager.AddErrorEventOnce("Limb.SetPullJointAnchorA:ExcessiveValue", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
#if DEBUG
DebugConsole.ThrowError(errorMsg);
#endif
@@ -497,7 +504,7 @@ namespace Barotrauma
if (!MathUtils.IsValid(value))
{
string errorMsg = "Attempted to set the anchor B of a limb's pull joint to an invalid value (" + value + ")\n" + Environment.StackTrace.CleanupStackTrace();
GameAnalyticsManager.AddErrorEventOnce("Limb.SetPullJointAnchorB:InvalidValue", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
GameAnalyticsManager.AddErrorEventOnce("Limb.SetPullJointAnchorB:InvalidValue", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
#if DEBUG
DebugConsole.ThrowError(errorMsg);
#endif
@@ -511,7 +518,7 @@ namespace Barotrauma
", limb enabled: " + body.Enabled +
", simple physics enabled: " + character.AnimController.SimplePhysicsEnabled + ")\n"
+ Environment.StackTrace.CleanupStackTrace();
GameAnalyticsManager.AddErrorEventOnce("Limb.SetPullJointAnchorB:ExcessiveValue", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
GameAnalyticsManager.AddErrorEventOnce("Limb.SetPullJointAnchorB:ExcessiveValue", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
#if DEBUG
DebugConsole.ThrowError(errorMsg);
#endif
@@ -656,6 +656,34 @@ namespace Barotrauma
return target != null;
}
public bool TryGetTarget(Character targetCharacter, out TargetParams target)
{
if (!TryGetTarget(targetCharacter.SpeciesName, out target))
{
target = targets.FirstOrDefault(t => string.Equals(t.Tag, targetCharacter.Params.Group.ToString(), StringComparison.OrdinalIgnoreCase));
}
return target != null;
}
public bool TryGetTarget(IEnumerable<string> tags, out TargetParams target)
{
target = null;
if (tags == null || tags.None()) { return false; }
float priority = -1;
foreach (var potentialTarget in targets)
{
if (potentialTarget.Priority > priority)
{
if (tags.Any(t => string.Equals(t, potentialTarget.Tag, StringComparison.OrdinalIgnoreCase)))
{
target = potentialTarget;
priority = target.Priority;
}
}
}
return target != null;
}
public TargetParams GetTarget(string targetTag, bool throwError = true)
{
if (!TryGetTarget(targetTag, out TargetParams target))
@@ -96,6 +96,13 @@ namespace Barotrauma
set;
}
[Serialize(500.0f, true)]
public float MaximumOlympianSkill
{
get;
set;
}
private SkillSettings(XElement element)
{
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
@@ -1,4 +1,5 @@
using Barotrauma.Items.Components;
using System;
using System.Linq;
using System.Xml.Linq;
@@ -10,24 +11,25 @@ namespace Barotrauma.Abilities
{
Any = 0,
Melee = 1,
Ranged = 2
Ranged = 2,
HandheldRanged = 3,
Turret = 4
};
private readonly string itemIdentifier;
private readonly string[] tags;
private readonly WeaponType weapontype;
private readonly bool ignoreNonHarmfulAttacks;
public AbilityConditionAttackData(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
{
itemIdentifier = conditionElement.GetAttributeString("itemidentifier", "");
itemIdentifier = conditionElement.GetAttributeString("itemidentifier", string.Empty);
tags = conditionElement.GetAttributeStringArray("tags", new string[0], convertToLowerInvariant: true);
switch (conditionElement.GetAttributeString("weapontype", ""))
ignoreNonHarmfulAttacks = conditionElement.GetAttributeBool("ignorenonharmfulattacks", false);
string weaponTypeStr = conditionElement.GetAttributeString("weapontype", "Any");
if (!Enum.TryParse(weaponTypeStr, ignoreCase: true, out weapontype))
{
case "melee":
weapontype = WeaponType.Melee;
break;
case "ranged":
weapontype = WeaponType.Ranged;
break;
DebugConsole.ThrowError($"Error in talent \"{characterTalent.DebugIdentifier}\": \"{weaponTypeStr}\" is not a valid weapon type.");
}
}
@@ -35,17 +37,19 @@ namespace Barotrauma.Abilities
{
if (abilityObject is AbilityAttackData attackData)
{
Item item = attackData?.SourceAttack?.SourceItem;
if (item == null)
if (ignoreNonHarmfulAttacks && attackData.SourceAttack != null)
{
DebugConsole.AddWarning($"Source Item was not found in {this} for talent {characterTalent.DebugIdentifier}!");
return false;
if (attackData.SourceAttack.Stun <= 0.0f && (attackData.SourceAttack.Afflictions?.All(a => a.Key.Prefab.IsBuff) ?? true))
{
return false;
}
}
Item item = attackData?.SourceAttack?.SourceItem;
if (!string.IsNullOrEmpty(itemIdentifier))
{
if (item.prefab.Identifier != itemIdentifier)
if (item?.prefab.Identifier != itemIdentifier)
{
return false;
}
@@ -53,18 +57,34 @@ namespace Barotrauma.Abilities
if (tags.Any())
{
if (!tags.All(t => item.HasTag(t)))
if (!tags.All(t => item?.HasTag(t) ?? false))
{
return false;
}
}
switch (weapontype)
if (weapontype != WeaponType.Any)
{
case WeaponType.Melee:
return item.GetComponent<MeleeWeapon>() != null;
case WeaponType.Ranged:
return item.GetComponent<RangedWeapon>() != null;
switch (weapontype)
{
// it is possible that an item that has both a melee and a projectile component will return true
// even when not used as a melee/ranged weapon respectively
// attackdata should contain data regarding whether the attack is melee or not
case WeaponType.Melee:
return item?.GetComponent<MeleeWeapon>() != null;
case WeaponType.Ranged:
return item?.GetComponent<Projectile>() != null;
case WeaponType.HandheldRanged:
{
var projectile = item?.GetComponent<Projectile>();
return projectile?.Launcher?.GetComponent<Holdable>() != null;
}
case WeaponType.Turret:
{
var projectile = item?.GetComponent<Projectile>();
return projectile?.Launcher?.GetComponent<Turret>() != null;
}
}
}
return true;
@@ -18,13 +18,13 @@ namespace Barotrauma.Abilities
protected void LogAbilityConditionError(AbilityObject abilityObject, Type expectedData)
{
DebugConsole.ThrowError($"Used data-reliant ability condition when data is incompatible! Expected {expectedData}, but received {abilityObject}");
DebugConsole.ThrowError($"Used data-reliant ability condition when data is incompatible! Expected {expectedData}, but received {abilityObject} in talent {characterTalent.DebugIdentifier}");
}
protected abstract bool MatchesConditionSpecific(AbilityObject abilityObject);
public override bool MatchesCondition()
{
DebugConsole.ThrowError("Used data-reliant ability condition in a state-based ability! This is not allowed.");
DebugConsole.ThrowError($"Used data-reliant ability condition in a state-based ability in talent {characterTalent.DebugIdentifier}! This is not allowed.");
return false;
}
public override bool MatchesCondition(AbilityObject abilityObject)
@@ -0,0 +1,28 @@
using System.Xml.Linq;
using static Barotrauma.StatusEffect;
namespace Barotrauma.Abilities
{
class AbilityConditionStatusEffectIdentifier : AbilityConditionData
{
private string effectIdentifier;
public AbilityConditionStatusEffectIdentifier(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
{
effectIdentifier = conditionElement.GetAttributeString("effectidentifier", "").ToLowerInvariant();
}
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
if (abilityObject is AbilityStatusEffectIdentifier abilityStatusEffectIdentifier)
{
return abilityStatusEffectIdentifier.EffectIdentifier == effectIdentifier;
}
else
{
LogAbilityConditionError(abilityObject, typeof(AbilityStatusEffectIdentifier));
return false;
}
}
}
}
@@ -79,17 +79,17 @@ namespace Barotrauma.Abilities
protected virtual void ApplyEffect()
{
DebugConsole.AddWarning($"Ability {this} used improperly! This ability does not have a definition for ApplyEffect");
DebugConsole.AddWarning($"Ability {this} used improperly! This ability does not have a definition for ApplyEffect in talent {CharacterTalent.DebugIdentifier}");
}
protected virtual void ApplyEffect(AbilityObject abilityObject)
{
DebugConsole.AddWarning($"Ability {this} used improperly! This ability does not take a parameter for ApplyEffect");
DebugConsole.AddWarning($"Ability {this} used improperly! This ability does not take a parameter for ApplyEffect in talent {CharacterTalent.DebugIdentifier}");
}
protected void LogabilityObjectMismatch()
{
DebugConsole.ThrowError($"Incompatible ability! Ability {this} is incompatitible with this type of ability effect type.");
DebugConsole.ThrowError($"Incompatible ability! Ability {this} is incompatitible with this type of ability effect type in talent {CharacterTalent.DebugIdentifier}");
}
// XML
@@ -10,8 +10,10 @@ namespace Barotrauma.Abilities
protected readonly List<StatusEffect> statusEffects;
private readonly bool applyToSelf;
private readonly bool nearbyCharactersAppliesToSelf;
private readonly bool nearbyCharactersAppliesToAllies;
private readonly bool nearbyCharactersAppliesToEnemies;
private readonly bool applyToSelected;
readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
@@ -19,9 +21,11 @@ namespace Barotrauma.Abilities
public CharacterAbilityApplyStatusEffects(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
statusEffects = CharacterAbilityGroup.ParseStatusEffects(CharacterTalent, abilityElement.GetChildElement("statuseffects"));
applyToSelf = abilityElement.GetAttributeBool("applytoself", false);
applyToSelected = abilityElement.GetAttributeBool("applytoselected", false);
nearbyCharactersAppliesToSelf = abilityElement.GetAttributeBool("nearbycharactersappliestoself", true);
nearbyCharactersAppliesToAllies = abilityElement.GetAttributeBool("nearbycharactersappliestoallies", true);
nearbyCharactersAppliesToEnemies = abilityElement.GetAttributeBool("nearbycharactersappliestoenemies", true);
}
protected void ApplyEffectSpecific(Character targetCharacter)
@@ -46,6 +50,10 @@ namespace Barotrauma.Abilities
{
targets.RemoveAll(c => c is Character otherCharacter && HumanAIController.IsFriendly(otherCharacter, Character));
}
if (!nearbyCharactersAppliesToEnemies)
{
targets.RemoveAll(c => c is Character otherCharacter && !HumanAIController.IsFriendly(otherCharacter, Character));
}
statusEffect.SetUser(Character);
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, targetCharacter, targets);
}
@@ -75,7 +83,7 @@ namespace Barotrauma.Abilities
protected override void ApplyEffect(AbilityObject abilityObject)
{
if ((abilityObject as IAbilityCharacter)?.Character is Character targetCharacter)
if ((abilityObject as IAbilityCharacter)?.Character is Character targetCharacter && !applyToSelf)
{
ApplyEffectSpecific(targetCharacter);
}
@@ -31,5 +31,10 @@ namespace Barotrauma.Abilities
}
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
ApplyEffect();
}
}
}
@@ -5,18 +5,21 @@ namespace Barotrauma.Abilities
{
class CharacterAbilityGainSimultaneousSkill : CharacterAbility
{
private string skillIdentifier;
private readonly string skillIdentifier;
private readonly bool ignoreAbilitySkillGain;
public CharacterAbilityGainSimultaneousSkill(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
skillIdentifier = abilityElement.GetAttributeString("skillidentifier", "").ToLowerInvariant();
ignoreAbilitySkillGain = abilityElement.GetAttributeBool("ignoreabilityskillgain", true);
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
if ((abilityObject as IAbilityValue)?.Value is float skillIncrease)
if (abilityObject is AbilitySkillGain abilitySkillGain)
{
Character.Info?.IncreaseSkillLevel(skillIdentifier, skillIncrease);
if (ignoreAbilitySkillGain && !abilitySkillGain.GainedFromAbility) { return; }
Character.Info?.IncreaseSkillLevel(skillIdentifier, abilitySkillGain.Value, gainedFromAbility: true);
}
else
{
@@ -26,7 +26,7 @@ namespace Barotrauma.Abilities
value = abilityElement.GetAttributeFloat("value", 0f);
maxValue = abilityElement.GetAttributeFloat("maxvalue", float.MaxValue);
targetAllies = abilityElement.GetAttributeBool("targetallies", false);
removeOnDeath = abilityElement.GetAttributeBool("removeondeath", true);
removeOnDeath = abilityElement.GetAttributeBool("removeondeath", false);
giveOnAddingFirstTime = abilityElement.GetAttributeBool("giveonaddingfirsttime", characterAbilityGroup.AbilityEffectType == AbilityEffectType.None);
setValue = abilityElement.GetAttributeBool("setvalue", false);
}
@@ -49,11 +49,11 @@ namespace Barotrauma.Abilities
{
var skill = character.Info?.Job?.Skills?.GetRandom();
if (skill == null) { return; }
character.Info?.IncreaseSkillLevel(skill.Identifier, skillIncrease);
character.Info?.IncreaseSkillLevel(skill.Identifier, skillIncrease, gainedFromAbility: true);
}
else
{
character.Info?.IncreaseSkillLevel(skillIdentifier, skillIncrease);
character.Info?.IncreaseSkillLevel(skillIdentifier, skillIncrease, gainedFromAbility: true);
}
}
}
@@ -5,7 +5,7 @@ namespace Barotrauma.Abilities
{
class CharacterAbilityModifyAttackData : CharacterAbility
{
private readonly List<Affliction> afflictions;
private readonly List<Affliction> afflictions = new List<Affliction>();
private readonly float addedDamageMultiplier;
private readonly float addedPenetration;
@@ -1,28 +0,0 @@
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityModifyReduceAffliction : CharacterAbility
{
float addedAmountMultiplier;
public override bool AllowClientSimulation => true;
public CharacterAbilityModifyReduceAffliction(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
addedAmountMultiplier = abilityElement.GetAttributeFloat("addedamountmultiplier", 0f);
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
if (abilityObject is AbilityValueAffliction afflictionReduceAmount)
{
afflictionReduceAmount.Affliction.Strength -= addedAmountMultiplier * afflictionReduceAmount.Value;
}
else
{
LogabilityObjectMismatch();
}
}
}
}
@@ -1,6 +1,4 @@
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Xml.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
@@ -13,6 +11,10 @@ namespace Barotrauma.Abilities
{
itemIdentifier = abilityElement.GetAttributeString("itemidentifier", "");
amount = abilityElement.GetAttributeInt("amount", 1);
if (string.IsNullOrEmpty(itemIdentifier))
{
DebugConsole.ThrowError($"Error in talent \"{characterAbilityGroup.CharacterTalent.DebugIdentifier}\" - itemIdentifier not defined.");
}
}
protected override void ApplyEffect()
@@ -34,7 +36,13 @@ namespace Barotrauma.Abilities
if (GameMain.GameSession?.RoundEnding ?? true)
{
Item item = new Item(itemPrefab, Character.WorldPosition, Character.Submarine);
Character.Inventory.TryPutItem(item, Character, new List<InvSlotType>() { InvSlotType.Any });
if (!Character.Inventory.TryPutItem(item, Character, item.AllowedSlots))
{
foreach (Item containedItem in Character.Inventory.AllItemsMod)
{
if (containedItem.OwnInventory?.TryPutItem(item, Character) ?? false) { break; }
}
}
}
else
{
@@ -1,4 +1,5 @@
using System.Linq;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
@@ -15,14 +16,19 @@ namespace Barotrauma.Abilities
if (!TalentTree.JobTalentTrees.TryGetValue(Character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return; }
var subTree = talentTree.TalentSubTrees.Find(t => t.TalentOptionStages.Any(ts => ts.Talents.Contains(CharacterTalent.Prefab)));
if (subTree != null)
{
subTree.ForceUnlock = true;
foreach (var talentOption in subTree.TalentOptionStages)
{
foreach (var talent in talentOption.Talents)
{
if (talent == CharacterTalent.Prefab) { continue; }
Character.GiveTalent(talent);
if (Character.GiveTalent(talent))
{
Character.Info.AdditionalTalentPoints++;
}
}
}
}
@@ -6,15 +6,19 @@ namespace Barotrauma.Abilities
{
class CharacterAbilityApprenticeship : CharacterAbility
{
private readonly bool ignoreAbilitySkillGain;
public CharacterAbilityApprenticeship(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
ignoreAbilitySkillGain = abilityElement.GetAttributeBool("ignoreabilityskillgain", true);
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
if (abilityObject is AbilitySkillGain abilitySkillGain && !abilitySkillGain.GainedFromApprenticeship && abilitySkillGain.Character != Character)
if (abilityObject is AbilitySkillGain abilitySkillGain && abilitySkillGain.Character != Character)
{
Character.Info?.IncreaseSkillLevel(abilitySkillGain.String, 1.0f, gainedFromApprenticeship: true);
if (ignoreAbilitySkillGain && !abilitySkillGain.GainedFromAbility) { return; }
Character.Info?.IncreaseSkillLevel(abilitySkillGain.String, 1.0f, gainedFromAbility: true);
}
}
}
@@ -18,7 +18,7 @@ namespace Barotrauma.Abilities
if (skillIdentifier != lastSkillIdentifier)
{
lastSkillIdentifier = skillIdentifier;
Character.Info?.IncreaseSkillLevel(skillIdentifier, 1.0f);
Character.Info?.IncreaseSkillLevel(skillIdentifier, 1.0f, gainedFromAbility: true);
}
}
}
@@ -31,7 +31,7 @@ namespace Barotrauma.Abilities
}
}
if (closestCharacter.SelectedConstruction == null || !closestCharacter.SelectedConstruction.HasTag(tag)) { return; }
if (closestCharacter?.SelectedConstruction == null || !closestCharacter.SelectedConstruction.HasTag(tag)) { return; }
if (closestDistance < squaredMaxDistance)
{
@@ -31,7 +31,6 @@ namespace Barotrauma
ConfigElement = element;
Identifier = element.GetAttributeString("identifier", "noidentifier");
DisplayName = TextManager.Get("talentname." + Identifier, returnNull: true) ?? Identifier;
this.CalculatePrefabUIntIdentifier(TalentPrefabs);
foreach (XElement subElement in element.Elements())
{
@@ -101,37 +100,42 @@ namespace Barotrauma
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
if (doc == null) { return; }
var rootElement = doc.Root;
switch (rootElement.Name.ToString().ToLowerInvariant())
void loadSinglePrefab(XElement element, bool isOverride)
{
case "talent":
TalentPrefabs.Add(new TalentPrefab(rootElement, file.Path), false);
break;
case "talents":
foreach (var element in rootElement.Elements())
{
if (element.IsOverride())
{
var itemElement = element.GetChildElement("talent");
if (itemElement != null)
{
TalentPrefabs.Add(new TalentPrefab(rootElement, file.Path), true);
}
else
{
DebugConsole.ThrowError($"Cannot find a talent element from the children of the override element defined in {file.Path}");
}
}
else
{
TalentPrefabs.Add(new TalentPrefab(element, file.Path), false);
}
}
break;
default:
DebugConsole.ThrowError($"Invalid XML root element: '{rootElement.Name.ToString()}' in {file.Path}");
break;
var newPrefab = new TalentPrefab(element, file.Path) { ContentPackage = file.ContentPackage };
TalentPrefabs.Add(newPrefab, isOverride);
newPrefab.CalculatePrefabUIntIdentifier(TalentPrefabs);
}
void loadMultiplePrefabs(XElement element, bool isOverride)
{
foreach (var subElement in element.Elements())
{
interpretElement(subElement, isOverride);
}
}
void interpretElement(XElement subElement, bool isOverride)
{
if (subElement.IsOverride())
{
loadMultiplePrefabs(subElement, true);
}
else if (subElement.Name.LocalName.Equals("talents", StringComparison.OrdinalIgnoreCase))
{
loadMultiplePrefabs(subElement, isOverride);
}
else if (subElement.Name.LocalName.Equals("talent", StringComparison.OrdinalIgnoreCase))
{
loadSinglePrefab(subElement, isOverride);
}
else
{
DebugConsole.ThrowError($"Invalid XML element for the {nameof(TalentPrefab)} prefab type: '{subElement.Name}' in {file.Path}");
}
}
interpretElement(doc.Root, false);
}
public static void LoadAll(IEnumerable<ContentFile> files)
@@ -5,7 +5,7 @@ using System.Xml.Linq;
namespace Barotrauma
{
class TalentTree
class TalentTree : IPrefab, IDisposable
{
public enum TalentTreeStageState
{
@@ -16,7 +16,7 @@ namespace Barotrauma
Highlighted
}
public static readonly Dictionary<string, TalentTree> JobTalentTrees = new Dictionary<string, TalentTree>();
public static readonly PrefabCollection<TalentTree> JobTalentTrees = new PrefabCollection<TalentTree>();
public readonly List<TalentSubTree> TalentSubTrees = new List<TalentSubTree>();
@@ -26,13 +26,21 @@ namespace Barotrauma
private set;
}
public string OriginalName => Identifier;
public string Identifier { get; }
public string FilePath { get; }
public ContentPackage ContentPackage { get; set; }
public TalentTree(XElement element, string filePath)
{
ConfigElement = element;
FilePath = filePath;
Identifier = element.GetAttributeString("jobidentifier", "").ToLowerInvariant();
string jobIdentifier = element.GetAttributeString("jobidentifier", "").ToLowerInvariant();
if (string.IsNullOrEmpty(jobIdentifier))
if (string.IsNullOrEmpty(Identifier))
{
DebugConsole.ThrowError($"No job defined for talent tree in \"{filePath}\"!");
return;
@@ -50,20 +58,15 @@ namespace Barotrauma
TalentPrefab talentPrefab = TalentPrefab.TalentPrefabs.Find(c => c.Identifier.Equals(talent, StringComparison.OrdinalIgnoreCase));
if (talentPrefab == null)
{
DebugConsole.AddWarning($"Talent tree for job {jobIdentifier} contains non-existent talent {talent}! Talent tree not added.");
DebugConsole.AddWarning($"Talent tree for job {Identifier} contains non-existent talent {talent}! Talent tree not added.");
return;
}
if (!duplicateSet.Add(talent))
{
DebugConsole.ThrowError($"Talent tree for job {jobIdentifier} contains duplicate talent {talent}! Talent tree not added.");
DebugConsole.ThrowError($"Talent tree for job {Identifier} contains duplicate talent {talent}! Talent tree not added.");
return;
}
}
if (!JobTalentTrees.TryAdd(jobIdentifier, this))
{
DebugConsole.ThrowError($"Could not add talent tree for job {jobIdentifier}! A talent tree for this job is already likely defined");
}
}
public bool TalentIsInTree(string talentIdentifier)
@@ -78,37 +81,40 @@ namespace Barotrauma
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
if (doc == null) { return; }
var rootElement = doc.Root;
switch (rootElement.Name.ToString().ToLowerInvariant())
void loadSinglePrefab(XElement element, bool isOverride)
{
case "talenttree":
new TalentTree(rootElement, file.Path);
break;
case "talenttrees":
foreach (var element in rootElement.Elements())
{
if (element.IsOverride())
{
var treeElement = element.GetChildElement("talenttree");
if (treeElement != null)
{
new TalentTree(rootElement, file.Path);
}
else
{
DebugConsole.ThrowError($"Cannot find a talent tree element from the children of the override element defined in {file.Path}");
}
}
else
{
new TalentTree(element, file.Path);
}
}
break;
default:
DebugConsole.ThrowError($"Invalid XML root element: '{rootElement.Name}' in {file.Path}");
break;
JobTalentTrees.Add(new TalentTree(element, file.Path) { ContentPackage = file.ContentPackage }, isOverride);
}
void loadMultiplePrefabs(XElement element, bool isOverride)
{
foreach (var subElement in element.Elements())
{
interpretElement(subElement, isOverride);
}
}
void interpretElement(XElement subElement, bool isOverride)
{
if (subElement.IsOverride())
{
loadMultiplePrefabs(subElement, true);
}
else if (subElement.Name.LocalName.Equals("talenttrees", StringComparison.OrdinalIgnoreCase))
{
loadMultiplePrefabs(subElement, isOverride);
}
else if (subElement.Name.LocalName.Equals("talenttree", StringComparison.OrdinalIgnoreCase))
{
loadSinglePrefab(subElement, isOverride);
}
else
{
DebugConsole.ThrowError($"Invalid XML element for the {nameof(TalentTree)} prefab type: '{subElement.Name}' in {file.Path}");
}
}
interpretElement(doc.Root, false);
}
public static void LoadAll(IEnumerable<ContentFile> files)
@@ -190,6 +196,8 @@ namespace Barotrauma
foreach (var subTree in talentTree.TalentSubTrees)
{
if (subTree.ForceUnlock && subTree.TalentOptionStages.Any(option => option.Talents.Any(t => t.Identifier == talentIdentifier))) { return true; }
foreach (var talentOptionStage in subTree.TalentOptionStages)
{
bool hasTalentInThisTier = talentOptionStage.Talents.Any(t => selectedTalents.Contains(t.Identifier));
@@ -220,7 +228,7 @@ namespace Barotrauma
canStillUnlock = false;
foreach (string talent in selectedTalents)
{
if (IsViableTalentForCharacter(controlledCharacter, talent, viableTalents))
if (!viableTalents.Contains(talent) && IsViableTalentForCharacter(controlledCharacter, talent, viableTalents))
{
viableTalents.Add(talent);
canStillUnlock = true;
@@ -229,6 +237,14 @@ namespace Barotrauma
}
return viableTalents;
}
private bool disposed = false;
public void Dispose()
{
if (disposed) { return; }
disposed = true;
JobTalentTrees.Remove(this);
}
}
class TalentSubTree
@@ -237,6 +253,8 @@ namespace Barotrauma
public string DisplayName { get; }
public bool ForceUnlock;
public readonly List<TalentOption> TalentOptionStages = new List<TalentOption>();
public TalentSubTree(XElement subTreeElement)
@@ -5,14 +5,64 @@ using System.Threading;
namespace Barotrauma
{
enum CoroutineStatus
abstract class CoroutineStatus
{
Running, Success, Failure
public static CoroutineStatus Running => EnumCoroutineStatus.Running;
public static CoroutineStatus Success => EnumCoroutineStatus.Success;
public static CoroutineStatus Failure => EnumCoroutineStatus.Failure;
public abstract bool CheckFinished(float deltaTime);
public abstract bool EndsCoroutine(CoroutineHandle handle);
}
class EnumCoroutineStatus : CoroutineStatus
{
private enum StatusValue
{
Running, Success, Failure
}
private readonly StatusValue Value;
private EnumCoroutineStatus(StatusValue value) { Value = value; }
public new readonly static EnumCoroutineStatus Running = new EnumCoroutineStatus(StatusValue.Running);
public new readonly static EnumCoroutineStatus Success = new EnumCoroutineStatus(StatusValue.Success);
public new readonly static EnumCoroutineStatus Failure = new EnumCoroutineStatus(StatusValue.Failure);
public override bool CheckFinished(float deltaTime)
{
return true;
}
public override bool EndsCoroutine(CoroutineHandle handle)
{
if (Value == StatusValue.Failure)
{
DebugConsole.ThrowError("Coroutine \"" + handle.Name + "\" has failed");
}
return Value != StatusValue.Running;
}
public override bool Equals(object obj)
{
return obj is EnumCoroutineStatus other && Value == other.Value;
}
public override int GetHashCode()
{
return Value.GetHashCode();
}
public override string ToString()
{
return Value.ToString();
}
}
class CoroutineHandle
{
public readonly IEnumerator<object> Coroutine;
public readonly IEnumerator<CoroutineStatus> Coroutine;
public readonly string Name;
public Exception Exception;
@@ -20,7 +70,7 @@ namespace Barotrauma
public Thread Thread;
public CoroutineHandle(IEnumerator<object> coroutine, string name = "", bool useSeparateThread = false)
public CoroutineHandle(IEnumerator<CoroutineStatus> coroutine, string name = "", bool useSeparateThread = false)
{
Coroutine = coroutine;
Name = string.IsNullOrWhiteSpace(name) ? coroutine.ToString() : name;
@@ -36,7 +86,7 @@ namespace Barotrauma
public static float UnscaledDeltaTime, DeltaTime;
public static CoroutineHandle StartCoroutine(IEnumerable<object> func, string name = "", bool useSeparateThread = false)
public static CoroutineHandle StartCoroutine(IEnumerable<CoroutineStatus> func, string name = "", bool useSeparateThread = false)
{
var handle = new CoroutineHandle(func.GetEnumerator(), name);
lock (Coroutines)
@@ -63,7 +113,7 @@ namespace Barotrauma
return StartCoroutine(DoInvokeAfter(action, delay));
}
private static IEnumerable<object> DoInvokeAfter(Action action, float delay)
private static IEnumerable<CoroutineStatus> DoInvokeAfter(Action action, float delay)
{
if (action == null)
{
@@ -127,9 +177,7 @@ namespace Barotrauma
bool joined = false;
while (!joined)
{
#if CLIENT
CrossThread.ProcessTasks();
#endif
joined = coroutine.Thread.Join(TimeSpan.FromMilliseconds(500));
}
}
@@ -137,35 +185,26 @@ namespace Barotrauma
}
}
private static bool PerformCoroutineStep(CoroutineHandle handle)
{
var current = handle.Coroutine.Current;
if (current != null)
{
if (current.EndsCoroutine(handle) || handle.AbortRequested) { return true; }
if (!current.CheckFinished(UnscaledDeltaTime)) { return false; }
}
if (!handle.Coroutine.MoveNext()) { return true; }
return false;
}
public static void ExecuteCoroutineThread(CoroutineHandle handle)
{
try
{
while (!handle.AbortRequested)
{
if (handle.Coroutine.Current != null)
{
WaitForSeconds wfs = handle.Coroutine.Current as WaitForSeconds;
if (wfs != null)
{
Thread.Sleep((int)(wfs.TotalTime * 1000));
}
else
{
switch ((CoroutineStatus)handle.Coroutine.Current)
{
case CoroutineStatus.Success:
return;
case CoroutineStatus.Failure:
DebugConsole.ThrowError("Coroutine \"" + handle.Name + "\" has failed");
return;
}
}
}
Thread.Yield();
if (!handle.Coroutine.MoveNext()) return;
if (PerformCoroutineStep(handle)) { return; }
Thread.Sleep((int)(UnscaledDeltaTime * 1000));
}
}
catch (ThreadAbortException)
@@ -187,36 +226,13 @@ namespace Barotrauma
#endif
if (handle.Thread == null)
{
if (handle.AbortRequested) { return true; }
if (handle.Coroutine.Current != null)
{
WaitForSeconds wfs = handle.Coroutine.Current as WaitForSeconds;
if (wfs != null)
{
if (!wfs.CheckFinished(UnscaledDeltaTime)) return false;
}
else
{
switch ((CoroutineStatus)handle.Coroutine.Current)
{
case CoroutineStatus.Success:
return true;
case CoroutineStatus.Failure:
DebugConsole.ThrowError("Coroutine \"" + handle.Name + "\" has failed");
return true;
}
}
}
handle.Coroutine.MoveNext();
return false;
return PerformCoroutineStep(handle);
}
else
{
if (handle.Thread.ThreadState.HasFlag(ThreadState.Stopped))
{
if (handle.Exception!=null || (CoroutineStatus)handle.Coroutine.Current == CoroutineStatus.Failure)
if (handle.Exception!=null || handle.Coroutine.Current == CoroutineStatus.Failure)
{
DebugConsole.ThrowError("Coroutine \"" + handle.Name + "\" has failed");
}
@@ -262,7 +278,7 @@ namespace Barotrauma
}
}
class WaitForSeconds
class WaitForSeconds : CoroutineStatus
{
public readonly float TotalTime;
@@ -276,7 +292,7 @@ namespace Barotrauma
this.ignorePause = ignorePause;
}
public bool CheckFinished(float deltaTime)
public override bool CheckFinished(float deltaTime)
{
#if !SERVER
if (ignorePause || !GUI.PauseMenuOpen)
@@ -288,5 +304,10 @@ namespace Barotrauma
#endif
return timer <= 0.0f;
}
public override bool EndsCoroutine(CoroutineHandle handle)
{
return false;
}
}
}
@@ -110,7 +110,7 @@ namespace Barotrauma
public static bool CheatsEnabled;
private static readonly List<ColoredText> unsavedMessages = new List<ColoredText>();
private static readonly int messagesPerFile = 5000;
private static readonly int messagesPerFile = 800;
public const string SavePath = "ConsoleLogs";
public static void AssignOnExecute(string names, Action<string[]> onExecute)
@@ -219,7 +219,7 @@ namespace Barotrauma
try
{
#if CLIENT
SpawnItem(args, GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition), Character.Controlled, out string errorMsg);
SpawnItem(args, Screen.Selected.Cam?.ScreenToWorld(PlayerInput.MousePosition) ?? PlayerInput.MousePosition, Character.Controlled, out string errorMsg);
#elif SERVER
SpawnItem(args, Vector2.Zero, null, out string errorMsg);
#endif
@@ -232,7 +232,7 @@ namespace Barotrauma
{
string errorMsg = "Failed to spawn an item. Arguments: \"" + string.Join(" ", args) + "\".";
ThrowError(errorMsg, e);
GameAnalyticsManager.AddErrorEventOnce("DebugConsole.SpawnItem:Error", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg + '\n' + e.Message + '\n' + e.StackTrace.CleanupStackTrace());
GameAnalyticsManager.AddErrorEventOnce("DebugConsole.SpawnItem:Error", GameAnalyticsManager.ErrorSeverity.Error, errorMsg + '\n' + e.Message + '\n' + e.StackTrace.CleanupStackTrace());
}
},
() =>
@@ -240,7 +240,10 @@ namespace Barotrauma
List<string> itemNames = new List<string>();
foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs)
{
itemNames.Add(itemPrefab.Name);
if (!itemNames.Contains(itemPrefab.Name))
{
itemNames.Add(itemPrefab.Name);
}
}
List<string> spawnPosParams = new List<string>() { "cursor", "inventory" };
@@ -251,8 +254,8 @@ namespace Barotrauma
return new string[][]
{
itemNames.ToArray(),
spawnPosParams.ToArray()
itemNames.ToArray(),
spawnPosParams.ToArray()
};
}, isCheat: true));
@@ -879,7 +882,7 @@ namespace Barotrauma
List<TalentTree> talentTrees = new List<TalentTree>();
if (args.Length == 0 || args[0].Equals("all", StringComparison.OrdinalIgnoreCase))
{
talentTrees.AddRange(TalentTree.JobTalentTrees.Values);
talentTrees.AddRange(TalentTree.JobTalentTrees);
}
else
{
@@ -1063,7 +1066,7 @@ namespace Barotrauma
},
null));
IEnumerable<object> TestLevels()
IEnumerable<CoroutineStatus> TestLevels()
{
SubmarineInfo selectedSub = null;
string subName = GameMain.Config.QuickStartSubmarineName;
@@ -1206,7 +1209,7 @@ namespace Barotrauma
catch (InvalidOperationException e)
{
string errorMsg = "Error while executing the fixhulls command.\n" + e.StackTrace.CleanupStackTrace();
GameAnalyticsManager.AddErrorEventOnce("DebugConsole.FixHulls", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
GameAnalyticsManager.AddErrorEventOnce("DebugConsole.FixHulls", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
}
}
}, null, true));
@@ -1411,6 +1414,29 @@ namespace Barotrauma
}
}, null, isCheat: true));
commands.Add(new Command("despawnnow", "despawnnow [character]: Immediately despawns the specified dead character. If the character argument is omitted, all dead characters are despawned.", (string[] args) =>
{
if (args.Length == 0)
{
foreach (Character c in Character.CharacterList.Where(c => c.IsDead).ToList())
{
c.DespawnNow();
}
}
else
{
Character character = FindMatchingCharacter(args);
character?.DespawnNow();
}
},
() =>
{
return new string[][]
{
Character.CharacterList.Where(c => c.IsDead).Select(c => c.Name).Distinct().ToArray()
};
}, isCheat: true));
commands.Add(new Command("setclientcharacter", "setclientcharacter [client name] [character name]: Gives the client control of the specified character.", null,
() =>
{
@@ -1506,6 +1532,11 @@ namespace Barotrauma
}
}, isCheat: true));
commands.Add(new Command("skipeventcooldown", "skipeventcooldown: Skips the currently active event cooldown and triggers pending monster spawns immediately.", args =>
{
GameMain.GameSession?.EventManager?.SkipEventCooldown();
}, isCheat: true));
commands.Add(new Command("ballastflora", "infectballast [options]: Infect ballasts and control its growth.", args =>
{
if (args.Length == 0)
@@ -1833,7 +1864,7 @@ namespace Barotrauma
#if CLIENT
activeQuestionText = null;
#endif
NewMessage(command, Color.White, true);
NewCommand(command);
//reset the variable before invoking the delegate because the method may need to activate another question
var temp = activeQuestionCallback;
activeQuestionCallback = null;
@@ -1849,7 +1880,7 @@ namespace Barotrauma
ThrowError("Failed to execute command \"" + command + "\"!");
GameAnalyticsManager.AddErrorEventOnce(
"DebugConsole.ExecuteCommand:LengthZero",
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
GameAnalyticsManager.ErrorSeverity.Error,
"Failed to execute command \"" + command + "\"!");
return;
}
@@ -1858,7 +1889,7 @@ namespace Barotrauma
if (!firstCommand.Equals("admin", StringComparison.OrdinalIgnoreCase))
{
NewMessage(command, Color.White, true);
NewCommand(command);
}
#if CLIENT
@@ -2138,7 +2169,7 @@ namespace Barotrauma
{
if (spawnPos != null)
{
if (Entity.Spawner == null)
if (Entity.Spawner == null || Entity.Spawner.Removed)
{
new Item(itemPrefab, spawnPos.Value, null);
}
@@ -2174,15 +2205,37 @@ namespace Barotrauma
}
}
public static void NewMessage(string msg, bool isCommand = false)
public static void ShowError(string msg, Color? color = null)
{
color ??= Color.Red;
NewMessage(msg, color.Value, isCommand: false, isError: true);
}
public static void NewCommand(string command, Color? color = null)
{
color ??= Color.White;
NewMessage(command, color.Value, isCommand: true, isError: false);
}
public static void NewMessage(string msg, Color? color = null, bool debugOnly = false)
{
color ??= Color.White;
if (debugOnly)
{
#if DEBUG
NewMessage(msg, color.Value, isCommand: false, isError: false);
#endif
}
else
{
NewMessage(msg, color.Value, isCommand: false, isError: false);
}
#if DEBUG
Console.WriteLine(msg);
#endif
NewMessage(msg, Color.White, isCommand);
}
public static void NewMessage(string msg, Color color, bool isCommand = false, bool isError = false)
private static void NewMessage(string msg, Color color, bool isCommand, bool isError)
{
if (string.IsNullOrEmpty(msg)) { return; }
@@ -2272,7 +2325,10 @@ namespace Barotrauma
public static void Log(string message)
{
if (GameSettings.VerboseLogging) NewMessage(message, Color.Gray);
if (GameSettings.VerboseLogging)
{
NewMessage(message, Color.Gray);
}
}
public static void ThrowError(string error, Exception e = null, bool createMessageBox = false, bool appendStackTrace = false)
@@ -2310,7 +2366,7 @@ namespace Barotrauma
}
#endif
NewMessage(error, Color.Red, isError: true);
ShowError(error);
}
public static void AddWarning(string warning)
@@ -2320,7 +2376,7 @@ namespace Barotrauma
}
#if CLIENT
private static IEnumerable<object> CreateMessageBox(string errorMsg)
private static IEnumerable<CoroutineStatus> CreateMessageBox(string errorMsg)
{
while (GUI.Style == null)
{
@@ -74,24 +74,12 @@ namespace Barotrauma
continue;
}
}
Prefabs.Add(new DecalPrefab(element, configFile), allowOverriding || sourceElement.IsOverride());
var newPrefab = new DecalPrefab(element, configFile);
Prefabs.Add(newPrefab, allowOverriding || sourceElement.IsOverride());
newPrefab.CalculatePrefabUIntIdentifier(Prefabs);
break;
}
}
using MD5 md5 = MD5.Create();
foreach (DecalPrefab prefab in Prefabs)
{
prefab.UIntIdentifier = ToolBox.StringToUInt32Hash(prefab.Identifier, md5);
//it's theoretically possible for two different values to generate the same hash, but the probability is astronomically small
var collision = Prefabs.Find(p => p != prefab && p.UIntIdentifier == prefab.UIntIdentifier);
if (collision != null)
{
DebugConsole.ThrowError("Hashing collision when generating uint identifiers for Decals: " + prefab.Identifier + " has the same identifier as " + collision.Identifier + " (" + prefab.UIntIdentifier + ")");
collision.UIntIdentifier++;
}
}
}
public void RemoveByFile(string filePath)
@@ -5,7 +5,7 @@ using System.Xml.Linq;
namespace Barotrauma
{
class DecalPrefab : IPrefab, IDisposable
class DecalPrefab : IPrefab, IHasUintIdentifier, IDisposable
{
public readonly string Name;
@@ -21,7 +21,7 @@ namespace Barotrauma
/// Unique identifier that's generated by hashing the prefab's string identifier.
/// Used to reduce the amount of bytes needed to write decal data into network messages in multiplayer.
/// </summary>
public uint UIntIdentifier;
public uint UIntIdentifier { get; set; }
public string FilePath { get; private set; }
@@ -60,6 +60,7 @@
OnGainMissionMoney,
OnLocationDiscovered,
OnItemDeconstructed,
OnItemDeconstructedByAlly,
OnItemDeconstructedMaterial,
OnItemDeconstructedInventory,
OnStopTinkering,
@@ -68,6 +69,7 @@
OnCrewGeneticMaterialCombinedOrRefined,
AfterSubmarineAttacked,
OnApplyTreatment,
OnStatusEffectIdentifier,
}
public enum StatTypes
@@ -111,6 +113,7 @@
GeneticMaterialRefineBonus,
GeneticMaterialTaintedProbabilityReductionOnCombine,
SkillGainSpeed,
MedicalItemApplyingMultiplier,
// Tinker
TinkeringDuration,
TinkeringStrength,
@@ -1,5 +1,4 @@
using System.Xml.Linq;
using NLog.Targets;
namespace Barotrauma
{
@@ -121,7 +121,7 @@ namespace Barotrauma
{
foreach (Item item in newCharacter.Inventory.AllItems)
{
item.SpawnedInOutpost = true;
item.SpawnedInCurrentOutpost = true;
item.AllowStealing = false;
}
}
@@ -92,6 +92,11 @@ namespace Barotrauma
ParentEvent.AddTargetPredicate(Tag, e => e is Item it && SubmarineTypeMatches(it.Submarine) && it.HasTag(tag));
}
private void TagHullsByName(string name)
{
ParentEvent.AddTargetPredicate(Tag, e => e is Hull h && SubmarineTypeMatches(h.Submarine) && h.RoomName.Contains(name, StringComparison.OrdinalIgnoreCase));
}
private bool SubmarineTypeMatches(Submarine sub)
{
if (SubmarineType == SubType.Any) { return true; }
@@ -144,6 +149,9 @@ namespace Barotrauma
case "itemtag":
if (kvp.Length > 1) { TagItemsByTag(kvp[1].Trim()); }
break;
case "hullname":
if (kvp.Length > 1) { TagHullsByName(kvp[1].Trim()); }
break;
}
}
@@ -1,5 +1,6 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
@@ -34,6 +35,9 @@ namespace Barotrauma
[Serialize(false, true, description: "If true, one target must interact with the other to trigger the action.")]
public bool WaitForInteraction { get; set; }
[Serialize(false, true, description: "If true, the action can be triggered by interacting with any matching target (not just the 1st one).")]
public bool AllowMultipleTargets { get; set; }
private float distance;
public TriggerAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element)
@@ -55,7 +59,7 @@ namespace Barotrauma
public bool isRunning = false;
private Either<Character, Item> npcOrItem = null;
private readonly List<Either<Character, Item>> npcsOrItems = new List<Either<Character, Item>>();
public override void Update(float deltaTime)
{
@@ -93,12 +97,16 @@ namespace Barotrauma
Character player = null;
Character npc = null;
Item item = null;
npcOrItem?.TryGet(out npc);
npcOrItem?.TryGet(out item);
if (e1 is Character char1)
{
if (char1.IsBot) { npc ??= char1; }
else { player = char1; }
if (char1.IsBot)
{
npc ??= char1;
}
else
{
player = char1;
}
}
else
{
@@ -106,8 +114,14 @@ namespace Barotrauma
}
if (e2 is Character char2)
{
if (char2.IsBot) { npc ??= char2; }
else { player = char2; }
if (char2.IsBot)
{
npc ??= char2;
}
else
{
player = char2;
}
}
else
{
@@ -120,7 +134,10 @@ namespace Barotrauma
{
if (npc.CampaignInteractionType != CampaignMode.InteractionType.Examine)
{
npcOrItem = npc;
if (!npcsOrItems.Any(n => n.TryGet(out Character npc2) && npc2 == npc))
{
npcsOrItems.Add(npc);
}
npc.CampaignInteractionType = CampaignMode.InteractionType.Examine;
npc.RequireConsciousnessForCustomInteract = DisableIfTargetIncapacitated;
#if CLIENT
@@ -134,12 +151,14 @@ namespace Barotrauma
GameMain.NetworkMember.CreateEntityEvent(npc, new object[] { NetEntityEvent.Type.AssignCampaignInteraction });
#endif
}
return;
if (!AllowMultipleTargets) { return; }
}
else if (item != null)
{
npcOrItem = item;
if (!npcsOrItems.Any(n => n.TryGet(out Item item2) && item2 == item))
{
npcsOrItems.Add(item);
}
item.CampaignInteractionType = CampaignMode.InteractionType.Examine;
if (player.SelectedConstruction == item ||
player.Inventory != null && player.Inventory.Contains(item) ||
@@ -170,19 +189,21 @@ namespace Barotrauma
private void ResetTargetIcons()
{
if (npcOrItem == null) { return; }
if (npcOrItem.TryGet(out Character npc))
foreach (var npcOrItem in npcsOrItems)
{
npc.CampaignInteractionType = CampaignMode.InteractionType.None;
npc.SetCustomInteract(null, null);
npc.RequireConsciousnessForCustomInteract = true;
#if SERVER
GameMain.NetworkMember.CreateEntityEvent(npc, new object[] { NetEntityEvent.Type.AssignCampaignInteraction });
#endif
}
else if (npcOrItem.TryGet(out Item item))
{
item.CampaignInteractionType = CampaignMode.InteractionType.None;
if (npcOrItem.TryGet(out Character npc))
{
npc.CampaignInteractionType = CampaignMode.InteractionType.None;
npc.SetCustomInteract(null, null);
npc.RequireConsciousnessForCustomInteract = true;
#if SERVER
GameMain.NetworkMember.CreateEntityEvent(npc, new object[] { NetEntityEvent.Type.AssignCampaignInteraction });
#endif
}
else if (npcOrItem.TryGet(out Item item))
{
item.CampaignInteractionType = CampaignMode.InteractionType.None;
}
}
}
@@ -269,7 +290,7 @@ namespace Barotrauma
return
$"{ToolBox.GetDebugSymbol(isFinished, isRunning)} {nameof(TriggerAction)} -> (" +
(WaitForInteraction ?
$"Selected non-player target: {(npcOrItem?.ToString() ?? "<null>").ColorizeObject()}, " :
$"Selected non-player target: {(npcsOrItems?.ToString() ?? "<null>").ColorizeObject()}, " :
$"Distance: {((int)distance).ColorizeObject()}, ") +
$"Radius: {Radius.ColorizeObject()}, " +
$"TargetTags: {Target1Tag.ColorizeObject()}, " +
@@ -1,10 +1,9 @@
using FarseerPhysics;
using Barotrauma.Extensions;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
using NLog;
namespace Barotrauma
{
@@ -33,6 +32,8 @@ namespace Barotrauma
private float currentIntensity;
//The exact intensity of the current situation, current intensity is lerped towards this value
private float targetIntensity;
//follows targetIntensity a bit faster than currentIntensity to prevent e.g. combat musing staying on very long after the monsters are dead
private float musicIntensity;
//How low the intensity has to be for an event to be triggered.
//Gradually increases with time, so additional problems can still appear eventually even if
@@ -50,7 +51,11 @@ namespace Barotrauma
private float calculateDistanceTraveledTimer;
private float distanceTraveled;
private float avgCrewHealth, avgHullIntegrity, floodingAmount, fireAmount, enemyDanger, monsterTotalStrength;
private float avgCrewHealth, avgHullIntegrity, floodingAmount, fireAmount, enemyDanger, monsterStrength;
public float CumulativeMonsterStrengthMain;
public float CumulativeMonsterStrengthRuins;
public float CumulativeMonsterStrengthWrecks;
public float CumulativeMonsterStrengthCaves;
private float roundDuration;
@@ -78,6 +83,10 @@ namespace Barotrauma
{
get { return currentIntensity; }
}
public float MusicIntensity
{
get { return musicIntensity; }
}
public List<Event> ActiveEvents
{
@@ -85,7 +94,22 @@ namespace Barotrauma
}
public readonly Queue<Event> QueuedEvents = new Queue<Event>();
private struct TimeStamp
{
public readonly double Time;
public readonly Event Event;
public TimeStamp(Event e)
{
Event = e;
Time = Timing.TotalTime;
}
}
private readonly List<TimeStamp> timeStamps = new List<TimeStamp>();
public void AddTimeStamp(Event e) => timeStamps.Add(new TimeStamp(e));
public EventManager()
{
isClient = GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient;
@@ -99,6 +123,7 @@ namespace Barotrauma
if (isClient) { return; }
timeStamps.Clear();
pendingEventSets.Clear();
selectedEvents.Clear();
activeEvents.Clear();
@@ -124,12 +149,12 @@ namespace Barotrauma
}
MTRandom rand = new MTRandom(seed);
EventSet initialEventSet = SelectRandomEvents(EventSet.List, rand);
EventSet initialEventSet = SelectRandomEvents(EventSet.List, requireCampaignSet: GameMain.GameSession?.GameMode is CampaignMode, rand);
EventSet additiveSet = null;
if (initialEventSet != null && initialEventSet.Additive)
{
additiveSet = initialEventSet;
initialEventSet = SelectRandomEvents(EventSet.List.FindAll(e => !e.Additive), rand);
initialEventSet = SelectRandomEvents(EventSet.List.FindAll(e => !e.Additive), requireCampaignSet: GameMain.GameSession?.GameMode is CampaignMode, rand);
}
if (initialEventSet != null)
{
@@ -202,8 +227,12 @@ namespace Barotrauma
crewAwayResetTimer = 0.0f;
intensityUpdateTimer = 0.0f;
CalculateCurrentIntensity(0.0f);
currentIntensity = targetIntensity;
currentIntensity = musicIntensity = targetIntensity;
eventCoolDown = 0.0f;
CumulativeMonsterStrengthMain = 0;
CumulativeMonsterStrengthRuins = 0;
CumulativeMonsterStrengthWrecks = 0;
CumulativeMonsterStrengthCaves = 0;
}
private void SelectSettings()
@@ -389,6 +418,11 @@ namespace Barotrauma
pathFinder = null;
}
public void SkipEventCooldown()
{
eventCoolDown = 0.0f;
}
private float CalculateCommonness(EventPrefab eventPrefab, float baseCommonness)
{
if (level.LevelData.NonRepeatableEvents.Contains(eventPrefab)) { return 0.0f; }
@@ -401,11 +435,7 @@ namespace Barotrauma
{
if (level == null) { return; }
if (level.LevelData.HasHuntingGrounds && eventSet.DisableInHuntingGrounds) { return; }
#if DEBUG
DebugConsole.NewMessage($"Loading event set {eventSet.DebugIdentifier}", Color.LightBlue);
#else
DebugConsole.Log($"Loading event set {eventSet.DebugIdentifier}");
#endif
DebugConsole.NewMessage($"Loading event set {eventSet.DebugIdentifier}", Color.LightBlue, debugOnly: true);
int applyCount = 1;
List<Func<Level.InterestingPosition, bool>> spawnPosFilter = new List<Func<Level.InterestingPosition, bool>>();
if (eventSet.PerRuin)
@@ -413,7 +443,7 @@ namespace Barotrauma
applyCount = level.Ruins.Count();
foreach (var ruin in level.Ruins)
{
spawnPosFilter.Add((Level.InterestingPosition pos) => { return pos.Ruin == ruin; });
spawnPosFilter.Add(pos => pos.Ruin == ruin);
}
}
else if (eventSet.PerCave)
@@ -421,7 +451,7 @@ namespace Barotrauma
applyCount = level.Caves.Count();
foreach (var cave in level.Caves)
{
spawnPosFilter.Add((Level.InterestingPosition pos) => { return pos.Cave == cave; });
spawnPosFilter.Add(pos => pos.Cave == cave);
}
}
else if (eventSet.PerWreck)
@@ -430,7 +460,7 @@ namespace Barotrauma
applyCount = wrecks.Count();
foreach (var wreck in wrecks)
{
spawnPosFilter.Add((Level.InterestingPosition pos) => { return pos.Submarine == wreck; });
spawnPosFilter.Add(pos => pos.Submarine == wreck);
}
}
@@ -463,11 +493,7 @@ namespace Barotrauma
if (newEvent == null) { continue; }
newEvent.Init(true);
if (i < spawnPosFilter.Count) { newEvent.SpawnPosFilter = spawnPosFilter[i]; }
#if DEBUG
DebugConsole.NewMessage($"Initialized event {newEvent}");
#else
DebugConsole.Log($"Initialized event {newEvent}");
#endif
DebugConsole.NewMessage($"Initialized event {newEvent}", debugOnly: true);
if (!selectedEvents.ContainsKey(eventSet))
{
selectedEvents.Add(eventSet, new List<Event>());
@@ -479,7 +505,7 @@ namespace Barotrauma
}
if (eventSet.ChildSets.Count > 0)
{
var newEventSet = SelectRandomEvents(eventSet.ChildSets, rand);
var newEventSet = SelectRandomEvents(eventSet.ChildSets, random: rand);
if (newEventSet != null)
{
CreateEvents(newEventSet, rand);
@@ -498,11 +524,7 @@ namespace Barotrauma
var newEvent = eventPrefab.CreateInstance();
if (newEvent == null) { continue; }
newEvent.Init(true);
#if DEBUG
DebugConsole.NewMessage($"Initialized event {newEvent}");
#else
DebugConsole.Log($"Initialized event {newEvent}");
#endif
DebugConsole.NewMessage($"Initialized event {newEvent}", debugOnly: true);
if (!selectedEvents.ContainsKey(eventSet))
{
selectedEvents.Add(eventSet, new List<Event>());
@@ -518,7 +540,7 @@ namespace Barotrauma
}
}
private EventSet SelectRandomEvents(List<EventSet> eventSets, Random random = null)
private EventSet SelectRandomEvents(List<EventSet> eventSets, bool? requireCampaignSet = null, Random random = null)
{
if (level == null) { return null; }
Random rand = random ?? new MTRandom(ToolBox.StringToInt(level.Seed));
@@ -529,6 +551,27 @@ namespace Barotrauma
level.LevelData.Type == es.LevelType &&
(string.IsNullOrEmpty(es.BiomeIdentifier) || es.BiomeIdentifier.Equals(level.LevelData.Biome.Identifier, StringComparison.OrdinalIgnoreCase)));
if (requireCampaignSet.HasValue)
{
if (requireCampaignSet.Value)
{
if (allowedEventSets.Any(es => es.IsCampaignSet))
{
allowedEventSets =
allowedEventSets.Where(es => es.IsCampaignSet);
}
else
{
DebugConsole.AddWarning("No campaign event sets available. Using a non-campaign-specific set instead.");
}
}
else
{
allowedEventSets =
allowedEventSets.Where(es => !es.IsCampaignSet);
}
}
Location location = (GameMain.GameSession?.GameMode as CampaignMode)?.Map?.CurrentLocation ?? level?.StartLocation;
LocationType locationType = location?.GetLocationType();
@@ -647,6 +690,7 @@ namespace Barotrauma
isCrewAway = false;
crewAwayDuration = 0.0f;
eventThreshold += settings.EventThresholdIncrease * deltaTime;
eventThreshold = Math.Min(eventThreshold, 1.0f);
eventCoolDown -= deltaTime;
}
@@ -739,7 +783,7 @@ namespace Barotrauma
// enemy amount --------------------------------------------------------
enemyDanger = 0.0f;
monsterTotalStrength = 0;
monsterStrength = 0;
foreach (Character character in Character.CharacterList)
{
if (character.IsIncapacitated || !character.Enabled || character.IsPet || character.Params.CompareGroup("human")) { continue; }
@@ -749,28 +793,9 @@ namespace Barotrauma
if (!enemyAI.AIParams.StayInAbyss)
{
// Ignore abyss monsters because they can stay active for quite great distances. They'll be taken into account when they target the sub.
monsterTotalStrength += enemyAI.CombatStrength;
monsterStrength += enemyAI.CombatStrength;
}
// Example combat strengths:
// Hammerheadspawn 1
// Moloch Pupa 1
// Terminal cell 20
// Leucocyte 40
// Husk 90
// Crawler 100
// Unarmored Mudraptor 140
// Spineling 150
// Tigerthresher 200
// Armored Mudraptor 210
// Watcher 400
// Golden Hammerhead 400
// Hammerhead 500
// Hammerhead Matriarch 550
// Bonethresher 600
// Moloch 1250
// Black Moloch 1500
// Endworm 10000
if (character.CurrentHull?.Submarine != null &&
(character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine)))
{
@@ -792,7 +817,7 @@ namespace Barotrauma
// 5 Mudraptors -> +0.21 (0.42 in total, before they get inside).
// 3 Hammerheads -> +0.3 (0.6 in total, if they all target the sub).
// 2 Molochs -> +0.5 (1.0 in total, if both target the sub).
enemyDanger += monsterTotalStrength / 5000f;
enemyDanger += monsterStrength / 5000f;
enemyDanger = MathHelper.Clamp(enemyDanger, 0.0f, 1.0f);
// The definitions above aim for that we never spawn more monsters that the player (and the performance) can handle.
@@ -868,11 +893,15 @@ namespace Barotrauma
{
//25 seconds for intensity to go from 0.0 to 1.0
currentIntensity = Math.Min(currentIntensity + 0.04f * IntensityUpdateInterval, targetIntensity);
//20 seconds for intensity to go from 0.0 to 1.0
musicIntensity = Math.Min(musicIntensity + 0.05f * IntensityUpdateInterval, targetIntensity);
}
else
{
//400 seconds for intensity to go from 1.0 to 0.0
currentIntensity = Math.Max(currentIntensity - 0.0025f * IntensityUpdateInterval, targetIntensity);
//20 seconds for intensity to go from 1.0 to 0.0
musicIntensity = Math.Max(musicIntensity - 0.05f * IntensityUpdateInterval, targetIntensity);
}
}
@@ -13,6 +13,7 @@ namespace Barotrauma
public float Commonness;
public string Identifier;
public string BiomeIdentifier;
public float SpawnDistance;
public bool UnlockPathEvent;
public string UnlockPathTooltip;
@@ -46,25 +47,30 @@ namespace Barotrauma
UnlockPathTooltip = element.GetAttributeString("unlockpathtooltip", "lockedpathtooltip");
UnlockPathReputation = element.GetAttributeInt("unlockpathreputation", 0);
UnlockPathFaction = element.GetAttributeString("unlockpathfaction", "");
SpawnDistance = element.GetAttributeFloat("spawndistance", 0);
}
public bool TryCreateInstance<T>(out T instance) where T : Event
{
instance = CreateInstance() as T;
return instance is T;
}
public Event CreateInstance()
{
ConstructorInfo constructor = EventType.GetConstructor(new[] { typeof(EventPrefab) });
object instance = null;
Event instance = null;
try
{
instance = constructor.Invoke(new object[] { this });
instance = constructor.Invoke(new object[] { this }) as Event;
}
catch (Exception ex)
{
DebugConsole.ThrowError(ex.InnerException != null ? ex.InnerException.ToString() : ex.ToString());
}
Event ev = (Event)instance;
if (!ev.LevelMeetsRequirements()) { return null; }
return (Event)instance;
if (instance != null && !instance.LevelMeetsRequirements()) { return null; }
return instance;
}
public override string ToString()
@@ -14,6 +14,7 @@ namespace Barotrauma
{
public readonly EventSet RootSet;
public readonly Dictionary<string, int> MonsterCounts = new Dictionary<string, int>();
public float MonsterStrength;
public EventDebugStats(EventSet rootSet)
{
@@ -63,6 +64,8 @@ namespace Barotrauma
return GetAllEventPrefabs().Find(prefab => string.Equals(prefab.Identifier, identifer, StringComparison.Ordinal));
}
public readonly bool IsCampaignSet;
//0-100
public readonly float MinLevelDifficulty, MaxLevelDifficulty;
@@ -193,6 +196,7 @@ namespace Barotrauma
DelayWhenCrewAway = element.GetAttributeBool("delaywhencrewaway", !PerRuin && !PerCave && !PerWreck);
OncePerOutpost = element.GetAttributeBool("onceperoutpost", false);
TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", true);
IsCampaignSet = element.GetAttributeBool("campaign", LevelType == LevelData.LevelType.Outpost || (parentSet?.IsCampaignSet ?? false));
Commonness[""] = element.GetAttributeFloat("commonness", 1.0f);
foreach (XElement subElement in element.Elements())
@@ -205,7 +209,7 @@ namespace Barotrauma
{
if (overrideElement.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase))
{
string levelType = overrideElement.GetAttributeString("leveltype", "");
string levelType = overrideElement.GetAttributeString("leveltype", "").ToLowerInvariant();
if (!Commonness.ContainsKey(levelType))
{
Commonness.Add(levelType, overrideElement.GetAttributeFloat("commonness", 0.0f));
@@ -227,8 +231,8 @@ namespace Barotrauma
EventPrefabs.Add(new SubEventPrefab(
debugIdentifier,
identifiers,
commonness>=0f ? commonness : (float?)null,
probability>=0f ? probability : (float?)null));
commonness >= 0f ? commonness : (float?)null,
probability >= 0f ? probability : (float?)null));
}
else
{
@@ -347,7 +351,7 @@ namespace Barotrauma
}
}
public static List<string> GetDebugStatistics(int simulatedRoundCount = 100)
public static List<string> GetDebugStatistics(int simulatedRoundCount = 100, Func<MonsterEvent, bool> filter = null)
{
List<string> debugLines = new List<string>();
@@ -357,82 +361,75 @@ namespace Barotrauma
for (int i = 0; i < simulatedRoundCount; i++)
{
var newStats = new EventDebugStats(eventSet);
CheckEventSet(newStats, eventSet);
CheckEventSet(newStats, eventSet, filter);
stats.Add(newStats);
}
debugLines.Add($"Event stats ({eventSet.DebugIdentifier}): ");
LogEventStats(stats, debugLines);
}
for (int difficulty = 0; difficulty <= 100; difficulty += 10)
{
debugLines.Add($"Event stats on difficulty level {difficulty}: ");
List<EventDebugStats> stats = new List<EventDebugStats>();
for (int i = 0; i < simulatedRoundCount; i++)
{
EventSet selectedSet = List.Where(s => difficulty >= s.MinLevelDifficulty && difficulty <= s.MaxLevelDifficulty).GetRandom();
if (selectedSet == null) { continue; }
var newStats = new EventDebugStats(selectedSet);
CheckEventSet(newStats, selectedSet);
stats.Add(newStats);
}
LogEventStats(stats, debugLines);
}
return debugLines;
static void CheckEventSet(EventDebugStats stats, EventSet thisSet)
static void CheckEventSet(EventDebugStats stats, EventSet thisSet, Func<MonsterEvent, bool> filter = null)
{
if (thisSet.ChooseRandom)
{
var unusedEvents = thisSet.EventPrefabs.ToList();
for (int i = 0; i < thisSet.EventCount; i++)
if (unusedEvents.Any())
{
var eventPrefab = ToolBox.SelectWeightedRandom(unusedEvents, unusedEvents.Select(e => e.Commonness).ToList(), Rand.RandSync.Unsynced);
if (eventPrefab.Prefabs.Any(p => p != null))
for (int i = 0; i < thisSet.EventCount; i++)
{
AddEvents(stats, eventPrefab.Prefabs);
unusedEvents.Remove(eventPrefab);
var eventPrefab = ToolBox.SelectWeightedRandom(unusedEvents, unusedEvents.Select(e => e.Commonness).ToList());
if (eventPrefab.Prefabs.Any(p => p != null))
{
AddEvents(stats, eventPrefab.Prefabs, filter);
unusedEvents.Remove(eventPrefab);
}
}
}
List<float> values = thisSet.ChildSets.SelectMany(s => s.Commonness.Values).ToList();
EventSet childSet = ToolBox.SelectWeightedRandom(thisSet.ChildSets, values);
if (childSet != null)
{
CheckEventSet(stats, childSet, filter);
}
}
else
{
foreach (var eventPrefab in thisSet.EventPrefabs)
{
AddEvents(stats, eventPrefab.Prefabs);
AddEvents(stats, eventPrefab.Prefabs, filter);
}
foreach (var childSet in thisSet.ChildSets)
{
CheckEventSet(stats, childSet, filter);
}
}
foreach (var childSet in thisSet.ChildSets)
{
CheckEventSet(stats, childSet);
}
}
static void AddEvents(EventDebugStats stats, IEnumerable<EventPrefab> eventPrefabs)
=> eventPrefabs.ForEach(p => AddEvent(stats, p));
static void AddEvents(EventDebugStats stats, IEnumerable<EventPrefab> eventPrefabs, Func<MonsterEvent, bool> filter = null)
=> eventPrefabs.ForEach(p => AddEvent(stats, p, filter));
static void AddEvent(EventDebugStats stats, EventPrefab eventPrefab)
static void AddEvent(EventDebugStats stats, EventPrefab eventPrefab, Func<MonsterEvent, bool> filter = null)
{
if (eventPrefab.EventType == typeof(MonsterEvent))
if (eventPrefab.EventType == typeof(MonsterEvent) && eventPrefab.TryCreateInstance(out MonsterEvent monsterEvent))
{
float spawnProbability = eventPrefab.ConfigElement.GetAttributeFloat("spawnprobability", 1.0f);
if (Rand.Value(Rand.RandSync.Server) > spawnProbability)
{
return;
}
if (filter != null && !filter(monsterEvent)) { return; }
string character = eventPrefab.ConfigElement.GetAttributeString("characterfile", "");
System.Diagnostics.Debug.Assert(!string.IsNullOrEmpty(character));
int amount = eventPrefab.ConfigElement.GetAttributeInt("amount", 0);
int minAmount = eventPrefab.ConfigElement.GetAttributeInt("minamount", amount);
int maxAmount = eventPrefab.ConfigElement.GetAttributeInt("maxamount", amount);
float spawnProbability = monsterEvent.Prefab.Probability;
if (Rand.Value() > spawnProbability) { return; }
int count = Rand.Range(minAmount, maxAmount + 1);
string character = monsterEvent.speciesName;
int count = Rand.Range(monsterEvent.MinAmount, monsterEvent.MaxAmount + 1);
if (count <= 0) { return; }
if (!stats.MonsterCounts.ContainsKey(character)) { stats.MonsterCounts[character] = 0; }
stats.MonsterCounts[character] += count;
var aiElement = CharacterPrefab.FindBySpeciesName(character)?.XDocument?.Root?.GetChildElement("ai");
if (aiElement != null)
{
stats.MonsterStrength += aiElement.GetAttributeFloat("combatstrength", 0) * count;
}
}
}
@@ -445,16 +442,21 @@ namespace Barotrauma
}
else
{
stats.Sort((s1, s2) => { return s1.MonsterCounts.Values.Sum().CompareTo(s2.MonsterCounts.Values.Sum()); });
EventDebugStats minStats = stats.First();
EventDebugStats maxStats = stats.First();
debugLines.Add($" Minimum monster spawns: {stats.First().MonsterCounts.Values.Sum()}");
stats.Sort((s1, s2) => s1.MonsterCounts.Values.Sum().CompareTo(s2.MonsterCounts.Values.Sum()));
debugLines.Add($" Minimum monster count: {stats.First().MonsterCounts.Values.Sum()}");
debugLines.Add($" {LogMonsterCounts(stats.First())}");
debugLines.Add($" Median monster spawns: {stats[stats.Count / 2].MonsterCounts.Values.Sum()}");
debugLines.Add($" Median monster count: {stats[stats.Count / 2].MonsterCounts.Values.Sum()}");
debugLines.Add($" {LogMonsterCounts(stats[stats.Count / 2])}");
debugLines.Add($" Maximum monster spawns: {stats.Last().MonsterCounts.Values.Sum()}");
debugLines.Add($" Maximum monster count: {stats.Last().MonsterCounts.Values.Sum()}");
debugLines.Add($" {LogMonsterCounts(stats.Last())}");
debugLines.Add($" Average monster count: {StringFormatter.FormatZeroDecimal((float)stats.Average(s => s.MonsterCounts.Values.Sum()))}");
debugLines.Add($" ");
stats.Sort((s1, s2) => s1.MonsterStrength.CompareTo(s2.MonsterStrength));
debugLines.Add($" Minimum monster strength: {StringFormatter.FormatZeroDecimal(stats.First().MonsterStrength)}");
debugLines.Add($" Median monster strength: {StringFormatter.FormatZeroDecimal(stats[stats.Count / 2].MonsterStrength)}");
debugLines.Add($" Maximum monster strength: {StringFormatter.FormatZeroDecimal(stats.Last().MonsterStrength)}");
debugLines.Add($" Average monster strength: {StringFormatter.FormatZeroDecimal(stats.Average(s => s.MonsterStrength))}");
debugLines.Add($" ");
}
}
@@ -1,4 +1,5 @@
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
@@ -207,7 +207,7 @@ namespace Barotrauma
var item = new Item(itemPrefab, position.Value, cargoRoomSub)
{
SpawnedInOutpost = true,
SpawnedInCurrentOutpost = true,
AllowStealing = false
};
item.FindHull();
@@ -1,4 +1,3 @@
using Barotrauma.Extensions;
using System.Collections.Generic;
namespace Barotrauma
@@ -6,8 +5,6 @@ namespace Barotrauma
partial class CombatMission : Mission
{
private Submarine[] subs;
// TODO: not used
private List<Character>[] crews;
private readonly string[] descriptions;
private static string[] teamNames = { "Team A", "Team B" };
@@ -103,15 +100,16 @@ namespace Barotrauma
subs[0].NeutralizeBallast();
subs[0].TeamID = CharacterTeamType.Team1;
subs[0].DockedTo.ForEach(s => s.TeamID = CharacterTeamType.Team1);
subs[0].GetConnectedSubs().ForEach(s => s.TeamID = CharacterTeamType.Team1);
subs[1].NeutralizeBallast();
subs[1].TeamID = CharacterTeamType.Team2;
subs[1].DockedTo.ForEach(s => s.TeamID = CharacterTeamType.Team2);
subs[1].GetConnectedSubs().ForEach(s => s.TeamID = CharacterTeamType.Team2);
subs[1].SetPosition(subs[1].FindSpawnPos(Level.Loaded.EndPosition));
subs[1].FlipX();
#if SERVER
crews = new List<Character>[] { new List<Character>(), new List<Character>() };
#endif
}
public override void End()
@@ -203,6 +203,14 @@ namespace Barotrauma
enemySub.TeamID = CharacterTeamType.None;
//make the enemy sub withstand atleast the same depth as the player sub
enemySub.RealWorldCrushDepth = Math.Max(enemySub.RealWorldCrushDepth, Submarine.MainSub.RealWorldCrushDepth);
if (Level.Loaded != null)
{
//...and the depth of the patrol positions + 1000 m
foreach (var patrolPos in patrolPositions)
{
enemySub.RealWorldCrushDepth = Math.Max(enemySub.RealWorldCrushDepth, Level.Loaded.GetRealWorldDepth(patrolPos.Y) + 1000);
}
}
enemySub.ImmuneToBallastFlora = true;
}
@@ -9,8 +9,8 @@ namespace Barotrauma
{
class MonsterEvent : Event
{
private readonly string speciesName;
private readonly int minAmount, maxAmount;
public readonly string speciesName;
public readonly int minAmount, maxAmount;
private List<Character> monsters;
private readonly float scatter;
@@ -20,7 +20,7 @@ namespace Barotrauma
private bool disallowed;
private readonly Level.PositionType spawnPosType;
public readonly Level.PositionType SpawnPosType;
private readonly string spawnPointTag;
private bool spawnPending;
@@ -42,15 +42,15 @@ namespace Barotrauma
{
if (maxAmount <= 1)
{
return "MonsterEvent (" + speciesName + ")";
return $"MonsterEvent ({speciesName}, {SpawnPosType})";
}
else if (minAmount < maxAmount)
{
return "MonsterEvent (" + speciesName + " x" + minAmount + "-" + maxAmount + ")";
return $"MonsterEvent ({speciesName} x{minAmount}-{maxAmount}, {SpawnPosType})";
}
else
{
return "MonsterEvent (" + speciesName + " x" + maxAmount + ")";
return $"MonsterEvent ({speciesName} x{maxAmount}, {SpawnPosType})";
}
}
@@ -77,15 +77,15 @@ namespace Barotrauma
var spawnPosTypeStr = prefab.ConfigElement.GetAttributeString("spawntype", "");
if (string.IsNullOrWhiteSpace(spawnPosTypeStr) ||
!Enum.TryParse(spawnPosTypeStr, true, out spawnPosType))
!Enum.TryParse(spawnPosTypeStr, true, out SpawnPosType))
{
spawnPosType = Level.PositionType.MainPath;
SpawnPosType = Level.PositionType.MainPath;
}
//backwards compatibility
if (prefab.ConfigElement.GetAttributeBool("spawndeep", false))
{
spawnPosType = Level.PositionType.Abyss;
SpawnPosType = Level.PositionType.Abyss;
}
spawnPointTag = prefab.ConfigElement.GetAttributeString("spawnpointtag", string.Empty);
@@ -143,7 +143,7 @@ namespace Barotrauma
private List<Level.InterestingPosition> GetAvailableSpawnPositions()
{
var availablePositions = Level.Loaded.PositionsOfInterest.FindAll(p => spawnPosType.HasFlag(p.PositionType));
var availablePositions = Level.Loaded.PositionsOfInterest.FindAll(p => SpawnPosType.HasFlag(p.PositionType));
var removals = new List<Level.InterestingPosition>();
foreach (var position in availablePositions)
{
@@ -188,8 +188,8 @@ namespace Barotrauma
spawnPos = Vector2.Zero;
var availablePositions = GetAvailableSpawnPositions();
var chosenPosition = new Level.InterestingPosition(Point.Zero, Level.PositionType.MainPath, isValid: false);
bool isRuinOrWreck = spawnPosType.HasFlag(Level.PositionType.Ruin) || spawnPosType.HasFlag(Level.PositionType.Wreck);
if (affectSubImmediately && !isRuinOrWreck && !spawnPosType.HasFlag(Level.PositionType.Abyss))
bool isRuinOrWreck = SpawnPosType.HasFlag(Level.PositionType.Ruin) || SpawnPosType.HasFlag(Level.PositionType.Wreck);
if (affectSubImmediately && !isRuinOrWreck && !SpawnPosType.HasFlag(Level.PositionType.Abyss))
{
if (availablePositions.None())
{
@@ -288,11 +288,14 @@ namespace Barotrauma
spawnPos = chosenPosition.Position.ToVector2();
if (chosenPosition.Submarine != null || chosenPosition.Ruin != null)
{
var spawnPoint =
WayPoint.GetRandom(SpawnType.Enemy, sub: chosenPosition.Submarine ?? chosenPosition.Ruin?.Submarine, useSyncedRand: false, spawnPointTag: spawnPointTag);
bool ignoreSubmarine = chosenPosition.Ruin != null;
var spawnPoint = WayPoint.GetRandom(SpawnType.Enemy, sub: chosenPosition.Submarine, useSyncedRand: false, spawnPointTag: spawnPointTag, ignoreSubmarine: ignoreSubmarine);
if (spawnPoint != null)
{
System.Diagnostics.Debug.Assert(spawnPoint.Submarine == (chosenPosition.Submarine ?? chosenPosition.Ruin?.Submarine));
if (!ignoreSubmarine)
{
System.Diagnostics.Debug.Assert(spawnPoint.Submarine == chosenPosition.Submarine);
}
spawnPos = spawnPoint.WorldPosition;
}
else
@@ -303,32 +306,42 @@ namespace Barotrauma
return;
}
}
else if ((chosenPosition.PositionType == Level.PositionType.MainPath || chosenPosition.PositionType == Level.PositionType.SidePath)
&& offset > 0)
else if (chosenPosition.PositionType == Level.PositionType.MainPath || chosenPosition.PositionType == Level.PositionType.SidePath)
{
Vector2 dir;
var waypoints = WayPoint.WayPointList.FindAll(wp => wp.Submarine == null);
var nearestWaypoint = waypoints.OrderBy(wp => Vector2.DistanceSquared(wp.WorldPosition, spawnPos.Value)).FirstOrDefault();
if (nearestWaypoint != null)
if (offset > 0)
{
int currentIndex = waypoints.IndexOf(nearestWaypoint);
var nextWaypoint = waypoints[Math.Min(currentIndex + 20, waypoints.Count - 1)];
dir = Vector2.Normalize(nextWaypoint.WorldPosition - nearestWaypoint.WorldPosition);
// Ensure that the spawn position is not offset to the left.
if (dir.X < 0)
Vector2 dir;
var waypoints = WayPoint.WayPointList.FindAll(wp => wp.Submarine == null && wp.Ruin == null);
var nearestWaypoint = waypoints.OrderBy(wp => Vector2.DistanceSquared(wp.WorldPosition, spawnPos.Value)).FirstOrDefault();
if (nearestWaypoint != null)
{
dir.X = 0;
int currentIndex = waypoints.IndexOf(nearestWaypoint);
var nextWaypoint = waypoints[Math.Min(currentIndex + 20, waypoints.Count - 1)];
dir = Vector2.Normalize(nextWaypoint.WorldPosition - nearestWaypoint.WorldPosition);
// Ensure that the spawn position is not offset to the left.
if (dir.X < 0)
{
dir.X = 0;
}
}
else
{
dir = new Vector2(1, Rand.Range(-1, 1));
}
Vector2 targetPos = spawnPos.Value + dir * offset;
var targetWaypoint = waypoints.OrderBy(wp => Vector2.DistanceSquared(wp.WorldPosition, targetPos)).FirstOrDefault();
if (targetWaypoint != null)
{
spawnPos = targetWaypoint.WorldPosition;
}
}
else
// Ensure that the position is not inside a submarine (in practice wrecks).
if (Submarine.Loaded.Any(s => ToolBox.GetWorldBounds(s.Borders.Center, s.Borders.Size).ContainsWorld(spawnPos.Value)))
{
dir = new Vector2(1, Rand.Range(-1, 1));
}
Vector2 targetPos = spawnPos.Value + dir * offset;
var targetWaypoint = waypoints.OrderBy(wp => Vector2.DistanceSquared(wp.WorldPosition, targetPos)).FirstOrDefault();
if (targetWaypoint != null)
{
spawnPos = targetWaypoint.WorldPosition;
//no suitable position found, disable the event
spawnPos = null;
Finished();
return;
}
}
spawnPending = true;
@@ -371,7 +384,7 @@ namespace Barotrauma
if (spawnPending)
{
//wait until there are no submarines at the spawnpos
if (spawnPosType.HasFlag(Level.PositionType.MainPath) || spawnPosType.HasFlag(Level.PositionType.SidePath) || spawnPosType.HasFlag(Level.PositionType.Abyss))
if (SpawnPosType.HasFlag(Level.PositionType.MainPath) || SpawnPosType.HasFlag(Level.PositionType.SidePath) || SpawnPosType.HasFlag(Level.PositionType.Abyss))
{
foreach (Submarine submarine in Submarine.Loaded)
{
@@ -380,17 +393,29 @@ namespace Barotrauma
if (Vector2.DistanceSquared(submarine.WorldPosition, spawnPos.Value) < minDist * minDist) { return; }
}
}
//if spawning in a ruin/cave, wait for someone to be close to it to spawning
//unnecessary monsters in places the players might never visit during the round
if (spawnPosType.HasFlag(Level.PositionType.Ruin) || spawnPosType.HasFlag(Level.PositionType.Cave) || spawnPosType.HasFlag(Level.PositionType.Wreck))
float minDistance = Prefab.SpawnDistance;
if (minDistance <= 0)
{
if (SpawnPosType.HasFlag(Level.PositionType.Cave))
{
minDistance = 8000;
}
else if (SpawnPosType.HasFlag(Level.PositionType.Ruin))
{
minDistance = 5000;
}
else if (SpawnPosType.HasFlag(Level.PositionType.Wreck))
{
minDistance = 3000;
}
}
if (minDistance > 0)
{
bool someoneNearby = false;
float minDist = Sonar.DefaultSonarRange * 0.8f;
foreach (Submarine submarine in Submarine.Loaded)
{
if (submarine.Info.Type != SubmarineType.Player) { continue; }
if (Vector2.DistanceSquared(submarine.WorldPosition, spawnPos.Value) < minDist * minDist)
if (Vector2.DistanceSquared(submarine.WorldPosition, spawnPos.Value) < MathUtils.Pow2(minDistance))
{
someoneNearby = true;
break;
@@ -400,7 +425,7 @@ namespace Barotrauma
{
if (c == Character.Controlled || c.IsRemotePlayer)
{
if (Vector2.DistanceSquared(c.WorldPosition, spawnPos.Value) < minDist * minDist)
if (Vector2.DistanceSquared(c.WorldPosition, spawnPos.Value) < MathUtils.Pow2(minDistance))
{
someoneNearby = true;
break;
@@ -411,7 +436,7 @@ namespace Barotrauma
}
if (spawnPosType.HasFlag(Level.PositionType.Abyss) || spawnPosType.HasFlag(Level.PositionType.AbyssCave))
if (SpawnPosType.HasFlag(Level.PositionType.Abyss) || SpawnPosType.HasFlag(Level.PositionType.AbyssCave))
{
bool anyInAbyss = false;
foreach (Submarine submarine in Submarine.Loaded)
@@ -432,7 +457,7 @@ namespace Barotrauma
int amount = Rand.Range(minAmount, maxAmount + 1);
monsters = new List<Character>();
float scatterAmount = scatter;
if (spawnPosType.HasFlag(Level.PositionType.SidePath))
if (SpawnPosType.HasFlag(Level.PositionType.SidePath))
{
var sidePaths = Level.Loaded.Tunnels.Where(t => t.Type == Level.TunnelType.SidePath);
if (sidePaths.Any())
@@ -444,7 +469,7 @@ namespace Barotrauma
scatterAmount = scatter;
}
}
else if (!spawnPosType.HasFlag(Level.PositionType.MainPath))
else if (!SpawnPosType.HasFlag(Level.PositionType.MainPath))
{
scatterAmount = 0;
}
@@ -474,6 +499,27 @@ namespace Barotrauma
}
Character createdCharacter = Character.Create(speciesName, pos, seed, characterInfo: null, isRemotePlayer: false, hasAi: true, createNetworkEvent: true);
var eventManager = GameMain.GameSession.EventManager;
if (eventManager != null)
{
if (SpawnPosType.HasFlag(Level.PositionType.MainPath) || SpawnPosType.HasFlag(Level.PositionType.SidePath))
{
eventManager.CumulativeMonsterStrengthMain += createdCharacter.Params.AI.CombatStrength;
eventManager.AddTimeStamp(this);
}
else if (SpawnPosType.HasFlag(Level.PositionType.Ruin))
{
eventManager.CumulativeMonsterStrengthRuins += createdCharacter.Params.AI.CombatStrength;
}
else if (SpawnPosType.HasFlag(Level.PositionType.Wreck))
{
eventManager.CumulativeMonsterStrengthWrecks += createdCharacter.Params.AI.CombatStrength;
}
else if (SpawnPosType.HasFlag(Level.PositionType.Cave))
{
eventManager.CumulativeMonsterStrengthCaves += createdCharacter.Params.AI.CombatStrength;
}
}
if (GameMain.GameSession.IsCurrentLocationRadiated())
{
AfflictionPrefab radiationPrefab = AfflictionPrefab.RadiationSickness;
@@ -490,6 +536,7 @@ namespace Barotrauma
//this will do nothing if the monsters have no swarm behavior defined,
//otherwise it'll make the spawned characters act as a swarm
SwarmBehavior.CreateSwarm(monsters.Cast<AICharacter>());
DebugConsole.NewMessage($"Spawned: {ToString()}. Strength: {StringFormatter.FormatZeroDecimal(monsters.Sum(m => m.Params.AI.CombatStrength))}.", Color.LightBlue, debugOnly: true);
}
}, Rand.Range(0f, amount / 2f));
}
@@ -187,5 +187,14 @@ namespace Barotrauma.Extensions
}
return retVal;
}
public static int FindIndex<T>(this IReadOnlyList<T> list, Predicate<T> predicate)
{
for (int i=0; i<list.Count; i++)
{
if (predicate(list[i])) { return i; }
}
return -1;
}
}
}
@@ -56,7 +56,7 @@ namespace Barotrauma
public static string Format(this float value, int decimalCount)
{
return value.ToString($"F{decimalCount.ToString()}", CultureInfo.InvariantCulture);
return value.ToString($"F{decimalCount}", CultureInfo.InvariantCulture);
}
public static string FormatSingleDecimal(this Vector2 value)
@@ -0,0 +1,230 @@
using System;
using Barotrauma.Steam;
using RestSharp;
using System.Net;
using System.Threading.Tasks;
namespace Barotrauma
{
public static partial class GameAnalyticsManager
{
public enum Consent
{
/// <summary>
/// No attempt to contact the consent server has been made
/// </summary>
Unknown,
/// <summary>
/// An error occurred while attempting to retrieve consent status
/// </summary>
Error,
/// <summary>
/// The consent status was not saved on the remote database
/// </summary>
Ask,
/// <summary>
/// The user explicitly denied consent
/// </summary>
No,
/// <summary>
/// The user explicitly granted consent
/// </summary>
Yes
}
public static Consent UserConsented { get; private set; } = Consent.Unknown;
public static bool SendUserStatistics => UserConsented == Consent.Yes && loadedImplementation != null;
private static bool consentTextAvailable
=> TextManager.ContainsTag("statisticsconsentheader")
&& TextManager.ContainsTag("statisticsconsenttext");
private readonly static string consentServerUrl = "https://barotraumagame.com/baromaster/";
private readonly static string consentServerFile = "consentserver.php";
private static string GetAuthTicket()
{
Steamworks.AuthTicket authTicket = SteamManager.GetAuthSessionTicket();
//convert byte array to hex
return BitConverter.ToString(authTicket.Data).Replace("-", "");
}
/// <summary>
/// Sets the consent status. This method cannot be called to
/// set the status to Consent.Yes; only a positive response from
/// the database or the user accepting via the privacy policy
/// prompt should enable it.
/// </summary>
public static void SetConsent(Consent consent)
{
if (consent == Consent.Yes)
{
throw new Exception(
"Cannot call SetConsent with value Consent.Yes, must only be set to this value via consent prompt");
}
SetConsentInternal(consent);
}
/// <summary>
/// Implementation of the bulk of SetConsent.
/// DO NOT CALL THIS UNLESS NEEDED.
/// </summary>
private static void SetConsentInternal(Consent consent)
{
if (UserConsented == consent) { return; }
if (consent == Consent.Ask)
{
CreateConsentPrompt();
}
if (consent != Consent.No && consent != Consent.Yes)
{
UserConsented = consent;
ShutDown();
return;
}
if (consent == Consent.No)
{
UserConsented = consent;
ShutDown();
}
string authTicketStr;
try
{
authTicketStr = GetAuthTicket();
}
catch (Exception e)
{
DebugConsole.ThrowError("Error in GameAnalyticsManager.SetConsent. Could not get a Steam authentication ticket.", e);
return;
}
RestClient client = null;
try
{
client = new RestClient(consentServerUrl);
}
catch (Exception e)
{
DebugConsole.ThrowError("Error while connecting to consent server", e);
}
if (client == null) { return; }
var request = new RestRequest(consentServerFile, Method.GET);
request.AddParameter("authticket", authTicketStr);
request.AddParameter("action", "setconsent");
request.AddParameter("consent", consent == Consent.Yes ? 1 : 0);
var response = client.Execute(request, Method.GET);
if (CheckResponse(response))
{
UserConsented = consent;
if (consent == Consent.Yes)
{
Init();
}
}
}
static partial void CreateConsentPrompt();
public static void InitIfConsented()
{
if (!consentTextAvailable)
{
SetConsent(Consent.Unknown);
return;
}
static void error(string reason, Exception exception)
{
DebugConsole.ThrowError($"Error in GameAnalyticsManager.GetConsent: {reason}", exception);
SetConsent(Consent.Error);
}
string authTicketStr;
try
{
authTicketStr = GetAuthTicket();
}
catch (Exception e)
{
error("Could not get a Steam authentication ticket.", e);
return;
}
RestClient client;
try
{
client = new RestClient(consentServerUrl);
}
catch (Exception e)
{
error("Error while connecting to consent server.", e);
return;
}
var request = new RestRequest(consentServerFile, Method.GET);
request.AddParameter("authticket", authTicketStr);
request.AddParameter("action", "getconsent");
TaskPool.Add($"{nameof(GameAnalyticsManager)}.{nameof(InitIfConsented)}", client.ExecuteAsync(request), (t) =>
{
if (t.Exception != null)
{
error("Error executing the request to the consent server.", t.Exception.InnerException);
return;
}
var response = ((Task<IRestResponse>)t).Result;
if (!CheckResponse(response))
{
SetConsent(Consent.Error);
}
else if (string.IsNullOrEmpty(response.Content))
{
SetConsent(Consent.Ask);
}
else
{
SetConsentInternal(response.Content[0] == '1'
? Consent.Yes
: Consent.No);
}
});
}
private static bool CheckResponse(IRestResponse response)
{
if (response.ErrorException != null)
{
DebugConsole.ThrowError(TextManager.GetWithVariable("MasterServerErrorException", "[error]", response.ErrorException.ToString()));
return false;
}
else if (response.StatusCode != HttpStatusCode.OK)
{
switch (response.StatusCode)
{
case HttpStatusCode.NotFound:
DebugConsole.ThrowError(TextManager.GetWithVariable("MasterServerError404", "[masterserverurl]", consentServerUrl));
break;
case HttpStatusCode.ServiceUnavailable:
DebugConsole.ThrowError(TextManager.Get("MasterServerErrorUnavailable"));
break;
default:
DebugConsole.ThrowError(TextManager.GetWithVariables("MasterServerErrorDefault", new string[2] { "[statuscode]", "[statusdescription]" },
new string[2] { response.StatusCode.ToString(), response.StatusDescription }));
break;
}
}
return response.StatusCode == HttpStatusCode.OK;
}
}
}
@@ -0,0 +1,390 @@
#nullable enable
using Barotrauma.IO;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using System.Text;
namespace Barotrauma
{
public static partial class GameAnalyticsManager
{
public enum ErrorSeverity
{
Undefined = 0,
Debug = 1,
Info = 2,
Warning = 3,
Error = 4,
Critical = 5
}
public enum ProgressionStatus
{
Undefined = 0,
Start = 1,
Complete = 2,
Fail = 3
}
private readonly static HashSet<string> sentEventIdentifiers = new HashSet<string>();
private class Implementation : IDisposable
{
#region GameAnalytics methods
private readonly Action<string, string> initialize;
internal void Initialize(string gameKey, string secretKey)
=> initialize(gameKey, secretKey);
private readonly Action<string> configureBuild;
internal void ConfigureBuild(string config) => configureBuild(config);
private readonly Action<ErrorSeverity, string> addErrorEvent;
internal void AddErrorEvent(ErrorSeverity severity, string message)
=> addErrorEvent(severity, message);
private readonly Action<string, IDictionary<string, object>?> addDesignEvent0;
internal void AddDesignEvent(string message, IDictionary<string, object>? fields = null)
=> addDesignEvent0(message, fields);
private readonly Action<string, double> addDesignEvent1;
internal void AddDesignEvent(string message, double value)
=> addDesignEvent1(message, value);
private readonly Action<ProgressionStatus, string> addProgressionEvent01;
internal void AddProgressionEvent(ProgressionStatus status, string progression01)
=> addProgressionEvent01(status, progression01);
private readonly Action<ProgressionStatus, string, double> addProgressionEvent01Score;
internal void AddProgressionEvent(ProgressionStatus status, string progression01, double score)
=> addProgressionEvent01Score(status, progression01, score);
private readonly Action<ProgressionStatus, string, string> addProgressionEvent02;
internal void AddProgressionEvent(ProgressionStatus status, string progression01, string progression02)
=> addProgressionEvent02(status, progression01, progression02);
private readonly Action<ProgressionStatus, string, string, string> addProgressionEvent03;
internal void AddProgressionEvent(ProgressionStatus status, string progression01, string progression02, string progression03)
=> addProgressionEvent03(status, progression01, progression02, progression03);
private readonly Action<string> setCustomDimension01;
internal void SetCustomDimension01(string dimension01)
=> setCustomDimension01(dimension01);
private readonly Action<string[]> configureAvailableCustomDimensions01;
internal void ConfigureAvailableCustomDimensions01(params string[] customDimensions)
=> configureAvailableCustomDimensions01(customDimensions);
private readonly Action<bool> setEnabledInfoLog;
internal void SetEnabledInfoLog(bool enabled)
=> setEnabledInfoLog(enabled);
#endregion
#region Data required to fetch methods via reflection
private const string AssemblyName = "GameAnalytics.NetStandard";
private const string Namespace = "GameAnalyticsSDK.Net";
private const string MainClass = "GameAnalytics";
private const string EnumPrefix = "EGA";
#endregion
#region Call implementations
private readonly object?[] args1 = new object?[1];
private readonly object?[] args2 = new object?[2];
private readonly object?[] args3 = new object?[3];
private readonly object?[] args4 = new object?[4];
private Action Call(MethodInfo methodInfo)
=> () => methodInfo?.Invoke(null, null);
private Action<T> Call<T>(MethodInfo methodInfo)
=> (T arg1) =>
{
args1[0] = arg1;
methodInfo.Invoke(null, args1);
};
private Action<T1, T2> Call<T1, T2>(MethodInfo methodInfo)
=> (T1 arg1, T2 arg2) =>
{
args2[0] = arg1;
args2[1] = arg2;
methodInfo.Invoke(null, args2);
};
private Action<T1, T2, T3> Call<T1, T2, T3>(MethodInfo methodInfo)
=> (T1 arg1, T2 arg2, T3 arg3) =>
{
args3[0] = arg1;
args3[1] = arg2;
args3[2] = arg3;
methodInfo.Invoke(null, args3);
};
private Action<T1, T2, T3, T4> Call<T1, T2, T3, T4>(MethodInfo methodInfo)
=> (T1 arg1, T2 arg2, T3 arg3, T4 arg4) =>
{
args4[0] = arg1;
args4[1] = arg2;
args4[2] = arg3;
args4[3] = arg4;
methodInfo.Invoke(null, args4);
};
#endregion
private AssemblyLoadContext? loadContext;
private Assembly? assembly;
private string GetAssemblyPath(string assemblyName)
=> Path.Combine(
Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location),
$"{assemblyName}.dll");
private bool resolvingDependency;
private Assembly? ResolveDependency(AssemblyLoadContext context, AssemblyName dependencyName)
{
if (resolvingDependency) { return null; }
resolvingDependency = true;
Assembly dep = context.LoadFromAssemblyPath(GetAssemblyPath(dependencyName.Name ?? throw new Exception("Dependency name was null")));
resolvingDependency = false;
return dep;
}
internal Implementation()
{
loadContext = new AssemblyLoadContext(AssemblyName, isCollectible: true);
loadContext.Resolving += ResolveDependency;
assembly = loadContext.LoadFromAssemblyPath(
GetAssemblyPath(AssemblyName));
Type getType(string name)
=> assembly.GetType($"{Namespace}.{name}")
?? throw new Exception($"Could not find type\"{Namespace}.{name}\"");
var mainClass = getType(MainClass);
var errorSeverityEnumType = getType($"{EnumPrefix}{nameof(ErrorSeverity)}");
var progressionStatusEnumType = getType($"{EnumPrefix}{nameof(ProgressionStatus)}");
MethodInfo getMethod(string name, Type[] types)
{
return mainClass?.GetMethod(name, BindingFlags.Public | BindingFlags.Static, binder: null, types: types, modifiers: null)
?? throw new Exception($"Could not find method \"{name}\" with types {string.Join(',', types.Select(t => t.Name))}");
}
initialize = Call<string, string>(getMethod(nameof(Initialize),
new Type[] { typeof(string), typeof(string) }));
configureBuild = Call<string>(getMethod(nameof(ConfigureBuild),
new Type[] { typeof(string) }));
addErrorEvent = Call<ErrorSeverity, string>(getMethod(nameof(AddErrorEvent),
new Type[] { errorSeverityEnumType, typeof(string) }));
addDesignEvent0 = Call<string, IDictionary<string, object>?>(getMethod(nameof(AddDesignEvent),
new Type[] { typeof(string), typeof(IDictionary<string, object>) }));
addDesignEvent1 = Call<string, double>(getMethod(nameof(AddDesignEvent),
new Type[] { typeof(string), typeof(double) }));
addProgressionEvent01 = Call<ProgressionStatus, string>(getMethod(nameof(AddProgressionEvent),
new Type[] { progressionStatusEnumType, typeof(string) }));
addProgressionEvent01Score = Call<ProgressionStatus, string, double>(getMethod(nameof(AddProgressionEvent),
new Type[] { progressionStatusEnumType, typeof(string), typeof(double) }));
addProgressionEvent02 = Call<ProgressionStatus, string, string>(getMethod(nameof(AddProgressionEvent),
new Type[] { progressionStatusEnumType, typeof(string), typeof(string) }));
addProgressionEvent03 = Call<ProgressionStatus, string, string, string>(getMethod(nameof(AddProgressionEvent),
new Type[] { progressionStatusEnumType, typeof(string), typeof(string), typeof(string) }));
setCustomDimension01 = Call<string>(getMethod(nameof(SetCustomDimension01),
new Type[] { typeof(string) }));
configureAvailableCustomDimensions01 = Call<string[]>(getMethod(nameof(ConfigureAvailableCustomDimensions01),
new Type[] { typeof(string[]) }));
setEnabledInfoLog = Call<bool>(getMethod(nameof(SetEnabledInfoLog),
new Type[] { typeof(bool) }));
onQuit = Call(getMethod("OnQuit", Array.Empty<Type>()));
}
private readonly Action? onQuit;
private void OnQuit()
{
if (assembly != null) { onQuit?.Invoke(); }
}
public void Dispose()
{
if (loadContext is null) { return; }
OnQuit();
loadContext?.Unload();
loadContext = null;
assembly = null;
}
~Implementation()
{
OnQuit();
}
}
private static Implementation? loadedImplementation;
public static void AddErrorEvent(ErrorSeverity errorSeverity, string message)
{
if (!SendUserStatistics) { return; }
loadedImplementation?.AddErrorEvent(errorSeverity, message);
}
/// <summary>
/// Adds an error event to GameAnalytics if an event with the same identifier has not been added yet.
/// </summary>
public static void AddErrorEventOnce(string identifier, ErrorSeverity errorSeverity, string message)
{
if (!SendUserStatistics) { return; }
if (sentEventIdentifiers.Contains(identifier)) { return; }
if (GameMain.Config.AllEnabledPackages != null)
{
if (GameMain.VanillaContent == null || GameMain.Config.AllEnabledPackages.Any(p => p.HasMultiplayerIncompatibleContent && p != GameMain.VanillaContent))
{
message = "[MODDED] " + message;
}
}
loadedImplementation?.AddErrorEvent(errorSeverity, message);
sentEventIdentifiers.Add(identifier);
}
public static void AddDesignEvent(string eventID)
{
if (!SendUserStatistics) { return; }
loadedImplementation?.AddDesignEvent(eventID);
}
public static void AddDesignEvent(string eventID, double value)
{
if (!SendUserStatistics) { return; }
loadedImplementation?.AddDesignEvent(eventID, value);
}
public static void AddProgressionEvent(ProgressionStatus progressionStatus, string progression01)
{
if (!SendUserStatistics) { return; }
loadedImplementation?.AddProgressionEvent(progressionStatus, progression01);
}
public static void AddProgressionEvent(ProgressionStatus progressionStatus, string progression01, double score)
{
if (!SendUserStatistics) { return; }
loadedImplementation?.AddProgressionEvent(progressionStatus, progression01, score);
}
public static void AddProgressionEvent(ProgressionStatus progressionStatus, string progression01, string progression02)
{
if (!SendUserStatistics) { return; }
loadedImplementation?.AddProgressionEvent(progressionStatus, progression01, progression02);
}
public static void AddProgressionEvent(ProgressionStatus progressionStatus, string progression01, string progression02, string progression03)
{
if (!SendUserStatistics) { return; }
loadedImplementation?.AddProgressionEvent(progressionStatus, progression01, progression02, progression03);
}
public static void SetCustomDimension01(string dimension)
{
if (!SendUserStatistics) { return; }
loadedImplementation?.SetCustomDimension01(dimension);
}
private static void Init()
{
ShutDown();
try
{
loadedImplementation = new Implementation();
}
catch (Exception e)
{
DebugConsole.ThrowError("Initializing GameAnalytics failed. Disabling user statistics...", e);
SetConsent(Consent.Error);
return;
}
#if DEBUG
try
{
loadedImplementation?.SetEnabledInfoLog(true);
}
catch (Exception e)
{
DebugConsole.ThrowError("Initializing GameAnalytics failed. Disabling user statistics...", e);
SetConsent(Consent.Error);
return;
}
#endif
string exePath = Assembly.GetEntryAssembly()!.Location;
string? exeName = string.Empty;
#if SERVER
exeName = "s";
#endif
Md5Hash? exeHash = null;
try
{
using (var stream = File.OpenRead(exePath))
{
exeHash = new Md5Hash(stream);
}
}
catch (Exception e)
{
DebugConsole.ThrowError("Error while calculating MD5 hash for the executable \"" + exePath + "\"", e);
}
try
{
string buildConfiguration = "Release";
#if DEBUG
buildConfiguration = "Debug";
#elif UNSTABLE
buildConfiguration = "Unstable";
#endif
loadedImplementation?.ConfigureBuild(GameMain.Version.ToString()
+ exeName + ":"
+ AssemblyInfo.GitRevision + ":"
+ buildConfiguration);
loadedImplementation?.ConfigureAvailableCustomDimensions01("singleplayer", "multiplayer", "editor");
InitKeys();
loadedImplementation?.AddDesignEvent("Executable:"
+ GameMain.Version.ToString()
+ exeName + ":"
+ ((exeHash?.ShortHash == null) ? "Unknown" : exeHash.ShortHash) + ":"
+ AssemblyInfo.GitBranch + ":"
+ AssemblyInfo.GitRevision + ":"
+ buildConfiguration);
}
catch (Exception e)
{
DebugConsole.ThrowError("Initializing GameAnalytics failed. Disabling user statistics...", e);
SetConsent(Consent.Error);
return;
}
var allPackages = GameMain.Config?.AllEnabledPackages.ToList();
if (allPackages?.Count > 0)
{
StringBuilder sb = new StringBuilder("ContentPackage: ");
int i = 0;
foreach (ContentPackage cp in allPackages)
{
string trimmedName = cp.Name.Replace(":", "").Replace(" ", "");
sb.Append(trimmedName.Substring(0, Math.Min(32, trimmedName.Length)));
if (i < allPackages.Count - 1) { sb.Append(" "); }
}
loadedImplementation?.AddDesignEvent(sb.ToString());
}
}
static partial void InitKeys();
public static void ShutDown()
{
loadedImplementation?.Dispose();
loadedImplementation = null;
}
}
}
@@ -1,132 +0,0 @@
using GameAnalyticsSDK.Net;
using System;
using System.Text;
using System.Collections.Generic;
using Barotrauma.IO;
using System.Linq;
using System.Reflection;
using System.Security.Cryptography;
namespace Barotrauma
{
public static class GameAnalyticsManager
{
private static HashSet<string> sentEventIdentifiers = new HashSet<string>();
public static void Init()
{
#if DEBUG
try
{
GameAnalytics.SetEnabledInfoLog(true);
}
catch (Exception e)
{
DebugConsole.ThrowError("Initializing GameAnalytics failed. Disabling user statistics...", e);
GameSettings.SendUserStatistics = false;
return;
}
#endif
string exePath = Assembly.GetEntryAssembly().Location;
string exeName = null;
Md5Hash exeHash = null;
exeName = Path.GetFileNameWithoutExtension(exePath).Replace(":", "");
var md5 = MD5.Create();
try
{
using (var stream = File.OpenRead(exePath))
{
exeHash = new Md5Hash(stream);
}
}
catch (Exception e)
{
DebugConsole.ThrowError("Error while calculating MD5 hash for the executable \"" + exePath + "\"", e);
}
try
{
GameAnalytics.ConfigureBuild(GameMain.Version.ToString()
+ (string.IsNullOrEmpty(exeName) ? "Unknown" : exeName) + ":"
+ ((exeHash?.ShortHash == null) ? "Unknown" : exeHash.ShortHash));
GameAnalytics.ConfigureAvailableCustomDimensions01("singleplayer", "multiplayer", "editor");
GameAnalytics.Initialize("a3a073c20982de7c15d21e840e149122", "9010ad9a671233b8d9610d76cec8c897d9ff3ba7");
GameAnalytics.AddDesignEvent("Executable:"
+ (string.IsNullOrEmpty(exeName) ? "Unknown" : exeName) + ":"
+ ((exeHash?.ShortHash == null) ? "Unknown" : exeHash.ShortHash));
}
catch (Exception e)
{
DebugConsole.ThrowError("Initializing GameAnalytics failed. Disabling user statistics...", e);
GameSettings.SendUserStatistics = false;
return;
}
var allPackages = GameMain.Config?.AllEnabledPackages.ToList();
if (allPackages?.Count > 0)
{
StringBuilder sb = new StringBuilder("ContentPackage: ");
int i = 0;
foreach (ContentPackage cp in allPackages)
{
string trimmedName = cp.Name.Replace(":", "").Replace(" ", "");
sb.Append(trimmedName.Substring(0, Math.Min(32, trimmedName.Length)));
if (i < allPackages.Count - 1) { sb.Append(" "); }
}
GameAnalytics.AddDesignEvent(sb.ToString());
}
}
/// <summary>
/// Adds an error event to GameAnalytics if an event with the same identifier has not been added yet.
/// </summary>
public static void AddErrorEventOnce(string identifier, EGAErrorSeverity errorSeverity, string message)
{
if (!GameSettings.SendUserStatistics) { return; }
if (sentEventIdentifiers.Contains(identifier)) { return; }
if (GameMain.Config.AllEnabledPackages != null)
{
if (GameMain.VanillaContent == null || GameMain.Config.AllEnabledPackages.Any(p => p.HasMultiplayerIncompatibleContent && p != GameMain.VanillaContent))
{
message = "[MODDED] " + message;
}
}
GameAnalytics.AddErrorEvent(errorSeverity, message);
sentEventIdentifiers.Add(identifier);
}
public static void AddDesignEvent(string eventID)
{
if (!GameSettings.SendUserStatistics) return;
GameAnalytics.AddDesignEvent(eventID);
}
public static void AddDesignEvent(string eventID, double value)
{
if (!GameSettings.SendUserStatistics) return;
GameAnalytics.AddDesignEvent(eventID, value);
}
public static void AddProgressionEvent(EGAProgressionStatus progressionStatus, string progression01)
{
if (!GameSettings.SendUserStatistics) return;
GameAnalytics.AddProgressionEvent(progressionStatus, progression01);
}
public static void AddProgressionEvent(EGAProgressionStatus progressionStatus, string progression01, string progression02)
{
if (!GameSettings.SendUserStatistics) return;
GameAnalytics.AddProgressionEvent(progressionStatus, progression01, progression02);
}
public static void SetCustomDimension01(string dimension)
{
if (!GameSettings.SendUserStatistics) return;
GameAnalytics.SetCustomDimension01(dimension);
}
}
}
@@ -168,9 +168,9 @@ namespace Barotrauma
{
if (itemPrefab == null)
{
string errorMsg = "Error in AutoItemPlacer.SpawnItems - itemPrefab was null.\n"+Environment.StackTrace.CleanupStackTrace();
string errorMsg = "Error in AutoItemPlacer.SpawnItems - itemPrefab was null.\n" + Environment.StackTrace.CleanupStackTrace();
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("AutoItemPlacer.SpawnItems:ItemNull", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
GameAnalyticsManager.AddErrorEventOnce("AutoItemPlacer.SpawnItems:ItemNull", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
return false;
}
bool success = false;
@@ -254,7 +254,7 @@ namespace Barotrauma
if (!validContainer.Key.Inventory.CanBePut(itemPrefab, quality: quality)) { break; }
var item = new Item(itemPrefab, validContainer.Key.Item.Position, validContainer.Key.Item.Submarine)
{
SpawnedInOutpost = validContainer.Key.Item.SpawnedInOutpost,
SpawnedInCurrentOutpost = validContainer.Key.Item.SpawnedInCurrentOutpost,
AllowStealing = validContainer.Key.Item.AllowStealing,
Quality = quality,
OriginalModuleIndex = validContainer.Key.Item.OriginalModuleIndex,
@@ -246,7 +246,8 @@ namespace Barotrauma
continue;
}
Item containerItem = new Item(containerPrefab, position, wp.Submarine);
Vector2 containerPosition = GetCargoPos(cargoRoom, containerPrefab);
Item containerItem = new Item(containerPrefab, containerPosition, wp.Submarine);
itemContainer = containerItem.GetComponent<ItemContainer>();
if (itemContainer == null)
{
@@ -11,10 +11,10 @@ namespace Barotrauma
{
internal struct CampaignSettings
{
public static CampaignSettings Empty = new CampaignSettings();
public static CampaignSettings Empty => new CampaignSettings();
// Anything that uses this field I wasn't sure if actually needed the proper campaign settings to be passed down
public static CampaignSettings Unsure = Empty;
public static CampaignSettings Unsure => Empty;
public bool RadiationEnabled { get; set; }
public int TotalMaxMissionCount => MaxMissionCount + GetAddedMissionCount();
@@ -34,7 +34,7 @@ namespace Barotrauma
{
maxMissionCount = DefaultMaxMissionCount;
RadiationEnabled = inc.ReadBoolean();
MaxMissionCount = inc.ReadInt32();
MaxMissionCount = inc.ReadRangedInteger(MinMissionCountLimit, MaxMissionCountLimit);
}
public CampaignSettings(XElement element)
@@ -47,7 +47,7 @@ namespace Barotrauma
public void Serialize(IWriteMessage msg)
{
msg.Write(RadiationEnabled);
msg.Write(MaxMissionCount);
msg.WriteRangedInteger(MaxMissionCount, MinMissionCountLimit, MaxMissionCountLimit);
}
public int GetAddedMissionCount()
@@ -393,7 +393,7 @@ namespace Barotrauma
/// </summary>
protected abstract void LoadInitialLevel();
protected abstract IEnumerable<object> DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror, List<TraitorMissionResult> traitorResults = null);
protected abstract IEnumerable<CoroutineStatus> DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror, List<TraitorMissionResult> traitorResults = null);
/// <summary>
/// Which type of transition between levels is currently possible (if any)
@@ -484,6 +484,10 @@ namespace Barotrauma
/// </summary>
private Submarine GetLeavingSub()
{
if (Level.IsLoadedOutpost)
{
return Submarine.MainSub;
}
//in single player, only the sub the controlled character is inside can transition between levels
//in multiplayer, if there's subs at both ends of the level, only the one with more players inside can transition
//TODO: ignore players who don't have the permission to trigger a transition between levels?
@@ -493,11 +497,6 @@ namespace Barotrauma
Submarine leavingSubAtStart = GetLeavingSubAtStart(leavingPlayers);
Submarine leavingSubAtEnd = GetLeavingSubAtEnd(leavingPlayers);
if (Level.IsLoadedOutpost)
{
leavingSubAtStart ??= Submarine.MainSub;
leavingSubAtEnd ??= Submarine.MainSub;
}
int playersInSubAtStart = leavingSubAtStart == null || !leavingSubAtStart.AtStartExit ? 0 :
leavingPlayers.Count(c => c.Submarine == leavingSubAtStart || leavingSubAtStart.DockedTo.Contains(c.Submarine) || (Level.Loaded.StartOutpost != null && c.Submarine == Level.Loaded.StartOutpost));
int playersInSubAtEnd = leavingSubAtEnd == null || !leavingSubAtEnd.AtEndExit ? 0 :
@@ -572,7 +571,7 @@ namespace Barotrauma
{
foreach (Item item in Item.ItemList)
{
if (!item.SpawnedInOutpost || item.OriginalModuleIndex < 0) { continue; }
if (!item.SpawnedInCurrentOutpost || item.OriginalModuleIndex < 0) { continue; }
var owner = item.GetRootInventoryOwner();
if ((!(owner?.Submarine?.Info?.IsOutpost ?? false)) || (owner is Character character && character.TeamID == CharacterTeamType.Team1) || item.Submarine == null || !item.Submarine.Info.IsOutpost)
{
@@ -686,6 +685,14 @@ namespace Barotrauma
int loops = CampaignMetadata.GetInt("campaign.endings", 0);
CampaignMetadata.SetValue("campaign.endings", loops + 1);
}
GameAnalyticsManager.AddProgressionEvent(
GameAnalyticsManager.ProgressionStatus.Complete,
Name ?? "none");
string eventId = "FinishCampaign:";
GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none"));
GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count() ?? 0));
GameAnalyticsManager.AddDesignEvent(eventId + "Money", Money);
}
protected virtual void EndCampaignProjSpecific() { }
@@ -712,7 +719,7 @@ namespace Barotrauma
}
}
private IEnumerable<object> DoCharacterWait(Character npc, Character interactor)
private IEnumerable<CoroutineStatus> DoCharacterWait(Character npc, Character interactor)
{
if (npc == null || interactor == null) { yield return CoroutineStatus.Failure; }
@@ -907,7 +914,7 @@ namespace Barotrauma
public int NumberOfMissionsAtLocation(Location location)
{
return Map.CurrentLocation.SelectedMissions.Count(m => m.Locations.Contains(location));
return Map?.CurrentLocation?.SelectedMissions?.Count(m => m.Locations.Contains(location)) ?? 0;
}
public void CheckTooManyMissions(Location currentLocation, Client sender)
@@ -2,6 +2,7 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
@@ -119,12 +120,6 @@ namespace Barotrauma
#endif
}
#if SERVER
List<SubmarineInfo> availableSubs = new List<SubmarineInfo>();
List<SubmarineInfo> sourceList = new List<SubmarineInfo>();
sourceList.AddRange(SubmarineInfo.SavedSubmarines);
#endif
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
@@ -165,14 +160,6 @@ namespace Barotrauma
petsElement = subElement;
break;
#if SERVER
case "availablesubs":
foreach (XElement availableSub in subElement.Elements())
{
string subName = availableSub.GetAttributeString("name", "");
SubmarineInfo matchingSub = sourceList.Find(s => s.Name == subName);
if (matchingSub != null) { availableSubs.Add(matchingSub); }
}
break;
case "savedexperiencepoints":
foreach (XElement savedExp in subElement.Elements())
{
@@ -188,14 +175,6 @@ namespace Barotrauma
InitCampaignData();
#if SERVER
// Fallback if using a save with no available subs assigned, use vanilla submarines
if (availableSubs.Count == 0)
{
GameMain.NetLobbyScreen.CampaignSubmarines.AddRange(sourceList.FindAll(s => s.IsCampaignCompatible && s.IsVanillaSubmarine()));
}
GameMain.NetLobbyScreen.CampaignSubmarines = availableSubs;
characterData.Clear();
string characterDataPath = GetCharacterDataSavePath();
if (!File.Exists(characterDataPath))
@@ -283,7 +283,8 @@ namespace Barotrauma
public void PurchaseSubmarine(SubmarineInfo newSubmarine)
{
if (Campaign == null) return;
if (Campaign is null) { return; }
if (Campaign.Money < newSubmarine.Price) { return; }
if (!OwnedSubmarines.Any(s => s.Name == newSubmarine.Name))
{
Campaign.Money -= newSubmarine.Price;
@@ -312,7 +313,7 @@ namespace Barotrauma
return isRadiated;
}
public void StartRound(string levelSeed, float? difficulty = null)
public void StartRound(string levelSeed, float? difficulty = null, LevelGenerationParams levelGenerationParams = null)
{
LevelData randomLevel = null;
foreach (Mission mission in Missions.Union(GameMode.Missions))
@@ -324,11 +325,11 @@ namespace Barotrauma
{
LocationType locationType = LocationType.List.FirstOrDefault(lt => missionPrefab.AllowedLocationTypes.Any(m => m.Equals(lt.Identifier, StringComparison.OrdinalIgnoreCase)));
CreateDummyLocations(locationType);
randomLevel = LevelData.CreateRandom(levelSeed, difficulty, requireOutpost: true);
randomLevel = LevelData.CreateRandom(levelSeed, difficulty, levelGenerationParams, requireOutpost: true);
break;
}
}
randomLevel ??= LevelData.CreateRandom(levelSeed, difficulty);
randomLevel ??= LevelData.CreateRandom(levelSeed, difficulty, levelGenerationParams);
StartRound(randomLevel);
}
@@ -351,6 +352,8 @@ namespace Barotrauma
return;
}
Submarine.LockX = Submarine.LockY = false;
LevelData = levelData;
Submarine.Unload();
@@ -404,6 +407,21 @@ namespace Barotrauma
InitializeLevel(level);
GameAnalyticsManager.AddProgressionEvent(
GameAnalyticsManager.ProgressionStatus.Start,
GameMode?.Name ?? "none");
string eventId = "StartRound:GameMode:" + (GameMode?.Name ?? "none") + ":";
GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none"));
GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Name ?? "none"));
GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count() ?? 0));
foreach (Mission mission in missions)
{
GameAnalyticsManager.AddDesignEvent(eventId + "MissionType:" + (mission.Prefab.Type.ToString() ?? "none") + ":" + mission.Prefab.Identifier);
}
GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + (Level.Loaded?.Type.ToString() ?? "none"));
GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier ?? "none"));
#if CLIENT
if (GameMode is CampaignMode) { SteamAchievementManager.OnBiomeDiscovered(levelData.Biome); }
@@ -511,10 +529,6 @@ namespace Barotrauma
mpCampaign.UpgradeManager.ApplyUpgrades();
mpCampaign.UpgradeManager.SanityCheckUpgrades(Submarine);
}
if (GameMode is CampaignMode)
{
Submarine.WarmStartPower();
}
}
GameMain.Config.RecentlyEncounteredCreatures.Clear();
@@ -676,6 +690,8 @@ namespace Barotrauma
{
IEnumerable<Character> crewCharacters = GetSessionCrewCharacters();
int prevMoney = (GameMode as CampaignMode)?.Money ?? 0;
foreach (Mission mission in missions)
{
mission.End();
@@ -733,6 +749,32 @@ namespace Barotrauma
missions.Clear();
IsRunning = false;
bool success = false;
#if CLIENT
success = CrewManager.GetCharacters().Any(c => !c.IsDead);
#else
success = GameMain.Server.ConnectedClients.Any(c => c.InGame && c.Character != null && !c.Character.IsDead);
#endif
double roundDuration = Timing.TotalTime - RoundStartTime;
GameAnalyticsManager.AddProgressionEvent(
success ? GameAnalyticsManager.ProgressionStatus.Complete : GameAnalyticsManager.ProgressionStatus.Fail,
GameMode?.Name ?? "none",
roundDuration);
string eventId = "EndRound:GameMode:" + (GameMode?.Name ?? "none") + ":";
GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none"), roundDuration);
GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Name ?? "none"), roundDuration);
GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count() ?? 0), roundDuration);
foreach (Mission mission in missions)
{
GameAnalyticsManager.AddDesignEvent(eventId + "MissionType:" + (mission.Prefab.Type.ToString() ?? "none") + ":" + mission.Prefab.Identifier + ":" + (mission.Completed ? "Completed" : "Failed"), roundDuration);
}
GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + (Level.Loaded?.Type.ToString() ?? "none"), roundDuration);
GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier ?? "none"), roundDuration);
if (GameMode is CampaignMode campaignMode)
{
GameAnalyticsManager.AddDesignEvent(eventId + "MoneyEarned", campaignMode.Money - prevMoney);
}
#if CLIENT
HintManager.OnRoundEnded();
#endif
@@ -757,25 +757,6 @@ namespace Barotrauma
public bool CampaignDisclaimerShown, EditorDisclaimerShown;
private static bool sendUserStatistics = true;
public static bool SendUserStatistics
{
get
{
return false;
/*#if DEBUG
return false;
#endif
return sendUserStatistics;*/
}
set
{
sendUserStatistics = value;
GameMain.Config.SaveNewPlayerConfig();
}
}
public static bool ShowUserStatisticsPrompt { get; set; }
public bool ShowLanguageSelectionPrompt { get; set; }
public static bool ShowOffensiveServerPrompt { get; set; }
@@ -858,7 +839,6 @@ namespace Barotrauma
if (!fileFound)
{
ShowLanguageSelectionPrompt = true;
ShowUserStatisticsPrompt = true;
SaveNewPlayerConfig();
}
}
@@ -870,9 +850,8 @@ namespace Barotrauma
private bool LoadPlayerConfigInternal()
{
XDocument doc = XMLExtensions.LoadXml(PlayerSavePath);
if (doc == null || doc.Root == null)
if (doc?.Root == null)
{
ShowUserStatisticsPrompt = true;
ShowTutorialSkipWarning = true;
return false;
}
@@ -989,12 +968,7 @@ namespace Barotrauma
if (!string.IsNullOrEmpty(overrideMultiplayerSaveFolder))
{
doc.Root.Add(new XAttribute("overridemultiplayersavefolder", overrideMultiplayerSaveFolder));
}
if (!ShowUserStatisticsPrompt)
{
doc.Root.Add(new XAttribute("senduserstatistics", sendUserStatistics));
}
}
XElement gMode = doc.Root.Element("graphicsmode");
if (gMode == null)
@@ -1194,7 +1168,7 @@ namespace Barotrauma
catch (Exception e)
{
DebugConsole.ThrowError("Saving game settings failed.", e);
GameAnalyticsManager.AddErrorEventOnce("GameSettings.Save:SaveFailed", GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
GameAnalyticsManager.AddErrorEventOnce("GameSettings.Save:SaveFailed", GameAnalyticsManager.ErrorSeverity.Error,
"Saving game settings failed.\n" + e.Message + "\n" + e.StackTrace.CleanupStackTrace());
return false;
}
@@ -1211,7 +1185,6 @@ namespace Barotrauma
Language = doc.Root.GetAttributeString("language", Language);
}
AutoCheckUpdates = doc.Root.GetAttributeBool("autocheckupdates", AutoCheckUpdates);
sendUserStatistics = doc.Root.GetAttributeBool("senduserstatistics", sendUserStatistics);
QuickStartSubmarineName = doc.Root.GetAttributeString("quickstartsub", QuickStartSubmarineName);
EnableSubmarineAutoSave = doc.Root.GetAttributeBool("submarineautosave", true);
MaximumAutoSaves = doc.Root.GetAttributeInt("maxautosaves", 8);
@@ -430,7 +430,7 @@ namespace Barotrauma
if (index < 0 || index >= slots.Length)
{
string errorMsg = "CharacterInventory.TryPutItem failed: index was out of range(" + index + ").\n" + Environment.StackTrace.CleanupStackTrace();
GameAnalyticsManager.AddErrorEventOnce("CharacterInventory.TryPutItem:IndexOutOfRange", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
GameAnalyticsManager.AddErrorEventOnce("CharacterInventory.TryPutItem:IndexOutOfRange", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
return false;
}
#if CLIENT
@@ -207,8 +207,14 @@ namespace Barotrauma.Items.Components
if (!item.linkedTo.Contains(target.item)) { item.linkedTo.Add(target.item); }
if (!target.item.linkedTo.Contains(item)) { target.item.linkedTo.Add(item); }
if (!target.item.Submarine.DockedTo.Contains(item.Submarine)) target.item.Submarine.ConnectedDockingPorts.Add(item.Submarine, target);
if (!item.Submarine.DockedTo.Contains(target.item.Submarine)) item.Submarine.ConnectedDockingPorts.Add(target.item.Submarine, this);
if (!target.item.Submarine.DockedTo.Contains(item.Submarine))
{
target.item.Submarine.ConnectedDockingPorts.Add(item.Submarine, target);
}
if (!item.Submarine.DockedTo.Contains(target.item.Submarine))
{
item.Submarine.ConnectedDockingPorts.Add(target.item.Submarine, this);
}
DockingTarget = target;
DockingTarget.DockingTarget = this;
@@ -484,7 +490,7 @@ namespace Barotrauma.Items.Components
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce(
"DockingPort.CreateDoorBody:InvalidPosition",
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
GameAnalyticsManager.ErrorSeverity.Error,
errorMsg);
position = Vector2.Zero;
}
@@ -779,29 +785,25 @@ namespace Barotrauma.Items.Components
if (IsHorizontal)
{
if (hulls[0].WorldRect.X < hulls[1].WorldRect.X)
if (hulls[0].WorldRect.X > hulls[1].WorldRect.X)
{
gap.linkedTo.Add(hulls[0]);
gap.linkedTo.Add(hulls[1]);
}
else
{
gap.linkedTo.Add(hulls[1]);
gap.linkedTo.Add(hulls[0]);
var temp = hulls[0];
hulls[0] = hulls[1];
hulls[1] = temp;
}
gap.linkedTo.Add(hulls[0]);
gap.linkedTo.Add(hulls[1]);
}
else
{
if (hulls[0].WorldRect.Y > hulls[1].WorldRect.Y)
if (hulls[0].WorldRect.Y < hulls[1].WorldRect.Y)
{
gap.linkedTo.Add(hulls[0]);
gap.linkedTo.Add(hulls[1]);
}
else
{
gap.linkedTo.Add(hulls[1]);
gap.linkedTo.Add(hulls[0]);
var temp = hulls[0];
hulls[0] = hulls[1];
hulls[1] = temp;
}
gap.linkedTo.Add(hulls[0]);
gap.linkedTo.Add(hulls[1]);
}
for (int i = 0; i < 2; i++)
@@ -813,7 +815,7 @@ namespace Barotrauma.Items.Components
if (IsHorizontal)
{
if (item.WorldPosition.X < DockingTarget.item.WorldPosition.X)
if (doorGap.WorldPosition.X < gap.WorldPosition.X)
{
if (!doorGap.linkedTo.Contains(hulls[0])) { doorGap.linkedTo.Add(hulls[0]); }
}
@@ -831,7 +833,7 @@ namespace Barotrauma.Items.Components
}
else
{
if (item.WorldPosition.Y > DockingTarget.item.WorldPosition.Y)
if (doorGap.WorldPosition.Y > gap.WorldPosition.Y)
{
if (!doorGap.linkedTo.Contains(hulls[0])) { doorGap.linkedTo.Add(hulls[0]); }
}
@@ -873,11 +875,17 @@ namespace Barotrauma.Items.Components
if (myWayPoint != null && targetWayPoint != null)
{
myWayPoint.FindHull();
myWayPoint.linkedTo.Remove(targetWayPoint);
myWayPoint.OnLinksChanged?.Invoke(myWayPoint);
if (myWayPoint.linkedTo.Contains(targetWayPoint))
{
myWayPoint.linkedTo.Remove(targetWayPoint);
myWayPoint.OnLinksChanged?.Invoke(myWayPoint);
}
targetWayPoint.FindHull();
targetWayPoint.linkedTo.Remove(myWayPoint);
targetWayPoint.OnLinksChanged?.Invoke(targetWayPoint);
if (targetWayPoint.linkedTo.Contains(myWayPoint))
{
targetWayPoint.linkedTo.Remove(myWayPoint);
targetWayPoint.OnLinksChanged?.Invoke(targetWayPoint);
}
}
}

Some files were not shown because too many files have changed in this diff Show More