Unstable v0.15.17.0 (Hex is out of town edition)
This commit is contained in:
@@ -1,8 +1,6 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using NLog.Targets;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
@@ -601,7 +598,7 @@ namespace Barotrauma
|
||||
takeMaskOff = false;
|
||||
break;
|
||||
}
|
||||
else if (gotoObjective.mimic)
|
||||
else if (gotoObjective.Mimic)
|
||||
{
|
||||
if (!removeSuit)
|
||||
{
|
||||
@@ -880,7 +877,8 @@ namespace Barotrauma
|
||||
if (target.CurrentHull != hull || !target.Enabled) { continue; }
|
||||
if (AIObjectiveFightIntruders.IsValidTarget(target, Character))
|
||||
{
|
||||
if (AddTargets<AIObjectiveFightIntruders, Character>(Character, target) && newOrder == null)
|
||||
bool arrested = AIObjectiveFightIntruders.ShouldArrest(target, Character) && target.HasEquippedItem("handlocker");
|
||||
if (!arrested && AddTargets<AIObjectiveFightIntruders, Character>(Character, target) && newOrder == null)
|
||||
{
|
||||
var orderPrefab = Order.GetPrefab("reportintruders");
|
||||
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
|
||||
@@ -1033,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
|
||||
@@ -1076,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)
|
||||
@@ -1128,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)
|
||||
{
|
||||
@@ -1197,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)
|
||||
{
|
||||
@@ -1601,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;
|
||||
bool someoneSpoke = false;
|
||||
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;
|
||||
|
||||
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;
|
||||
@@ -1624,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)
|
||||
{
|
||||
@@ -1663,7 +1677,7 @@ namespace Barotrauma
|
||||
}
|
||||
else if (item.OwnInventory?.FindItem(it => it.SpawnedInCurrentOutpost && !item.AllowStealing, true) is { } foundItem)
|
||||
{
|
||||
ItemTaken(foundItem, character);
|
||||
ItemTaken(foundItem, thief);
|
||||
}
|
||||
|
||||
bool TriggerSecurity(HumanAIController humanAI)
|
||||
@@ -1791,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))
|
||||
|
||||
@@ -26,10 +26,9 @@ namespace Barotrauma
|
||||
|
||||
private float findPathTimer;
|
||||
|
||||
private float checkDoorsTimer;
|
||||
private float buttonPressCooldown;
|
||||
|
||||
const float ButtonPressInterval = 0.25f;
|
||||
|
||||
public SteeringPath CurrentPath
|
||||
{
|
||||
get { return currentPath; }
|
||||
@@ -97,9 +96,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;
|
||||
buttonPressCooldown -= step;
|
||||
findPathTimer -= step;
|
||||
}
|
||||
|
||||
public void SetPath(SteeringPath path)
|
||||
@@ -204,24 +204,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 (!useNewPath && currentPath != null && currentPath.CurrentNode != null && newPath.Nodes.Any() && !newPath.Unreachable)
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -315,7 +314,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;
|
||||
@@ -336,7 +336,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (character.CanInteractWith(ladders.Item))
|
||||
{
|
||||
ladders.Item.TryInteract(character, false, true);
|
||||
ladders.Item.TryInteract(character, forceSelectKey: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -347,7 +347,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -387,7 +387,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)
|
||||
@@ -487,7 +487,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);
|
||||
}
|
||||
}
|
||||
@@ -500,8 +507,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++)
|
||||
{
|
||||
@@ -518,8 +539,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;
|
||||
@@ -540,7 +560,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;
|
||||
@@ -566,28 +586,49 @@ 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)
|
||||
// Check that the button is on the right side of the door. If the door is open, doesn't matter
|
||||
if (!door.IsOpen)
|
||||
{
|
||||
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 ?? 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; }
|
||||
}
|
||||
}
|
||||
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;
|
||||
@@ -596,18 +637,39 @@ namespace Barotrauma
|
||||
});
|
||||
if (canAccess)
|
||||
{
|
||||
bool pressButton = buttonPressCooldown <= 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);
|
||||
buttonPressCooldown = 3;
|
||||
}
|
||||
else
|
||||
{
|
||||
buttonPressCooldown = 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);
|
||||
buttonPressCooldown = 3;
|
||||
}
|
||||
else
|
||||
{
|
||||
buttonPressCooldown = 0;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
else
|
||||
@@ -627,6 +689,7 @@ namespace Barotrauma
|
||||
// The button is on the wrong side of the door or a wall
|
||||
currentPath.Unreachable = true;
|
||||
}
|
||||
lastDoor = (null, false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -283,9 +283,9 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public float CalculatePriority()
|
||||
{
|
||||
ForceWalk = false;
|
||||
Priority = GetPriority();
|
||||
ForceHighestPriority = false;
|
||||
ForceWalk = false;
|
||||
return Priority;
|
||||
}
|
||||
|
||||
@@ -501,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);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-28
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
+33
-7
@@ -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.IsHuman && Enemy.HasEquippedItem("handlocker") && !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:
|
||||
@@ -363,6 +371,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (WeaponComponent == null)
|
||||
{
|
||||
SpeakNoWeapons();
|
||||
Mode = CombatMode.Retreat;
|
||||
}
|
||||
}
|
||||
@@ -409,6 +418,7 @@ namespace Barotrauma
|
||||
onCompleted: () => RemoveSubObjective(ref seekWeaponObjective),
|
||||
onAbandon: () =>
|
||||
{
|
||||
SpeakNoWeapons();
|
||||
RemoveSubObjective(ref seekWeaponObjective);
|
||||
Mode = CombatMode.Retreat;
|
||||
});
|
||||
@@ -680,6 +690,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
SpeakNoWeapons();
|
||||
Weapon = null;
|
||||
Mode = CombatMode.Retreat;
|
||||
return false;
|
||||
@@ -892,7 +903,7 @@ namespace Barotrauma
|
||||
TryAddSubObjective(ref seekAmmunitionObjective,
|
||||
constructor: () => new AIObjectiveContainItem(character, ammunitionIdentifiers, Weapon.GetComponent<ItemContainer>(), objectiveManager)
|
||||
{
|
||||
targetItemCount = Weapon.GetComponent<ItemContainer>().Capacity,
|
||||
ItemCount = Weapon.GetComponent<ItemContainer>().Capacity,
|
||||
checkInventory = false
|
||||
},
|
||||
onCompleted: () => RemoveSubObjective(ref seekAmmunitionObjective),
|
||||
@@ -1099,7 +1110,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;
|
||||
}
|
||||
@@ -1155,6 +1166,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;
|
||||
|
||||
+20
-7
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+15
-1
@@ -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
|
||||
},
|
||||
|
||||
+8
-2
@@ -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 (ShouldArrest(target, character) && target.HasEquippedItem("handlocker")) { return false; }
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool ShouldArrest(Character target, Character character)
|
||||
{
|
||||
return target != null && target.IsEscorted && character.TeamID == CharacterTeamType.Team1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-3
@@ -52,9 +52,11 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
Priority = (objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() ||
|
||||
objectiveManager.IsCurrentOrder<AIObjectiveReturn>() ||
|
||||
objectiveManager.Objectives.Any(o => o.Priority > 0 && o is AIObjectiveCombat))
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
+58
-16
@@ -21,7 +21,7 @@ namespace Barotrauma
|
||||
public float TargetCondition { get; set; } = 1;
|
||||
public bool AllowDangerousPressure { get; set; }
|
||||
|
||||
private readonly ImmutableArray<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;
|
||||
@@ -58,6 +58,17 @@ namespace Barotrauma
|
||||
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; }
|
||||
|
||||
@@ -81,7 +92,7 @@ namespace Barotrauma
|
||||
Equip = equip;
|
||||
this.spawnItemIfNotFound = spawnItemIfNotFound;
|
||||
this.checkInventory = checkInventory;
|
||||
this.identifiersOrTags = ParseGearTags(identifiersOrTags).ToImmutableArray();
|
||||
IdentifiersOrTags = ParseGearTags(identifiersOrTags).ToImmutableArray();
|
||||
ignoredIdentifiersOrTags = ParseIgnoredTags(identifiersOrTags).ToArray();
|
||||
}
|
||||
|
||||
@@ -113,7 +124,7 @@ namespace Barotrauma
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -123,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)
|
||||
@@ -135,7 +159,7 @@ namespace Barotrauma
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
if (identifiersOrTags != null && !isDoneSeeking)
|
||||
if (IdentifiersOrTags != null && !isDoneSeeking)
|
||||
{
|
||||
if (checkInventory)
|
||||
{
|
||||
@@ -152,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;
|
||||
@@ -245,7 +269,18 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
IsCompleted = true;
|
||||
if (IdentifiersOrTags == null)
|
||||
{
|
||||
IsCompleted = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
IsCompleted = CountItems();
|
||||
if (!IsCompleted)
|
||||
{
|
||||
ResetInternal();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -297,7 +332,7 @@ namespace Barotrauma
|
||||
|
||||
private void FindTargetItem()
|
||||
{
|
||||
if (identifiersOrTags == null)
|
||||
if (IdentifiersOrTags == null)
|
||||
{
|
||||
if (targetItem == null)
|
||||
{
|
||||
@@ -316,7 +351,7 @@ namespace Barotrauma
|
||||
// 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);
|
||||
CheckPathForEachItem = priority >= AIObjectiveManager.LowestOrderPriority && (objectiveManager.IsCurrentOrder<AIObjectiveFixLeaks>() || objectiveManager.CurrentOrder is AIObjectiveGoTo gotoOrder && gotoOrder.IsFollowOrderObjective);
|
||||
}
|
||||
bool checkPath = CheckPathForEachItem;
|
||||
bool hasCalledPathFinder = false;
|
||||
@@ -430,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;
|
||||
}
|
||||
@@ -452,7 +487,7 @@ 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
|
||||
Abandon = true;
|
||||
}
|
||||
@@ -468,13 +503,20 @@ namespace Barotrauma
|
||||
// Not yet ready
|
||||
return false;
|
||||
}
|
||||
if (Equip && EquipSlotType.HasValue)
|
||||
if (IdentifiersOrTags != null && ItemCount > 1)
|
||||
{
|
||||
return character.HasEquippedItem(targetItem, EquipSlotType.Value);
|
||||
return CountItems();
|
||||
}
|
||||
else
|
||||
{
|
||||
return character.HasItem(targetItem, Equip);
|
||||
if (Equip && EquipSlotType.HasValue)
|
||||
{
|
||||
return character.HasEquippedItem(targetItem, EquipSlotType.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
return character.HasItem(targetItem, Equip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -487,7 +529,7 @@ namespace Barotrauma
|
||||
if (item.Condition < TargetCondition) { return false; }
|
||||
if (ItemFilter != null && !ItemFilter(item)) { return false; }
|
||||
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));
|
||||
return IdentifiersOrTags.Any(id => id == item.Prefab.Identifier || item.HasTag(id) || (AllowVariants && item.Prefab.VariantOf?.Identifier == id));
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
@@ -528,7 +570,7 @@ namespace Barotrauma
|
||||
{
|
||||
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);
|
||||
|
||||
+13
-3
@@ -11,6 +11,7 @@ namespace Barotrauma
|
||||
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; }
|
||||
@@ -21,6 +22,7 @@ namespace Barotrauma
|
||||
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;
|
||||
@@ -47,9 +49,11 @@ namespace Barotrauma
|
||||
{
|
||||
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)
|
||||
TryAddSubObjective(ref getItem, () =>
|
||||
new AIObjectiveGetItem(character, tag, objectiveManager, Equip, CheckInventory && count <= 1)
|
||||
{
|
||||
AllowVariants = AllowVariants,
|
||||
Wear = Wear,
|
||||
@@ -57,7 +61,9 @@ namespace Barotrauma
|
||||
AllowStealing = AllowStealing,
|
||||
ignoredIdentifiersOrTags = ignoredTags,
|
||||
CheckPathForEachItem = CheckPathForEachItem,
|
||||
RequireLoaded = RequireLoaded
|
||||
RequireLoaded = RequireLoaded,
|
||||
ItemCount = count,
|
||||
SpeakIfFails = RequireAllItems
|
||||
},
|
||||
onCompleted: () =>
|
||||
{
|
||||
@@ -75,6 +81,10 @@ namespace Barotrauma
|
||||
achievedItems.Remove(item);
|
||||
}
|
||||
RemoveSubObjective(ref getItem);
|
||||
if (RequireAllItems)
|
||||
{
|
||||
Abandon = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
subObjectivesCreated = true;
|
||||
|
||||
+52
-48
@@ -22,16 +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;
|
||||
@@ -45,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;
|
||||
}
|
||||
@@ -109,9 +117,9 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
if (priorityGetter != null)
|
||||
if (PriorityGetter != null)
|
||||
{
|
||||
Priority = priorityGetter();
|
||||
Priority = PriorityGetter();
|
||||
}
|
||||
else if (OverridePriority.HasValue)
|
||||
{
|
||||
@@ -177,7 +185,7 @@ namespace Barotrauma
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
if (cannotFollow || Target == character || character.SelectedBy != null && HumanAIController.IsFriendly(character.SelectedBy))
|
||||
if (Target == character || character.SelectedBy != null && HumanAIController.IsFriendly(character.SelectedBy))
|
||||
{
|
||||
// Wait
|
||||
character.AIController.SteeringManager.Reset();
|
||||
@@ -200,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
|
||||
@@ -249,7 +257,7 @@ namespace Barotrauma
|
||||
Character followTarget = Target as Character;
|
||||
bool needsDivingSuit = (!isInside || hasOutdoorNodes) && character.NeedsAir && !character.HasAbilityFlag(AbilityFlags.ImmuneToPressure);
|
||||
bool needsDivingGear = (needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit)) && character.NeedsAir;
|
||||
if (mimic)
|
||||
if (Mimic)
|
||||
{
|
||||
if (HumanAIController.HasDivingSuit(followTarget) && character.NeedsAir)
|
||||
{
|
||||
@@ -277,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;
|
||||
@@ -287,7 +295,7 @@ namespace Barotrauma
|
||||
else
|
||||
{
|
||||
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager),
|
||||
onAbandon: () => Abort(),
|
||||
onAbandon: () => Abandon = true,
|
||||
onCompleted: () =>
|
||||
{
|
||||
cannotFollow = false;
|
||||
@@ -320,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)
|
||||
{
|
||||
@@ -363,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;
|
||||
@@ -382,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))
|
||||
{
|
||||
@@ -391,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);
|
||||
@@ -597,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; }
|
||||
@@ -678,18 +694,6 @@ namespace Barotrauma
|
||||
return IsCompleted;
|
||||
}
|
||||
|
||||
private void Abort()
|
||||
{
|
||||
if (!objectiveManager.IsOrder(this))
|
||||
{
|
||||
Abandon = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
cannotFollow = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnAbandon()
|
||||
{
|
||||
StopMovement();
|
||||
|
||||
+235
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+115
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
-3
@@ -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)
|
||||
|
||||
+39
-21
@@ -128,7 +128,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; }
|
||||
@@ -279,6 +279,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();
|
||||
@@ -354,6 +355,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))
|
||||
{
|
||||
@@ -406,13 +408,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;
|
||||
@@ -523,7 +526,7 @@ namespace Barotrauma
|
||||
newObjective = new AIObjectiveEscapeHandcuffs(character, this, priorityModifier: priorityModifier);
|
||||
break;
|
||||
case "prepareforexpedition":
|
||||
newObjective = new AIObjectivePrepare(character, this, order.TargetItems)
|
||||
newObjective = new AIObjectivePrepare(character, this, order.GetTargetItems(option), order.RequireItems)
|
||||
{
|
||||
KeepActiveWhenReady = true,
|
||||
CheckInventory = true,
|
||||
@@ -532,16 +535,29 @@ namespace Barotrauma
|
||||
};
|
||||
break;
|
||||
case "findweapon":
|
||||
newObjective = new AIObjectivePrepare(character, this, order.TargetItems)
|
||||
AIObjectivePrepare prepareObjective;
|
||||
if (order.TargetEntity is Item tItem)
|
||||
{
|
||||
KeepActiveWhenReady = false,
|
||||
CheckInventory = false,
|
||||
Equip = true,
|
||||
EvaluateCombatPriority = true,
|
||||
FindAllItems = false
|
||||
};
|
||||
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; }
|
||||
@@ -573,17 +589,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; }
|
||||
@@ -632,10 +651,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)
|
||||
{
|
||||
|
||||
+1
-1
@@ -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))
|
||||
{
|
||||
|
||||
+86
-36
@@ -1,5 +1,5 @@
|
||||
#nullable enable
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Extensions;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
@@ -11,11 +11,15 @@ namespace Barotrauma
|
||||
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? getMultipleItemsObjective;
|
||||
private AIObjectiveGetItem getSingleItemObjective;
|
||||
private AIObjectiveGetItems getAllItemsObjective;
|
||||
private AIObjectiveGetItems getMultipleItemsObjective;
|
||||
private bool subObjectivesCreated;
|
||||
private readonly ImmutableArray<string> gearTags;
|
||||
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; }
|
||||
@@ -23,12 +27,29 @@ namespace Barotrauma
|
||||
public bool Equip { get; set; }
|
||||
public bool EvaluateCombatPriority { get; set; }
|
||||
|
||||
private AIObjective? GetSubObjective() => getSingleItemObjective ?? getMultipleItemsObjective as AIObjective;
|
||||
|
||||
public AIObjectivePrepare(Character character, AIObjectiveManager objectiveManager, IEnumerable<string> items, float priorityModifier = 1)
|
||||
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)
|
||||
{
|
||||
gearTags = items.ToImmutableArray();
|
||||
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;
|
||||
@@ -69,48 +90,76 @@ namespace Barotrauma
|
||||
}
|
||||
if (!subObjectivesCreated)
|
||||
{
|
||||
if (FindAllItems)
|
||||
if (FindAllItems && targetItem == null)
|
||||
{
|
||||
if (!TryAddSubObjective(ref getMultipleItemsObjective, () => new AIObjectiveGetItems(character, objectiveManager, gearTags)
|
||||
getMultipleItemsObjective = CreateObjectives(optionalItems, requireAll: false);
|
||||
if (requiredItems != null && requiredItems.Any())
|
||||
{
|
||||
CheckInventory = CheckInventory,
|
||||
Equip = Equip,
|
||||
EvaluateCombatPriority = EvaluateCombatPriority,
|
||||
RequireLoaded = true
|
||||
},
|
||||
onCompleted: () =>
|
||||
getAllItemsObjective = CreateObjectives(requiredItems, requireAll: true);
|
||||
}
|
||||
AIObjectiveGetItems CreateObjectives(IEnumerable<string> itemTags, bool requireAll)
|
||||
{
|
||||
if (KeepActiveWhenReady)
|
||||
AIObjectiveGetItems objectiveReference = null;
|
||||
if (!TryAddSubObjective(ref objectiveReference, () => new AIObjectiveGetItems(character, objectiveManager, itemTags)
|
||||
{
|
||||
if (getMultipleItemsObjective != null)
|
||||
CheckInventory = CheckInventory,
|
||||
Equip = Equip,
|
||||
EvaluateCombatPriority = EvaluateCombatPriority,
|
||||
RequireLoaded = true,
|
||||
RequireAllItems = requireAll
|
||||
},
|
||||
onCompleted: () =>
|
||||
{
|
||||
if (KeepActiveWhenReady)
|
||||
{
|
||||
foreach (var item in getMultipleItemsObjective.achievedItems)
|
||||
if (objectiveReference != null)
|
||||
{
|
||||
if (item?.IsOwnedBy(character) != null)
|
||||
foreach (var item in objectiveReference.achievedItems)
|
||||
{
|
||||
items.Add(item);
|
||||
if (item?.IsOwnedBy(character) != null)
|
||||
{
|
||||
items.Add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
else
|
||||
{
|
||||
IsCompleted = true;
|
||||
}
|
||||
},
|
||||
onAbandon: () => Abandon = true))
|
||||
{
|
||||
IsCompleted = true;
|
||||
Abandon = true;
|
||||
}
|
||||
},
|
||||
onAbandon: () => Abandon = true))
|
||||
{
|
||||
Abandon = true;
|
||||
return objectiveReference;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!TryAddSubObjective(ref getSingleItemObjective, () => new AIObjectiveGetItem(character, gearTags, objectiveManager, equip: Equip, checkInventory: CheckInventory)
|
||||
Func<AIObjectiveGetItem> getItemConstructor;
|
||||
if (targetItem != null)
|
||||
{
|
||||
EvaluateCombatPriority = EvaluateCombatPriority,
|
||||
SpeakIfFails = true,
|
||||
RequireLoaded = true
|
||||
},
|
||||
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)
|
||||
@@ -143,8 +192,9 @@ namespace Barotrauma
|
||||
base.Reset();
|
||||
items.Clear();
|
||||
subObjectivesCreated = false;
|
||||
RemoveSubObjective(ref getMultipleItemsObjective);
|
||||
RemoveSubObjective(ref getSingleItemObjective);
|
||||
getMultipleItemsObjective = null;
|
||||
getSingleItemObjective = null;
|
||||
getAllItemsObjective = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
-1
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
@@ -177,7 +183,7 @@ namespace Barotrauma
|
||||
if (character.SelectedConstruction != Item)
|
||||
{
|
||||
if (!Item.TryInteract(character, ignoreRequiredItems: true, forceSelectKey: true) &&
|
||||
!Item.TryInteract(character, ignoreRequiredItems: true, forceActionKey: true))
|
||||
!Item.TryInteract(character, ignoreRequiredItems: true, forceUseKey: true))
|
||||
{
|
||||
Abandon = true;
|
||||
}
|
||||
@@ -209,6 +215,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
waitTimer = 0.0f;
|
||||
RemoveSubObjective(ref refuelObjective);
|
||||
// If cannot reach the item, approach it.
|
||||
TryAddSubObjective(ref goToObjective,
|
||||
|
||||
+111
-82
@@ -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)
|
||||
@@ -418,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),
|
||||
@@ -449,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;
|
||||
@@ -472,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();
|
||||
|
||||
+22
-10
@@ -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);
|
||||
}
|
||||
|
||||
@@ -83,8 +90,7 @@ namespace Barotrauma
|
||||
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;
|
||||
}
|
||||
@@ -106,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>() ||
|
||||
@@ -120,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;
|
||||
|
||||
@@ -209,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>();
|
||||
@@ -306,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);
|
||||
@@ -317,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);
|
||||
@@ -365,6 +405,7 @@ namespace Barotrauma
|
||||
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>
|
||||
@@ -380,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;
|
||||
@@ -397,6 +440,7 @@ namespace Barotrauma
|
||||
DrawIconWhenContained = prefab.DrawIconWhenContained;
|
||||
Hidden = prefab.Hidden;
|
||||
IgnoreAtOutpost = prefab.IgnoreAtOutpost;
|
||||
AssignmentPriority = prefab.AssignmentPriority;
|
||||
|
||||
OrderGiver = orderGiver;
|
||||
TargetEntity = targetEntity;
|
||||
@@ -512,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; }
|
||||
@@ -536,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)
|
||||
@@ -582,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)
|
||||
|
||||
+9
-6
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+68
-46
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -1058,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);
|
||||
|
||||
@@ -1089,6 +1097,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
float stepHeight = ConvertUnits.ToSimUnits(30.0f);
|
||||
if (climbFast) { stepHeight *= 2; }
|
||||
|
||||
if (currentHull == null && ladder.Item.Submarine != null)
|
||||
{
|
||||
@@ -1104,58 +1113,69 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -1171,12 +1191,8 @@ 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;
|
||||
@@ -1185,8 +1201,11 @@ namespace Barotrauma
|
||||
Vector2 subSpeed = currentHull != null || ladder.Item.Submarine == null
|
||||
? Vector2.Zero : ladder.Item.Submarine.Velocity;
|
||||
|
||||
Vector2 climbForce = new Vector2(0.0f, movement.Y + 0.3f) * movementFactor;
|
||||
//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; }
|
||||
|
||||
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;
|
||||
@@ -1200,8 +1219,11 @@ namespace Barotrauma
|
||||
|
||||
//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 (!ladder.Item.Prefab.Triggers.Any())
|
||||
{
|
||||
@@ -1688,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;
|
||||
}
|
||||
|
||||
@@ -1722,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -64,8 +64,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];
|
||||
@@ -885,7 +885,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;
|
||||
}
|
||||
|
||||
@@ -923,7 +923,7 @@ 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;
|
||||
}
|
||||
@@ -1354,19 +1354,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)
|
||||
{
|
||||
@@ -1384,11 +1384,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)
|
||||
{
|
||||
@@ -1627,8 +1627,8 @@ namespace Barotrauma
|
||||
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; }
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -612,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -643,6 +642,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
|
||||
@@ -691,12 +695,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);
|
||||
}
|
||||
@@ -808,7 +812,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; }
|
||||
}
|
||||
@@ -826,7 +830,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
return IsKnockedDown || LockHands;
|
||||
return IsKnockedDown || LockHands || IsBot && TeamID != CharacterTeamType.FriendlyNPC;
|
||||
}
|
||||
}
|
||||
set { canInventoryBeAccessed = value; }
|
||||
@@ -855,7 +859,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";
|
||||
@@ -865,11 +869,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;
|
||||
@@ -1359,7 +1363,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)
|
||||
@@ -1985,7 +1993,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.
|
||||
@@ -2020,16 +2028,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2038,7 +2041,7 @@ 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)
|
||||
{
|
||||
@@ -2236,6 +2239,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)
|
||||
@@ -2469,7 +2476,10 @@ namespace Barotrauma
|
||||
{
|
||||
minDist = dist;
|
||||
nearbyLadder = ladder;
|
||||
if (isControlled) ladder.Item.IsHighlighted = true;
|
||||
if (isControlled)
|
||||
{
|
||||
ladder.Item.IsHighlighted = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -2477,7 +2487,10 @@ namespace Barotrauma
|
||||
|
||||
if (nearbyLadder != null && climbInput)
|
||||
{
|
||||
if (nearbyLadder.Select(this)) SelectedConstruction = nearbyLadder.Item;
|
||||
if (nearbyLadder.Select(this))
|
||||
{
|
||||
SelectedConstruction = nearbyLadder.Item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2499,14 +2512,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
|
||||
}
|
||||
}
|
||||
@@ -2873,6 +2892,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
|
||||
@@ -3329,6 +3360,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)
|
||||
{
|
||||
@@ -3346,13 +3379,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)
|
||||
{
|
||||
@@ -3429,9 +3462,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();
|
||||
}
|
||||
|
||||
@@ -3620,7 +3653,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);
|
||||
@@ -3850,7 +3883,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;
|
||||
}
|
||||
|
||||
@@ -3895,7 +3928,7 @@ namespace Barotrauma
|
||||
|
||||
AnimController.Frozen = false;
|
||||
|
||||
if (GameSettings.SendUserStatistics)
|
||||
if (GameAnalyticsManager.SendUserStatistics)
|
||||
{
|
||||
string characterType = "Unknown";
|
||||
|
||||
@@ -4078,6 +4111,10 @@ namespace Barotrauma
|
||||
Submarine = null;
|
||||
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)
|
||||
@@ -4448,6 +4485,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
|
||||
@@ -1387,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()),
|
||||
@@ -1745,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)
|
||||
@@ -1784,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))
|
||||
{
|
||||
@@ -1800,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); }
|
||||
@@ -1812,14 +1849,12 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+13
-6
@@ -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; }
|
||||
|
||||
+18
-8
@@ -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; }
|
||||
@@ -272,11 +280,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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
@@ -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; }
|
||||
|
||||
@@ -272,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
@@ -194,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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -471,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
|
||||
@@ -485,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
|
||||
@@ -504,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
|
||||
@@ -518,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))
|
||||
|
||||
+19
-9
@@ -1,4 +1,5 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
@@ -10,26 +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 bool ignoreNonHarmfulAttacks;
|
||||
private readonly bool ignoreNonHarmfulAttacks;
|
||||
public AbilityConditionAttackData(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
|
||||
{
|
||||
itemIdentifier = conditionElement.GetAttributeString("itemidentifier", "");
|
||||
tags = conditionElement.GetAttributeStringArray("tags", new string[0], convertToLowerInvariant: true);
|
||||
ignoreNonHarmfulAttacks = conditionElement.GetAttributeBool("ignorenonharmfulattacks", false);
|
||||
switch (conditionElement.GetAttributeString("weapontype", ""))
|
||||
|
||||
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.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +77,16 @@ namespace Barotrauma.Abilities
|
||||
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;
|
||||
|
||||
+28
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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);
|
||||
}
|
||||
|
||||
+12
-4
@@ -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))
|
||||
{
|
||||
foreach (Item containedItem in Character.Inventory.AllItemsMod)
|
||||
{
|
||||
if (containedItem.OwnInventory?.TryPutItem(item, Character) ?? false) { break; }
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
+1
-1
@@ -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())
|
||||
{
|
||||
@@ -103,7 +102,9 @@ namespace Barotrauma
|
||||
|
||||
void loadSinglePrefab(XElement element, bool isOverride)
|
||||
{
|
||||
TalentPrefabs.Add(new TalentPrefab(element, file.Path) { ContentPackage = file.ContentPackage }, isOverride);
|
||||
var newPrefab = new TalentPrefab(element, file.Path) { ContentPackage = file.ContentPackage };
|
||||
TalentPrefabs.Add(newPrefab, isOverride);
|
||||
newPrefab.CalculatePrefabUIntIdentifier(TalentPrefabs);
|
||||
}
|
||||
|
||||
void loadMultiplePrefabs(XElement element, bool isOverride)
|
||||
|
||||
@@ -218,7 +218,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
|
||||
@@ -231,7 +231,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());
|
||||
}
|
||||
},
|
||||
() =>
|
||||
@@ -239,7 +239,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" };
|
||||
@@ -250,8 +253,8 @@ namespace Barotrauma
|
||||
|
||||
return new string[][]
|
||||
{
|
||||
itemNames.ToArray(),
|
||||
spawnPosParams.ToArray()
|
||||
itemNames.ToArray(),
|
||||
spawnPosParams.ToArray()
|
||||
};
|
||||
}, isCheat: true));
|
||||
|
||||
@@ -1205,7 +1208,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));
|
||||
@@ -1871,7 +1874,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;
|
||||
}
|
||||
@@ -2160,7 +2163,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (spawnPos != null)
|
||||
{
|
||||
if (Entity.Spawner == null)
|
||||
if (Entity.Spawner == null || Entity.Spawner.Removed)
|
||||
{
|
||||
new Item(itemPrefab, spawnPos.Value, 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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -162,24 +162,6 @@ namespace Barotrauma
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
//if any of the target items is a reactor, prevent exploding it from damaging the player's sub
|
||||
foreach (Item item in items)
|
||||
{
|
||||
if (item.GetComponent<Reactor>() is Reactor reactor && (reactor.statusEffectLists?.ContainsKey(ActionType.OnBroken) ?? false))
|
||||
{
|
||||
foreach (var statusEffect in reactor.statusEffectLists[ActionType.OnBroken])
|
||||
{
|
||||
foreach (Explosion explosion in statusEffect.Explosions)
|
||||
{
|
||||
foreach (Submarine sub in Submarine.Loaded)
|
||||
{
|
||||
if (sub.TeamID == CharacterTeamType.Team1) { explosion.IgnoredSubmarines.Add(sub); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void InitCharacters(Submarine submarine)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,377 @@
|
||||
#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 = null;
|
||||
Md5Hash? exeHash = null;
|
||||
exeName = Path.GetFileNameWithoutExtension(exePath).Replace(":", "");
|
||||
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
|
||||
{
|
||||
loadedImplementation?.ConfigureBuild(GameMain.Version.ToString()
|
||||
+ (string.IsNullOrEmpty(exeName) ? "Unknown" : exeName) + ":"
|
||||
+ ((exeHash?.ShortHash == null) ? "Unknown" : exeHash.ShortHash));
|
||||
loadedImplementation?.ConfigureAvailableCustomDimensions01("singleplayer", "multiplayer", "editor");
|
||||
|
||||
InitKeys();
|
||||
|
||||
loadedImplementation?.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);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
@@ -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 :
|
||||
@@ -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() { }
|
||||
|
||||
+1
-22
@@ -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))
|
||||
|
||||
@@ -406,6 +406,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); }
|
||||
|
||||
@@ -674,6 +689,8 @@ namespace Barotrauma
|
||||
{
|
||||
IEnumerable<Character> crewCharacters = GetSessionCrewCharacters();
|
||||
|
||||
int prevMoney = (GameMode as CampaignMode)?.Money ?? 0;
|
||||
|
||||
foreach (Mission mission in missions)
|
||||
{
|
||||
mission.End();
|
||||
@@ -731,6 +748,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
|
||||
|
||||
@@ -484,7 +484,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 +779,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 +809,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 +827,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]); }
|
||||
}
|
||||
|
||||
@@ -204,9 +204,12 @@ namespace Barotrauma.Items.Components
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(true, true, description: ""), Editable]
|
||||
[Editable, Serialize(true, true, description: "", alwaysUseInstanceValues: true)]
|
||||
public bool UseBetweenOutpostModules { get; private set; }
|
||||
|
||||
[Editable, Serialize(false, false, description: "If true, bots won't try to close this door behind them.", alwaysUseInstanceValues: true)]
|
||||
public bool BotsShouldKeepOpen { get; private set; }
|
||||
|
||||
public Door(Item item, XElement element)
|
||||
: base(item, element)
|
||||
{
|
||||
@@ -291,7 +294,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public override bool Pick(Character picker)
|
||||
{
|
||||
if (item.Condition < RepairThreshold) { return true; }
|
||||
if (item.Condition < RepairThreshold && item.GetComponent<Repairable>().HasRequiredItems(picker, addMessage: false)) { return true; }
|
||||
if (requiredItems.None()) { return false; }
|
||||
if (HasAccess(picker) && HasRequiredItems(picker, false)) { return false; }
|
||||
return base.Pick(picker);
|
||||
@@ -299,7 +302,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public override bool OnPicked(Character picker)
|
||||
{
|
||||
if (item.Condition < RepairThreshold) { return true; }
|
||||
if (item.Condition < RepairThreshold && item.GetComponent<Repairable>().HasRequiredItems(picker, addMessage: false)) { return true; }
|
||||
if (!HasAccess(picker))
|
||||
{
|
||||
ToggleState(ActionType.OnPicked, picker);
|
||||
@@ -339,6 +342,7 @@ namespace Barotrauma.Items.Components
|
||||
ToggleState(ActionType.OnUse, character);
|
||||
PickingTime = originalPickingTime;
|
||||
StopPicking(picker);
|
||||
return true;
|
||||
}
|
||||
#if CLIENT
|
||||
else if (hasRequiredItems && character != null && character == Character.Controlled)
|
||||
@@ -545,7 +549,7 @@ namespace Barotrauma.Items.Components
|
||||
if (!itemPosErrorShown)
|
||||
{
|
||||
DebugConsole.ThrowError("Failed to push a character out of a doorway - position of the door is not valid (" + item.SimPosition + ")");
|
||||
GameAnalyticsManager.AddErrorEventOnce("PushCharactersAway:DoorPosInvalid", GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
|
||||
GameAnalyticsManager.AddErrorEventOnce("PushCharactersAway:DoorPosInvalid", GameAnalyticsManager.ErrorSeverity.Error,
|
||||
"Failed to push a character out of a doorway - position of the door is not valid (" + item.SimPosition + ").");
|
||||
itemPosErrorShown = true;
|
||||
}
|
||||
@@ -568,8 +572,8 @@ namespace Barotrauma.Items.Components
|
||||
if (!characterPosErrorShown.Contains(c))
|
||||
{
|
||||
if (GameSettings.VerboseLogging) { DebugConsole.ThrowError("Failed to push a character out of a doorway - position of the character \"" + c.Name + "\" is not valid (" + c.SimPosition + ")"); }
|
||||
GameAnalyticsManager.AddErrorEventOnce("PushCharactersAway:CharacterPosInvalid", GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
|
||||
"Failed to push a character out of a doorway - position of the character \"" + c.Name + "\" is not valid (" + c.SimPosition + ")." +
|
||||
GameAnalyticsManager.AddErrorEventOnce("PushCharactersAway:CharacterPosInvalid", GameAnalyticsManager.ErrorSeverity.Error,
|
||||
"Failed to push a character out of a doorway - position of the character \"" + c.SpeciesName + "\" is not valid (" + c.SimPosition + ")." +
|
||||
" Removed: " + c.Removed +
|
||||
" Remoteplayer: " + c.IsRemotePlayer);
|
||||
characterPosErrorShown.Add(c);
|
||||
@@ -598,8 +602,8 @@ namespace Barotrauma.Items.Components
|
||||
if (!MathUtils.IsValid(body.SimPosition))
|
||||
{
|
||||
DebugConsole.ThrowError("Failed to push a limb out of a doorway - position of the body (character \"" + c.Name + "\") is not valid (" + body.SimPosition + ")");
|
||||
GameAnalyticsManager.AddErrorEventOnce("PushCharactersAway:LimbPosInvalid", GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
|
||||
"Failed to push a character out of a doorway - position of the character \"" + c.Name + "\" is not valid (" + body.SimPosition + ")." +
|
||||
GameAnalyticsManager.AddErrorEventOnce("PushCharactersAway:LimbPosInvalid", GameAnalyticsManager.ErrorSeverity.Error,
|
||||
"Failed to push a character out of a doorway - position of the character \"" + c.SpeciesName + "\" is not valid (" + body.SimPosition + ")." +
|
||||
" Removed: " + c.Removed +
|
||||
" Remoteplayer: " + c.IsRemotePlayer);
|
||||
return false;
|
||||
|
||||
@@ -310,12 +310,6 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
internal static class GrowthSideExtension
|
||||
{
|
||||
// Enum.HasFlag() sucks
|
||||
public static bool IsBitSet(this TileSide side, TileSide bit)
|
||||
{
|
||||
return ((int) side & (int) bit) != 0;
|
||||
}
|
||||
|
||||
// K&R algorithm for counting how many bits are set in a bit field
|
||||
public static int Count(this TileSide side)
|
||||
{
|
||||
|
||||
@@ -169,7 +169,7 @@ namespace Barotrauma.Items.Components
|
||||
CollidesWith = Physics.CollisionCharacter,
|
||||
CollisionCategories = Physics.CollisionItemBlocking,
|
||||
Enabled = false,
|
||||
UserData = "Holdable.Pusher"
|
||||
UserData = this
|
||||
};
|
||||
Pusher.FarseerBody.OnCollision += OnPusherCollision;
|
||||
Pusher.FarseerBody.FixedRotation = false;
|
||||
|
||||
@@ -69,14 +69,28 @@ namespace Barotrauma.Items.Components
|
||||
public override bool Pick(Character picker)
|
||||
{
|
||||
//return if someone is already trying to pick the item
|
||||
if (pickTimer > 0.0f) return false;
|
||||
if (picker == null || picker.Inventory == null) return false;
|
||||
if (pickTimer > 0.0f) { return false; }
|
||||
if (picker == null || picker.Inventory == null) { return false; }
|
||||
|
||||
if (PickingTime > 0.0f)
|
||||
{
|
||||
var abilityPickingTime = new AbilityValueItem(PickingTime, item.Prefab);
|
||||
picker.CheckTalents(AbilityEffectType.OnItemPicked, abilityPickingTime);
|
||||
|
||||
if (requiredItems.ContainsKey(RelatedItem.RelationType.Equipped))
|
||||
{
|
||||
foreach (RelatedItem ri in requiredItems[RelatedItem.RelationType.Equipped])
|
||||
{
|
||||
foreach (var heldItem in picker.HeldItems)
|
||||
{
|
||||
if (ri.MatchesItem(heldItem))
|
||||
{
|
||||
abilityPickingTime.Value /= 1 + heldItem.Prefab.AddedPickingSpeedMultiplier;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((picker.PickingItem == null || picker.PickingItem == item) && PickingTime <= float.MaxValue)
|
||||
{
|
||||
#if SERVER
|
||||
|
||||
@@ -32,6 +32,13 @@ namespace Barotrauma.Items.Components
|
||||
set { reload = Math.Max(value, 0.0f); }
|
||||
}
|
||||
|
||||
[Serialize(false, false, description: "Tells the AI to hold the trigger down when it uses this weapon")]
|
||||
public bool HoldTrigger
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(1, false, description: "How projectiles the weapon launches when fired once.")]
|
||||
public int ProjectileCount
|
||||
{
|
||||
@@ -110,9 +117,7 @@ namespace Barotrauma.Items.Components
|
||||
if (ReloadTimer < 0.0f)
|
||||
{
|
||||
ReloadTimer = 0.0f;
|
||||
// was this an optimization or related to something else? it cannot occur for charge-type weapons
|
||||
//IsActive = false;
|
||||
if (MaxChargeTime == 0.0f)
|
||||
if (MaxChargeTime <= 0f)
|
||||
{
|
||||
IsActive = false;
|
||||
return;
|
||||
@@ -147,7 +152,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
private float GetSpread(Character user)
|
||||
{
|
||||
float degreeOfFailure = 1.0f - DegreeOfSuccess(user);
|
||||
float degreeOfFailure = MathHelper.Clamp(1.0f - DegreeOfSuccess(user), 0.0f, 1.0f);
|
||||
degreeOfFailure *= degreeOfFailure;
|
||||
float spread = MathHelper.Lerp(Spread, UnskilledSpread, degreeOfFailure) / (1f + user.GetStatValue(StatTypes.RangedSpreadReduction));
|
||||
return MathHelper.ToRadians(spread);
|
||||
@@ -204,7 +209,8 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
lastProjectile?.Item.GetComponent<Rope>()?.Snap();
|
||||
}
|
||||
float damageMultiplier = 1f + item.GetQualityModifier(Quality.StatType.StoppingPowerMultiplier);
|
||||
float damageMultiplier = 1f + item.GetQualityModifier(Quality.StatType.FirepowerMultiplier);
|
||||
projectile.Launcher = item;
|
||||
projectile.Shoot(character, character.AnimController.AimSourceSimPos, barrelPos, rotation + spread, ignoredBodies: limbBodies.ToList(), createNetworkEvent: false, damageMultiplier);
|
||||
projectile.Item.GetComponent<Rope>()?.Attach(Item, projectile.Item);
|
||||
if (i == 0)
|
||||
|
||||
@@ -613,7 +613,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
string errorMsg = "ItemComponent.DegreeOfSuccess failed (character was null).\n" + Environment.StackTrace.CleanupStackTrace();
|
||||
DebugConsole.ThrowError(errorMsg);
|
||||
GameAnalyticsManager.AddErrorEventOnce("ItemComponent.DegreeOfSuccess:CharacterNull", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
|
||||
GameAnalyticsManager.AddErrorEventOnce("ItemComponent.DegreeOfSuccess:CharacterNull", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
@@ -889,7 +889,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
DebugConsole.ThrowError("Error while loading entity of the type " + t + ".", e.InnerException);
|
||||
GameAnalyticsManager.AddErrorEventOnce("ItemComponent.Load:TargetInvocationException" + item.Name + element.Name,
|
||||
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
|
||||
GameAnalyticsManager.ErrorSeverity.Error,
|
||||
"Error while loading entity of the type " + t + " (" + e.InnerException + ")\n" + Environment.StackTrace.CleanupStackTrace());
|
||||
}
|
||||
|
||||
@@ -994,7 +994,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
containObjective = new AIObjectiveContainItem(character, container.ContainableItemIdentifiers.ToArray(), container, currentObjective.objectiveManager, spawnItemIfNotFound: spawnItemIfNotFound)
|
||||
{
|
||||
targetItemCount = itemCount,
|
||||
ItemCount = itemCount,
|
||||
Equip = equip,
|
||||
RemoveEmpty = removeEmpty,
|
||||
GetItemPriority = i =>
|
||||
|
||||
@@ -471,7 +471,7 @@ namespace Barotrauma.Items.Components
|
||||
if (!slotRestrictions.Any(s => s.MatchesItem(item))) { return false; }
|
||||
if (user != null && !user.CanAccessInventory(Inventory)) { return false; }
|
||||
//genetic materials use special logic for combining, don't allow doing it by placing them inside each other here
|
||||
if (item.GetComponent<GeneticMaterial>() != null) { return false; }
|
||||
if (this.Item.GetComponent<GeneticMaterial>() != null) { return false; }
|
||||
|
||||
if (Inventory.TryPutItem(item, user))
|
||||
{
|
||||
@@ -577,7 +577,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
DebugConsole.Log("SetTransformIgnoreContacts threw an exception in SetContainedItemPositions (" + e.Message + ")\n" + e.StackTrace.CleanupStackTrace());
|
||||
GameAnalyticsManager.AddErrorEventOnce("ItemContainer.SetContainedItemPositions.InvalidPosition:" + contained.Name,
|
||||
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
|
||||
GameAnalyticsManager.ErrorSeverity.Error,
|
||||
"SetTransformIgnoreContacts threw an exception in SetContainedItemPositions (" + e.Message + ")\n" + e.StackTrace.CleanupStackTrace());
|
||||
}
|
||||
contained.body.Submarine = item.Submarine;
|
||||
|
||||
@@ -61,14 +61,14 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public IEnumerable<LimbPos> LimbPositions { get { return limbPositions; } }
|
||||
|
||||
[Editable, Serialize(false, false, description: "When enabled, the item will continuously send out a 0/1 signal and interacting with it will flip the signal (making the item behave like a switch). When disabled, the item will simply send out 1 when interacted with.")]
|
||||
[Editable, Serialize(false, false, description: "When enabled, the item will continuously send out a 0/1 signal and interacting with it will flip the signal (making the item behave like a switch). When disabled, the item will simply send out 1 when interacted with.", alwaysUseInstanceValues: true)]
|
||||
public bool IsToggle
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Editable, Serialize(false, false, description: "Whether the item is toggled on/off. Only valid if IsToggle is set to true.")]
|
||||
[Editable, Serialize(false, false, description: "Whether the item is toggled on/off. Only valid if IsToggle is set to true.", alwaysUseInstanceValues: true)]
|
||||
public bool State
|
||||
{
|
||||
get;
|
||||
|
||||
+39
-11
@@ -158,10 +158,21 @@ namespace Barotrauma.Items.Components
|
||||
// In multiplayer, the server handles the deconstruction into new items
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
|
||||
|
||||
float amountMultiplier = 1f;
|
||||
|
||||
if (user != null && !user.Removed)
|
||||
{
|
||||
var abilityTargetItem = new AbilityItem(targetItem);
|
||||
var abilityTargetItem = new AbilityDeconstructedItem(targetItem, user);
|
||||
user.CheckTalents(AbilityEffectType.OnItemDeconstructed, abilityTargetItem);
|
||||
|
||||
foreach (Character character in Character.GetFriendlyCrew(user))
|
||||
{
|
||||
character.CheckTalents(AbilityEffectType.OnItemDeconstructedByAlly, abilityTargetItem);
|
||||
}
|
||||
|
||||
var itemCreationMultiplier = new AbilityValueItem(amountMultiplier, targetItem.Prefab);
|
||||
user.CheckTalents(AbilityEffectType.OnItemDeconstructedMaterial, itemCreationMultiplier);
|
||||
amountMultiplier = (int)itemCreationMultiplier.Value;
|
||||
}
|
||||
|
||||
if (targetItem.Prefab.RandomDeconstructionOutput)
|
||||
@@ -187,18 +198,18 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
foreach (DeconstructItem deconstructProduct in products)
|
||||
{
|
||||
CreateDeconstructProduct(deconstructProduct, inputItems);
|
||||
CreateDeconstructProduct(deconstructProduct, inputItems, amountMultiplier);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (DeconstructItem deconstructProduct in validDeconstructItems)
|
||||
{
|
||||
CreateDeconstructProduct(deconstructProduct, inputItems);
|
||||
CreateDeconstructProduct(deconstructProduct, inputItems, amountMultiplier);
|
||||
}
|
||||
}
|
||||
|
||||
void CreateDeconstructProduct(DeconstructItem deconstructProduct, IEnumerable<Item> inputItems)
|
||||
void CreateDeconstructProduct(DeconstructItem deconstructProduct, IEnumerable<Item> inputItems, float amountMultiplier)
|
||||
{
|
||||
float percentageHealth = targetItem.Condition / targetItem.MaxCondition;
|
||||
|
||||
@@ -247,19 +258,15 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
int amount = 1;
|
||||
|
||||
if (user != null && !user.Removed)
|
||||
{
|
||||
var itemsCreated = new AbilityValueItem(amount, targetItem.Prefab);
|
||||
user.CheckTalents(AbilityEffectType.OnItemDeconstructedMaterial, itemsCreated);
|
||||
amount = (int)itemsCreated.Value;
|
||||
|
||||
// used to spawn items directly into the deconstructor
|
||||
var itemContainer = new AbilityItemPrefabItem(item, targetItem.Prefab);
|
||||
user.CheckTalents(AbilityEffectType.OnItemDeconstructedInventory, itemContainer);
|
||||
}
|
||||
|
||||
int amount = (int)amountMultiplier;
|
||||
|
||||
for (int i = 0; i < amount; i++)
|
||||
{
|
||||
Entity.Spawner.AddToSpawnQueue(itemPrefab, outputContainer.Inventory, condition, onSpawned: (Item spawnedItem) =>
|
||||
@@ -269,7 +276,17 @@ namespace Barotrauma.Items.Components
|
||||
for (int i = 0; i < outputContainer.Capacity; i++)
|
||||
{
|
||||
var containedItem = outputContainer.Inventory.GetItemAt(i);
|
||||
if (containedItem?.Combine(spawnedItem, null) ?? false)
|
||||
if (containedItem?.OwnInventory != null)
|
||||
{
|
||||
foreach (Item subItem in containedItem.ContainedItems.ToList())
|
||||
{
|
||||
if (subItem.Combine(spawnedItem, null))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (containedItem?.Combine(spawnedItem, null) ?? false)
|
||||
{
|
||||
break;
|
||||
}
|
||||
@@ -409,4 +426,15 @@ namespace Barotrauma.Items.Components
|
||||
inputContainer.Inventory.Locked = IsActive;
|
||||
}
|
||||
}
|
||||
class AbilityDeconstructedItem : AbilityObject, IAbilityItem, IAbilityCharacter
|
||||
{
|
||||
public AbilityDeconstructedItem(Item item, Character character)
|
||||
{
|
||||
Item = item;
|
||||
Character = character;
|
||||
}
|
||||
public Item Item { get; set; }
|
||||
public Character Character { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -376,7 +376,7 @@ namespace Barotrauma.Items.Components
|
||||
int quality = 0;
|
||||
if (user?.Info != null)
|
||||
{
|
||||
foreach (Character character in Character.CharacterList.Where(c => c.TeamID == user.TeamID))
|
||||
foreach (Character character in Character.GetFriendlyCrew(user))
|
||||
{
|
||||
character.CheckTalents(AbilityEffectType.OnAllyItemFabricatedAmount, fabricationValueItem);
|
||||
}
|
||||
@@ -433,13 +433,12 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
float userSkill = user.GetSkillLevel(skill.Identifier);
|
||||
float addedSkill = skill.Level * SkillSettings.Current.SkillIncreasePerFabricatorRequiredSkill / Math.Max(userSkill, 1.0f);
|
||||
var addedSkillValue = new AbilityValueString(0f, skill.Identifier);
|
||||
var addedSkillValue = new AbilityValueString(addedSkill, skill.Identifier);
|
||||
user.CheckTalents(AbilityEffectType.OnItemFabricationSkillGain, addedSkillValue);
|
||||
addedSkill += addedSkillValue.Value;
|
||||
|
||||
user.Info.IncreaseSkillLevel(
|
||||
skill.Identifier,
|
||||
addedSkill);
|
||||
addedSkillValue.Value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -147,16 +147,12 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
case "water_data_in":
|
||||
//cheating a bit because water detectors don't actually send the water level
|
||||
float waterAmount;
|
||||
if (source.GetComponent<WaterDetector>() == null)
|
||||
bool fromWaterDetector = source.GetComponent<WaterDetector>() != null;
|
||||
hullData.ReceivedWaterAmount = null;
|
||||
if (fromWaterDetector)
|
||||
{
|
||||
waterAmount = Rand.Range(0.0f, 1.0f);
|
||||
hullData.ReceivedWaterAmount = Math.Min(sourceHull.WaterVolume / sourceHull.Volume, 1.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
waterAmount = Math.Min(sourceHull.WaterVolume / sourceHull.Volume, 1.0f);
|
||||
}
|
||||
hullData.ReceivedWaterAmount = waterAmount;
|
||||
foreach (var linked in sourceHull.linkedTo)
|
||||
{
|
||||
if (!(linked is Hull linkedHull)) { continue; }
|
||||
@@ -165,7 +161,11 @@ namespace Barotrauma.Items.Components
|
||||
linkedHullData = new HullData();
|
||||
hullDatas.Add(linkedHull, linkedHullData);
|
||||
}
|
||||
linkedHullData.ReceivedWaterAmount = waterAmount;
|
||||
linkedHullData.ReceivedWaterAmount = null;
|
||||
if (fromWaterDetector)
|
||||
{
|
||||
linkedHullData.ReceivedWaterAmount = Math.Min(linkedHull.WaterVolume / linkedHull.Volume, 1.0f);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "oxygen_data_in":
|
||||
|
||||
@@ -186,6 +186,13 @@ namespace Barotrauma.Items.Components
|
||||
[Serialize(0.0f, true)]
|
||||
public float CorrectTurbineOutput { get; set; }
|
||||
|
||||
[Editable, Serialize(true, true)]
|
||||
public bool ExplosionDamagesOtherSubs
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public Reactor(Item item, XElement element)
|
||||
: base(item, element)
|
||||
{
|
||||
@@ -544,6 +551,20 @@ namespace Barotrauma.Items.Components
|
||||
if (item.Condition <= 0.0f) { return; }
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
|
||||
|
||||
if (!ExplosionDamagesOtherSubs && (statusEffectLists?.ContainsKey(ActionType.OnBroken) ?? false))
|
||||
{
|
||||
foreach (var statusEffect in statusEffectLists[ActionType.OnBroken])
|
||||
{
|
||||
foreach (Explosion explosion in statusEffect.Explosions)
|
||||
{
|
||||
foreach (Submarine sub in Submarine.Loaded)
|
||||
{
|
||||
if (sub != item.Submarine) { explosion.IgnoredSubmarines.Add(sub); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item.Condition = 0.0f;
|
||||
fireTimer = 0.0f;
|
||||
meltDownTimer = 0.0f;
|
||||
@@ -600,7 +621,7 @@ namespace Barotrauma.Items.Components
|
||||
var container = item.GetComponent<ItemContainer>();
|
||||
if (objective.SubObjectives.None())
|
||||
{
|
||||
var containObjective = AIContainItems<Reactor>(container, character, objective, itemCount: 1, equip: true, removeEmpty: true, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC, dropItemOnDeselected: true);
|
||||
var containObjective = AIContainItems<Reactor>(container, character, objective, itemCount: 1, equip: true, removeEmpty: true, spawnItemIfNotFound: !character.IsOnPlayerTeam, dropItemOnDeselected: true);
|
||||
containObjective.Completed += ReportFuelRodCount;
|
||||
containObjective.Abandoned += ReportFuelRodCount;
|
||||
character.Speak(TextManager.Get("DialogReactorFuel"), null, 0.0f, "reactorfuel", 30.0f);
|
||||
|
||||
@@ -61,6 +61,11 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public List<Body> IgnoredBodies;
|
||||
|
||||
/// <summary>
|
||||
/// The item that launched this projectile (if any)
|
||||
/// </summary>
|
||||
public Item Launcher;
|
||||
|
||||
private Character stickTargetCharacter;
|
||||
|
||||
private Character _user;
|
||||
@@ -324,6 +329,7 @@ namespace Barotrauma.Items.Components
|
||||
item.body.SetTransform(item.body.SimPosition, launchAngle);
|
||||
float modifiedLaunchImpulse = LaunchImpulse * (1 + Rand.Range(-ImpulseSpread, ImpulseSpread));
|
||||
DoLaunch(launchDir * modifiedLaunchImpulse * item.body.Mass);
|
||||
System.Diagnostics.Debug.WriteLine("launch: " + modifiedLaunchImpulse + " - " + item.body.LinearVelocity);
|
||||
}
|
||||
}
|
||||
User = character;
|
||||
@@ -347,7 +353,7 @@ namespace Barotrauma.Items.Components
|
||||
launchPos = item.SimPosition;
|
||||
|
||||
item.body.Enabled = true;
|
||||
item.body.ApplyLinearImpulse(impulse, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.9f);
|
||||
item.body.ApplyLinearImpulse(impulse, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.95f);
|
||||
|
||||
item.body.FarseerBody.OnCollision += OnProjectileCollision;
|
||||
item.body.FarseerBody.IsBullet = true;
|
||||
@@ -523,11 +529,13 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (fixture.Body.UserData is Item item && (item.GetComponent<Door>() == null && !item.Prefab.DamagedByProjectiles || item.Condition <= 0)) { return -1; }
|
||||
if (fixture.Body.UserData as string == "ruinroom" || fixture.Body?.UserData is Hull || fixture.UserData is Hull) { return -1; }
|
||||
|
||||
//ignore everything else than characters, sub walls and level walls
|
||||
if (!fixture.CollisionCategories.HasFlag(Physics.CollisionCharacter) &&
|
||||
!fixture.CollisionCategories.HasFlag(Physics.CollisionWall) &&
|
||||
!fixture.CollisionCategories.HasFlag(Physics.CollisionLevel)) { return -1; }
|
||||
if (!(fixture.Body.UserData is Holdable holdable && holdable.CanPush))
|
||||
{
|
||||
//ignore everything else than characters, sub walls and level walls
|
||||
if (!fixture.CollisionCategories.HasFlag(Physics.CollisionCharacter) &&
|
||||
!fixture.CollisionCategories.HasFlag(Physics.CollisionWall) &&
|
||||
!fixture.CollisionCategories.HasFlag(Physics.CollisionLevel)) { return -1; }
|
||||
}
|
||||
|
||||
//if doing the raycast in a submarine's coordinate space, ignore anything that's not in that sub
|
||||
if (submarine != null)
|
||||
@@ -566,7 +574,7 @@ namespace Barotrauma.Items.Components
|
||||
hits.Add(new HitscanResult(fixture, point, normal, fraction));
|
||||
|
||||
return 1;
|
||||
}, rayStart, rayEnd, Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel);
|
||||
}, rayStart, rayEnd, Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking);
|
||||
|
||||
return hits;
|
||||
}
|
||||
@@ -754,7 +762,7 @@ namespace Barotrauma.Items.Components
|
||||
if (Attack != null) { attackResult = Attack.DoDamageToLimb(User ?? Attacker, limb, item.WorldPosition, 1.0f); }
|
||||
if (limb.character != null) { character = limb.character; }
|
||||
}
|
||||
else if (target.Body.UserData is Item targetItem)
|
||||
else if ((target.Body.UserData as Item ?? (target.Body.UserData as ItemComponent)?.Item) is Item targetItem)
|
||||
{
|
||||
if (targetItem.Removed) { return false; }
|
||||
if (Attack != null && targetItem.Prefab.DamagedByProjectiles && targetItem.Condition > 0)
|
||||
|
||||
@@ -26,12 +26,13 @@ namespace Barotrauma.Items.Components
|
||||
RepairToolStructureRepairMultiplier,
|
||||
RepairToolStructureDamageMultiplier,
|
||||
RepairToolDeattachTimeMultiplier,
|
||||
StoppingPowerMultiplier,
|
||||
FirepowerMultiplier,
|
||||
StrikingPowerMultiplier,
|
||||
StrikingSpeedMultiplier,
|
||||
FiringRateMultiplier,
|
||||
// unused as of now
|
||||
AttackMultiplier,
|
||||
// unused as of now
|
||||
AttackSpeedMultiplier,
|
||||
ForceDoorsOpenSpeedMultiplier,
|
||||
RangedSpreadReduction,
|
||||
@@ -46,7 +47,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
private int qualityLevel;
|
||||
|
||||
[Serialize(0, true)]
|
||||
[Editable, Serialize(0, true)]
|
||||
public int QualityLevel
|
||||
{
|
||||
get { return qualityLevel; }
|
||||
|
||||
@@ -16,6 +16,8 @@ namespace Barotrauma.Items.Components
|
||||
private float deteriorationTimer;
|
||||
private float deteriorateAlwaysResetTimer;
|
||||
|
||||
private float repairBoost;
|
||||
|
||||
bool wasBroken;
|
||||
bool wasGoodCondition;
|
||||
|
||||
@@ -214,6 +216,18 @@ namespace Barotrauma.Items.Components
|
||||
return ((average + 100.0f) / 2.0f) / 100.0f;
|
||||
}
|
||||
|
||||
public void RepairBoost(bool qteSuccess)
|
||||
{
|
||||
if (qteSuccess)
|
||||
{
|
||||
repairBoost = RepairDegreeOfSuccess(CurrentFixer, requiredSkills) * 3 * (currentFixerAction == FixActions.Repair ? 1.0f : -1.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
repairBoost = (1 - RepairDegreeOfSuccess(CurrentFixer, requiredSkills)) * 10 * (currentFixerAction == FixActions.Repair ? -1.0f : 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
public bool StartRepairing(Character character, FixActions action)
|
||||
{
|
||||
if (character == null || character.IsDead || action == FixActions.None)
|
||||
@@ -409,6 +423,12 @@ namespace Barotrauma.Items.Components
|
||||
wasGoodCondition = true;
|
||||
}
|
||||
|
||||
if (!MathUtils.NearlyEqual(repairBoost, 0.0f))
|
||||
{
|
||||
item.Condition += repairBoost;
|
||||
repairBoost = 0.0f;
|
||||
}
|
||||
|
||||
float fixDuration = MathHelper.Lerp(FixDurationLowSkill, FixDurationHighSkill, successFactor);
|
||||
fixDuration /= 1 + CurrentFixer.GetStatValue(StatTypes.RepairSpeed) + currentRepairItem?.Prefab.AddedRepairSpeedMultiplier ?? 0f;
|
||||
fixDuration /= 1 + item.GetQualityModifier(Quality.StatType.RepairSpeed);
|
||||
@@ -444,6 +464,7 @@ namespace Barotrauma.Items.Components
|
||||
SteamAchievementManager.OnItemRepaired(item, CurrentFixer);
|
||||
CurrentFixer.CheckTalents(AbilityEffectType.OnRepairComplete);
|
||||
}
|
||||
if (CurrentFixer?.SelectedConstruction == item) { CurrentFixer.SelectedConstruction = null; }
|
||||
deteriorationTimer = Rand.Range(MinDeteriorationDelay, MaxDeteriorationDelay);
|
||||
wasBroken = false;
|
||||
StopRepairing(CurrentFixer);
|
||||
|
||||
@@ -36,9 +36,9 @@ namespace Barotrauma.Items.Components
|
||||
if (UseHSV)
|
||||
{
|
||||
Color hsvColor = ToolBox.HSVToRGB(signalR, signalG, signalB);
|
||||
signalR = hsvColor.R / (float) byte.MaxValue;
|
||||
signalG = hsvColor.G / (float) byte.MaxValue;
|
||||
signalB = hsvColor.B / (float) byte.MaxValue;
|
||||
signalR = hsvColor.R;
|
||||
signalG = hsvColor.G;
|
||||
signalB = hsvColor.B;
|
||||
}
|
||||
|
||||
output = signalR.ToString("G", CultureInfo.InvariantCulture);
|
||||
|
||||
@@ -36,9 +36,17 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
case FunctionType.Round:
|
||||
value = MathF.Round(value);
|
||||
if (value == -0)
|
||||
{
|
||||
value = 0;
|
||||
}
|
||||
break;
|
||||
case FunctionType.Ceil:
|
||||
value = MathF.Ceiling(value);
|
||||
if (value == -0)
|
||||
{
|
||||
value = 0;
|
||||
}
|
||||
break;
|
||||
case FunctionType.Floor:
|
||||
value = MathF.Floor(value);
|
||||
|
||||
@@ -105,7 +105,7 @@ namespace Barotrauma.Items.Components
|
||||
switch (connection.Name)
|
||||
{
|
||||
case "set_text":
|
||||
|
||||
case "signal_in":
|
||||
if (signal.value.Length > MaxMessageLength)
|
||||
{
|
||||
signal.value = signal.value.Substring(0, MaxMessageLength);
|
||||
@@ -128,6 +128,8 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
history.ClearChildren();
|
||||
}
|
||||
|
||||
CreateFillerBlock();
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -100,7 +100,12 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (item.CurrentHull != null)
|
||||
{
|
||||
int waterPercentage = MathHelper.Clamp((int)Math.Ceiling(item.CurrentHull.WaterPercentage), 0, 100);
|
||||
int waterPercentage = 0;
|
||||
//ignore minuscule amounts of water
|
||||
if (item.CurrentHull.WaterVolume < 1.0f)
|
||||
{
|
||||
waterPercentage = MathHelper.Clamp((int)Math.Ceiling(item.CurrentHull.WaterPercentage), 0, 100);
|
||||
}
|
||||
item.SendSignal(waterPercentage.ToString(), "water_%");
|
||||
}
|
||||
string highPressureOut = (item.CurrentHull == null || item.CurrentHull.LethalPressure > 5.0f) ? "1" : "0";
|
||||
|
||||
@@ -112,7 +112,10 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
return HasRequiredContainedItems(user: null, addMessage: false);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns the wifi components that can receive signals from this one
|
||||
/// </summary>
|
||||
public IEnumerable<WifiComponent> GetReceiversInRange()
|
||||
{
|
||||
return list.Where(w => w != this && w.CanReceive(this));
|
||||
@@ -121,11 +124,7 @@ namespace Barotrauma.Items.Components
|
||||
public bool CanReceive(WifiComponent sender)
|
||||
{
|
||||
if (sender == null || sender.channel != channel) { return false; }
|
||||
|
||||
if (sender.TeamID != TeamID && !AllowCrossTeamCommunication)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (sender.TeamID != TeamID && !AllowCrossTeamCommunication) { return false; }
|
||||
|
||||
//if the component is not linked to chat and has nothing connected to the output, sending a signal to it does nothing
|
||||
// = no point in receiving
|
||||
@@ -142,6 +141,21 @@ namespace Barotrauma.Items.Components
|
||||
return HasRequiredContainedItems(user: null, addMessage: false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the wifi components that can transmit signals to this one
|
||||
/// </summary>
|
||||
public IEnumerable<WifiComponent> GetTransmittersInRange()
|
||||
{
|
||||
return list.Where(w => w != this && w.CanTransmit(this));
|
||||
}
|
||||
|
||||
public bool CanTransmit(WifiComponent sender)
|
||||
{
|
||||
if (sender == null || sender.channel != channel) { return false; }
|
||||
if (sender.TeamID != TeamID && !AllowCrossTeamCommunication) { return false; }
|
||||
if (Vector2.DistanceSquared(item.WorldPosition, sender.item.WorldPosition) > sender.range * sender.range) { return false; }
|
||||
return HasRequiredContainedItems(user: null, addMessage: false);
|
||||
}
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
chatMsgCooldown -= deltaTime;
|
||||
|
||||
@@ -756,14 +756,17 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (item.ParentInventory != null) { return; }
|
||||
#if CLIENT
|
||||
if (!relativeToSub && Screen.Selected != GameMain.SubEditorScreen) { return; }
|
||||
if (!relativeToSub)
|
||||
{
|
||||
if (Screen.Selected != GameMain.SubEditorScreen || (item.Submarine?.Loading ?? false)) { return; }
|
||||
}
|
||||
#else
|
||||
if (!relativeToSub) { return; }
|
||||
#endif
|
||||
|
||||
Vector2 refPos = item.Submarine == null ?
|
||||
Vector2.Zero :
|
||||
item.Position - item.Submarine.HiddenSubPosition;
|
||||
item.Position - item.Submarine.HiddenSubPosition;
|
||||
|
||||
for (int i = 0; i < nodes.Count; i++)
|
||||
{
|
||||
|
||||
@@ -64,6 +64,8 @@ namespace Barotrauma.Items.Components
|
||||
private Character currentTarget;
|
||||
const float aiFindTargetInterval = 5.0f;
|
||||
|
||||
private int currentLoaderIndex;
|
||||
|
||||
private const float TinkeringPowerCostReduction = 0.2f;
|
||||
private const float TinkeringDamageIncrease = 0.2f;
|
||||
private const float TinkeringReloadDecrease = 0.2f;
|
||||
@@ -592,16 +594,17 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (MapEntity e in item.linkedTo)
|
||||
for (int j = 0; j < item.linkedTo.Count; j++)
|
||||
{
|
||||
var e = item.linkedTo[(j + currentLoaderIndex) % item.linkedTo.Count];
|
||||
//use linked projectile containers in case they have to react to the turret being launched somehow
|
||||
//(play a sound, spawn more projectiles)
|
||||
if (!(e is Item linkedItem)) { continue; }
|
||||
if (!item.prefab.IsLinkAllowed(e.prefab)) { continue; }
|
||||
if (linkedItem.Condition <= 0.0f)
|
||||
if (linkedItem.Condition <= 0.0f)
|
||||
{
|
||||
loaderBroken = true;
|
||||
continue;
|
||||
continue;
|
||||
}
|
||||
ItemContainer projectileContainer = linkedItem.GetComponent<ItemContainer>();
|
||||
if (projectileContainer != null)
|
||||
@@ -610,7 +613,6 @@ namespace Barotrauma.Items.Components
|
||||
projectiles = GetLoadedProjectiles();
|
||||
if (projectiles.Any()) { break; }
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
if (projectiles.Count == 0 && !LaunchWithoutProjectile)
|
||||
@@ -705,6 +707,10 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
ShiftItemsInProjectileContainer(container.GetComponent<ItemContainer>());
|
||||
}
|
||||
if (item.linkedTo.Count > 0)
|
||||
{
|
||||
currentLoaderIndex = (currentLoaderIndex + 1) % item.linkedTo.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -759,6 +765,7 @@ namespace Barotrauma.Items.Components
|
||||
Projectile projectileComponent = projectile.GetComponent<Projectile>();
|
||||
if (projectileComponent != null)
|
||||
{
|
||||
projectileComponent.Launcher = item;
|
||||
projectileComponent.Attacker = projectileComponent.User = user;
|
||||
if (projectileComponent.Attack != null)
|
||||
{
|
||||
@@ -1163,7 +1170,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
targetPos = closestEnemy.WorldPosition;
|
||||
//if the enemy is inside another sub, aim at the room they're in to make it less obvious that the enemy "knows" exactly where the target is
|
||||
if (closestEnemy.Submarine != null && closestEnemy.CurrentHull != null && closestEnemy.Submarine != item.Submarine)
|
||||
if (closestEnemy.Submarine != null && closestEnemy.CurrentHull != null && closestEnemy.Submarine != item.Submarine && !closestEnemy.CanSeeTarget(Item))
|
||||
{
|
||||
targetPos = closestEnemy.CurrentHull.WorldPosition;
|
||||
}
|
||||
@@ -1442,8 +1449,9 @@ namespace Barotrauma.Items.Components
|
||||
List<Projectile> projectiles = new List<Projectile>();
|
||||
// check the item itself first
|
||||
CheckProjectileContainer(item, projectiles, out bool _);
|
||||
foreach (MapEntity e in item.linkedTo)
|
||||
for (int j = 0; j < item.linkedTo.Count; j++)
|
||||
{
|
||||
var e = item.linkedTo[(j + currentLoaderIndex) % item.linkedTo.Count];
|
||||
if (!item.prefab.IsLinkAllowed(e.prefab)) { continue; }
|
||||
if (e is Item projectileContainer)
|
||||
{
|
||||
@@ -1451,7 +1459,6 @@ namespace Barotrauma.Items.Components
|
||||
if (projectiles.Any() || stopSearching) { return projectiles; }
|
||||
}
|
||||
}
|
||||
|
||||
return projectiles;
|
||||
}
|
||||
|
||||
|
||||
@@ -480,7 +480,7 @@ namespace Barotrauma
|
||||
if (i < 0 || i >= slots.Length)
|
||||
{
|
||||
string errorMsg = "Inventory.TryPutItem failed: index was out of range(" + i + ").\n" + Environment.StackTrace.CleanupStackTrace();
|
||||
GameAnalyticsManager.AddErrorEventOnce("Inventory.TryPutItem:IndexOutOfRange", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
|
||||
GameAnalyticsManager.AddErrorEventOnce("Inventory.TryPutItem:IndexOutOfRange", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -531,7 +531,7 @@ namespace Barotrauma
|
||||
if (i < 0 || i >= slots.Length)
|
||||
{
|
||||
string errorMsg = "Inventory.PutItem failed: index was out of range(" + i + ").\n" + Environment.StackTrace.CleanupStackTrace();
|
||||
GameAnalyticsManager.AddErrorEventOnce("Inventory.PutItem:IndexOutOfRange", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
|
||||
GameAnalyticsManager.AddErrorEventOnce("Inventory.PutItem:IndexOutOfRange", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace Barotrauma
|
||||
|
||||
public static bool ShowLinks = true;
|
||||
|
||||
private HashSet<string> tags;
|
||||
private readonly HashSet<string> tags;
|
||||
|
||||
private bool isWire, isLogic;
|
||||
|
||||
@@ -40,6 +40,10 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public float HullOxygenPercentage
|
||||
{
|
||||
get { return CurrentHull?.OxygenPercentage ?? 0.0f; }
|
||||
}
|
||||
|
||||
private CampaignMode.InteractionType campaignInteractionType = CampaignMode.InteractionType.None;
|
||||
public CampaignMode.InteractionType CampaignInteractionType
|
||||
@@ -64,13 +68,13 @@ namespace Barotrauma
|
||||
#endif
|
||||
|
||||
//components that determine the functionality of the item
|
||||
private Dictionary<Type, ItemComponent> componentsByType = new Dictionary<Type, ItemComponent>();
|
||||
private List<ItemComponent> components;
|
||||
private readonly Dictionary<Type, ItemComponent> componentsByType = new Dictionary<Type, ItemComponent>();
|
||||
private readonly List<ItemComponent> components;
|
||||
/// <summary>
|
||||
/// Components that are Active or need to be updated for some other reason (status effects, sounds)
|
||||
/// </summary>
|
||||
private readonly List<ItemComponent> updateableComponents = new List<ItemComponent>();
|
||||
private List<IDrawableComponent> drawableComponents;
|
||||
private readonly List<IDrawableComponent> drawableComponents;
|
||||
private bool hasComponentsToDraw;
|
||||
|
||||
public PhysicsBody body;
|
||||
@@ -99,7 +103,7 @@ namespace Barotrauma
|
||||
|
||||
private readonly List<Repairable> repairables;
|
||||
|
||||
private Quality qualityComponent;
|
||||
private readonly Quality qualityComponent;
|
||||
|
||||
private readonly Queue<float> impactQueue = new Queue<float>();
|
||||
|
||||
@@ -358,7 +362,6 @@ namespace Barotrauma
|
||||
protected set;
|
||||
}
|
||||
|
||||
[Serialize("", false)]
|
||||
/// <summary>
|
||||
/// Can be used by status effects or conditionals to check what item this item is contained inside
|
||||
/// </summary>
|
||||
@@ -371,7 +374,6 @@ namespace Barotrauma
|
||||
ParentInventory?.Owner?.ToString() ??
|
||||
"";
|
||||
}
|
||||
set { /*do nothing*/ }
|
||||
}
|
||||
|
||||
|
||||
@@ -392,7 +394,6 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
[Serialize(false, false)]
|
||||
/// <summary>
|
||||
/// Can be used by status effects or conditionals to check if the physics body of the item is active
|
||||
/// </summary>
|
||||
@@ -402,7 +403,6 @@ namespace Barotrauma
|
||||
{
|
||||
return body != null && body.Enabled;
|
||||
}
|
||||
set { /*do nothing*/ }
|
||||
}
|
||||
|
||||
[Serialize(0.0f, false)]
|
||||
@@ -540,9 +540,19 @@ namespace Barotrauma
|
||||
isActive = true;
|
||||
}
|
||||
}
|
||||
|
||||
LastConditionChange = condition - prev;
|
||||
ConditionLastUpdated = Timing.TotalTime;
|
||||
}
|
||||
}
|
||||
|
||||
private double ConditionLastUpdated { get; set; }
|
||||
private float LastConditionChange { get; set; }
|
||||
/// <summary>
|
||||
/// Return true if the condition of this item increased within the last second.
|
||||
/// </summary>
|
||||
public bool ConditionIncreasedRecently => (Timing.TotalTime < ConditionLastUpdated + 1.0f) && LastConditionChange > 0.0f;
|
||||
|
||||
public float Health
|
||||
{
|
||||
get { return condition; }
|
||||
@@ -998,7 +1008,8 @@ namespace Barotrauma
|
||||
|
||||
partial void InitProjSpecific();
|
||||
|
||||
public bool IsContainerPreferred(ItemContainer container, out bool isPreferencesDefined, out bool isSecondary) => Prefab.IsContainerPreferred(this, container, out isPreferencesDefined, out isSecondary);
|
||||
public bool IsContainerPreferred(ItemContainer container, out bool isPreferencesDefined, out bool isSecondary, bool requireConditionRestriction = false)
|
||||
=> Prefab.IsContainerPreferred(this, container, out isPreferencesDefined, out isSecondary, requireConditionRestriction);
|
||||
|
||||
public override MapEntity Clone()
|
||||
{
|
||||
@@ -1018,7 +1029,7 @@ namespace Barotrauma
|
||||
errorMsg += "Original components: " + string.Join(", ", components.Select(c => c.GetType().ToString()));
|
||||
errorMsg += ", cloned components: " + string.Join(", ", clone.components.Select(c => c.GetType().ToString()));
|
||||
DebugConsole.ThrowError(errorMsg);
|
||||
GameAnalyticsManager.AddErrorEventOnce("Item.Clone:" + Name, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
|
||||
GameAnalyticsManager.AddErrorEventOnce("Item.Clone:" + Name, GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
|
||||
}
|
||||
|
||||
for (int i = 0; i < components.Count && i < clone.components.Count; i++)
|
||||
@@ -1190,7 +1201,7 @@ namespace Barotrauma
|
||||
DebugConsole.ThrowError(errorMsg);
|
||||
GameAnalyticsManager.AddErrorEventOnce(
|
||||
"Item.SetPosition:InvalidPosition" + ID,
|
||||
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
|
||||
GameAnalyticsManager.ErrorSeverity.Error,
|
||||
errorMsg);
|
||||
return;
|
||||
}
|
||||
@@ -2014,7 +2025,7 @@ namespace Barotrauma
|
||||
var wifiComponent = recipient.Item.GetComponent<WifiComponent>();
|
||||
if (wifiComponent != null && wifiComponent.CanTransmit())
|
||||
{
|
||||
foreach (var wifiReceiver in wifiComponent.GetReceiversInRange())
|
||||
foreach (var wifiReceiver in wifiComponent.GetTransmittersInRange())
|
||||
{
|
||||
var receiverConnections = wifiReceiver.Item.Connections;
|
||||
if (receiverConnections == null) { continue; }
|
||||
@@ -2171,7 +2182,7 @@ namespace Barotrauma
|
||||
return c != null && c.Character != null && c.Character.CanInteractWith(this);
|
||||
}
|
||||
|
||||
public bool TryInteract(Character picker, bool ignoreRequiredItems = false, bool forceSelectKey = false, bool forceActionKey = false)
|
||||
public bool TryInteract(Character user, bool ignoreRequiredItems = false, bool forceSelectKey = false, bool forceUseKey = false)
|
||||
{
|
||||
if (CampaignInteractionType != CampaignMode.InteractionType.None) { return false; }
|
||||
|
||||
@@ -2181,12 +2192,11 @@ namespace Barotrauma
|
||||
Skill requiredSkill = null;
|
||||
float skillMultiplier = 1;
|
||||
#endif
|
||||
if (!IsInteractable(picker)) { return false; }
|
||||
if (!IsInteractable(user)) { return false; }
|
||||
foreach (ItemComponent ic in components)
|
||||
{
|
||||
bool pickHit = false, selectHit = false;
|
||||
|
||||
if (picker.IsKeyDown(InputType.Aim))
|
||||
if (user.IsKeyDown(InputType.Aim))
|
||||
{
|
||||
pickHit = false;
|
||||
selectHit = false;
|
||||
@@ -2195,26 +2205,44 @@ namespace Barotrauma
|
||||
{
|
||||
if (forceSelectKey)
|
||||
{
|
||||
if (ic.PickKey == InputType.Select) pickHit = true;
|
||||
if (ic.SelectKey == InputType.Select) selectHit = true;
|
||||
if (ic.PickKey == InputType.Select)
|
||||
{
|
||||
pickHit = true;
|
||||
}
|
||||
if (ic.SelectKey == InputType.Select)
|
||||
{
|
||||
selectHit = true;
|
||||
}
|
||||
}
|
||||
else if (forceActionKey)
|
||||
else if (forceUseKey)
|
||||
{
|
||||
if (ic.PickKey == InputType.Use) pickHit = true;
|
||||
if (ic.SelectKey == InputType.Use) selectHit = true;
|
||||
if (ic.PickKey == InputType.Use)
|
||||
{
|
||||
pickHit = true;
|
||||
}
|
||||
if (ic.SelectKey == InputType.Use)
|
||||
{
|
||||
selectHit = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
pickHit = picker.IsKeyHit(ic.PickKey);
|
||||
selectHit = picker.IsKeyHit(ic.SelectKey);
|
||||
pickHit = user.IsKeyHit(ic.PickKey);
|
||||
selectHit = user.IsKeyHit(ic.SelectKey);
|
||||
|
||||
#if CLIENT
|
||||
//if the cursor is on a UI component, disable interaction with the left mouse button
|
||||
//to prevent accidentally selecting items when clicking UI elements
|
||||
if (picker == Character.Controlled && GUI.MouseOn != null)
|
||||
if (user == Character.Controlled && GUI.MouseOn != null)
|
||||
{
|
||||
if (GameMain.Config.KeyBind(ic.PickKey).MouseButton == 0) pickHit = false;
|
||||
if (GameMain.Config.KeyBind(ic.SelectKey).MouseButton == 0) selectHit = false;
|
||||
if (GameMain.Config.KeyBind(ic.PickKey).MouseButton == 0)
|
||||
{
|
||||
pickHit = false;
|
||||
}
|
||||
if (GameMain.Config.KeyBind(ic.SelectKey).MouseButton == 0)
|
||||
{
|
||||
selectHit = false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -2225,50 +2253,50 @@ namespace Barotrauma
|
||||
if (Screen.Selected == GameMain.SubEditorScreen && GameMain.SubEditorScreen.WiringMode)
|
||||
{
|
||||
pickHit = selectHit = GameMain.Config.KeyBind(InputType.Use).MouseButton == MouseButton.None ?
|
||||
picker.IsKeyHit(InputType.Use) :
|
||||
picker.IsKeyHit(InputType.Select);
|
||||
user.IsKeyHit(InputType.Use) :
|
||||
user.IsKeyHit(InputType.Select);
|
||||
}
|
||||
#endif
|
||||
if (!pickHit && !selectHit) { continue; }
|
||||
|
||||
bool showUiMsg = false;
|
||||
#if CLIENT
|
||||
if (!ic.HasRequiredSkills(picker, out Skill tempRequiredSkill)) { hasRequiredSkills = false; skillMultiplier = ic.GetSkillMultiplier(); }
|
||||
showUiMsg = picker == Character.Controlled && Screen.Selected != GameMain.SubEditorScreen;
|
||||
if (!ic.HasRequiredSkills(user, out Skill tempRequiredSkill)) { hasRequiredSkills = false; skillMultiplier = ic.GetSkillMultiplier(); }
|
||||
showUiMsg = user == Character.Controlled && Screen.Selected != GameMain.SubEditorScreen;
|
||||
#endif
|
||||
if (!ignoreRequiredItems && !ic.HasRequiredItems(picker, showUiMsg)) { continue; }
|
||||
if ((ic.CanBePicked && pickHit && ic.Pick(picker)) ||
|
||||
(ic.CanBeSelected && selectHit && ic.Select(picker)))
|
||||
if (!ignoreRequiredItems && !ic.HasRequiredItems(user, showUiMsg)) { continue; }
|
||||
if ((ic.CanBePicked && pickHit && ic.Pick(user)) ||
|
||||
(ic.CanBeSelected && selectHit && ic.Select(user)))
|
||||
{
|
||||
picked = true;
|
||||
ic.ApplyStatusEffects(ActionType.OnPicked, 1.0f, picker);
|
||||
ic.ApplyStatusEffects(ActionType.OnPicked, 1.0f, user);
|
||||
#if CLIENT
|
||||
if (picker == Character.Controlled) { GUI.ForceMouseOn(null); }
|
||||
if (user == Character.Controlled) { GUI.ForceMouseOn(null); }
|
||||
if (tempRequiredSkill != null) { requiredSkill = tempRequiredSkill; }
|
||||
#endif
|
||||
if (ic.CanBeSelected) { selected = true; }
|
||||
if (ic.CanBeSelected && !(ic is Door)) { selected = true; }
|
||||
}
|
||||
}
|
||||
|
||||
if (!picked) { return false; }
|
||||
|
||||
if (picker != null)
|
||||
if (user != null)
|
||||
{
|
||||
if (picker.SelectedConstruction == this)
|
||||
if (user.SelectedConstruction == this)
|
||||
{
|
||||
if (picker.IsKeyHit(InputType.Select) || forceSelectKey)
|
||||
if (user.IsKeyHit(InputType.Select) || forceSelectKey)
|
||||
{
|
||||
picker.SelectedConstruction = null;
|
||||
user.SelectedConstruction = null;
|
||||
}
|
||||
}
|
||||
else if (selected)
|
||||
{
|
||||
picker.SelectedConstruction = this;
|
||||
user.SelectedConstruction = this;
|
||||
}
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
if (!hasRequiredSkills && Character.Controlled == picker && Screen.Selected != GameMain.SubEditorScreen)
|
||||
if (!hasRequiredSkills && Character.Controlled == user && Screen.Selected != GameMain.SubEditorScreen)
|
||||
{
|
||||
if (requiredSkill != null)
|
||||
{
|
||||
@@ -2278,7 +2306,10 @@ namespace Barotrauma
|
||||
}
|
||||
#endif
|
||||
|
||||
if (Container != null) Container.RemoveContained(this);
|
||||
if (Container != null)
|
||||
{
|
||||
Container.RemoveContained(this);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -2553,21 +2584,21 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
object value = property.GetValue(propertyOwner.First);
|
||||
if (value is string)
|
||||
if (value is string stringVal)
|
||||
{
|
||||
msg.Write((string)value);
|
||||
msg.Write(stringVal);
|
||||
}
|
||||
else if (value is float)
|
||||
else if (value is float floatVal)
|
||||
{
|
||||
msg.Write((float)value);
|
||||
msg.Write(floatVal);
|
||||
}
|
||||
else if (value is int)
|
||||
else if (value is int intVal)
|
||||
{
|
||||
msg.Write((int)value);
|
||||
msg.Write(intVal);
|
||||
}
|
||||
else if (value is bool)
|
||||
else if (value is bool boolVal)
|
||||
{
|
||||
msg.Write((bool)value);
|
||||
msg.Write(boolVal);
|
||||
}
|
||||
else if (value is Color color)
|
||||
{
|
||||
@@ -2576,35 +2607,35 @@ namespace Barotrauma
|
||||
msg.Write(color.B);
|
||||
msg.Write(color.A);
|
||||
}
|
||||
else if (value is Vector2)
|
||||
else if (value is Vector2 vector2)
|
||||
{
|
||||
msg.Write(((Vector2)value).X);
|
||||
msg.Write(((Vector2)value).Y);
|
||||
msg.Write(vector2.X);
|
||||
msg.Write(vector2.Y);
|
||||
}
|
||||
else if (value is Vector3)
|
||||
else if (value is Vector3 vector3)
|
||||
{
|
||||
msg.Write(((Vector3)value).X);
|
||||
msg.Write(((Vector3)value).Y);
|
||||
msg.Write(((Vector3)value).Z);
|
||||
msg.Write(vector3.X);
|
||||
msg.Write(vector3.Y);
|
||||
msg.Write(vector3.Z);
|
||||
}
|
||||
else if (value is Vector4)
|
||||
else if (value is Vector4 vector4)
|
||||
{
|
||||
msg.Write(((Vector4)value).X);
|
||||
msg.Write(((Vector4)value).Y);
|
||||
msg.Write(((Vector4)value).Z);
|
||||
msg.Write(((Vector4)value).W);
|
||||
msg.Write(vector4.X);
|
||||
msg.Write(vector4.Y);
|
||||
msg.Write(vector4.Z);
|
||||
msg.Write(vector4.W);
|
||||
}
|
||||
else if (value is Point)
|
||||
else if (value is Point point)
|
||||
{
|
||||
msg.Write(((Point)value).X);
|
||||
msg.Write(((Point)value).Y);
|
||||
msg.Write(point.X);
|
||||
msg.Write(point.Y);
|
||||
}
|
||||
else if (value is Rectangle)
|
||||
else if (value is Rectangle rect)
|
||||
{
|
||||
msg.Write(((Rectangle)value).X);
|
||||
msg.Write(((Rectangle)value).Y);
|
||||
msg.Write(((Rectangle)value).Width);
|
||||
msg.Write(((Rectangle)value).Height);
|
||||
msg.Write(rect.X);
|
||||
msg.Write(rect.Y);
|
||||
msg.Write(rect.Width);
|
||||
msg.Write(rect.Height);
|
||||
}
|
||||
else if (value is Enum)
|
||||
{
|
||||
@@ -2756,7 +2787,7 @@ namespace Barotrauma
|
||||
#endif
|
||||
GameAnalyticsManager.AddErrorEventOnce(
|
||||
"Item.ReadPropertyChange:" + Name + ":" + type,
|
||||
GameAnalyticsSDK.Net.EGAErrorSeverity.Warning,
|
||||
GameAnalyticsManager.ErrorSeverity.Warning,
|
||||
"Failed to convert the int value \"" + intVal + "\" to " + type + " (item " + Name + ")");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ namespace Barotrauma
|
||||
DebugConsole.ThrowError(errorMsg);
|
||||
GameAnalyticsManager.AddErrorEventOnce(
|
||||
"ItemInventory.CreateServerEvent:EventForUninitializedItem" + container.Item.Name + container.Item.ID,
|
||||
GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
|
||||
GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@ namespace Barotrauma
|
||||
float maxCondition = subElement.GetAttributeFloat("maxcondition", 1.0f);
|
||||
//Substract mincondition from required item's condition or delete it regardless?
|
||||
bool useCondition = subElement.GetAttributeBool("usecondition", true);
|
||||
int count = subElement.GetAttributeInt("count", 1);
|
||||
int amount = subElement.GetAttributeInt("count", subElement.GetAttributeInt("amount", 1));
|
||||
|
||||
if (!string.IsNullOrEmpty(requiredItemIdentifier))
|
||||
{
|
||||
@@ -153,11 +153,11 @@ namespace Barotrauma
|
||||
MathUtils.NearlyEqual(r.MinCondition, minCondition) && MathUtils.NearlyEqual(r.MaxCondition, maxCondition));
|
||||
if (existing == null)
|
||||
{
|
||||
RequiredItems.Add(new RequiredItem(requiredItem, count, minCondition, maxCondition, useCondition));
|
||||
RequiredItems.Add(new RequiredItem(requiredItem, amount, minCondition, maxCondition, useCondition));
|
||||
}
|
||||
else
|
||||
{
|
||||
existing.Amount += count;
|
||||
existing.Amount += amount;
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -175,11 +175,11 @@ namespace Barotrauma
|
||||
MathUtils.NearlyEqual(r.MaxCondition, maxCondition));
|
||||
if (existing == null)
|
||||
{
|
||||
RequiredItems.Add(new RequiredItem(matchingItems, count, minCondition, maxCondition, useCondition));
|
||||
RequiredItems.Add(new RequiredItem(matchingItems, amount, minCondition, maxCondition, useCondition));
|
||||
}
|
||||
else
|
||||
{
|
||||
existing.Amount += count;
|
||||
existing.Amount += amount;
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -549,6 +549,13 @@ namespace Barotrauma
|
||||
private set;
|
||||
}
|
||||
|
||||
[Serialize(0.0f, false)]
|
||||
public float AddedPickingSpeedMultiplier
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[Serialize(false, false)]
|
||||
public bool CannotRepairFail
|
||||
{
|
||||
@@ -1298,21 +1305,24 @@ namespace Barotrauma
|
||||
public ImmutableDictionary<string, PriceInfo> GetBuyPricesUnder(int maxCost = 0)
|
||||
{
|
||||
Dictionary<string, PriceInfo> priceLocations = new Dictionary<string, PriceInfo>();
|
||||
foreach (KeyValuePair<string, PriceInfo> locationPrice in locationPrices)
|
||||
if (locationPrices != null)
|
||||
{
|
||||
PriceInfo priceInfo = locationPrice.Value;
|
||||
foreach (KeyValuePair<string, PriceInfo> locationPrice in locationPrices)
|
||||
{
|
||||
PriceInfo priceInfo = locationPrice.Value;
|
||||
|
||||
if (priceInfo == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!priceInfo.CanBeBought)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (priceInfo.Price < maxCost || maxCost == 0)
|
||||
{
|
||||
priceLocations.Add(locationPrice.Key, priceInfo);
|
||||
if (priceInfo == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!priceInfo.CanBeBought)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (priceInfo.Price < maxCost || maxCost == 0)
|
||||
{
|
||||
priceLocations.Add(locationPrice.Key, priceInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
return priceLocations.ToImmutableDictionary();
|
||||
@@ -1343,17 +1353,18 @@ namespace Barotrauma
|
||||
return priceLocations.ToImmutableDictionary();
|
||||
}
|
||||
|
||||
public bool IsContainerPreferred(Item item, ItemContainer targetContainer, out bool isPreferencesDefined, out bool isSecondary)
|
||||
public bool IsContainerPreferred(Item item, ItemContainer targetContainer, out bool isPreferencesDefined, out bool isSecondary, bool requireConditionRequirement = false)
|
||||
{
|
||||
isPreferencesDefined = PreferredContainers.Any();
|
||||
isSecondary = false;
|
||||
if (!isPreferencesDefined) { return true; }
|
||||
if (PreferredContainers.Any(pc => IsItemConditionAcceptable(item, pc) && IsContainerPreferred(pc.Primary, targetContainer)))
|
||||
if (PreferredContainers.Any(pc => (!requireConditionRequirement || HasConditionRequirement(pc)) && IsItemConditionAcceptable(item, pc) && IsContainerPreferred(pc.Primary, targetContainer)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
isSecondary = true;
|
||||
return PreferredContainers.Any(pc => IsItemConditionAcceptable(item, pc) && IsContainerPreferred(pc.Secondary, targetContainer));
|
||||
return PreferredContainers.Any(pc => (!requireConditionRequirement || HasConditionRequirement(pc)) && IsItemConditionAcceptable(item, pc) && IsContainerPreferred(pc.Secondary, targetContainer));
|
||||
static bool HasConditionRequirement(PreferredContainer pc) => pc.MinCondition > 0 || pc.MaxCondition < 100;
|
||||
}
|
||||
|
||||
public bool IsContainerPreferred(Item item, string[] identifiersOrTags, out bool isPreferencesDefined, out bool isSecondary)
|
||||
|
||||
@@ -159,7 +159,7 @@ namespace Barotrauma
|
||||
DebugConsole.ThrowError($"Error while removing entity \"{e}\"", exception);
|
||||
GameAnalyticsManager.AddErrorEventOnce(
|
||||
$"Entity.RemoveAll:Exception{e}",
|
||||
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
|
||||
GameAnalyticsManager.ErrorSeverity.Error,
|
||||
$"Error while removing entity \"{e} ({exception.Message})\n{exception.StackTrace.CleanupStackTrace()}");
|
||||
}
|
||||
}
|
||||
@@ -223,7 +223,7 @@ namespace Barotrauma
|
||||
{
|
||||
DebugConsole.ThrowError(errorLine);
|
||||
}
|
||||
GameAnalyticsManager.AddErrorEventOnce("Entity.RemoveAll", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg.ToString());
|
||||
GameAnalyticsManager.AddErrorEventOnce("Entity.RemoveAll", GameAnalyticsManager.ErrorSeverity.Error, errorMsg.ToString());
|
||||
}
|
||||
|
||||
dictionary.Clear();
|
||||
@@ -243,14 +243,14 @@ namespace Barotrauma
|
||||
DebugConsole.ThrowError($"Entity {ToString()} ({ID}) not present in entity dictionary.");
|
||||
GameAnalyticsManager.AddErrorEventOnce(
|
||||
$"Entity.FreeID:EntityNotFound{ID}",
|
||||
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
|
||||
GameAnalyticsManager.ErrorSeverity.Error,
|
||||
$"Entity {ToString()} ({ID}) not present in entity dictionary.\n{Environment.StackTrace.CleanupStackTrace()}");
|
||||
}
|
||||
else if (existingEntity != this)
|
||||
{
|
||||
DebugConsole.ThrowError($"Entity ID mismatch in entity dictionary. Entity {existingEntity} had the ID {ID} (expecting {ToString()})");
|
||||
GameAnalyticsManager.AddErrorEventOnce("Entity.FreeID:EntityMismatch" + ID,
|
||||
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
|
||||
GameAnalyticsManager.ErrorSeverity.Error,
|
||||
$"Entity ID mismatch in entity dictionary. Entity {existingEntity} had the ID {ID} (expecting {ToString()})");
|
||||
}
|
||||
else
|
||||
|
||||
@@ -1575,7 +1575,7 @@ namespace Barotrauma
|
||||
{
|
||||
string errorMsg = "Error - tried to save a hull that's not a part of any submarine.\n" + Environment.StackTrace.CleanupStackTrace();
|
||||
DebugConsole.ThrowError(errorMsg);
|
||||
GameAnalyticsManager.AddErrorEventOnce("Hull.Save:WorldHull", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
|
||||
GameAnalyticsManager.AddErrorEventOnce("Hull.Save:WorldHull", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user