(3dc4135ce) v0.9.5.1

This commit is contained in:
Regalis
2019-11-21 18:22:25 +01:00
parent b39922a074
commit 5c95c53118
287 changed files with 12655 additions and 5048 deletions
@@ -1,8 +1,9 @@
using Microsoft.Xna.Framework;
using System.Collections.Generic;
namespace Barotrauma
{
public enum AIState { Idle, Attack, Escape, Eat }
public enum AIState { Idle, Attack, Escape, Eat, Flee }
abstract partial class AIController : ISteerable
{
@@ -11,7 +12,11 @@ namespace Barotrauma
public readonly Character Character;
private AIState state;
private AIState previousState;
// Update only when the value changes, not when it keeps the same.
protected AITarget _lastAiTarget;
// Updated each time the value is updated (also when the value is the same).
protected AITarget _previousAiTarget;
protected AITarget _selectedAiTarget;
public AITarget SelectedAiTarget
@@ -21,6 +26,13 @@ namespace Barotrauma
{
_previousAiTarget = _selectedAiTarget;
_selectedAiTarget = value;
if (_selectedAiTarget != _previousAiTarget)
{
if (_previousAiTarget != null)
{
_lastAiTarget = _previousAiTarget;
}
}
}
}
@@ -72,16 +84,38 @@ namespace Barotrauma
get { return state; }
set
{
if (state == value) return;
if (state == value) { return; }
previousState = state;
OnStateChanged(state, value);
state = value;
}
}
public AIState PreviousState => previousState;
private IEnumerable<Hull> visibleHulls;
private float hullVisibilityTimer;
const float hullVisibilityInterval = 0.5f;
public IEnumerable<Hull> VisibleHulls
{
get
{
if (visibleHulls == null)
{
visibleHulls = Character.GetVisibleHulls();
}
return visibleHulls;
}
private set
{
visibleHulls = value;
}
}
public AIController (Character c)
{
Character = c;
hullVisibilityTimer = Rand.Range(0f, hullVisibilityTimer);
Enabled = true;
}
@@ -89,7 +123,18 @@ namespace Barotrauma
public virtual void SelectTarget(AITarget target) { }
public virtual void Update(float deltaTime) { }
public virtual void Update(float deltaTime)
{
if (hullVisibilityTimer > 0)
{
hullVisibilityTimer--;
}
else
{
hullVisibilityTimer = hullVisibilityInterval;
VisibleHulls = Character.GetVisibleHulls();
}
}
protected virtual void OnStateChanged(AIState from, AIState to) { }
File diff suppressed because it is too large Load Diff
@@ -4,6 +4,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
namespace Barotrauma
{
@@ -16,16 +17,20 @@ namespace Barotrauma
private float sortTimer;
private float crouchRaycastTimer;
private float reactTimer;
private float hullVisibilityTimer;
private float unreachableClearTimer;
private bool shouldCrouch;
const float reactionTime = 0.5f;
const float hullVisibilityInterval = 0.5f;
const float crouchRaycastInterval = 1;
const float sortObjectiveInterval = 1;
const float clearUnreachableInterval = 30;
private float flipTimer;
private const float FlipInterval = 0.5f;
public static float HULL_SAFETY_THRESHOLD = 50;
public HashSet<Hull> UnreachableHulls { get; private set; } = new HashSet<Hull>();
public HashSet<Hull> UnsafeHulls { get; private set; } = new HashSet<Hull>();
private SteeringManager outsideSteering, insideSteering;
@@ -50,23 +55,6 @@ namespace Barotrauma
private set;
}
private IEnumerable<Hull> visibleHulls;
public IEnumerable<Hull> VisibleHulls
{
get
{
if (visibleHulls == null)
{
visibleHulls = Character.GetVisibleHulls();
}
return visibleHulls;
}
private set
{
visibleHulls = value;
}
}
public HumanAIController(Character c) : base(c)
{
if (!c.IsHuman)
@@ -78,7 +66,6 @@ namespace Barotrauma
objectiveManager = new AIObjectiveManager(c);
reactTimer = Rand.Range(0f, reactionTime);
sortTimer = Rand.Range(0f, sortObjectiveInterval);
hullVisibilityTimer = Rand.Range(0f, hullVisibilityTimer);
InitProjSpecific();
}
partial void InitProjSpecific();
@@ -86,6 +73,17 @@ namespace Barotrauma
public override void Update(float deltaTime)
{
if (DisableCrewAI || Character.IsUnconscious || Character.Removed) { return; }
base.Update(deltaTime);
if (unreachableClearTimer > 0)
{
unreachableClearTimer -= deltaTime;
}
else
{
unreachableClearTimer = clearUnreachableInterval;
UnreachableHulls.Clear();
}
float maxDistanceToSub = 3000;
if (Character.Submarine != null || SelectedAiTarget?.Entity?.Submarine != null &&
@@ -110,17 +108,6 @@ namespace Barotrauma
CheckCrouching(deltaTime);
Character.ClearInputs();
if (hullVisibilityTimer > 0)
{
hullVisibilityTimer--;
}
else
{
hullVisibilityTimer = hullVisibilityInterval;
VisibleHulls = Character.GetVisibleHulls();
}
objectiveManager.UpdateObjectives(deltaTime);
if (sortTimer > 0.0f)
{
sortTimer -= deltaTime;
@@ -130,6 +117,8 @@ namespace Barotrauma
objectiveManager.SortObjectives();
sortTimer = sortObjectiveInterval;
}
objectiveManager.UpdateObjectives(deltaTime);
if (reactTimer > 0.0f)
{
reactTimer -= deltaTime;
@@ -221,96 +210,316 @@ namespace Barotrauma
float speedMultiplier = Character.SpeedMultiplier;
if (run || speedMultiplier <= 0.0f) targetMovement *= speedMultiplier;
Character.ResetSpeedMultiplier(); // Reset, items will set the value before the next update
if (Character.AnimController.InWater && targetMovement.LengthSquared() < 0.000001f)
{
bool isAiming = false;
var holdable = Character.SelectedConstruction?.GetComponent<Holdable>();
if (holdable != null)
{
isAiming = holdable.ControlPose;
}
bool swimInPlace = !isAiming;
if (swimInPlace && ObjectiveManager.GetActiveObjective() is AIObjectiveGoTo goToObjective)
{
if (goToObjective.Target != Character)
{
swimInPlace = false;
}
}
if (swimInPlace)
{
// Swim in place so that we don't fall motionless and look dead.
targetMovement = new Vector2(targetMovement.X, Rand.Range(-0.001f, 0.001f));
}
}
Character.AnimController.TargetMovement = targetMovement;
if (!Character.LockHands)
{
DropUnnecessaryItems();
UnequipUnnecessaryItems();
}
if (Character.IsKeyDown(InputType.Aim))
flipTimer -= deltaTime;
if (flipTimer <= 0.0f)
{
var cursorDiffX = Character.CursorPosition.X - Character.Position.X;
if (cursorDiffX > 10.0f)
Direction newDir = Character.AnimController.TargetDir;
if (Character.IsKeyDown(InputType.Aim))
{
Character.AnimController.TargetDir = Direction.Right;
var cursorDiffX = Character.CursorPosition.X - Character.Position.X;
if (cursorDiffX > 10.0f)
{
newDir = Direction.Right;
}
else if (cursorDiffX < -10.0f)
{
newDir = Direction.Left;
}
if (Character.SelectedConstruction != null) Character.SelectedConstruction.SecondaryUse(deltaTime, Character);
}
else if (cursorDiffX < -10.0f)
else if (Math.Abs(Character.AnimController.TargetMovement.X) > 0.1f && !Character.AnimController.InWater)
{
Character.AnimController.TargetDir = Direction.Left;
newDir = Character.AnimController.TargetMovement.X > 0.0f ? Direction.Right : Direction.Left;
}
if (newDir != Character.AnimController.TargetDir)
{
Character.AnimController.TargetDir = newDir;
flipTimer = FlipInterval;
}
if (Character.SelectedConstruction != null) Character.SelectedConstruction.SecondaryUse(deltaTime, Character);
}
else if (Math.Abs(Character.AnimController.TargetMovement.X) > 0.1f && !Character.AnimController.InWater)
{
Character.AnimController.TargetDir = Character.AnimController.TargetMovement.X > 0.0f ? Direction.Right : Direction.Left;
}
}
private void DropUnnecessaryItems()
private void UnequipUnnecessaryItems()
{
if (!NeedsDivingGear(Character.CurrentHull))
if (ObjectiveManager.HasActiveObjective<AIObjectiveDecontainItem>()) { return; }
if (findItemState == FindItemState.None || findItemState == FindItemState.Extinguisher)
{
bool oxygenLow = Character.OxygenAvailable < CharacterHealth.LowOxygenThreshold;
bool highPressure = Character.CurrentHull == null || Character.CurrentHull.LethalPressure > 0 && Character.PressureProtection <= 0;
bool shouldKeepTheGearOn = !ObjectiveManager.IsCurrentObjective<AIObjectiveIdle>();
bool removeDivingSuit = oxygenLow && !highPressure;
if (!removeDivingSuit)
if (!ObjectiveManager.IsCurrentObjective<AIObjectiveExtinguishFires>() && !objectiveManager.HasActiveObjective<AIObjectiveExtinguishFire>())
{
bool targetHasNoSuit = objectiveManager.CurrentOrder is AIObjectiveGoTo gtObj && gtObj.mimic && !HasDivingSuit(gtObj.Target as Character);
bool canDropTheSuit = Character.CurrentHull.WaterPercentage < 1 && !Character.IsClimbing && steeringManager == insideSteering && !PathSteering.InStairs;
removeDivingSuit = (!shouldKeepTheGearOn || targetHasNoSuit) && canDropTheSuit;
}
if (removeDivingSuit)
{
var divingSuit = Character.Inventory.FindItemByIdentifier("divingsuit") ?? Character.Inventory.FindItemByTag("divingsuit");
if (divingSuit != null)
var extinguisher = Character.Inventory.FindItemByTag("extinguisher");
if (extinguisher != null && Character.HasEquippedItem(extinguisher))
{
// TODO: take the item where it was taken from?
divingSuit.Drop(Character);
}
}
bool targetHasNoMask = objectiveManager.CurrentOrder is AIObjectiveGoTo gotoObjective && gotoObjective.mimic && !HasDivingMask(gotoObjective.Target as Character);
bool takeMaskOff = oxygenLow || (!shouldKeepTheGearOn && Character.CurrentHull.WaterPercentage < 20) || targetHasNoMask;
if (takeMaskOff)
{
var mask = Character.Inventory.FindItemByIdentifier("divingmask");
if (mask != null && Character.Inventory.IsInLimbSlot(mask, InvSlotType.Head))
{
// Try to put the mask in an Any slot, and drop it if that fails
if (!mask.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(mask, Character, new List<InvSlotType>() { InvSlotType.Any }))
if (ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
{
mask.Drop(Character);
extinguisher.Drop(Character);
}
else
{
findItemState = FindItemState.Extinguisher;
if (FindSuitableContainer(Character, extinguisher, out Item targetContainer))
{
findItemState = FindItemState.None;
itemIndex = 0;
if (targetContainer != null)
{
var decontainObjective = new AIObjectiveDecontainItem(Character, extinguisher, targetContainer.GetComponent<ItemContainer>(), ObjectiveManager, targetContainer.GetComponent<ItemContainer>());
decontainObjective.Abandoned += () => ignoredContainers.Add(targetContainer);
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
return;
}
else
{
extinguisher.Drop(Character);
}
}
}
}
}
}
if (!ObjectiveManager.IsCurrentObjective<AIObjectiveExtinguishFires>() && !ObjectiveManager.IsCurrentObjective<AIObjectiveExtinguishFire>())
if (findItemState == FindItemState.None || findItemState == FindItemState.DivingSuit || findItemState == FindItemState.DivingMask)
{
var extinguisherItem = Character.Inventory.FindItemByIdentifier("extinguisher") ?? Character.Inventory.FindItemByTag("extinguisher");
if (extinguisherItem != null && Character.HasEquippedItem(extinguisherItem))
if (!NeedsDivingGear(Character, Character.CurrentHull, out _))
{
// TODO: take the item where it was taken from?
extinguisherItem.Drop(Character);
}
}
foreach (var item in Character.Inventory.Items)
{
if (item == null) { continue; }
if (ObjectiveManager.CurrentObjective is AIObjectiveIdle)
{
if (item.AllowedSlots.Contains(InvSlotType.RightHand | InvSlotType.LeftHand) && Character.HasEquippedItem(item))
bool oxygenLow = Character.OxygenAvailable < CharacterHealth.LowOxygenThreshold;
bool shouldKeepTheGearOn = Character.AnimController.HeadInWater
|| Character.CurrentHull.WaterPercentage > 50
|| ObjectiveManager.IsCurrentObjective<AIObjectiveFindSafety>()
|| ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn);
bool removeDivingSuit = !Character.AnimController.HeadInWater && oxygenLow;
AIObjectiveGoTo gotoObjective = ObjectiveManager.CurrentOrder as AIObjectiveGoTo;
if (!removeDivingSuit)
{
// Try to put the weapon in an Any slot, and drop it if that fails
if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, new List<InvSlotType>() { InvSlotType.Any }))
bool targetHasNoSuit = gotoObjective != null && gotoObjective.mimic && !HasDivingSuit(gotoObjective.Target as Character);
removeDivingSuit = !shouldKeepTheGearOn && (gotoObjective == null || targetHasNoSuit);
}
bool takeMaskOff = !Character.AnimController.HeadInWater && oxygenLow;
if (!takeMaskOff && Character.CurrentHull.WaterPercentage < 40)
{
bool targetHasNoMask = gotoObjective != null && gotoObjective.mimic && !HasDivingMask(gotoObjective.Target as Character);
takeMaskOff = !shouldKeepTheGearOn && (gotoObjective == null || targetHasNoMask);
}
if (gotoObjective != null)
{
if (gotoObjective.Target is Hull h)
{
item.Drop(Character);
if (NeedsDivingGear(Character, h, out _))
{
removeDivingSuit = false;
takeMaskOff = false;
}
}
else if (gotoObjective.Target is Character c)
{
if (NeedsDivingGear(Character, c.CurrentHull, out _))
{
removeDivingSuit = false;
takeMaskOff = false;
}
}
else if (gotoObjective.Target is Item i)
{
if (NeedsDivingGear(Character, i.CurrentHull, out _))
{
removeDivingSuit = false;
takeMaskOff = false;
}
}
}
if (findItemState == FindItemState.None || findItemState == FindItemState.DivingSuit)
{
if (removeDivingSuit)
{
var divingSuit = Character.Inventory.FindItemByTag("divingsuit");
if (divingSuit != null)
{
if (oxygenLow || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
{
divingSuit.Drop(Character);
}
else
{
findItemState = FindItemState.DivingSuit;
if (FindSuitableContainer(Character, divingSuit, out Item targetContainer))
{
findItemState = FindItemState.None;
itemIndex = 0;
if (targetContainer != null)
{
var decontainObjective = new AIObjectiveDecontainItem(Character, divingSuit, targetContainer.GetComponent<ItemContainer>(), ObjectiveManager, targetContainer.GetComponent<ItemContainer>());
decontainObjective.Abandoned += () => ignoredContainers.Add(targetContainer);
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
return;
}
else
{
divingSuit.Drop(Character);
}
}
}
}
}
}
if (findItemState == FindItemState.None || findItemState == FindItemState.DivingMask)
{
if (takeMaskOff)
{
var mask = Character.Inventory.FindItemByTag("divingmask");
if (mask != null && Character.Inventory.IsInLimbSlot(mask, InvSlotType.Head))
{
if (!mask.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(mask, Character, new List<InvSlotType>() { InvSlotType.Any }))
{
if (oxygenLow || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
{
mask.Drop(Character);
}
else
{
findItemState = FindItemState.DivingMask;
if (FindSuitableContainer(Character, mask, out Item targetContainer))
{
findItemState = FindItemState.None;
itemIndex = 0;
if (targetContainer != null)
{
var decontainObjective = new AIObjectiveDecontainItem(Character, mask, targetContainer.GetComponent<ItemContainer>(), ObjectiveManager, targetContainer.GetComponent<ItemContainer>());
decontainObjective.Abandoned += () => ignoredContainers.Add(targetContainer);
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
return;
}
else
{
mask.Drop(Character);
}
}
}
}
}
}
}
}
}
if (findItemState == FindItemState.None || findItemState == FindItemState.OtherItem)
{
if (ObjectiveManager.IsCurrentObjective<AIObjectiveIdle>() ||
ObjectiveManager.IsCurrentObjective<AIObjectiveOperateItem>() ||
ObjectiveManager.IsCurrentObjective<AIObjectivePumpWater>() ||
ObjectiveManager.IsCurrentObjective<AIObjectiveChargeBatteries>())
{
foreach (var item in Character.Inventory.Items)
{
if (item == null) { continue; }
if (Character.HasEquippedItem(item) &&
(Character.Inventory.IsInLimbSlot(item, InvSlotType.RightHand) ||
Character.Inventory.IsInLimbSlot(item, InvSlotType.LeftHand) ||
Character.Inventory.IsInLimbSlot(item, InvSlotType.RightHand | InvSlotType.LeftHand)))
{
if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, new List<InvSlotType>() { InvSlotType.Any }))
{
if (FindSuitableContainer(Character, item, out Item targetContainer))
{
findItemState = FindItemState.None;
itemIndex = 0;
if (targetContainer != null)
{
var decontainObjective = new AIObjectiveDecontainItem(Character, item, targetContainer.GetComponent<ItemContainer>(), ObjectiveManager, targetContainer.GetComponent<ItemContainer>());
decontainObjective.Abandoned += () => ignoredContainers.Add(targetContainer);
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
return;
}
else
{
item.Drop(Character);
}
}
else
{
findItemState = FindItemState.OtherItem;
}
}
}
}
}
}
}
private enum FindItemState
{
None,
DivingSuit,
DivingMask,
Extinguisher,
OtherItem
}
private FindItemState findItemState;
private int itemIndex;
private List<Item> ignoredContainers = new List<Item>();
public bool FindSuitableContainer(Character character, Item containableItem, out Item suitableContainer)
{
suitableContainer = null;
if (character.FindItem(ref itemIndex, out Item targetContainer, ignoredItems: ignoredContainers, customPriorityFunction: i =>
{
var container = i.GetComponent<ItemContainer>();
if (container == null) { return 0; }
if (container.Inventory.IsFull()) { return 0; }
if (container.ShouldBeContained(containableItem, out bool isRestrictionsDefined))
{
if (isRestrictionsDefined)
{
return 3;
}
else
{
if (containableItem.Prefab.IsContainerPreferred(container, out bool isPreferencesDefined))
{
return isPreferencesDefined ? 2 : 1;
}
else
{
return isPreferencesDefined ? 0 : 1;
}
}
}
else
{
return 0;
}
}))
{
suitableContainer = targetContainer;
return true;
}
return false;
}
protected void ReportProblems()
@@ -322,7 +531,7 @@ namespace Barotrauma
{
foreach (Character c in Character.CharacterList)
{
if (c.CurrentHull != hull) { continue; }
if (c.CurrentHull != hull || !c.Enabled) { continue; }
if (AIObjectiveFightIntruders.IsValidTarget(c, Character))
{
AddTargets<AIObjectiveFightIntruders, Character>(Character, c);
@@ -559,23 +768,42 @@ namespace Barotrauma
shouldCrouch = Submarine.PickBody(startPos, startPos + Vector2.UnitY * minCeilingDist, null, Physics.CollisionWall) != null;
}
public static bool NeedsDivingGear(Hull hull) => hull == null || hull.OxygenPercentage < 50 || hull.WaterPercentage > 50;
public static bool NeedsDivingGear(Character character, Hull hull, out bool needsSuit)
{
needsSuit = false;
if (hull == null ||
hull.WaterPercentage > 80 ||
(hull.LethalPressure > 0 && character.PressureProtection <= 0) ||
(hull.ConnectedGaps.Any() && hull.ConnectedGaps.Max(g => AIObjectiveFixLeaks.GetLeakSeverity(g)) > 60))
{
needsSuit = true;
return true;
}
if (hull.WaterPercentage > 60 || hull.OxygenPercentage < CharacterHealth.LowOxygenThreshold)
{
return true;
}
return false;
}
public static bool HasDivingGear(Character character, float conditionPercentage = 0) => HasDivingSuit(character, conditionPercentage) || HasDivingMask(character, conditionPercentage);
/// <summary>
/// Check whether the character has a diving suit in usable condition plus some oxygen.
/// </summary>
public static bool HasDivingSuit(Character character) => HasItem(character, "divingsuit", "oxygensource");
public static bool HasDivingSuit(Character character, float conditionPercentage = 0) => HasItem(character, "divingsuit", "oxygensource", conditionPercentage);
/// <summary>
/// Check whether the character has a diving mask in usable condition plus some oxygen.
/// </summary>
public static bool HasDivingMask(Character character) => HasItem(character, "diving", "oxygensource");
public static bool HasDivingMask(Character character, float conditionPercentage = 0) => HasItem(character, "divingmask", "oxygensource", conditionPercentage);
public static bool HasItem(Character character, string tag, string containedTag, float conditionPercentage = 0)
public static bool HasItem(Character character, string identifier, string containedTag, float conditionPercentage = 0)
{
if (character == null) { return false; }
if (character.Inventory == null) { return false; }
var item = character.Inventory.FindItemByTag(tag);
var item = character.Inventory.FindItemByIdentifier(identifier) ?? character.Inventory.FindItemByTag(identifier);
return item != null &&
item.ConditionPercentage > conditionPercentage &&
character.HasEquippedItem(item) &&
@@ -700,30 +928,27 @@ namespace Barotrauma
public float GetHullSafety(Hull hull, Character character, IEnumerable<Hull> visibleHulls = null)
{
bool updateCurrentHullSafety = character == Character && character.CurrentHull == hull;
bool isCurrentHull = character == Character && character.CurrentHull == hull;
if (hull == null)
{
if (updateCurrentHullSafety)
if (isCurrentHull)
{
CurrentHullSafety = 0;
}
return CurrentHullSafety;
}
if (character == Character)
if (isCurrentHull && visibleHulls == null)
{
// If the character is this character, we can use the cached hulls.
// If no visible hulls are provided, the calculations don't take visible/adjacent hulls into account.
if (visibleHulls == null)
{
visibleHulls = VisibleHulls;
}
// Use the cached visible hulls
visibleHulls = VisibleHulls;
}
bool ignoreFire = ObjectiveManager.IsCurrentObjective<AIObjectiveExtinguishFires>() || ObjectiveManager.IsCurrentObjective<AIObjectiveExtinguishFire>();
// TODO: should we calculate the visible hulls for each hull? -> could be a bit heavy.
bool ignoreFire = ObjectiveManager.IsCurrentObjective<AIObjectiveExtinguishFires>() || objectiveManager.HasActiveObjective<AIObjectiveExtinguishFire>();
bool ignoreWater = HasDivingSuit(character);
bool ignoreOxygen = ignoreWater || HasDivingMask(character);
bool ignoreEnemies = ObjectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>();
float safety = GetHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies);
if (updateCurrentHullSafety)
if (isCurrentHull)
{
CurrentHullSafety = safety;
}
@@ -752,7 +977,7 @@ namespace Barotrauma
float enemyFactor = 1;
if (!ignoreEnemies)
{
Func<Character, bool> isValidTarget = e => !e.IsDead && !e.IsUnconscious && !e.Removed && !IsFriendly(character, e);
Func<Character, bool> isValidTarget = e => IsActive(e) && !IsFriendly(character, e);
int enemyCount = visibleHulls == null ?
Character.CharacterList.Count(e => e.CurrentHull == hull && isValidTarget(e)) :
Character.CharacterList.Count(e => visibleHulls.Contains(e.CurrentHull) && isValidTarget(e));
@@ -763,11 +988,15 @@ namespace Barotrauma
return MathHelper.Clamp(safety * 100, 0, 100);
}
public void FaceTarget(ISpatialEntity target) => Character.AnimController.TargetDir = target.WorldPosition.X > Character.WorldPosition.X ? Direction.Right : Direction.Left;
public bool IsFriendly(Character other) => IsFriendly(Character, other);
public static bool IsFriendly(Character me, Character other) =>
(other.TeamID == me.TeamID ||
other.TeamID == Character.TeamType.FriendlyNPC ||
me.TeamID == Character.TeamType.FriendlyNPC) && (other.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.Group));
public static bool IsActive(Character other) => !other.Removed && !other.IsDead && !other.IsUnconscious;
}
}
@@ -3,6 +3,7 @@ using Microsoft.Xna.Framework;
using System;
using System.Linq;
using Barotrauma.Extensions;
using FarseerPhysics;
namespace Barotrauma
{
@@ -142,65 +143,60 @@ namespace Barotrauma
IsPathDirty = false;
}
public Func<PathNode, bool> startNodeFilter;
public Func<PathNode, bool> endNodeFilter;
protected override Vector2 DoSteeringSeek(Vector2 target, float weight)
public void SteeringSeek(Vector2 target, float weight, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null)
{
bool needsNewPath = currentPath != null && currentPath.Unreachable || Vector2.DistanceSquared(target, currentTarget) > 1;
steering += CalculateSteeringSeek(target, weight, startNodeFilter, endNodeFilter, nodeFilter);
}
private Vector2 CalculateSteeringSeek(Vector2 target, float weight, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null)
{
bool needsNewPath = currentPath == null || (currentPath.Unreachable || currentPath.NextNode == null) || Vector2.DistanceSquared(target, currentTarget) > 1;
//find a new path if one hasn't been found yet or the target is different from the current target
if (currentPath == null || needsNewPath || findPathTimer < -1.0f)
if (needsNewPath || findPathTimer < -1.0f)
{
IsPathDirty = true;
if (findPathTimer > 0.0f) return Vector2.Zero;
if (findPathTimer > 0.0f) { return Vector2.Zero; }
currentTarget = target;
Vector2 pos = host.SimPosition;
// TODO: remove this and handle differently?
Vector2 currentPos = host.SimPosition;
if (character != null && character.Submarine == null)
{
var targetHull = Hull.FindHull(FarseerPhysics.ConvertUnits.ToDisplayUnits(target), null, false);
if (targetHull != null && targetHull.Submarine != null)
{
pos -= targetHull.Submarine.SimPosition;
currentPos -= targetHull.Submarine.SimPosition;
}
}
var newPath = pathFinder.FindPath(pos, target, character.Submarine, "(Character: " + character.Name + ")", startNodeFilter, endNodeFilter);
bool useNewPath = currentPath == null || needsNewPath;
var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", startNodeFilter, endNodeFilter, nodeFilter);
bool useNewPath = currentPath == null || needsNewPath || currentPath.Finished;
if (!useNewPath && currentPath != null && currentPath.CurrentNode != null && newPath.Nodes.Any() && !newPath.Unreachable)
{
// It's possible that the current path was calculated from a start point that is no longer valid.
// 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.
useNewPath = newPath.Cost < currentPath.Cost ||
Vector2.DistanceSquared(character.WorldPosition, currentPath.CurrentNode.WorldPosition) > Math.Pow(Vector2.Distance(character.WorldPosition, newPath.Nodes.First().WorldPosition) * 2, 2);
useNewPath = newPath.Cost < currentPath.Cost ||
Vector2.DistanceSquared(character.WorldPosition, currentPath.CurrentNode.WorldPosition) > Math.Pow(Vector2.Distance(character.WorldPosition, newPath.Nodes.First().WorldPosition) * 3, 2);
}
if (useNewPath)
{
currentPath = newPath;
}
findPathTimer = Rand.Range(1.0f, 1.2f);
IsPathDirty = false;
return DiffToCurrentNode();
return DiffToCurrentNode();
}
Vector2 diff = DiffToCurrentNode();
var collider = character.AnimController.Collider;
//if not in water and the waypoint is between the top and bottom of the collider, no need to move vertically
if (!character.AnimController.InWater && !character.IsClimbing && diff.Y < collider.height / 2 + collider.radius)
{
diff.Y = 0.0f;
}
if (diff.LengthSquared() < 0.001f) return -host.Steering;
return Vector2.Normalize(diff) * weight;
if (diff.LengthSquared() < 0.001f) { return -host.Steering; }
return Vector2.Normalize(diff) * weight;
}
protected override Vector2 DoSteeringSeek(Vector2 target, float weight) => CalculateSteeringSeek(target, weight, null, null, null);
private Vector2 DiffToCurrentNode()
{
if (currentPath == null || currentPath.Unreachable) return Vector2.Zero;
@@ -270,13 +266,15 @@ namespace Barotrauma
{
diff.Y = Math.Max(diff.Y, 1.0f);
}
bool aboveFloor = heightFromFloor > 0 && heightFromFloor < collider.height * 1.5f;
// We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative.
float margin = 0.1f;
bool aboveFloor = heightFromFloor > -margin && heightFromFloor < collider.height * 1.5f;
if (aboveFloor || IsNextNodeLadder)
{
if (!nextLadderSameAsCurrent)
if (!nextLadderSameAsCurrent || currentPath.NextNode == null && aboveFloor)
{
character.AnimController.Anim = AnimController.Animation.None;
character.SelectedConstruction = null;
}
currentPath.SkipToNextNode();
}
@@ -302,7 +300,8 @@ namespace Barotrauma
character.SelectedConstruction = null;
}
float multiplier = MathHelper.Lerp(1, 10, MathHelper.Clamp(collider.LinearVelocity.Length() / 10, 0, 1));
if (Vector2.DistanceSquared(pos, currentPath.CurrentNode.SimPosition) < MathUtils.Pow(collider.radius * 2 * multiplier, 2))
float targetDistance = collider.GetSize().X * multiplier;
if (Vector2.DistanceSquared(pos, currentPath.CurrentNode.SimPosition) < MathUtils.Pow(targetDistance, 2))
{
currentPath.SkipToNextNode();
}
@@ -387,13 +386,13 @@ namespace Barotrauma
door = currentWaypoint.ConnectedGap.ConnectedDoor;
if (door.LinkedGap.IsHorizontal)
{
int currentDir = Math.Sign(nextWaypoint.WorldPosition.X - door.Item.WorldPosition.X);
shouldBeOpen = (door.Item.WorldPosition.X - character.WorldPosition.X) * currentDir > -50.0f;
int dir = Math.Sign(nextWaypoint.WorldPosition.X - door.Item.WorldPosition.X);
shouldBeOpen = (door.Item.WorldPosition.X - character.WorldPosition.X) * dir > -50.0f;
}
else
{
int currentDir = Math.Sign(nextWaypoint.WorldPosition.Y - door.Item.WorldPosition.Y);
shouldBeOpen = (door.Item.WorldPosition.Y - character.WorldPosition.Y) * currentDir > -80.0f;
int dir = Math.Sign(nextWaypoint.WorldPosition.Y - door.Item.WorldPosition.Y);
shouldBeOpen = (door.Item.WorldPosition.Y - character.WorldPosition.Y) * dir > -80.0f;
}
}
}
@@ -408,7 +407,18 @@ namespace Barotrauma
bool canAccess = CanAccessDoor(door, button =>
{
if (currentWaypoint == null) { return true; }
float distance = Vector2.DistanceSquared(button.Item.WorldPosition, door.Item.WorldPosition);
// Check that the button is on the right side of the door.
if (door.LinkedGap.IsHorizontal)
{
int dir = Math.Sign(nextWaypoint.WorldPosition.X - door.Item.WorldPosition.X);
if (button.Item.WorldPosition.X * dir > door.Item.WorldPosition.X * dir) { return false; }
}
else
{
int dir = Math.Sign(nextWaypoint.WorldPosition.Y - door.Item.WorldPosition.Y);
if (button.Item.WorldPosition.Y * dir > door.Item.WorldPosition.Y * dir) { return false; }
}
float distance = Vector2.DistanceSquared(button.Item.WorldPosition, character.WorldPosition);
if (closestButton == null || distance < closestDist)
{
closestButton = button;
@@ -434,17 +444,30 @@ namespace Barotrauma
}
else
{
// Can't reach the button closest to the door.
// Can't reach the button closest to the character.
// It's possible that we could reach another buttons.
// If this becomes an issue, we could go through them here and check if any of them are reachable
// (would have to cache a collection of buttons instead of a single reference in the CanAccess filter method above)
//currentPath.Unreachable = true;
var body = Submarine.PickBody(character.SimPosition, character.GetRelativeSimPosition(closestButton.Item), collisionCategory: Physics.CollisionWall | Physics.CollisionLevel);
if (body != null)
{
if (body.UserData is Item item)
{
var d = item.GetComponent<Door>();
if (d == null || d.IsOpen) { return; }
}
// The button is on the wrong side of the door or a wall
currentPath.Unreachable = true;
}
return;
}
}
}
else if (shouldBeOpen)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Pathfinding error: Cannot access the door", Color.Yellow);
#endif
currentPath.Unreachable = true;
return;
}
@@ -520,6 +543,72 @@ namespace Barotrauma
return penalty;
}
}
public void Wander(float deltaTime, float wallAvoidDistance = 150, bool stayStillInTightSpace = true)
{
//steer away from edges of the hull
bool wander = false;
bool inWater = character.AnimController.InWater;
var currentHull = character.CurrentHull;
if (currentHull != null && !inWater)
{
float roomWidth = currentHull.Rect.Width;
if (stayStillInTightSpace && roomWidth < wallAvoidDistance * 4)
{
Reset();
}
else
{
float leftDist = character.Position.X - currentHull.Rect.X;
float rightDist = currentHull.Rect.Right - character.Position.X;
if (leftDist < wallAvoidDistance && rightDist < wallAvoidDistance)
{
if (Math.Abs(rightDist - leftDist) > wallAvoidDistance / 2)
{
SteeringManual(deltaTime, Vector2.UnitX * Math.Sign(rightDist - leftDist));
return;
}
else if (stayStillInTightSpace)
{
Reset();
return;
}
}
if (leftDist < wallAvoidDistance)
{
float speed = (wallAvoidDistance - leftDist) / wallAvoidDistance;
SteeringManual(deltaTime, Vector2.UnitX * MathHelper.Clamp(speed, 0.25f, 1));
WanderAngle = 0.0f;
}
else if (rightDist < wallAvoidDistance)
{
float speed = (wallAvoidDistance - rightDist) / wallAvoidDistance;
SteeringManual(deltaTime, -Vector2.UnitX * MathHelper.Clamp(speed, 0.25f, 1));
WanderAngle = MathHelper.Pi;
}
else
{
wander = true;
}
}
}
else
{
wander = true;
}
if (wander)
{
SteeringWander();
if (currentHull == null)
{
SteeringAvoid(deltaTime, lookAheadDistance: ConvertUnits.ToSimUnits(wallAvoidDistance));
}
}
if (!inWater)
{
//reset vertical steering to prevent dropping down from platforms etc
ResetY();
}
}
}
}
@@ -133,32 +133,41 @@ namespace Barotrauma
case AIState.Idle:
if (attachToWalls && character.Submarine == null && Level.Loaded != null)
{
raycastTimer -= deltaTime;
//check if there are any walls nearby the character could attach to
if (raycastTimer < 0.0f)
if (!IsAttached)
{
wallAttachPos = Vector2.Zero;
var cells = Level.Loaded.GetCells(character.WorldPosition, 1);
if (cells.Count > 0)
raycastTimer -= deltaTime;
//check if there are any walls nearby the character could attach to
if (raycastTimer < 0.0f)
{
foreach (Voronoi2.VoronoiCell cell in cells)
wallAttachPos = Vector2.Zero;
var cells = Level.Loaded.GetCells(character.WorldPosition, 1);
if (cells.Count > 0)
{
foreach (Voronoi2.GraphEdge edge in cell.Edges)
float closestDist = float.PositiveInfinity;
foreach (Voronoi2.VoronoiCell cell in cells)
{
if (MathUtils.GetLineIntersection(edge.Point1, edge.Point2, character.WorldPosition, cell.Center, out Vector2 intersection))
foreach (Voronoi2.GraphEdge edge in cell.Edges)
{
attachSurfaceNormal = edge.GetNormal(cell);
attachTargetBody = cell.Body;
wallAttachPos = ConvertUnits.ToSimUnits(intersection);
break;
if (MathUtils.GetLineIntersection(edge.Point1, edge.Point2, character.WorldPosition, cell.Center, out Vector2 intersection))
{
attachSurfaceNormal = edge.GetNormal(cell);
attachTargetBody = cell.Body;
Vector2 potentialAttachPos = ConvertUnits.ToSimUnits(intersection);
float distSqr = Vector2.DistanceSquared(character.SimPosition, wallAttachPos);
if (distSqr < closestDist)
{
wallAttachPos = potentialAttachPos;
closestDist = distSqr;
}
break;
}
}
}
if (WallAttachPos != Vector2.Zero) break;
}
raycastTimer = RaycastInterval;
}
raycastTimer = RaycastInterval;
}
}
}
else
{
@@ -12,6 +12,16 @@ namespace Barotrauma
public abstract string DebugTag { get; }
public virtual bool ForceRun => false;
public virtual bool IgnoreUnsafeHulls => false;
public virtual bool AbandonWhenCannotCompleteSubjectives => true;
public virtual bool AllowSubObjectiveSorting => false;
public virtual bool ReportFailures => true;
/// <summary>
/// Can there be multiple objective instaces of the same type? Currently multiple instances allowed only for main objectives and the subobjectives of objetive loops.
/// In theory, there could be multiple subobjectives of same type for concurrent objectives, but that would make things more complex -> potential issues
/// </summary>
public virtual bool AllowMultipleInstances => false;
/// <summary>
/// Run the main objective with all subobjectives concurrently?
@@ -19,19 +29,30 @@ namespace Barotrauma
/// </summary>
public virtual bool ConcurrentObjectives => false;
public virtual bool KeepDivingGearOn => false;
protected readonly List<AIObjective> subObjectives = new List<AIObjective>();
public float Priority { get; set; }
public float PriorityModifier { get; private set; } = 1;
public readonly Character character;
public readonly AIObjectiveManager objectiveManager;
public string Option { get; protected set; }
public string Option { get; private set; }
protected bool abandon;
private bool _abandon;
public bool Abandon
{
get { return _abandon; }
set
{
_abandon = value;
if (_abandon)
{
OnAbandon();
}
}
}
/// <summary>
/// Can the objective be completed. That is, does the objective have failing subobjectives or other conditions that prevent it from completing.
/// </summary>
public virtual bool CanBeCompleted => !abandon && subObjectives.All(so => so.CanBeCompleted);
public virtual bool CanBeCompleted => !Abandon;
/// <summary>
/// When true, the objective is never completed, unless CanBeCompleted returns false.
@@ -39,71 +60,85 @@ namespace Barotrauma
public virtual bool IsLoop { get; set; }
public IEnumerable<AIObjective> SubObjectives => subObjectives;
private readonly List<AIObjective> all = new List<AIObjective>();
public IEnumerable<AIObjective> GetSubObjectivesRecursive(bool includingSelf = false)
{
all.Clear();
if (includingSelf)
{
all.Add(this);
}
foreach (var subObjective in subObjectives)
{
all.AddRange(subObjective.GetSubObjectivesRecursive(true));
}
return all;
}
public event Action Completed;
public event Action Abandoned;
public event Action Selected;
public event Action Deselected;
protected HumanAIController HumanAIController => character.AIController as HumanAIController;
protected IndoorsSteeringManager PathSteering => HumanAIController.PathSteering;
protected SteeringManager SteeringManager => HumanAIController.SteeringManager;
public AIObjective GetActiveObjective()
{
var subObjective = SubObjectives.FirstOrDefault();
return subObjective == null ? this : subObjective.GetActiveObjective();
}
public AIObjective(Character character, AIObjectiveManager objectiveManager, float priorityModifier, string option = null)
{
this.objectiveManager = objectiveManager;
this.character = character;
Option = option ?? string.Empty;
PriorityModifier = priorityModifier;
#if DEBUG
IsDuplicate(null);
#endif
}
/// <summary>
/// makes the character act according to the objective, or according to any subobjectives that
/// need to be completed before this one
/// Makes the character act according to the objective, or according to any subobjectives that need to be completed before this one
/// </summary>
public void TryComplete(float deltaTime)
{
if (isCompleted) { return; }
//if (Abandon && !IsLoop && subObjectives.None()) { return; }
if (CheckState()) { return; }
// Not ready -> act (can't do foreach because it's possible that the collection is modified in event callbacks.
for (int i = 0; i < subObjectives.Count; i++)
{
var subObjective = subObjectives[i];
if (subObjective.IsCompleted())
{
#if DEBUG
DebugConsole.NewMessage($"Removing subobjective {subObjective.DebugTag} of {DebugTag}, because it is completed.");
#endif
subObjective.OnCompleted();
subObjectives.Remove(subObjective);
}
else if (!subObjective.CanBeCompleted)
{
#if DEBUG
DebugConsole.NewMessage($"Removing subobjective {subObjective.DebugTag} of {DebugTag}, because it cannot be completed.");
#endif
subObjectives.Remove(subObjective);
}
subObjectives[i].TryComplete(deltaTime);
if (!ConcurrentObjectives) { return; }
}
foreach (AIObjective objective in subObjectives)
{
objective.TryComplete(deltaTime);
if (!ConcurrentObjectives)
{
return;
}
}
Act(deltaTime);
if (IsCompleted())
}
// TODO: check turret aioperate
public void AddSubObjective(AIObjective objective, bool addFirst = false)
{
var type = objective.GetType();
subObjectives.RemoveAll(o => o.GetType() == type);
if (addFirst)
{
OnCompleted();
subObjectives.Insert(0, objective);
}
else
{
subObjectives.Add(objective);
}
}
// TODO: go through AIOperate methods where subobjectives are added and ensure that they add the subobjectives correctly -> use TryAddSubObjective method instead?
public void AddSubObjective(AIObjective objective)
/// <summary>
/// This method allows multiple subobjectives of same type. Use with caution.
/// </summary>
public void AddSubObjectiveInQueue(AIObjective objective)
{
if (subObjectives.Any(o => o.IsDuplicate(objective))) { return; }
subObjectives.Add(objective);
if (!subObjectives.Contains(objective))
{
subObjectives.Add(objective);
}
}
public void RemoveSubObjective<T>(ref T objective) where T : AIObjective
@@ -120,6 +155,7 @@ namespace Barotrauma
public void SortSubObjectives()
{
if (!AllowSubObjectiveSorting) { return; }
if (subObjectives.None()) { return; }
subObjectives.Sort((x, y) => y.GetPriority().CompareTo(x.GetPriority()));
if (ConcurrentObjectives)
@@ -134,6 +170,8 @@ namespace Barotrauma
public virtual float GetPriority() => Priority * PriorityModifier;
public virtual bool IsDuplicate<T>(T otherObjective) where T : AIObjective => otherObjective.Option == Option;
public virtual void Update(float deltaTime)
{
if (objectiveManager.CurrentOrder == this)
@@ -150,8 +188,8 @@ namespace Barotrauma
}
}
Priority = MathHelper.Clamp(Priority, 0, 100);
subObjectives.ForEach(so => so.Update(deltaTime));
}
subObjectives.ForEach(so => so.Update(deltaTime));
}
/// <summary>
@@ -174,9 +212,9 @@ namespace Barotrauma
/// <summary>
/// Checks if the objective already is created and added in subobjectives. If not, creates it.
/// Handles objectives that cannot be completed. If the objective has been removed form the subobjectives, a null value is assigned to the reference.
/// Returns true if the objective was created.
/// Returns true if the objective was created and successfully added.
/// </summary>
protected bool TryAddSubObjective<T>(ref T objective, Func<T> constructor, Action onAbandon = null) where T : AIObjective
protected bool TryAddSubObjective<T>(ref T objective, Func<T> constructor, Action onCompleted = null, Action onAbandon = null) where T : AIObjective
{
if (objective != null)
{
@@ -184,11 +222,6 @@ namespace Barotrauma
// If the sub objective is removed -> it's either completed or impossible to complete.
if (!subObjectives.Contains(objective))
{
if (!objective.CanBeCompleted)
{
abandon = true;
onAbandon?.Invoke();
}
objective = null;
}
return false;
@@ -198,37 +231,122 @@ namespace Barotrauma
objective = constructor();
if (!subObjectives.Contains(objective))
{
AddSubObjective(objective);
if (objective.AllowMultipleInstances)
{
subObjectives.Add(objective);
}
else
{
AddSubObjective(objective);
}
if (onCompleted != null)
{
objective.Completed += onCompleted;
}
if (onAbandon != null)
{
objective.Abandoned += onAbandon;
}
return true;
}
return true;
#if DEBUG
DebugConsole.ThrowError("Attempted to add a duplicate subobjective!\n" + Environment.StackTrace);
#endif
return false;
}
}
public virtual void OnSelected()
{
// Should we reset steering here?
//if (!ConcurrentObjectives)
//{
// SteeringManager.Reset();
//}
Reset();
Selected?.Invoke();
}
public virtual void OnDeselected()
{
Deselected?.Invoke();
}
protected virtual void OnCompleted()
{
Completed?.Invoke();
//if (Completed != null)
//{
// Completed();
// Completed = null;
//}
}
public virtual void Reset() { }
protected virtual void OnAbandon()
{
Abandoned?.Invoke();
}
public virtual void Reset()
{
isCompleted = false;
hasBeenChecked = false;
_abandon = false;
}
protected abstract void Act(float deltaTime);
public abstract bool IsCompleted();
private bool isCompleted;
private bool hasBeenChecked;
public abstract bool IsDuplicate(AIObjective otherObjective);
public bool IsCompleted
{
get
{
if (!hasBeenChecked)
{
CheckState();
}
return isCompleted;
}
protected set
{
isCompleted = value;
}
}
protected abstract bool Check();
private bool CheckState()
{
hasBeenChecked = true;
CheckSubObjectives();
if (subObjectives.None())
{
if (Check())
{
isCompleted = true;
OnCompleted();
}
}
return isCompleted;
}
private void CheckSubObjectives()
{
for (int i = 0; i < subObjectives.Count; i++)
{
var subObjective = subObjectives[i];
subObjective.CheckState();
if (subObjective.IsCompleted)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Removing SUBobjective {subObjective.DebugTag} of {DebugTag}, because it is completed.", Color.LightGreen);
#endif
subObjectives.Remove(subObjective);
}
else if (!subObjective.CanBeCompleted)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Removing SUBobjective {subObjective.DebugTag} of {DebugTag}, because it cannot be completed.", Color.Red);
#endif
subObjectives.Remove(subObjective);
if (AbandonWhenCannotCompleteSubjectives)
{
Abandon = true;
}
}
}
}
}
}
@@ -15,11 +15,6 @@ namespace Barotrauma
public AIObjectiveChargeBatteries(Character character, AIObjectiveManager objectiveManager, string option, float priorityModifier)
: base(character, objectiveManager, priorityModifier, option) { }
public override bool IsDuplicate(AIObjective otherObjective)
{
return otherObjective is AIObjectiveChargeBatteries other && other.Option == Option;
}
protected override bool Filter(PowerContainer battery)
{
if (battery == null) { return false; }
@@ -29,15 +24,8 @@ namespace Barotrauma
if (item.Submarine.TeamID != character.TeamID) { return false; }
if (item.ConditionPercentage <= 0) { return false; }
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(item, true)) { return false; }
if (Character.CharacterList.Any(c => c.CurrentHull == item.CurrentHull && !HumanAIController.IsFriendly(c))) { return false; }
if (Option == "charge")
{
if (battery.RechargeRatio >= PowerContainer.aiRechargeTargetRatio - 0.01f) { return false; }
}
else
{
if (battery.RechargeRatio <= 0) { return false; }
}
if (Character.CharacterList.Any(c => c.CurrentHull == item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; }
if (IsReady(battery)) { return false; }
return true;
}
@@ -67,8 +55,26 @@ namespace Barotrauma
return batteryList;
}
protected override AIObjective ObjectiveConstructor(PowerContainer battery)
=> new AIObjectiveOperateItem(battery, character, objectiveManager, Option, false, priorityModifier: PriorityModifier) { IsLoop = false };
private bool IsReady(PowerContainer battery)
{
if (battery.HasBeenTuned && character.CurrentOrder == null) { return true; }
if (Option == "charge")
{
return battery.RechargeRatio >= PowerContainer.aiRechargeTargetRatio;
}
else
{
return battery.RechargeRatio <= 0;
}
}
protected override AIObjective ObjectiveConstructor(PowerContainer battery) =>
new AIObjectiveOperateItem(battery, character, objectiveManager, Option, false, priorityModifier: PriorityModifier)
{
IsLoop = false,
Override = character.CurrentOrder != null,
completionCondition = () => IsReady(battery)
};
protected override void OnObjectiveCompleted(AIObjective objective, PowerContainer target)
=> HumanAIController.RemoveTargets<AIObjectiveChargeBatteries, PowerContainer>(character, target);
@@ -4,7 +4,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
using FarseerPhysics;
namespace Barotrauma
{
@@ -12,9 +11,19 @@ namespace Barotrauma
{
public override string DebugTag => "combat";
public override bool KeepDivingGearOn => true;
public override bool IgnoreUnsafeHulls => true;
private readonly CombatMode initialMode;
private float seekWeaponsTimer;
const float seekWeaponsInterval = 1;
private float ignoreWeaponTimer;
const float ignoredWeaponsClearTime = 10;
const float coolDown = 10.0f;
// Won't take the offensive with weapons that have lower priority than this
const float goodWeaponPriority = 30;
public Character Enemy { get; private set; }
public bool HoldPosition { get; set; }
@@ -48,9 +57,11 @@ namespace Barotrauma
}
public override bool ConcurrentObjectives => true;
public override bool AbandonWhenCannotCompleteSubjectives => false;
private readonly AIObjectiveFindSafety findSafety;
private readonly HashSet<ItemComponent> weapons = new HashSet<ItemComponent>();
private readonly HashSet<Item> ignoredWeapons = new HashSet<Item>();
private AIObjectiveContainItem seekAmmunition;
private AIObjectiveGoTo retreatObjective;
@@ -79,7 +90,7 @@ namespace Barotrauma
if (findSafety != null)
{
findSafety.Priority = 0;
findSafety.unreachable.Clear();
HumanAIController.UnreachableHulls.Clear();
}
Mode = mode;
initialMode = Mode;
@@ -91,15 +102,19 @@ namespace Barotrauma
public override float GetPriority() => (Enemy != null && (Enemy.Removed || Enemy.IsDead)) ? 0 : Math.Min(100 * PriorityModifier, 100);
public override bool IsDuplicate(AIObjective otherObjective)
public override void Update(float deltaTime)
{
if (!(otherObjective is AIObjectiveCombat objective)) return false;
return objective.Enemy == Enemy;
base.Update(deltaTime);
ignoreWeaponTimer -= deltaTime;
seekWeaponsTimer -= deltaTime;
if (ignoreWeaponTimer < 0)
{
ignoredWeapons.Clear();
ignoreWeaponTimer = ignoredWeaponsClearTime;
}
}
public override void OnSelected() => Weapon = null;
public override bool IsCompleted()
protected override bool Check()
{
bool completed = (Enemy != null && (Enemy.Removed || Enemy.IsDead)) || (initialMode != CombatMode.Offensive && coolDownTimer <= 0);
if (completed)
@@ -122,18 +137,16 @@ namespace Barotrauma
{
coolDownTimer -= deltaTime;
}
if (abandon) { return; }
TryArm();
if (seekAmmunition == null || !subObjectives.Contains(seekAmmunition))
if (seekAmmunition == null)
{
if (!HoldPosition)
{
Move();
}
if (WeaponComponent != null)
if (TryArm())
{
OperateWeapon(deltaTime);
}
if (!HoldPosition && seekAmmunition == null)
{
Move();
}
}
}
@@ -153,42 +166,117 @@ namespace Barotrauma
}
}
private bool IsLoaded(ItemComponent weapon) => weapon.HasRequiredContainedItems(character, addMessage: false);
private bool TryArm()
{
if (character.LockHands) { return false; }
if (Weapon != null)
if (character.LockHands || Enemy == null)
{
if (!character.Inventory.Items.Contains(Weapon) || WeaponComponent == null)
Weapon = null;
return false;
}
if (seekWeaponsTimer < 0)
{
seekWeaponsTimer = seekWeaponsInterval;
// First go through all weapons and try to reload without seeking ammunition
var allWeapons = GetAllWeapons().ToList();
while (allWeapons.Any())
{
Weapon = null;
}
else if (!WeaponComponent.HasRequiredContainedItems(character, addMessage: false))
{
// Seek ammunition only if cannot find a new weapon
if (!Reload(!HoldPosition, () => GetWeapon(out _) == null))
Weapon = GetWeapon(allWeapons, out _weaponComponent);
if (Weapon == null)
{
if (seekAmmunition != null && subObjectives.Contains(seekAmmunition))
{
return false;
}
else
// No weapons
break;
}
if (!character.Inventory.Items.Contains(Weapon) || WeaponComponent == null)
{
// Not in the inventory anymore or cannot find the weapon component
allWeapons.Remove(WeaponComponent);
Weapon = null;
continue;
}
if (initialMode == CombatMode.Offensive)
{
// In the offensive mode, let's ignore weapons that cannot be used in the offensive mode
if (WeaponComponent.CombatPriority < goodWeaponPriority)
{
allWeapons.Remove(WeaponComponent);
Weapon = null;
continue;
}
}
if (IsLoaded(WeaponComponent))
{
// All good, the weapon is loaded
break;
}
if (Reload(seekAmmo: false))
{
// All good, reloading successful
break;
}
else
{
// No ammo.
allWeapons.Remove(WeaponComponent);
Weapon = null;
}
}
if (Weapon == null)
{
// No weapon found with the conditions above. Try again, now let's try to seek ammunition too
Weapon = GetWeapon(out _weaponComponent);
if (Weapon != null)
{
if (!CheckWeapon(seekAmmo: true))
{
if (seekAmmunition != null)
{
// No loaded weapon, but we are trying to seek ammunition.
return false;
}
else
{
Weapon = null;
}
}
}
}
}
if (Weapon == null)
else
{
Weapon = GetWeapon(out _weaponComponent);
if (!CheckWeapon(seekAmmo: false))
{
Weapon = null;
}
}
if (Weapon == null)
{
Weapon = GetWeapon(out _weaponComponent, ignoreRequiredItems: true);
Mode = CombatMode.Retreat;
}
else
{
Mode = WeaponComponent.CombatPriority >= goodWeaponPriority ? initialMode : CombatMode.Defensive;
}
Mode = Weapon == null ? CombatMode.Retreat : initialMode;
return Weapon != null;
bool CheckWeapon(bool seekAmmo)
{
if (!character.Inventory.Items.Contains(Weapon) || WeaponComponent == null)
{
// Not in the inventory anymore or cannot find the weapon component
return false;
}
if (!IsLoaded(WeaponComponent))
{
// Try reloading (and seek ammo)
if (!Reload(seekAmmo))
{
return false;
}
}
return true;
};
}
private void OperateWeapon(float deltaTime)
@@ -209,59 +297,72 @@ namespace Barotrauma
}
}
private Item GetWeapon(out ItemComponent weaponComponent, bool ignoreRequiredItems = false)
private Item GetWeapon(out ItemComponent weaponComponent)
{
weapons.Clear();
_weaponComponent = null;
foreach (var item in character.Inventory.Items)
{
if (item == null) { continue; }
SeekWeapons(item);
if (item.OwnInventory != null)
{
item.OwnInventory.Items.ForEach(i => SeekWeapons(i));
}
}
weaponComponent = weapons.OrderByDescending(w => w.CombatPriority).FirstOrDefault();
GetAllWeapons();
return GetWeapon(weapons, out weaponComponent);
}
private Item GetWeapon(IEnumerable<ItemComponent> weaponList, out ItemComponent weaponComponent)
{
weaponComponent = weaponList.OrderByDescending(w => CalculateWeaponPriority(w)).FirstOrDefault();
if (weaponComponent == null) { return null; }
if (weaponComponent.CombatPriority < 1) { return null; }
return weaponComponent.Item;
}
void SeekWeapons(Item item)
private float CalculateWeaponPriority(ItemComponent weapon)
{
float priority = weapon.CombatPriority;
// Halve the priority for weapons that don't have proper ammunition loaded.
if (!weapon.HasRequiredContainedItems(character, addMessage: false))
{
if (item == null) { return; }
foreach (var component in item.Components)
priority /= 2;
}
return priority;
}
private HashSet<ItemComponent> GetAllWeapons()
{
weapons.Clear();
foreach (var item in character.Inventory.Items)
{
if (item == null) { continue; }
if (ignoredWeapons.Contains(item)) { continue; }
SeekWeapons(item, weapons);
if (item.OwnInventory != null)
{
if (component is RangedWeapon rw)
item.OwnInventory.Items.ForEach(i => SeekWeapons(i, weapons));
}
}
return weapons;
}
private void SeekWeapons(Item item, ICollection<ItemComponent> weaponList)
{
if (item == null) { return; }
foreach (var component in item.Components)
{
if (component is RangedWeapon rw)
{
weaponList.Add(rw);
}
else if (component is MeleeWeapon mw)
{
weaponList.Add(mw);
}
else
{
var effects = component.statusEffectLists;
if (effects != null)
{
if (ignoreRequiredItems || rw.HasRequiredContainedItems(character, addMessage: false))
foreach (var statusEffects in effects.Values)
{
weapons.Add(rw);
}
}
else if (component is MeleeWeapon mw)
{
if (ignoreRequiredItems || mw.HasRequiredContainedItems(character, addMessage: false))
{
weapons.Add(mw);
}
}
else
{
var effects = component.statusEffectLists;
if (effects != null)
{
foreach (var statusEffects in effects.Values)
foreach (var statusEffect in statusEffects)
{
foreach (var statusEffect in statusEffects)
if (statusEffect.Afflictions.Any())
{
if (statusEffect.Afflictions.Any())
{
if (ignoreRequiredItems || component.HasRequiredContainedItems(character, addMessage: false))
{
weapons.Add(component);
}
}
weaponList.Add(component);
}
}
}
@@ -286,17 +387,14 @@ namespace Barotrauma
if (character.LockHands) { return false; }
if (!WeaponComponent.HasRequiredContainedItems(character, addMessage: false))
{
Mode = CombatMode.Retreat;
return false;
}
//if (!character.SelectedItems.Contains(Weapon))
if (!character.HasEquippedItem(Weapon))
{
Weapon.TryInteract(character, forceSelectKey: true);
var slots = Weapon.AllowedSlots.FindAll(s => s == InvSlotType.LeftHand || s == InvSlotType.RightHand || s == (InvSlotType.LeftHand | InvSlotType.RightHand));
if (character.Inventory.TryPutItem(Weapon, character, slots))
{
Weapon.Equip(character);
aimTimer = Rand.Range(0.5f, 1f);
}
else
@@ -323,13 +421,15 @@ namespace Barotrauma
}
if (character.CurrentHull != retreatTarget)
{
TryAddSubObjective(ref retreatObjective, () => new AIObjectiveGoTo(retreatTarget, character, objectiveManager, false, true));
TryAddSubObjective(ref retreatObjective, () => new AIObjectiveGoTo(retreatTarget, character, objectiveManager, false, true),
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref retreatObjective));
}
}
private void Engage()
{
if (character.LockHands)
if (character.LockHands || Enemy == null)
{
Mode = CombatMode.Retreat;
SteeringManager.Reset();
@@ -346,18 +446,18 @@ namespace Barotrauma
TryAddSubObjective(ref followTargetObjective,
constructor: () => new AIObjectiveGoTo(Enemy, character, objectiveManager, repeat: true, getDivingGearIfNeeded: true)
{
AllowGoingOutside = true,
IgnoreIfTargetDead = true
},
onAbandon: () =>
{
Mode = CombatMode.Retreat;
Mode = CombatMode.Defensive;
SteeringManager.Reset();
RemoveSubObjective(ref followTargetObjective);
});
if (followTargetObjective != null && subObjectives.Contains(followTargetObjective))
if (followTargetObjective != null)
{
followTargetObjective.CloseEnough =
WeaponComponent is RangedWeapon ? 300 :
WeaponComponent is RangedWeapon ? 1000 :
WeaponComponent is MeleeWeapon mw ? mw.Range :
WeaponComponent is RepairTool rt ? rt.Range : 50;
}
@@ -377,29 +477,40 @@ namespace Barotrauma
targetItemCount = Weapon.GetComponent<ItemContainer>().Capacity,
checkInventory = false
},
onCompleted: () => RemoveSubObjective(ref seekAmmunition),
onAbandon: () =>
{
Weapon = null;
Mode = CombatMode.Retreat;
SteeringManager.Reset();
RemoveSubObjective(ref seekAmmunition);
ignoredWeapons.Add(Weapon);
Weapon = null;
});
}
/// <summary>
/// Reloads the ammunition found in the inventory.
/// If seekAmmo is true and the condition is met or not provided, tries to get find the ammo elsewhere.
/// If seekAmmo is true, tries to get find the ammo elsewhere.
/// </summary>
private bool Reload(bool seekAmmo, Func<bool> condition = null)
private bool Reload(bool seekAmmo)
{
if (WeaponComponent == null) { return false; }
if (!WeaponComponent.requiredItems.ContainsKey(RelatedItem.RelationType.Contained)) { return false; }
var containedItems = Weapon.ContainedItems;
// Drop empty ammo
foreach (Item containedItem in containedItems)
{
if (containedItem == null) { continue; }
if (containedItem.Condition <= 0)
{
containedItem.Drop(character);
}
}
RelatedItem item = null;
Item ammunition = null;
string[] ammunitionIdentifiers = null;
foreach (RelatedItem requiredItem in WeaponComponent.requiredItems[RelatedItem.RelationType.Contained])
{
ammunition = containedItems.FirstOrDefault(it => it.Condition > 0.0f && requiredItem.MatchesItem(it));
ammunition = containedItems.FirstOrDefault(it => it.Condition > 0 && requiredItem.MatchesItem(it));
if (ammunition != null)
{
// Ammunition still remaining
@@ -411,20 +522,25 @@ namespace Barotrauma
// No ammo
if (ammunition == null)
{
var container = Weapon.GetComponent<ItemContainer>();
// Try reload ammunition in inventory
foreach (string identifier in ammunitionIdentifiers)
if (ammunitionIdentifiers != null)
{
foreach (var i in character.Inventory.Items)
// Try reload ammunition from inventory
ammunition = character.Inventory.FindItem(i => ammunitionIdentifiers.Any(id => id == i.Prefab.Identifier || i.HasTag(id)) && i.Condition > 0, true);
if (ammunition != null)
{
if (i == null) { continue; }
if (i.Prefab.Identifier == identifier || i.HasTag(identifier))
var container = Weapon.GetComponent<ItemContainer>();
if (container.Item.ParentInventory == character.Inventory)
{
if (i.Condition > 0)
character.Inventory.RemoveItem(ammunition);
if (!container.Inventory.TryPutItem(ammunition, null))
{
container.Inventory.TryPutItem(ammunition, null);
ammunition.Drop(character);
}
}
else
{
container.Combine(ammunition, character);
}
}
}
}
@@ -432,12 +548,9 @@ namespace Barotrauma
{
return true;
}
else if (ammunition == null)
else if (ammunition == null && !HoldPosition && initialMode == CombatMode.Offensive && seekAmmo && ammunitionIdentifiers != null)
{
if (seekAmmo && ammunitionIdentifiers != null && (condition == null || condition()))
{
SeekAmmunition(ammunitionIdentifiers);
}
SeekAmmunition(ammunitionIdentifiers);
}
return false;
}
@@ -495,7 +608,8 @@ namespace Barotrauma
{
myBodies = character.AnimController.Limbs.Select(l => l.body.FarseerBody);
}
var collisionCategories = Physics.CollisionCharacter | Physics.CollisionWall;
var collisionCategories = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel;
var pickedBody = Submarine.PickBody(Weapon.SimPosition, Enemy.SimPosition, myBodies, collisionCategories);
if (pickedBody != null)
{
@@ -512,7 +626,16 @@ namespace Barotrauma
{
character.SetInput(InputType.Shoot, false, true);
Weapon.Use(deltaTime, character);
aimTimer = Rand.Range(0.25f, 0.5f);
float reloadTime = 0;
if (WeaponComponent is RangedWeapon rangedWeapon)
{
reloadTime = rangedWeapon.Reload;
}
if (WeaponComponent is MeleeWeapon mw)
{
reloadTime = mw.Reload;
}
aimTimer = reloadTime * Rand.Range(1f, 1.5f);
}
}
}
@@ -19,17 +19,28 @@ namespace Barotrauma
//can either be a tag or an identifier
public readonly string[] itemIdentifiers;
public readonly ItemContainer container;
public readonly Item item;
private AIObjectiveGetItem getItemObjective;
private AIObjectiveGoTo goToObjective;
private readonly HashSet<Item> containedItems = new HashSet<Item>();
public bool AllowToFindDivingGear { get; set; } = true;
public float ConditionLevel { get; set; }
public AIObjectiveContainItem(Character character, Item item, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
this.container = container;
this.item = item;
}
public AIObjectiveContainItem(Character character, string itemIdentifier, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: this(character, new string[] { itemIdentifier }, container, objectiveManager, priorityModifier) { }
public AIObjectiveContainItem(Character character, string[] itemIdentifiers, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base (character, objectiveManager, priorityModifier)
: base(character, objectiveManager, priorityModifier)
{
this.itemIdentifiers = itemIdentifiers;
for (int i = 0; i < itemIdentifiers.Length; i++)
@@ -40,17 +51,25 @@ namespace Barotrauma
this.container = container;
}
public override bool IsCompleted()
protected override bool Check()
{
int containedItemCount = 0;
foreach (Item item in container.Inventory.Items)
if (IsCompleted) { return true; }
if (item != null)
{
if (item != null && itemIdentifiers.Any(id => item.Prefab.Identifier == id || item.HasTag(id)))
{
containedItemCount++;
}
return container.Inventory.Items.Contains(item);
}
else
{
int containedItemCount = 0;
foreach (Item i in container.Inventory.Items)
{
if (i != null && itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id)))
{
containedItemCount++;
}
}
return containedItemCount >= targetItemCount;
}
return containedItemCount >= targetItemCount;
}
public override float GetPriority()
@@ -62,20 +81,58 @@ namespace Barotrauma
return 1.0f;
}
private bool CheckItem(Item i) => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id)) && i.ConditionPercentage > ConditionLevel;
protected override void Act(float deltaTime)
{
//get the item that should be contained
Item itemToContain = null;
foreach (string identifier in itemIdentifiers)
Item itemToContain = item ?? character.Inventory.FindItem(i => CheckItem(i) && i.Container != container.Item, recursive: true);
if (itemToContain != null)
{
itemToContain = character.Inventory.FindItemByIdentifier(identifier) ?? character.Inventory.FindItemByTag(identifier);
if (itemToContain != null && itemToContain.Condition > 0.0f) { break; }
}
if (itemToContain == null)
{
if (getItemObjective != null)
// Contain the item
if (itemToContain.ParentInventory == character.Inventory)
{
if (getItemObjective.IsCompleted())
character.Inventory.RemoveItem(itemToContain);
if (!container.Inventory.TryPutItem(itemToContain, null))
{
itemToContain.Drop(character);
Abandon = true;
}
}
else
{
if (character.CanInteractWith(container.Item, out _, checkLinked: false))
{
if (container.Combine(itemToContain, character))
{
IsCompleted = true;
}
else
{
Abandon = true;
}
}
else
{
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(container.Item, character, objectiveManager, getDivingGearIfNeeded: AllowToFindDivingGear),
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref goToObjective));
}
}
}
else
{
// No matching items in the inventory, try to get an item
TryAddSubObjective(ref getItemObjective, () =>
new AIObjectiveGetItem(character, itemIdentifiers, objectiveManager, equip: false, checkInventory: checkInventory)
{
GetItemPriority = GetItemPriority,
ignoredContainerIdentifiers = ignoredContainerIdentifiers,
ignoredItems = containedItems,
AllowToFindDivingGear = this.AllowToFindDivingGear
}, onAbandon: () =>
{
Abandon = true;
}, onCompleted: () =>
{
if (getItemObjective.TargetItem != null)
{
@@ -83,55 +140,18 @@ namespace Barotrauma
}
else
{
// Reduce the target item count to prevent getting stuck here, if the target item for some reason is null, which shouldn't happen.
targetItemCount--;
if (container.Inventory.FindItem(i => CheckItem(i), recursive: false) != null)
{
IsCompleted = true;
}
else
{
Abandon = true;
}
}
getItemObjective = null;
}
else if (!getItemObjective.CanBeCompleted)
{
getItemObjective = null;
targetItemCount--;
}
}
TryAddSubObjective(ref getItemObjective, () =>
new AIObjectiveGetItem(character, itemIdentifiers, objectiveManager, checkInventory: checkInventory)
{
GetItemPriority = GetItemPriority,
ignoredContainerIdentifiers = ignoredContainerIdentifiers,
ignoredItems = containedItems
RemoveSubObjective(ref getItemObjective);
});
return;
}
if (container.Item.ParentInventory == character.Inventory)
{
character.Inventory.RemoveItem(itemToContain);
container.Inventory.TryPutItem(itemToContain, null);
}
else
{
if (!character.CanInteractWith(container.Item, out _, checkLinked: false))
{
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(container.Item, character, objectiveManager));
return;
}
container.Combine(itemToContain, character);
}
}
public override bool IsDuplicate(AIObjective otherObjective)
{
if (!(otherObjective is AIObjectiveContainItem objective)) { return false; }
if (objective.container != container) { return false; }
if (objective.itemIdentifiers.Length != itemIdentifiers.Length) { return false; }
for (int i = 0; i < itemIdentifiers.Length; i++)
{
if (objective.itemIdentifiers[i] != itemIdentifiers[i])
{
return false;
}
}
return true;
}
}
}
}
@@ -1,6 +1,7 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
namespace Barotrauma
{
@@ -12,24 +13,25 @@ namespace Barotrauma
//can either be a tag or an identifier
private readonly string[] itemIdentifiers;
private readonly ItemContainer container;
private readonly ItemContainer sourceContainer;
private ItemContainer targetContainer;
private readonly Item targetItem;
private AIObjectiveGoTo goToObjective;
private bool isCompleted;
private AIObjectiveContainItem containObjective;
public AIObjectiveDecontainItem(Character character, Item targetItem, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1)
public AIObjectiveDecontainItem(Character character, Item targetItem, ItemContainer sourceContainer, AIObjectiveManager objectiveManager, ItemContainer targetContainer = null, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
this.targetItem = targetItem;
this.container = container;
this.sourceContainer = sourceContainer;
this.targetContainer = targetContainer;
}
public AIObjectiveDecontainItem(Character character, string itemIdentifier, ItemContainer sourceContainer, AIObjectiveManager objectiveManager, ItemContainer targetContainer = null, float priorityModifier = 1)
: this(character, new string[] { itemIdentifier }, sourceContainer, objectiveManager, targetContainer, priorityModifier) { }
public AIObjectiveDecontainItem(Character character, string itemIdentifier, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: this(character, new string[] { itemIdentifier }, container, objectiveManager, priorityModifier) { }
public AIObjectiveDecontainItem(Character character, string[] itemIdentifiers, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1)
public AIObjectiveDecontainItem(Character character, string[] itemIdentifiers, ItemContainer sourceContainer, AIObjectiveManager objectiveManager, ItemContainer targetContainer = null, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
this.itemIdentifiers = itemIdentifiers;
@@ -37,10 +39,11 @@ namespace Barotrauma
{
itemIdentifiers[i] = itemIdentifiers[i].ToLowerInvariant();
}
this.container = container;
this.sourceContainer = sourceContainer;
this.targetContainer = targetContainer;
}
public override bool IsCompleted() => isCompleted;
protected override bool Check() => IsCompleted;
public override float GetPriority()
{
@@ -53,58 +56,50 @@ namespace Barotrauma
protected override void Act(float deltaTime)
{
if (isCompleted) { return; }
Item itemToDecontain = null;
//get the item that should be de-contained
if (targetItem == null)
Item itemToDecontain = targetItem ?? sourceContainer.Inventory.FindItem(i => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id)), recursive: false);
if (itemToDecontain == null)
{
if (itemIdentifiers != null)
Abandon = true;
return;
}
if (targetContainer == null)
{
if (itemToDecontain.Container != sourceContainer.Item)
{
foreach (string identifier in itemIdentifiers)
{
itemToDecontain = container.Inventory.FindItemByIdentifier(identifier) ?? container.Inventory.FindItemByTag(identifier);
if (itemToDecontain != null) { break; }
}
IsCompleted = true;
return;
}
}
else
{
itemToDecontain = targetItem;
}
if (itemToDecontain == null || itemToDecontain.Container != container.Item) // Item not found or already de-contained, consider complete
{
isCompleted = true;
return;
}
if (itemToDecontain.OwnInventory != character.Inventory && itemToDecontain.ParentInventory != character.Inventory)
{
if (!character.CanInteractWith(container.Item, out _, checkLinked: false))
if (targetContainer.Inventory.Items.Contains(itemToDecontain))
{
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(container.Item, character, objectiveManager));
IsCompleted = true;
return;
}
}
itemToDecontain.Drop(character);
isCompleted = true;
}
public override bool IsDuplicate(AIObjective otherObjective)
{
if (!(otherObjective is AIObjectiveDecontainItem decontainItem)) { return false; }
if (decontainItem.itemIdentifiers != null && itemIdentifiers != null)
if (goToObjective == null && !itemToDecontain.IsOwnedBy(character))
{
if (decontainItem.itemIdentifiers.Length != itemIdentifiers.Length) { return false; }
for (int i = 0; i < decontainItem.itemIdentifiers.Length; i++)
if (!character.CanInteractWith(sourceContainer.Item, out _, checkLinked: false))
{
if (decontainItem.itemIdentifiers[i] != itemIdentifiers[i]) { return false; }
TryAddSubObjective(ref goToObjective,
constructor: () => new AIObjectiveGoTo(sourceContainer.Item, character, objectiveManager),
onAbandon: () => Abandon = true);
return;
}
return true;
}
else if (decontainItem.itemIdentifiers == null && itemIdentifiers == null)
if (targetContainer != null)
{
return decontainItem.targetItem == targetItem;
TryAddSubObjective(ref containObjective,
constructor: () => new AIObjectiveContainItem(character, itemToDecontain, targetContainer, objectiveManager) { GetItemPriority = this.GetItemPriority },
onCompleted: () => IsCompleted = true,
onAbandon: () => targetContainer = null);
}
else
{
itemToDecontain.Drop(character);
IsCompleted = true;
}
return false;
}
}
}
@@ -12,6 +12,7 @@ namespace Barotrauma
public override string DebugTag => "extinguish fire";
public override bool ForceRun => true;
public override bool ConcurrentObjectives => true;
public override bool KeepDivingGearOn => true;
private readonly Hull targetHull;
@@ -27,19 +28,23 @@ namespace Barotrauma
public override float GetPriority()
{
if (Character.CharacterList.Any(c => c.CurrentHull == targetHull && !HumanAIController.IsFriendly(c))) { return 0; }
// Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally)
float dist = Math.Abs(character.WorldPosition.X - targetHull.WorldPosition.X) + Math.Abs(character.WorldPosition.Y - targetHull.WorldPosition.Y) * 2.0f;
float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 10000, dist));
if (!objectiveManager.IsCurrentOrder<AIObjectiveExtinguishFires>()
&& Character.CharacterList.Any(c => c.CurrentHull == targetHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return 0; }
float yDist = Math.Abs(character.WorldPosition.Y - targetHull.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 3 : 0;
float dist = Math.Abs(character.WorldPosition.X - targetHull.WorldPosition.X) + yDist;
float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, dist));
if (targetHull == character.CurrentHull)
{
distanceFactor = 1;
}
float severity = AIObjectiveExtinguishFires.GetFireSeverity(targetHull);
float severityFactor = MathHelper.Lerp(0, 1, severity / 100);
float devotion = Math.Min(Priority, 10) / 100;
return MathHelper.Lerp(0, 100, MathHelper.Clamp(devotion + severityFactor * distanceFactor, 0, 1));
}
public override bool IsCompleted() => targetHull.FireSources.None();
public override bool IsDuplicate(AIObjective otherObjective) => otherObjective is AIObjectiveExtinguishFire otherExtinguishFire && otherExtinguishFire.targetHull == targetHull;
protected override bool Check() => targetHull.FireSources.None();
protected override void Act(float deltaTime)
{
@@ -58,9 +63,9 @@ namespace Barotrauma
if (extinguisher == null)
{
#if DEBUG
DebugConsole.ThrowError("AIObjectiveExtinguishFire failed - the item \"" + extinguisherItem + "\" has no RepairTool component but is tagged as an extinguisher");
DebugConsole.ThrowError($"{character.Name}: AIObjectiveExtinguishFire failed - the item \"" + extinguisherItem + "\" has no RepairTool component but is tagged as an extinguisher");
#endif
abandon = true;
Abandon = true;
return;
}
foreach (FireSource fs in targetHull.FireSources)
@@ -119,7 +124,9 @@ namespace Barotrauma
if (move)
{
//go to the first firesource
TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(fs, character, objectiveManager));
TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(fs, character, objectiveManager),
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref gotoObjective));
}
break;
}
@@ -9,6 +9,7 @@ namespace Barotrauma
{
public override string DebugTag => "extinguish fires";
public override bool ForceRun => true;
public override bool IgnoreUnsafeHulls => true;
public AIObjectiveExtinguishFires(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { }
@@ -18,7 +19,6 @@ namespace Barotrauma
public static float GetFireSeverity(Hull hull) => hull.FireSources.Sum(fs => fs.Size.X);
public override bool IsDuplicate(AIObjective otherObjective) => otherObjective is AIObjectiveExtinguishFires;
protected override IEnumerable<Hull> GetList() => Hull.hullList;
protected override AIObjective ObjectiveConstructor(Hull target)
@@ -11,12 +11,11 @@ namespace Barotrauma
{
public override string DebugTag => "fight intruders";
protected override float IgnoreListClearInterval => 30;
public virtual bool IgnoreUnsafeHulls => true;
public AIObjectiveFightIntruders(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier) { }
public override bool IsDuplicate(AIObjective otherObjective) => otherObjective is AIObjectiveFightIntruders;
protected override bool Filter(Character target) => IsValidTarget(target, character);
protected override IEnumerable<Character> GetList() => Character.CharacterList;
@@ -1,53 +1,56 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
class AIObjectiveFindDivingGear : AIObjective
{
public override string DebugTag => "find diving gear";
public override string DebugTag => $"find diving gear ({gearTag})";
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
public override bool IgnoreUnsafeHulls => true;
private readonly string gearTag;
private readonly string fallbackTag;
private AIObjectiveGetItem getDivingGear;
private AIObjectiveContainItem getOxygen;
public override bool IsCompleted()
{
for (int i = 0; i < character.Inventory.Items.Length; i++)
{
if (character.Inventory.SlotTypes[i] == InvSlotType.Any || character.Inventory.Items[i] == null) { continue; }
if (character.Inventory.Items[i].HasTag(gearTag))
{
var containedItems = character.Inventory.Items[i].ContainedItems;
if (containedItems == null) { continue; }
return containedItems.Any(it => (it.Prefab.Identifier == "oxygentank" || it.HasTag("oxygensource")) && it.Condition > 0.0f);
}
}
return false;
}
public static float lowOxygenThreshold = 10;
public override float GetPriority() => MathHelper.Clamp(100 - character.OxygenAvailable, 0, 100);
public override bool IsDuplicate(AIObjective otherObjective) => otherObjective is AIObjectiveFindDivingGear;
protected override bool Check() => HumanAIController.HasItem(character, gearTag, "oxygensource") || HumanAIController.HasItem(character, fallbackTag, "oxygensource");
public AIObjectiveFindDivingGear(Character character, bool needDivingSuit, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier)
{
gearTag = needDivingSuit ? "divingsuit" : "diving";
gearTag = needDivingSuit ? "divingsuit" : "divingmask";
fallbackTag = needDivingSuit ? "divingsuit" : "diving";
}
protected override void Act(float deltaTime)
{
var item = character.Inventory.FindItemByTag(gearTag);
if (character.LockHands)
{
Abandon = true;
return;
}
var item = character.Inventory.FindItemByIdentifier(gearTag, true) ?? character.Inventory.FindItemByTag(gearTag, true);
if (item == null && fallbackTag != gearTag)
{
item = character.Inventory.FindItemByTag(fallbackTag, true);
}
if (item == null || !character.HasEquippedItem(item))
{
TryAddSubObjective(ref getDivingGear, () =>
{
character.Speak(TextManager.Get("DialogGetDivingGear"), null, 0.0f, "getdivinggear", 30.0f);
return new AIObjectiveGetItem(character, gearTag, objectiveManager, equip: true);
});
if (item == null)
{
character.Speak(TextManager.Get("DialogGetDivingGear"), null, 0.0f, "getdivinggear", 30.0f);
}
return new AIObjectiveGetItem(character, gearTag, objectiveManager, equip: true) { AllowToFindDivingGear = false };
},
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref getDivingGear));
}
else
{
@@ -55,9 +58,9 @@ namespace Barotrauma
if (containedItems == null)
{
#if DEBUG
DebugConsole.ThrowError("AIObjectiveFindDivingGear failed - the item \"" + item + "\" has no proper inventory");
DebugConsole.ThrowError($"{character.Name}: AIObjectiveFindDivingGear failed - the item \"" + item + "\" has no proper inventory");
#endif
abandon = true;
Abandon = true;
return;
}
// Drop empty tanks
@@ -69,13 +72,54 @@ namespace Barotrauma
containedItem.Drop(character);
}
}
if (containedItems.None(it => (it.Prefab.Identifier == "oxygentank" || it.HasTag("oxygensource")) && it.Condition > 0.0f))
if (containedItems.None(it => it.HasTag("oxygensource") && it.Condition > lowOxygenThreshold))
{
TryAddSubObjective(ref getOxygen, () =>
var oxygenTank = character.Inventory.FindItemByTag("oxygensource", true);
if (oxygenTank != null)
{
character.Speak(TextManager.Get("DialogGetOxygenTank"), null, 0, "getoxygentank", 30.0f);
return new AIObjectiveContainItem(character, new string[] { "oxygentank", "oxygensource" }, item.GetComponent<ItemContainer>(), objectiveManager);
});
var container = item.GetComponent<ItemContainer>();
if (container.Item.ParentInventory == character.Inventory)
{
character.Inventory.RemoveItem(oxygenTank);
if (!container.Inventory.TryPutItem(oxygenTank, null))
{
oxygenTank.Drop(character);
Abandon = true;
}
}
else
{
container.Combine(oxygenTank, character);
}
}
else
{
// Seek oxygen that has min 10% condition left
TryAddSubObjective(ref getOxygen, () =>
{
character.Speak(TextManager.Get("DialogGetOxygenTank"), null, 0, "getoxygentank", 30.0f);
return new AIObjectiveContainItem(character, new string[] { "oxygensource" }, item.GetComponent<ItemContainer>(), objectiveManager)
{
AllowToFindDivingGear = false,
ConditionLevel = lowOxygenThreshold
};
},
onAbandon: () =>
{
// Try to seek any oxygen sources
TryAddSubObjective(ref getOxygen, () =>
{
return new AIObjectiveContainItem(character, new string[] { "oxygensource" }, item.GetComponent<ItemContainer>(), objectiveManager)
{
AllowToFindDivingGear = false,
ConditionLevel = 0
};
},
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref getOxygen));
},
onCompleted: () => RemoveSubObjective(ref getOxygen));
}
}
}
}
@@ -9,17 +9,18 @@ namespace Barotrauma
{
public override string DebugTag => "find safety";
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
public override bool IgnoreUnsafeHulls => true;
public override bool ConcurrentObjectives => true;
public override bool IsLoop { get => true; set => throw new System.Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace); }
// TODO: expose?
const float priorityIncrease = 100;
const float priorityDecrease = 10;
const float SearchHullInterval = 3.0f;
const float clearUnreachableInterval = 30;
public readonly HashSet<Hull> unreachable = new HashSet<Hull>();
private float currenthullSafety;
private float unreachableClearTimer;
private float searchHullTimer;
private AIObjectiveGoTo goToObjective;
@@ -27,21 +28,18 @@ namespace Barotrauma
public AIObjectiveFindSafety(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { }
public override bool IsCompleted() => false;
protected override bool Check() => false;
public override bool CanBeCompleted => true;
public override bool IsDuplicate(AIObjective otherObjective) => otherObjective is AIObjectiveFindSafety;
private bool resetPriority;
public override void Update(float deltaTime)
{
if (unreachableClearTimer > 0)
if (resetPriority)
{
unreachableClearTimer -= deltaTime;
}
else
{
unreachableClearTimer = clearUnreachableInterval;
unreachable.Clear();
Priority = 0;
resetPriority = false;
return;
}
if (character.CurrentHull == null)
{
@@ -49,7 +47,10 @@ namespace Barotrauma
Priority = objectiveManager.CurrentOrder is AIObjectiveGoTo ? 0 : 100;
return;
}
if (character.OxygenAvailable < CharacterHealth.LowOxygenThreshold) { Priority = 100; }
if (HumanAIController.NeedsDivingGear(character, character.CurrentHull, out _) && !HumanAIController.HasDivingGear(character))
{
Priority = 100;
}
currenthullSafety = HumanAIController.CurrentHullSafety;
if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD)
{
@@ -61,7 +62,7 @@ namespace Barotrauma
Priority += dangerFactor * priorityIncrease * deltaTime;
}
Priority = MathHelper.Clamp(Priority, 0, 100);
if (divingGearObjective != null && !divingGearObjective.IsCompleted() && divingGearObjective.CanBeCompleted)
if (divingGearObjective != null && !divingGearObjective.IsCompleted && divingGearObjective.CanBeCompleted)
{
// Boost the priority while seeking the diving gear
Priority = Math.Max(Priority, Math.Min(AIObjectiveManager.OrderPriority + 20, 100));
@@ -72,32 +73,36 @@ namespace Barotrauma
private Hull previousSafeHull;
protected override void Act(float deltaTime)
{
var currentHull = character.AnimController.CurrentHull;
bool needsDivingGear = HumanAIController.NeedsDivingGear(currentHull);
bool needsDivingSuit = needsDivingGear && (currentHull == null || currentHull.WaterPercentage > 90);
var currentHull = character.CurrentHull;
bool needsDivingGear = HumanAIController.NeedsDivingGear(character, currentHull, out bool needsDivingSuit);
bool needsEquipment = false;
if (needsDivingSuit)
{
needsEquipment = !HumanAIController.HasDivingSuit(character);
needsEquipment = !HumanAIController.HasDivingSuit(character, AIObjectiveFindDivingGear.lowOxygenThreshold);
}
else if (needsDivingGear)
{
needsEquipment = !HumanAIController.HasDivingMask(character);
needsEquipment = !HumanAIController.HasDivingGear(character, AIObjectiveFindDivingGear.lowOxygenThreshold);
}
if (needsEquipment)
if (needsEquipment && divingGearObjective == null && !character.LockHands)
{
TryAddSubObjective(ref divingGearObjective,
() => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager),
onAbandon: () => searchHullTimer = Math.Min(1, searchHullTimer));
RemoveSubObjective(ref goToObjective);
TryAddSubObjective(ref divingGearObjective,
constructor: () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager),
onAbandon: () =>
{
searchHullTimer = Math.Min(1, searchHullTimer);
// Don't reset the diving gear objective, because it's possible that there is no diving gear -> seek a safe hull and then reset so that we can check again.
},
onCompleted: () =>
{
resetPriority = true;
searchHullTimer = Math.Min(1, searchHullTimer);
RemoveSubObjective(ref divingGearObjective);
});
}
else
else if (divingGearObjective == null || !divingGearObjective.CanBeCompleted)
{
if (divingGearObjective != null && divingGearObjective.IsCompleted())
{
// Reset the devotion.
Priority = 0;
divingGearObjective = null;
}
if (currenthullSafety < HumanAIController.HULL_SAFETY_THRESHOLD)
{
searchHullTimer = Math.Min(1, searchHullTimer);
@@ -108,7 +113,7 @@ namespace Barotrauma
}
else
{
searchHullTimer = SearchHullInterval;
searchHullTimer = SearchHullInterval * Rand.Range(0.9f, 1.1f);
previousSafeHull = currentSafeHull;
currentSafeHull = FindBestHull();
if (currentSafeHull == null)
@@ -119,70 +124,80 @@ namespace Barotrauma
{
if (goToObjective?.Target != currentSafeHull)
{
goToObjective = null;
RemoveSubObjective(ref goToObjective);
}
TryAddSubObjective(ref goToObjective,
constructor: () => new AIObjectiveGoTo(currentSafeHull, character, objectiveManager, getDivingGearIfNeeded: true)
{
AllowGoingOutside = HumanAIController.HasDivingSuit(character)
},
onAbandon: () => unreachable.Add(goToObjective.Target as Hull));
AllowGoingOutside = HumanAIController.HasDivingSuit(character, conditionPercentage: 50)
},
onCompleted: () =>
{
if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD ||
HumanAIController.NeedsDivingGear(character, currentHull, out bool needsSuit) && (needsSuit ? HumanAIController.HasDivingSuit(character) : HumanAIController.HasDivingMask(character)))
{
resetPriority = true;
searchHullTimer = Math.Min(1, searchHullTimer);
}
RemoveSubObjective(ref goToObjective);
// If diving gear objective failed, let's reset it here.
RemoveSubObjective(ref divingGearObjective);
},
onAbandon: () =>
{
if (currentHull != null)
{
HumanAIController.UnreachableHulls.Add(goToObjective.Target as Hull);
}
RemoveSubObjective(ref goToObjective);
});
}
else
{
goToObjective = null;
RemoveSubObjective(ref goToObjective);
}
}
if (goToObjective != null)
if (subObjectives.Any(so => so.CanBeCompleted)) { return; }
if (currentHull != null)
{
if (goToObjective.IsCompleted())
//goto objective doesn't exist (a safe hull not found, or a path to a safe hull not found)
// -> attempt to manually steer away from hazards
Vector2 escapeVel = Vector2.Zero;
// TODO: optimize
foreach (FireSource fireSource in HumanAIController.VisibleHulls.SelectMany(h => h.FireSources))
{
objectiveManager.GetObjective<AIObjectiveIdle>()?.Wander(deltaTime);
}
Priority = 0;
return;
}
if (currentHull == null) { return; }
//goto objective doesn't exist (a safe hull not found, or a path to a safe hull not found)
// -> attempt to manually steer away from hazards
Vector2 escapeVel = Vector2.Zero;
// TODO: optimize
foreach (FireSource fireSource in HumanAIController.VisibleHulls.SelectMany(h => h.FireSources))
{
Vector2 dir = character.Position - fireSource.Position;
float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(fireSource.Position, character.Position), 0.1f, 10.0f);
escapeVel += new Vector2(Math.Sign(dir.X) * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier);
}
foreach (Character enemy in Character.CharacterList)
{
if (enemy.IsDead || enemy.IsUnconscious || enemy.Removed || HumanAIController.IsFriendly(enemy)) { continue; }
if (HumanAIController.VisibleHulls.Contains(enemy.CurrentHull))
{
Vector2 dir = character.Position - enemy.Position;
float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(enemy.Position, character.Position), 0.1f, 10.0f);
Vector2 dir = character.Position - fireSource.Position;
float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(fireSource.Position, character.Position), 0.1f, 10.0f);
escapeVel += new Vector2(Math.Sign(dir.X) * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier);
}
}
if (escapeVel != Vector2.Zero)
{
float left = currentHull.Rect.X + 50;
float right = currentHull.Rect.Right - 50;
//only move if we haven't reached the edge of the room
if (escapeVel.X < 0 && character.Position.X > left || escapeVel.X > 0 && character.Position.X < right)
foreach (Character enemy in Character.CharacterList)
{
character.AIController.SteeringManager.SteeringManual(deltaTime, escapeVel);
if (HumanAIController.IsFriendly(enemy) || !HumanAIController.IsActive(enemy)) { continue; }
if (HumanAIController.VisibleHulls.Contains(enemy.CurrentHull))
{
Vector2 dir = character.Position - enemy.Position;
float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(enemy.Position, character.Position), 0.1f, 10.0f);
escapeVel += new Vector2(Math.Sign(dir.X) * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier);
}
}
else
if (escapeVel != Vector2.Zero)
{
character.AnimController.TargetDir = escapeVel.X < 0.0f ? Direction.Right : Direction.Left;
character.AIController.SteeringManager.Reset();
float left = currentHull.Rect.X + 50;
float right = currentHull.Rect.Right - 50;
//only move if we haven't reached the edge of the room
if (escapeVel.X < 0 && character.Position.X > left || escapeVel.X > 0 && character.Position.X < right)
{
character.AIController.SteeringManager.SteeringManual(deltaTime, escapeVel);
}
else
{
character.AnimController.TargetDir = escapeVel.X < 0.0f ? Direction.Right : Direction.Left;
character.AIController.SteeringManager.Reset();
}
return;
}
}
else
{
Priority = 0;
objectiveManager.GetObjective<AIObjectiveIdle>()?.Wander(deltaTime);
}
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
}
}
@@ -195,24 +210,28 @@ namespace Barotrauma
if (hull.Submarine == null) { continue; }
if (!allowChangingTheSubmarine && hull.Submarine != character.Submarine) { continue; }
if (ignoredHulls != null && ignoredHulls.Contains(hull)) { continue; }
if (unreachable.Contains(hull)) { continue; }
if (HumanAIController.UnreachableHulls.Contains(hull)) { continue; }
float hullSafety = 0;
if (character.CurrentHull != null && character.Submarine != null)
{
// Inside
if (!character.Submarine.IsConnectedTo(hull.Submarine)) { continue; }
hullSafety = HumanAIController.GetHullSafety(hull, character);
// Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally)
float dist = Math.Abs(character.WorldPosition.X - hull.WorldPosition.X) + Math.Abs(character.WorldPosition.Y - hull.WorldPosition.Y) * 2.0f;
hullSafety = HumanAIController.GetHullSafety(hull, hull.GetConnectedHulls(true, 1), character);
float yDist = Math.Abs(character.WorldPosition.Y - hull.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 3 : 0;
float dist = Math.Abs(character.WorldPosition.X - hull.WorldPosition.X) + yDist;
float distanceFactor = MathHelper.Lerp(1, 0.9f, MathUtils.InverseLerp(0, 10000, dist));
hullSafety *= distanceFactor;
//skip the hull if the safety is already less than the best hull
//(no need to do the expensive pathfinding if we already know we're not going to choose this hull)
if (hullSafety < bestValue) { continue; }
var path = PathSteering.PathFinder.FindPath(character.SimPosition, hull.SimPosition);
if (path.Unreachable)
// Don't allow to go outside if not already outside.
var path = character.CurrentHull != null ?
PathSteering.PathFinder.FindPath(character.SimPosition, hull.SimPosition, nodeFilter: node => node.Waypoint.CurrentHull != null) :
PathSteering.PathFinder.FindPath(character.SimPosition, hull.SimPosition);
if (path.Unreachable && character.CurrentHull != null)
{
unreachable.Add(hull);
HumanAIController.UnreachableHulls.Add(hull);
continue;
}
// Each unsafe node reduces the hull safety value.
@@ -11,10 +11,10 @@ namespace Barotrauma
{
public override string DebugTag => "fix leak";
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
public Gap Leak { get; private set; }
private AIObjectiveFindDivingGear findDivingGear;
private AIObjectiveGetItem getWeldingTool;
private AIObjectiveContainItem refuelObjective;
private AIObjectiveGoTo gotoObjective;
@@ -25,43 +25,30 @@ namespace Barotrauma
Leak = leak;
}
public override bool IsCompleted()
{
return Leak.Open <= 0.0f || Leak.Removed;
}
protected override bool Check() => Leak.Open <= 0 || Leak.Removed;
public override float GetPriority()
{
if (Leak.Open == 0.0f) { return 0.0f; }
// Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally)
float dist = Math.Abs(character.WorldPosition.X - Leak.WorldPosition.X) + Math.Abs(character.WorldPosition.Y - Leak.WorldPosition.Y) * 2.0f;
float distanceFactor = MathHelper.Lerp(1, 0.25f, MathUtils.InverseLerp(0, 10000, dist));
float severity = AIObjectiveFixLeaks.GetLeakSeverity(Leak);
if (Leak.Removed || Leak.Open <= 0) { return 0; }
float xDist = Math.Abs(character.WorldPosition.X - Leak.WorldPosition.X);
float yDist = Math.Abs(character.WorldPosition.Y - Leak.WorldPosition.Y);
// Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally).
// If the target is close, ignore the distance factor alltogether so that we keep fixing the leaks that are nearby.
float distanceFactor = xDist < 200 && yDist < 100 ? 1 : MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, xDist + yDist * 3.0f));
float severity = AIObjectiveFixLeaks.GetLeakSeverity(Leak) / 100;
float max = Math.Min((AIObjectiveManager.OrderPriority - 1), 90);
float devotion = Math.Min(Priority, 10) / 100;
return MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + severity * distanceFactor * PriorityModifier, 0, 1));
}
public override bool IsDuplicate(AIObjective otherObjective)
{
if (!(otherObjective is AIObjectiveFixLeak fixLeak)) { return false; }
return fixLeak.Leak == Leak;
}
protected override void Act(float deltaTime)
{
if (!Leak.IsRoomToRoom)
{
if (!HumanAIController.HasDivingSuit(character))
{
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, true, objectiveManager));
return;
}
}
var weldingTool = character.Inventory.FindItemByTag("weldingtool");
var weldingTool = character.Inventory.FindItemByTag("weldingtool", true);
if (weldingTool == null)
{
TryAddSubObjective(ref getWeldingTool, () => new AIObjectiveGetItem(character, "weldingtool", objectiveManager, true));
TryAddSubObjective(ref getWeldingTool, () => new AIObjectiveGetItem(character, "weldingtool", objectiveManager, true),
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref getWeldingTool));
return;
}
else
@@ -70,9 +57,9 @@ namespace Barotrauma
if (containedItems == null)
{
#if DEBUG
DebugConsole.ThrowError("AIObjectiveFixLeak failed - the item \"" + weldingTool + "\" has no proper inventory");
DebugConsole.ThrowError($"{character.Name}: AIObjectiveFixLeak failed - the item \"" + weldingTool + "\" has no proper inventory");
#endif
abandon = true;
Abandon = true;
return;
}
// Drop empty tanks
@@ -84,9 +71,11 @@ namespace Barotrauma
containedItem.Drop(character);
}
}
if (containedItems.None(i => i.HasTag("weldingfueltank") && i.Condition > 0.0f))
if (containedItems.None(i => i.HasTag("weldingfuel") && i.Condition > 0.0f))
{
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfueltank", weldingTool.GetComponent<ItemContainer>(), objectiveManager));
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfuel", weldingTool.GetComponent<ItemContainer>(), objectiveManager),
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref refuelObjective));
return;
}
}
@@ -95,30 +84,55 @@ namespace Barotrauma
if (repairTool == null)
{
#if DEBUG
DebugConsole.ThrowError("AIObjectiveFixLeak failed - the item \"" + weldingTool + "\" has no RepairTool component but is tagged as a welding tool");
DebugConsole.ThrowError($"{character.Name}: AIObjectiveFixLeak failed - the item \"" + weldingTool + "\" has no RepairTool component but is tagged as a welding tool");
#endif
abandon = true;
Abandon = true;
return;
}
Vector2 gapDiff = Leak.WorldPosition - character.WorldPosition;
Vector2 toLeak = Leak.WorldPosition - character.WorldPosition;
// TODO: use the collider size/reach?
if (!character.AnimController.InWater && Math.Abs(gapDiff.X) < 100 && gapDiff.Y < 0.0f && gapDiff.Y > -150)
if (!character.AnimController.InWater && Math.Abs(toLeak.X) < 100 && toLeak.Y < 0.0f && toLeak.Y > -150)
{
HumanAIController.AnimController.Crouching = true;
}
// Use a greater reach, because the distance is calculated from the character to the leak, not from the item to the leak.
float reach = repairTool.Range + ((HumanoidAnimController)character.AnimController).ArmLength;
bool canOperate = gapDiff.LengthSquared() < reach * reach;
float reach = repairTool.Range + ConvertUnits.ToDisplayUnits(((HumanoidAnimController)character.AnimController).ArmLength);
bool canOperate = toLeak.LengthSquared() < reach * reach;
if (canOperate)
{
TryAddSubObjective(ref operateObjective, () => new AIObjectiveOperateItem(repairTool, character, objectiveManager, option: "", requireEquip: true, operateTarget: Leak));
TryAddSubObjective(ref operateObjective, () => new AIObjectiveOperateItem(repairTool, character, objectiveManager, option: "", requireEquip: true, operateTarget: Leak),
onAbandon: () => Abandon = true,
onCompleted: () =>
{
if (Check()) { IsCompleted = true; }
else
{
// Failed to operate. Probably too far.
Abandon = true;
}
});
}
else
{
TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(Leak, character, objectiveManager)
{
AllowGoingOutside = objectiveManager.IsCurrentOrder<AIObjectiveFixLeaks>(),
CloseEnough = reach
});
},
onAbandon: () =>
{
if (Check()) { IsCompleted = true; }
else if ((Leak.WorldPosition - character.WorldPosition).LengthSquared() > reach * reach * 2)
{
// Too far
Abandon = true;
}
else
{
// We are close, try again.
RemoveSubObjective(ref gotoObjective);
}
},
onCompleted: () => RemoveSubObjective(ref gotoObjective));
}
}
}
@@ -10,6 +10,8 @@ namespace Barotrauma
{
public override string DebugTag => "fix leaks";
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
public override bool IgnoreUnsafeHulls => true;
public AIObjectiveFixLeaks(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { }
@@ -18,7 +20,7 @@ namespace Barotrauma
public static float GetLeakSeverity(Gap leak)
{
if (leak == null) { return 0; }
float sizeFactor = MathHelper.Lerp(1, 10, MathUtils.InverseLerp(0, 200, (leak.IsHorizontal ? leak.Rect.Width : leak.Rect.Height)));
float sizeFactor = MathHelper.Lerp(1, 10, MathUtils.InverseLerp(0, 200, leak.Size));
float severity = sizeFactor * leak.Open;
if (!leak.IsRoomToRoom)
{
@@ -32,7 +34,6 @@ namespace Barotrauma
}
}
public override bool IsDuplicate(AIObjective otherObjective) => otherObjective is AIObjectiveFixLeaks;
protected override float TargetEvaluation() => Targets.Max(t => GetLeakSeverity(t));
protected override IEnumerable<Gap> GetList() => Gap.GapList;
protected override AIObjective ObjectiveConstructor(Gap gap)
@@ -26,6 +26,8 @@ namespace Barotrauma
private AIObjectiveGoTo goToObjective;
private float currItemPriority;
public bool AllowToFindDivingGear { get; set; } = true;
public override float GetPriority()
{
if (objectiveManager.CurrentOrder == this)
@@ -35,7 +37,7 @@ namespace Barotrauma
return 1.0f;
}
public AIObjectiveGetItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, bool equip = false, float priorityModifier = 1)
public AIObjectiveGetItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, bool equip = true, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
currSearchIndex = -1;
@@ -43,10 +45,10 @@ namespace Barotrauma
this.targetItem = targetItem;
}
public AIObjectiveGetItem(Character character, string itemIdentifier, AIObjectiveManager objectiveManager, bool equip = false, bool checkInventory = true, float priorityModifier = 1)
public AIObjectiveGetItem(Character character, string itemIdentifier, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1)
: this(character, new string[] { itemIdentifier }, objectiveManager, equip, checkInventory, priorityModifier) { }
public AIObjectiveGetItem(Character character, string[] itemIdentifiers, AIObjectiveManager objectiveManager, bool equip = false, bool checkInventory = true, float priorityModifier = 1)
public AIObjectiveGetItem(Character character, string[] itemIdentifiers, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
currSearchIndex = -1;
@@ -65,32 +67,11 @@ namespace Barotrauma
private void CheckInventory()
{
if (itemIdentifiers == null) { return; }
for (int i = 0; i < character.Inventory.Items.Length; i++)
var item = character.Inventory.FindItem(i => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id)) && i.Condition > 0, recursive: true);
if (item != null)
{
if (character.Inventory.Items[i] == null || character.Inventory.Items[i].Condition <= 0.0f) { continue; }
if (itemIdentifiers.Any(id => character.Inventory.Items[i].Prefab.Identifier == id || character.Inventory.Items[i].HasTag(id)))
{
targetItem = character.Inventory.Items[i];
moveToTarget = targetItem;
currItemPriority = 100.0f;
break;
}
//check items inside items (tool inside a toolbox etc)
var containedItems = character.Inventory.Items[i].ContainedItems;
if (containedItems != null)
{
foreach (Item containedItem in containedItems)
{
if (containedItem == null || containedItem.Condition <= 0.0f) { continue; }
if (itemIdentifiers.Any(id => containedItem.Prefab.Identifier == id || containedItem.HasTag(id)))
{
targetItem = containedItem;
moveToTarget = character.Inventory.Items[i];
currItemPriority = 100.0f;
break;
}
}
}
targetItem = item;
moveToTarget = item.GetRootContainer() ?? item;
}
}
@@ -98,58 +79,91 @@ namespace Barotrauma
{
if (character.LockHands)
{
abandon = true;
Abandon = true;
return;
}
FindTargetItem();
if (targetItem == null || moveToTarget == null)
if (targetItem == null)
{
objectiveManager.GetObjective<AIObjectiveIdle>()?.Wander(deltaTime);
return;
FindTargetItem();
if (targetItem == null || moveToTarget == null)
{
if (targetItem != null && moveToTarget == null)
{
#if DEBUG
DebugConsole.ThrowError($"{character.Name}: Move to target is null!");
#endif
Abandon = true;
}
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
return;
}
}
if (character.IsItemTakenBySomeoneElse(targetItem))
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Found an item, but it's already equipped by someone else. Aborting.", Color.Yellow);
#endif
Abandon = true;
}
if (character.CanInteractWith(targetItem, out _, checkLinked: false))
{
if (IsTakenBySomeone(targetItem))
var pickable = targetItem.GetComponent<Pickable>();
if (pickable == null)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Found an item, but it's equipped by someone else. Aborting.", Color.Yellow);
DebugConsole.NewMessage($"{character.Name}: Target not pickable. Aborting.", Color.Yellow);
#endif
abandon = true;
Abandon = true;
return;
}
targetItem.TryInteract(character, forceSelectKey: true);
if (equip)
{
int targetSlot = -1;
//check if all the slots required by the item are free
foreach (InvSlotType slots in pickable.AllowedSlots)
{
if (slots.HasFlag(InvSlotType.Any)) { continue; }
for (int i = 0; i < character.Inventory.Items.Length; i++)
{
//slot not needed by the item, continue
if (!slots.HasFlag(character.Inventory.SlotTypes[i])) { continue; }
targetSlot = i;
//slot free, continue
var otherItem = character.Inventory.Items[i];
if (otherItem == null) { continue; }
//try to move the existing item to LimbSlot.Any and continue if successful
if (character.Inventory.TryPutItem(otherItem, character, new List<InvSlotType>() { InvSlotType.Any })) { continue; }
//if everything else fails, simply drop the existing item
otherItem.Drop(character);
}
}
if (character.Inventory.TryPutItem(targetItem, targetSlot, false, false, character))
{
IsCompleted = true;
}
else
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Failed to equip/move the item '{targetItem.Name}' into the character inventory. Aborting.", Color.Red);
#endif
Abandon = true;
}
}
else
{
int targetSlot = -1;
if (equip)
targetItem.ParentInventory.RemoveItem(targetItem);
if (character.Inventory.TryPutItem(targetItem, null, new List<InvSlotType>() { InvSlotType.Any }))
{
var pickable = targetItem.GetComponent<Pickable>();
if (pickable == null)
{
abandon = true;
return;
}
//check if all the slots required by the item are free
foreach (InvSlotType slots in pickable.AllowedSlots)
{
if (slots.HasFlag(InvSlotType.Any)) { continue; }
for (int i = 0; i < character.Inventory.Items.Length; i++)
{
//slot not needed by the item, continue
if (!slots.HasFlag(character.Inventory.SlotTypes[i])) { continue; }
targetSlot = i;
//slot free, continue
if (character.Inventory.Items[i] == null) { continue; }
//try to move the existing item to LimbSlot.Any and continue if successful
if (character.Inventory.TryPutItem(character.Inventory.Items[i], character, new List<InvSlotType>() { InvSlotType.Any })) { continue; }
//if everything else fails, simply drop the existing item
character.Inventory.Items[i].Drop(character);
}
}
IsCompleted = true;
}
targetItem.TryInteract(character, false, true);
if (targetSlot > -1 && !character.HasEquippedItem(targetItem))
else
{
character.Inventory.TryPutItem(targetItem, targetSlot, false, false, character);
Abandon = true;
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Failed to equip/move the item '{targetItem.Name}' into the character inventory. Aborting.", Color.Red);
#endif
targetItem.Drop(character);
}
}
}
@@ -158,17 +172,16 @@ namespace Barotrauma
TryAddSubObjective(ref goToObjective,
constructor: () =>
{
//check if we're already looking for a diving gear
bool gettingDivingGear = (targetItem != null && targetItem.Prefab.Identifier == "divingsuit" || targetItem.HasTag("diving")) ||
(itemIdentifiers != null && (itemIdentifiers.Contains("diving") || itemIdentifiers.Contains("divingsuit")));
return new AIObjectiveGoTo(moveToTarget, character, objectiveManager, repeat: false, getDivingGearIfNeeded: !gettingDivingGear);
return new AIObjectiveGoTo(moveToTarget, character, objectiveManager, repeat: false, getDivingGearIfNeeded: AllowToFindDivingGear);
},
onAbandon: () =>
{
targetItem = null;
moveToTarget = null;
ignoredItems.Add(targetItem);
});
RemoveSubObjective(ref goToObjective);
},
onCompleted: () => RemoveSubObjective(ref goToObjective));
}
}
@@ -182,9 +195,9 @@ namespace Barotrauma
if (targetItem == null)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot find the item, because neither identifiers nor item is was defined.", Color.Red);
DebugConsole.NewMessage($"{character.Name}: Cannot find the item, because neither identifiers nor item was defined.", Color.Red);
#endif
abandon = true;
Abandon = true;
}
return;
}
@@ -194,26 +207,29 @@ namespace Barotrauma
var item = Item.ItemList[currSearchIndex];
if (ignoredItems.Contains(item)) { continue; }
if (item.Submarine == null) { continue; }
else if (item.Submarine.TeamID != character.TeamID) { continue; }
else if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(item, true)) { continue; }
if (item.Submarine.TeamID != character.TeamID) { continue; }
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(item, true)) { continue; }
if (item.CurrentHull == null || item.Condition <= 0.0f) { continue; }
if (itemIdentifiers.None(id => item.Prefab.Identifier == id || item.HasTag(id))) { continue; }
if (ignoredContainerIdentifiers != null && item.Container != null)
{
if (ignoredContainerIdentifiers.Contains(item.ContainerIdentifier)) { continue; }
}
if (IsTakenBySomeone(item)) { continue; }
float itemPriority = 0.0f;
if (character.IsItemTakenBySomeoneElse(item)) { continue; }
float itemPriority = 1;
if (GetItemPriority != null)
{
//ignore if the item has zero priority
itemPriority = GetItemPriority(item);
if (itemPriority <= 0.0f) { continue; }
}
Item rootContainer = item.GetRootContainer();
itemPriority -= Vector2.Distance((rootContainer ?? item).Position, character.Position) * 0.01f;
Vector2 itemPos = (rootContainer ?? item).WorldPosition;
float yDist = Math.Abs(character.WorldPosition.Y - itemPos.Y);
yDist = yDist > 100 ? yDist * 5 : 0;
float dist = Math.Abs(character.WorldPosition.X - itemPos.X) + yDist;
float distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, 10000, dist));
itemPriority *= distanceFactor;
//ignore if the item has a lower priority than the currently selected one
if (moveToTarget != null && itemPriority < currItemPriority) { continue; }
if (itemPriority < currItemPriority) { continue; }
currItemPriority = itemPriority;
targetItem = item;
moveToTarget = rootContainer ?? item;
@@ -222,82 +238,29 @@ namespace Barotrauma
if (currSearchIndex >= Item.ItemList.Count - 1 && targetItem == null)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot find the item with the following identifier(s): {string.Join(", ", itemIdentifiers)}", Color.Red);
DebugConsole.NewMessage($"{character.Name}: Cannot find the item with the following identifier(s): {string.Join(", ", itemIdentifiers)}", Color.Yellow);
#endif
abandon = true;
Abandon = true;
}
}
public override bool IsDuplicate(AIObjective otherObjective)
{
if (!(otherObjective is AIObjectiveGetItem getItem)) { return false; }
if (getItem.equip != equip) { return false; }
if (getItem.itemIdentifiers != null && itemIdentifiers != null)
{
if (getItem.itemIdentifiers.Length != itemIdentifiers.Length) { return false; }
for (int i = 0; i < getItem.itemIdentifiers.Length; i++)
{
if (getItem.itemIdentifiers[i] != itemIdentifiers[i]) { return false; }
}
return true;
}
else if (getItem.itemIdentifiers == null && itemIdentifiers == null)
{
return getItem.targetItem == targetItem;
}
return false;
}
public override bool IsCompleted()
protected override bool Check()
{
if (IsCompleted) { return true; }
if (targetItem != null)
{
return HasItem(targetItem);
return character.HasItem(targetItem, equip);
}
else if (itemIdentifiers != null)
{
foreach (string itemName in itemIdentifiers)
var matchingItem = character.Inventory.FindItem(i => !ignoredItems.Contains(i) && itemIdentifiers.Any(id => id == i.Prefab.Identifier || i.HasTag(id)), recursive: true);
if (matchingItem != null)
{
var matchingItem = character.Inventory.FindItemByTag(itemName) ?? character.Inventory.FindItemByIdentifier(itemName);
if (matchingItem != null && (!equip || character.HasEquippedItem(matchingItem)))
{
return true;
}
return !equip || character.HasEquippedItem(matchingItem);
}
return false;
}
return false;
}
private bool HasItem(Item item)
{
bool isEquipped = !equip || character.HasEquippedItem(item);
if (character.Inventory.Items.Contains(item) && isEquipped) { return true; }
if (!equip)
{
Item rootContainer = item.GetRootContainer();
if (rootContainer != null && rootContainer.ParentInventory is CharacterInventory)
{
return rootContainer.ParentInventory.Owner == character;
}
}
return false;
}
private bool IsTakenBySomeone(Item item)
{
//if the item is inside a character's inventory, don't steal it unless the character is dead
if (item.ParentInventory is CharacterInventory)
{
if (item.ParentInventory.Owner is Character owner && owner != character && !owner.IsDead) { return true; }
}
//if the item is inside an item, which is inside a character's inventory, don't steal it unless the character is dead
Item rootContainer = item.GetRootContainer();
if (rootContainer != null && rootContainer.ParentInventory is CharacterInventory)
{
if (rootContainer.ParentInventory.Owner is Character owner && owner != character && !owner.IsDead) { return true; }
}
return false;
}
}
}
@@ -1,5 +1,6 @@
using Microsoft.Xna.Framework;
using System;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
@@ -9,7 +10,7 @@ namespace Barotrauma
public override string DebugTag => "go to";
private AIObjectiveFindDivingGear findDivingGear;
private bool repeat;
private readonly bool repeat;
//how long until the path to the target is declared unreachable
private float waitUntilPathUnreachable;
private bool getDivingGearIfNeeded;
@@ -21,13 +22,23 @@ namespace Barotrauma
public bool followControlledCharacter;
public bool mimic;
private float _closeEnough = 50;
/// <summary>
/// Display units
/// </summary>
public float CloseEnough { get; set; } = 50;
public float CloseEnough
{
get { return _closeEnough; }
set
{
_closeEnough = Math.Max(_closeEnough, value);
}
}
public bool IgnoreIfTargetDead { get; set; }
public bool AllowGoingOutside { get; set; }
public override bool AbandonWhenCannotCompleteSubjectives => !repeat;
public ISpatialEntity Target { get; private set; }
public override float GetPriority()
@@ -42,14 +53,18 @@ namespace Barotrauma
return 1.0f;
}
public AIObjectiveGoTo(ISpatialEntity target, Character character, AIObjectiveManager objectiveManager, bool repeat = false, bool getDivingGearIfNeeded = true, float priorityModifier = 1)
public AIObjectiveGoTo(ISpatialEntity target, Character character, AIObjectiveManager objectiveManager, bool repeat = false, bool getDivingGearIfNeeded = true, float priorityModifier = 1, float closeEnough = 0)
: base (character, objectiveManager, priorityModifier)
{
this.Target = target;
this.repeat = repeat;
waitUntilPathUnreachable = 3.0f;
this.getDivingGearIfNeeded = getDivingGearIfNeeded;
CalculateCloseEnough();
CloseEnough = closeEnough;
if (Target is Item i)
{
CloseEnough = Math.Max(CloseEnough, i.InteractDistance + Math.Max(i.Rect.Width, i.Rect.Height) / 2);
}
}
protected override void Act(float deltaTime)
@@ -58,17 +73,17 @@ namespace Barotrauma
{
if (Character.Controlled == null)
{
abandon = true;
Abandon = true;
return;
}
Target = Character.Controlled;
}
if (Target == character)
{
// Wait
character.AIController.SteeringManager.Reset();
abandon = true;
return;
}
}
waitUntilPathUnreachable -= deltaTime;
if (!character.IsClimbing)
{
@@ -78,24 +93,37 @@ namespace Barotrauma
{
if (e.Removed)
{
abandon = true;
Abandon = true;
}
else
{
character.AIController.SelectTarget(e.AiTarget);
}
}
bool isInside = character.CurrentHull != null;
bool insideSteering = SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.IsPathDirty;
var targetHull = Target is Hull h ? h : Target is Item i ? i.CurrentHull : Target is Character c ? c.CurrentHull : character.CurrentHull;
if (!followControlledCharacter)
{
// 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 = HumanAIController.CurrentOrder == null && !HumanAIController.ObjectiveManager.CurrentObjective.IgnoreUnsafeHulls
&& PathSteering != null && PathSteering.CurrentPath != null
&& PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull));
if (containsUnsafeNodes || HumanAIController.UnreachableHulls.Contains(targetHull))
{
Abandon = true;
SteeringManager.Reset();
return;
}
}
bool insideSteering = SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.IsPathDirty;
bool isInside = character.CurrentHull != null;
bool targetIsOutside = (Target != null && targetHull == null) || (insideSteering && PathSteering.CurrentPath.HasOutdoorsNodes);
if (isInside && targetIsOutside && !AllowGoingOutside)
{
abandon = true;
Abandon = true;
}
else if (waitUntilPathUnreachable < 0)
{
if (SteeringManager == PathSteering && PathSteering.CurrentPath != null && PathSteering.CurrentPath.Unreachable)
if (SteeringManager == PathSteering && PathSteering.CurrentPath != null && PathSteering.CurrentPath.Unreachable && !PathSteering.IsPathDirty)
{
if (repeat)
{
@@ -103,139 +131,147 @@ namespace Barotrauma
}
else
{
abandon = true;
Abandon = true;
}
}
}
if (abandon)
if (Abandon)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot reach the target: {Target.ToString()}", Color.Yellow);
#endif
if (objectiveManager.CurrentOrder != null)
if (objectiveManager.CurrentOrder != null && objectiveManager.CurrentOrder.ReportFailures)
{
character.Speak(TextManager.Get("DialogCannotReach"), identifier: "cannotreach", minDurationBetweenSimilar: 10.0f);
}
character.AIController.SteeringManager.Reset();
SteeringManager.Reset();
}
else
{
Vector2 currTargetSimPos = Vector2.Zero;
currTargetSimPos = Target.SimPosition;
// Take the sub position into account in the sim pos
if (SteeringManager != PathSteering && character.Submarine == null && Target.Submarine != null)
{
currTargetSimPos += Target.Submarine.SimPosition;
}
else if (character.Submarine != null && Target.Submarine == null)
{
currTargetSimPos -= character.Submarine.SimPosition;
}
else if (character.Submarine != Target.Submarine)
{
if (character.Submarine != null && Target.Submarine != null)
{
Vector2 diff = character.Submarine.SimPosition - Target.Submarine.SimPosition;
currTargetSimPos -= diff;
}
}
if (PathSteering != null)
{
PathSteering.startNodeFilter = startNodeFilter;
PathSteering.endNodeFilter = endNodeFilter;
}
SteeringManager.SteeringSeek(currTargetSimPos);
if (SteeringManager != PathSteering)
{
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: 5, weight: 1, heading: VectorExtensions.Forward(character.AnimController.Collider.Rotation));
}
if (getDivingGearIfNeeded)
if (getDivingGearIfNeeded && !character.LockHands)
{
Character followTarget = Target as Character;
bool needsDivingGear = HumanAIController.NeedsDivingGear(targetHull) || mimic && HumanAIController.HasDivingMask(followTarget);
bool needsDivingSuit = needsDivingGear && (targetHull == null || targetIsOutside || targetHull.WaterPercentage > 90) || mimic && HumanAIController.HasDivingSuit(followTarget);
bool needsDivingSuit = targetIsOutside;
bool needsDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(character, targetHull, out needsDivingSuit);
if (!needsDivingGear && mimic)
{
if (HumanAIController.HasDivingSuit(followTarget))
{
needsDivingGear = true;
needsDivingSuit = true;
}
else if (HumanAIController.HasDivingMask(followTarget))
{
needsDivingGear = true;
}
}
bool needsEquipment = false;
if (needsDivingSuit)
{
needsEquipment = !HumanAIController.HasDivingSuit(character);
needsEquipment = !HumanAIController.HasDivingSuit(character, AIObjectiveFindDivingGear.lowOxygenThreshold);
}
else if (needsDivingGear)
{
needsEquipment = !HumanAIController.HasDivingMask(character);
needsEquipment = !HumanAIController.HasDivingGear(character, AIObjectiveFindDivingGear.lowOxygenThreshold);
}
if (needsEquipment)
{
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager));
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager),
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref findDivingGear));
return;
}
}
if (repeat && IsCloseEnough)
{
OnCompleted();
return;
}
if (SteeringManager == PathSteering)
{
Func<PathNode, bool> nodeFilter = null;
if (isInside && !AllowGoingOutside)
{
nodeFilter = node => node.Waypoint.CurrentHull != null;
}
PathSteering.SteeringSeek(character.GetRelativeSimPosition(Target), 1, startNodeFilter, endNodeFilter, nodeFilter);
}
else
{
SteeringManager.SteeringSeek(character.GetRelativeSimPosition(Target), 10);
}
if (!insideSteering)
{
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: 5, weight: 1);
}
}
}
private bool isCompleted;
public override bool IsCompleted()
private bool IsCloseEnough
{
// First check the distance
// Then the custom condition
// And finally check if can interact (heaviest)
if (isCompleted) { return true; }
if (Target == null)
{
abandon = true;
return false;
}
bool closeEnough = Vector2.DistanceSquared(Target.WorldPosition, character.WorldPosition) < CloseEnough * CloseEnough;
if (repeat)
get
{
bool closeEnough = Vector2.DistanceSquared(Target.WorldPosition, character.WorldPosition) < CloseEnough * CloseEnough;
if (closeEnough)
{
closeEnough = !(Target is Character) || Target is Character c && c.CurrentHull == character.CurrentHull;
}
if (closeEnough)
{
OnCompleted();
}
return closeEnough;
}
}
protected override bool Check()
{
if (IsCompleted) { return true; }
// First check the distance
// Then the custom condition
// And finally check if can interact (heaviest)
if (Target == null)
{
Abandon = true;
return false;
}
else if (closeEnough)
if (repeat)
{
if (requiredCondition == null || requiredCondition())
return false;
}
else
{
if (IsCloseEnough)
{
if (Target is Item item)
if (requiredCondition == null || requiredCondition())
{
if (character.CanInteractWith(item, out _, checkLinked: false)) { isCompleted = true; }
}
else if (Target is Character targetCharacter)
{
if (character.CanInteractWith(targetCharacter, CloseEnough)) { isCompleted = true; }
}
else
{
isCompleted = true;
if (Target is Item item)
{
if (character.CanInteractWith(item, out _, checkLinked: false)) { IsCompleted = true; }
}
else if (Target is Character targetCharacter)
{
if (character.CanInteractWith(targetCharacter, CloseEnough)) { IsCompleted = true; }
}
else
{
IsCompleted = true;
}
}
}
}
return isCompleted;
return IsCompleted;
}
public override bool IsDuplicate(AIObjective otherObjective)
{
if (!(otherObjective is AIObjectiveGoTo objective)) { return false; }
return objective.Target == Target;
}
private void CalculateCloseEnough()
{
float interactionDistance = Target is Item i ? i.InteractDistance + Math.Max(i.Rect.Width, i.Rect.Height) / 2 : 0;
CloseEnough = Math.Max(interactionDistance, CloseEnough);
}
protected override void OnCompleted()
private void StopMovement()
{
character.AIController.SteeringManager.Reset();
if (Target != null)
{
character.AnimController.TargetDir = Target.WorldPosition.X > character.WorldPosition.X ? Direction.Right : Direction.Left;
}
}
protected override void OnCompleted()
{
StopMovement();
HumanAIController.FaceTarget(Target);
base.OnCompleted();
}
}
@@ -11,12 +11,11 @@ namespace Barotrauma
{
public override string DebugTag => "idle";
const float WallAvoidDistance = 150.0f;
private readonly float newTargetIntervalMin = 5;
private readonly float newTargetIntervalMax = 15;
private readonly float standStillMin = 1;
private readonly float newTargetIntervalMin = 10;
private readonly float newTargetIntervalMax = 20;
private readonly float standStillMin = 2;
private readonly float standStillMax = 10;
private readonly float walkDurationMin = 3;
private readonly float walkDurationMin = 5;
private readonly float walkDurationMax = 10;
private Hull currentTarget;
@@ -36,7 +35,7 @@ namespace Barotrauma
walkDuration = Rand.Range(0.0f, 10.0f);
}
public override bool IsCompleted() => false;
protected override bool Check() => false;
public override bool CanBeCompleted => true;
public override bool IsLoop { get => true; set => throw new System.Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace); }
@@ -93,8 +92,10 @@ namespace Barotrauma
if (currentTargetIsInvalid || currentTarget == null && HumanAIController.VisibleHulls.Any(h => IsForbidden(h)))
{
newTargetTimer = 0;
standStillTimer = 0;
//don't reset to zero, otherwise the character will keep calling FindTargetHulls
//almost constantly when there's a small number of potential hulls to move to
newTargetTimer = Math.Min(newTargetTimer, 0.5f);
//standStillTimer = 0.0f;
}
else if (character.IsClimbing)
{
@@ -102,7 +103,7 @@ namespace Barotrauma
{
newTargetTimer = 0;
}
else
else if (Math.Abs(character.AnimController.TargetMovement.Y) > 0)
{
// Don't allow new targets when climbing.
newTargetTimer = Math.Max(newTargetIntervalMin, newTargetTimer);
@@ -112,7 +113,7 @@ namespace Barotrauma
{
if (currentTarget == null)
{
newTargetTimer = 0;
newTargetTimer = Math.Min(newTargetTimer, 0.5f);
}
}
if (newTargetTimer <= 0.0f)
@@ -127,24 +128,32 @@ namespace Barotrauma
else if (targetHulls.Count > 0)
{
//choose a random available hull
var randomHull = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced);
bool isCurrentHullOK = !HumanAIController.UnsafeHulls.Contains(character.CurrentHull) && !IsForbidden(character.CurrentHull);
if (isCurrentHullOK)
currentTarget = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced);
bool isCurrentHullAllowed = !IsForbidden(character.CurrentHull);
var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, nodeFilter: node =>
{
if (node.Waypoint.CurrentHull == null) { return false; }
// Check that there is no unsafe or forbidden hulls on the way to the target
// Only do this when the current hull is ok, because otherwise would block all paths from the current hull to the target hull.
var path = PathSteering.PathFinder.FindPath(character.SimPosition, randomHull.SimPosition);
if (path.Unreachable || path.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull) || IsForbidden(n.CurrentHull)))
{
//can't go to this room, remove it from the list and try another room next frame
int index = targetHulls.IndexOf(randomHull);
targetHulls.RemoveAt(index);
hullWeights.RemoveAt(index);
PathSteering.Reset();
return;
}
if (node.Waypoint.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(node.Waypoint.CurrentHull)) { return false; }
if (isCurrentHullAllowed && IsForbidden(node.Waypoint.CurrentHull)) { return false; }
return true;
});
if (path.Unreachable)
{
//can't go to this room, remove it from the list and try another room next frame
int index = targetHulls.IndexOf(currentTarget);
targetHulls.RemoveAt(index);
hullWeights.RemoveAt(index);
PathSteering.Reset();
currentTarget = null;
return;
}
currentTarget = randomHull;
searchingNewHull = false;
}
else
{
// Couldn't find a target for some reason -> reset
newTargetTimer = Math.Max(newTargetIntervalMin, newTargetTimer);
searchingNewHull = false;
}
@@ -156,7 +165,7 @@ namespace Barotrauma
bool isRoomNameFound = currentTarget.DisplayName != null;
errorMsg = "(Character " + character.Name + " idling, target " + (isRoomNameFound ? currentTarget.DisplayName : currentTarget.ToString()) + ")";
#endif
var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, errorMsgStr: errorMsg);
var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, errorMsgStr: errorMsg, nodeFilter: node => node.Waypoint.CurrentHull != null);
PathSteering.SetPath(path);
}
@@ -174,20 +183,6 @@ namespace Barotrauma
if (SteeringManager != PathSteering || (PathSteering.CurrentPath != null &&
(PathSteering.CurrentPath.NextNode == null || PathSteering.CurrentPath.Unreachable || PathSteering.CurrentPath.HasOutdoorsNodes)))
{
if (!character.AnimController.InWater)
{
standStillTimer -= deltaTime;
if (standStillTimer > 0.0f)
{
walkDuration = Rand.Range(walkDurationMin, walkDurationMax);
PathSteering.Reset();
return;
}
if (standStillTimer < -walkDuration)
{
standStillTimer = Rand.Range(standStillMin, standStillMax);
}
}
Wander(deltaTime);
return;
}
@@ -195,64 +190,39 @@ namespace Barotrauma
if (currentTarget != null)
{
character.AIController.SteeringManager.SteeringSeek(currentTarget.SimPosition);
if (SteeringManager == PathSteering)
{
PathSteering.SteeringSeek(character.GetRelativeSimPosition(currentTarget), weight: 1, nodeFilter: node => node.Waypoint.CurrentHull != null);
}
else
{
character.AIController.SteeringManager.SteeringSeek(character.GetRelativeSimPosition(currentTarget));
}
}
else
{
Wander(deltaTime);
}
}
public void Wander(float deltaTime)
{
if (character.IsClimbing) { return; }
//steer away from edges of the hull
var currentHull = character.CurrentHull;
if (currentHull != null)
{
float roomWidth = currentHull.Rect.Width;
if (roomWidth < WallAvoidDistance * 4)
{
PathSteering.Reset();
}
else
{
float leftDist = character.Position.X - currentHull.Rect.X;
float rightDist = currentHull.Rect.Right - character.Position.X;
if (leftDist < WallAvoidDistance && rightDist < WallAvoidDistance)
{
if (Math.Abs(rightDist - leftDist) > WallAvoidDistance / 2)
{
PathSteering.SteeringManual(deltaTime, Vector2.UnitX * Math.Sign(rightDist - leftDist));
}
else
{
PathSteering.Reset();
}
}
else if (leftDist < WallAvoidDistance)
{
float speed = (WallAvoidDistance - leftDist) / WallAvoidDistance;
PathSteering.SteeringManual(deltaTime, Vector2.UnitX * MathHelper.Clamp(speed, 0.25f, 1));
PathSteering.WanderAngle = 0.0f;
}
else if (rightDist < WallAvoidDistance)
{
float speed = (WallAvoidDistance - rightDist) / WallAvoidDistance;
PathSteering.SteeringManual(deltaTime, -Vector2.UnitX * MathHelper.Clamp(speed, 0.25f, 1));
PathSteering.WanderAngle = MathHelper.Pi;
}
else
{
SteeringManager.SteeringWander();
}
}
}
else
{
SteeringManager.SteeringWander();
}
if (!character.AnimController.InWater)
{
//reset vertical steering to prevent dropping down from platforms etc
character.AIController.SteeringManager.ResetY();
standStillTimer -= deltaTime;
if (standStillTimer > 0.0f)
{
walkDuration = Rand.Range(walkDurationMin, walkDurationMax);
PathSteering.Reset();
return;
}
if (standStillTimer < -walkDuration)
{
standStillTimer = Rand.Range(standStillMin, standStillMax);
}
}
PathSteering.Wander(deltaTime);
}
private void FindTargetHulls()
@@ -280,8 +250,10 @@ namespace Barotrauma
targetHulls.Add(hull);
float weight = hull.Volume;
// Prefer rooms that are closer. Avoid rooms that are not in the same level.
float dist = Math.Abs(character.WorldPosition.X - hull.WorldPosition.X) + Math.Abs(character.WorldPosition.Y - hull.WorldPosition.Y) * 5.0f;
float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 2500, dist));
float yDist = Math.Abs(character.WorldPosition.Y - hull.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 5 : 0;
float dist = Math.Abs(character.WorldPosition.X - hull.WorldPosition.X) + yDist;
float distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, 2500, dist));
weight *= distanceFactor;
hullWeights.Add(weight);
}
@@ -295,10 +267,5 @@ namespace Barotrauma
if (hullName == null) { return false; }
return hullName.Contains("ballast") || hullName.Contains("airlock");
}
public override bool IsDuplicate(AIObjective otherObjective)
{
return (otherObjective is AIObjectiveIdle);
}
}
}
@@ -12,7 +12,7 @@ namespace Barotrauma
public Dictionary<T, AIObjective> Objectives { get; private set; } = new Dictionary<T, AIObjective>();
protected HashSet<T> ignoreList = new HashSet<T>();
private float ignoreListTimer;
private float targetUpdateTimer;
protected float targetUpdateTimer;
// By default, doesn't clear the list automatically
protected virtual float IgnoreListClearInterval => 0;
@@ -38,8 +38,11 @@ namespace Barotrauma
: base(character, objectiveManager, priorityModifier, option) { }
protected override void Act(float deltaTime) { }
public override bool IsCompleted() => false;
protected override bool Check() => false;
public override bool CanBeCompleted => true;
public override bool AbandonWhenCannotCompleteSubjectives => false;
public override bool AllowSubObjectiveSorting => true;
public override bool ReportFailures => false;
public override bool IsLoop { get => true; set => throw new System.Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace); }
@@ -69,18 +72,19 @@ namespace Barotrauma
foreach (var objective in Objectives)
{
var target = objective.Key;
if (!objective.Value.CanBeCompleted)
{
ignoreList.Add(target);
targetUpdateTimer = 0;
}
//if (!objective.Value.CanBeCompleted && !ignoreList.Contains(target))
//{
// // TODO: leaks that cannot be accessed from inside cause FixLeak objective to fail, but for some reason it's not ignored. Make sure that it is.
// ignoreList.Add(target);
// targetUpdateTimer = 0;
//}
if (!Targets.Contains(target))
{
subObjectives.Remove(objective.Value);
}
}
SyncRemovedObjectives(Objectives, GetList());
if (Objectives.None() && Targets.Any())
if (Objectives.None() && Targets.Any(t => !ignoreList.Contains(t)))
{
CreateObjectives();
}
@@ -91,6 +95,7 @@ namespace Barotrauma
public override void Reset()
{
base.Reset();
ignoreList.Clear();
ignoreListTimer = 0;
UpdateTargets();
@@ -98,6 +103,7 @@ namespace Barotrauma
public override float GetPriority()
{
if (character.LockHands) { return 0; }
if (character.Submarine == null) { return 0; }
if (Targets.None()) { return 0; }
// Allow the target value to be more than 100.
@@ -145,15 +151,26 @@ namespace Barotrauma
{
foreach (T target in Targets)
{
if (ignoreList.Contains(target)) { continue; }
if (!Objectives.TryGetValue(target, out AIObjective objective))
{
objective = ObjectiveConstructor(target);
objective.Completed += () => OnObjectiveCompleted(objective, target);
Objectives.Add(target, objective);
if (!subObjectives.Contains(objective))
{
subObjectives.Add(objective);
}
objective.Completed += () =>
{
Objectives.Remove(target);
OnObjectiveCompleted(objective, target);
};
objective.Abandoned += () =>
{
Objectives.Remove(target);
ignoreList.Add(target);
targetUpdateTimer = 0;
};
}
}
}
@@ -3,6 +3,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
@@ -12,7 +13,7 @@ namespace Barotrauma
public const float OrderPriority = 70;
public const float RunPriority = 50;
// Constantly increases the priority of the selected objective, unless overridden
public const float baseDevotion = 2;
public const float baseDevotion = 3;
public List<AIObjective> Objectives { get; private set; } = new List<AIObjective>();
@@ -35,7 +36,13 @@ namespace Barotrauma
public AIObjective CurrentOrder { get; private set; }
public AIObjective CurrentObjective { get; private set; }
public bool IsCurrentOrder<T>() where T : AIObjective => CurrentOrder is T;
public bool IsCurrentObjective<T>() where T : AIObjective => CurrentObjective is T;
public bool IsActiveObjective<T>() where T : AIObjective => GetActiveObjective() is T;
public AIObjective GetActiveObjective() => CurrentObjective?.GetActiveObjective();
public bool HasActiveObjective<T>() where T : AIObjective => CurrentObjective is T || CurrentObjective != null && CurrentObjective.GetSubObjectivesRecursive().Any(so => so is T);
public AIObjectiveManager(Character character)
{
@@ -43,7 +50,7 @@ namespace Barotrauma
CreateAutonomousObjectives();
}
public void AddObjective(AIObjective objective)
public void AddObjective<T>(T objective) where T : AIObjective
{
if (objective == null)
{
@@ -52,21 +59,32 @@ namespace Barotrauma
#endif
return;
}
var duplicate = Objectives.Find(o => o.IsDuplicate(objective));
if (duplicate != null)
// Can't use the generic type, because it's possible that the user of this method uses the base type AIObjective.
// We need to get the highest type.
var type = objective.GetType();
if (objective.AllowMultipleInstances)
{
duplicate.Reset();
if (Objectives.FirstOrDefault(o => o.GetType() == type) is T existingObjective && existingObjective.IsDuplicate(objective))
{
Objectives.Remove(existingObjective);
}
}
else
{
Objectives.Add(objective);
Objectives.RemoveAll(o => o.GetType() == type);
}
Objectives.Add(objective);
}
public Dictionary<AIObjective, CoroutineHandle> DelayedObjectives { get; private set; } = new Dictionary<AIObjective, CoroutineHandle>();
public void CreateAutonomousObjectives()
{
foreach (var delayedObjective in DelayedObjectives)
{
CoroutineManager.StopCoroutines(delayedObjective.Value);
}
DelayedObjectives.Clear();
Objectives.Clear();
AddObjective(new AIObjectiveFindSafety(character, this));
AddObjective(new AIObjectiveIdle(character, this));
@@ -82,8 +100,8 @@ namespace Barotrauma
matchingItems.RemoveAll(it => it.Submarine != character.Submarine);
var item = matchingItems.GetRandom();
var order = new Order(
orderPrefab,
item ?? character.CurrentHull as Entity,
orderPrefab,
item ?? character.CurrentHull as Entity,
item?.Components.FirstOrDefault(ic => ic.GetType() == orderPrefab.ItemComponentType),
orderGiver: character);
if (order == null) { continue; }
@@ -94,15 +112,15 @@ namespace Barotrauma
objectiveCount++;
}
}
WaitTimer = Math.Max(WaitTimer, Rand.Range(0.5f, 1f) * objectiveCount);
_waitTimer = Math.Max(_waitTimer, Rand.Range(0.5f, 1f) * objectiveCount);
}
public void AddObjective(AIObjective objective, float delay, Action callback = null)
public void AddObjective<T>(T objective, float delay, Action callback = null) where T : AIObjective
{
if (objective == null)
{
#if DEBUG
DebugConsole.ThrowError("Attempted to add a null objective to AIObjectiveManager\n" + Environment.StackTrace);
DebugConsole.ThrowError($"{character.Name}: Attempted to add a null objective to AIObjectiveManager\n" + Environment.StackTrace);
#endif
return;
}
@@ -120,14 +138,7 @@ namespace Barotrauma
DelayedObjectives.Add(objective, coroutine);
}
public T GetObjective<T>() where T : AIObjective
{
foreach (AIObjective objective in Objectives)
{
if (objective is T) return (T)objective;
}
return null;
}
public T GetObjective<T>() where T : AIObjective => Objectives.FirstOrDefault(o => o is T) as T;
private AIObjective GetCurrentObjective()
{
@@ -143,6 +154,7 @@ namespace Barotrauma
}
if (previousObjective != CurrentObjective)
{
previousObjective?.OnDeselected();
CurrentObjective?.OnSelected();
GetObjective<AIObjectiveIdle>().SetRandom();
}
@@ -165,17 +177,17 @@ namespace Barotrauma
for (int i = 0; i < Objectives.Count; i++)
{
var objective = Objectives[i];
if (objective.IsCompleted())
if (objective.IsCompleted)
{
#if DEBUG
DebugConsole.NewMessage($"Removing objective {objective.DebugTag}, because it is completed.");
DebugConsole.NewMessage($"{character.Name}: Removing objective {objective.DebugTag}, because it is completed.", Color.LightGreen);
#endif
Objectives.Remove(objective);
}
else if (!objective.CanBeCompleted)
{
#if DEBUG
DebugConsole.NewMessage($"Removing objective {objective.DebugTag}, because it cannot be completed.");
DebugConsole.NewMessage($"{character.Name}: Removing objective {objective.DebugTag}, because it cannot be completed.", Color.Red);
#endif
Objectives.Remove(objective);
}
@@ -193,7 +205,7 @@ namespace Barotrauma
{
Objectives.Sort((x, y) => y.GetPriority().CompareTo(x.GetPriority()));
}
CurrentObjective?.SortSubObjectives();
GetCurrentObjective()?.SortSubObjectives();
}
public void DoCurrentObjective(float deltaTime)
@@ -260,7 +272,10 @@ namespace Barotrauma
newObjective = new AIObjectiveRescueAll(character, this, priorityModifier);
break;
case "repairsystems":
newObjective = new AIObjectiveRepairItems(character, this, priorityModifier) { RequireAdequateSkills = option == "jobspecific" };
newObjective = new AIObjectiveRepairItems(character, this, priorityModifier)
{
RequireAdequateSkills = option == "jobspecific"
};
break;
case "pumpwater":
newObjective = new AIObjectivePumpWater(character, this, option, priorityModifier: priorityModifier);
@@ -275,11 +290,21 @@ namespace Barotrauma
var steering = (order?.TargetEntity as Item)?.GetComponent<Steering>();
if (steering != null) steering.PosToMaintain = steering.Item.Submarine?.WorldPosition;
if (order.TargetItemComponent == null) { return null; }
newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option, requireEquip: false, useController: order.UseController, priorityModifier: priorityModifier) { IsLoop = true };
newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option, requireEquip: false, useController: order.UseController, priorityModifier: priorityModifier)
{
IsLoop = true,
// Don't override auto pilot unless it's an order by a player
Override = orderGiver == Character.Controlled || orderGiver.IsRemotePlayer
};
break;
default:
if (order.TargetItemComponent == null) { return null; }
newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option, requireEquip: false, useController: order.UseController, priorityModifier: priorityModifier) { IsLoop = true };
newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option, requireEquip: false, useController: order.UseController, priorityModifier: priorityModifier)
{
IsLoop = true,
// Don't override auto control unless it's an order by a player
Override = orderGiver == Character.Controlled || orderGiver.IsRemotePlayer
};
break;
}
return newObjective;
@@ -12,17 +12,20 @@ namespace Barotrauma
private ItemComponent component, controller;
private Entity operateTarget;
private bool isCompleted;
private bool requireEquip;
private bool useController;
private AIObjectiveGoTo goToObjective;
private AIObjectiveGetItem getItemObjective;
public bool Override { get; set; } = true;
public override bool CanBeCompleted => base.CanBeCompleted && (!useController || controller != null);
public Entity OperateTarget => operateTarget;
public ItemComponent Component => component;
public Func<bool> completionCondition;
public override float GetPriority()
{
if (component.Item.ConditionPercentage <= 0) { return 0; }
@@ -32,7 +35,7 @@ namespace Barotrauma
}
if (component.Item.CurrentHull == null) { return 0; }
if (component.Item.CurrentHull.FireSources.Count > 0) { return 0; }
if (Character.CharacterList.Any(c => c.CurrentHull == component.Item.CurrentHull && !HumanAIController.IsFriendly(c))) { return 0; }
if (Character.CharacterList.Any(c => c.CurrentHull == component.Item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return 0; }
float devotion = MathHelper.Min(10, Priority);
float value = devotion + AIObjectiveManager.OrderPriority * PriorityModifier;
float max = MathHelper.Min((AIObjectiveManager.OrderPriority - 1), 90);
@@ -57,21 +60,27 @@ namespace Barotrauma
protected override void Act(float deltaTime)
{
if (character.LockHands)
{
Abandon = true;
return;
}
ItemComponent target = useController ? controller : component;
if (useController && controller == null)
{
character.Speak(TextManager.GetWithVariable("DialogCantFindController", "[item]", component.Item.Name, true), null, 2.0f, "cantfindcontroller", 30.0f);
abandon = true;
Abandon = true;
return;
}
if (target.CanBeSelected)
{
if (character.CanInteractWith(target.Item, out _, checkLinked: false))
{
HumanAIController.FaceTarget(target.Item);
// Don't allow to operate an item that someone already operates, unless this objective is an order
if (objectiveManager.CurrentOrder != this && Character.CharacterList.Any(c => c.SelectedConstruction == target.Item && c != character && HumanAIController.IsFriendly(c)))
if (objectiveManager.CurrentOrder != this && Character.CharacterList.Any(c => c.SelectedConstruction == target.Item && c != character && HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c)))
{
abandon = true;
// Don't abandon
return;
}
if (character.SelectedConstruction != target.Item)
@@ -80,12 +89,14 @@ namespace Barotrauma
}
if (component.AIOperate(deltaTime, character, this))
{
isCompleted = true;
IsCompleted = completionCondition == null || completionCondition();
}
}
else
{
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(target.Item, character, objectiveManager));
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(target.Item, character, objectiveManager, closeEnough: 50),
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref goToObjective));
}
}
else
@@ -93,12 +104,14 @@ namespace Barotrauma
if (component.Item.GetComponent<Pickable>() == null)
{
//controller/target can't be selected and the item cannot be picked -> objective can't be completed
abandon = true;
Abandon = true;
return;
}
else if (!character.Inventory.Items.Contains(component.Item))
{
TryAddSubObjective(ref getItemObjective, () => new AIObjectiveGetItem(character, component.Item, objectiveManager, equip: true));
TryAddSubObjective(ref getItemObjective, () => new AIObjectiveGetItem(character, component.Item, objectiveManager, equip: true),
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref getItemObjective));
}
else
{
@@ -109,7 +122,7 @@ namespace Barotrauma
if (holdable == null)
{
#if DEBUG
DebugConsole.ThrowError("AIObjectiveOperateItem failed - equipping item " + component.Item + " is required but the item has no Holdable component");
DebugConsole.ThrowError($"{character.Name}: AIObjectiveOperateItem failed - equipping item " + component.Item + " is required but the item has no Holdable component");
#endif
return;
}
@@ -139,18 +152,12 @@ namespace Barotrauma
}
if (component.AIOperate(deltaTime, character, this))
{
isCompleted = true;
IsCompleted = completionCondition == null || completionCondition();
}
}
}
}
public override bool IsCompleted() => isCompleted && !IsLoop;
public override bool IsDuplicate(AIObjective otherObjective)
{
if (!(otherObjective is AIObjectiveOperateItem operateItem)) { return false; }
return (operateItem.component == component || otherObjective.Option == Option);
}
protected override bool Check() => IsCompleted && !IsLoop;
}
}
@@ -10,13 +10,14 @@ namespace Barotrauma
class AIObjectivePumpWater : AIObjectiveLoop<Pump>
{
public override string DebugTag => "pump water";
public override bool KeepDivingGearOn => true;
public override bool IgnoreUnsafeHulls => true;
private IEnumerable<Pump> pumpList;
public AIObjectivePumpWater(Character character, AIObjectiveManager objectiveManager, string option, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier, option) { }
public override bool IsDuplicate(AIObjective otherObjective) => otherObjective is AIObjectivePumpWater && otherObjective.Option == Option;
protected override void FindTargets()
{
if (Option == null) { return; }
@@ -33,16 +34,8 @@ namespace Barotrauma
if (pump.Item.ConditionPercentage <= 0) { return false; }
if (pump.Item.CurrentHull.FireSources.Count > 0) { return false; }
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(pump.Item, true)) { return false; }
if (Character.CharacterList.Any(c => c.CurrentHull == pump.Item.CurrentHull && !HumanAIController.IsFriendly(c))) { return false; }
if (Option == "stoppumping")
{
if (!pump.IsActive || MathUtils.NearlyEqual(pump.FlowPercentage, 0)) { return false; }
}
else
{
if (!pump.Item.InWater) { return false; }
if (pump.IsActive && pump.FlowPercentage <= -99.9f) { return false; }
}
if (Character.CharacterList.Any(c => c.CurrentHull == pump.Item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; }
if (IsReady(pump)) { return false; }
return true;
}
protected override IEnumerable<Pump> GetList()
@@ -67,8 +60,24 @@ namespace Barotrauma
}
}
private bool IsReady(Pump pump)
{
if (Option == "stoppumping")
{
return !pump.IsActive || MathUtils.NearlyEqual(pump.FlowPercentage, 0);
}
else
{
return !pump.Item.InWater || pump.IsActive && pump.FlowPercentage <= -99.9f;
}
}
protected override AIObjective ObjectiveConstructor(Pump pump)
=> new AIObjectiveOperateItem(pump, character, objectiveManager, Option, false) { IsLoop = false };
=> new AIObjectiveOperateItem(pump, character, objectiveManager, Option, false)
{
IsLoop = false,
completionCondition = () => IsReady(pump)
};
protected override void OnObjectiveCompleted(AIObjective objective, Pump target)
=> HumanAIController.RemoveTargets<AIObjectivePumpWater, Pump>(character, target);
@@ -10,6 +10,7 @@ namespace Barotrauma
class AIObjectiveRepairItem : AIObjective
{
public override string DebugTag => "repair item";
public override bool KeepDivingGearOn => true;
public Item Item { get; private set; }
@@ -18,6 +19,8 @@ namespace Barotrauma
private float previousCondition = -1;
private RepairTool repairTool;
private bool IsRepairing => character.SelectedConstruction == Item && Item.GetComponent<Repairable>()?.CurrentFixer == character;
public AIObjectiveRepairItem(Character character, Item item, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier)
{
Item = item;
@@ -28,41 +31,36 @@ namespace Barotrauma
// TODO: priority list?
// Ignore items that are being repaired by someone else.
if (Item.Repairables.Any(r => r.CurrentFixer != null && r.CurrentFixer != character)) { return 0; }
// Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally)
float dist = Math.Abs(character.WorldPosition.X - Item.WorldPosition.X) + Math.Abs(character.WorldPosition.Y - Item.WorldPosition.Y) * 2.0f;
float distanceFactor = MathHelper.Lerp(1, 0.5f, MathUtils.InverseLerp(0, 10000, dist));
float yDist = Math.Abs(character.WorldPosition.Y - Item.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 5 : 0;
float dist = Math.Abs(character.WorldPosition.X - Item.WorldPosition.X) + yDist;
float distanceFactor = MathHelper.Lerp(1, 0.25f, MathUtils.InverseLerp(0, 5000, dist));
if (Item.CurrentHull == character.CurrentHull)
{
distanceFactor = 1;
}
float damagePriority = MathHelper.Lerp(1, 0, Item.Condition / Item.MaxCondition);
float successFactor = MathHelper.Lerp(0, 1, Item.Repairables.Average(r => r.DegreeOfSuccess(character)));
float isSelected = character.SelectedConstruction == Item ? 50 : 0;
float isSelected = IsRepairing ? 50 : 0;
float devotion = (Math.Min(Priority, 10) + isSelected) / 100;
float max = MathHelper.Min(AIObjectiveManager.OrderPriority - 1, 90);
bool isCompleted = Item.IsFullCondition;
if (isCompleted && character.SelectedConstruction == Item)
{
character?.Speak(TextManager.GetWithVariable("DialogItemRepaired", "[itemname]", Item.Name, true), null, 0.0f, "itemrepaired", 10.0f);
}
return MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + damagePriority * distanceFactor * successFactor * PriorityModifier, 0, 1));
}
public override bool IsCompleted()
protected override bool Check()
{
bool isCompleted = Item.IsFullCondition;
if (isCompleted && character.SelectedConstruction == Item)
IsCompleted = Item.IsFullCondition;
if (IsCompleted && IsRepairing)
{
character?.Speak(TextManager.GetWithVariable("DialogItemRepaired", "[itemname]", Item.Name, true), null, 0.0f, "itemrepaired", 10.0f);
}
return isCompleted;
}
public override bool IsDuplicate(AIObjective otherObjective)
{
return otherObjective is AIObjectiveRepairItem repairObjective && repairObjective.Item == Item;
return IsCompleted;
}
protected override void Act(float deltaTime)
{
// Only continue when the get item sub objectives have been completed.
if (subObjectives.Any()) { return; }
foreach (Repairable repairable in Item.Repairables)
{
if (!repairable.HasRequiredItems(character, false))
@@ -72,14 +70,12 @@ namespace Barotrauma
{
foreach (RelatedItem requiredItem in kvp.Value)
{
AddSubObjective(new AIObjectiveGetItem(character, requiredItem.Identifiers, objectiveManager, true));
subObjectives.Add(new AIObjectiveGetItem(character, requiredItem.Identifiers, objectiveManager, true));
}
}
return;
}
}
// Only continue when the get item sub objectives have been completed.
if (subObjectives.Any()) { return; }
if (repairTool == null)
{
FindRepairTool();
@@ -90,9 +86,9 @@ namespace Barotrauma
if (containedItems == null)
{
#if DEBUG
DebugConsole.ThrowError("AIObjectiveRepairItem failed - the item \"" + repairTool + "\" has no proper inventory");
DebugConsole.ThrowError($"{character.Name}: AIObjectiveRepairItem failed - the item \"" + repairTool + "\" has no proper inventory");
#endif
abandon = true;
Abandon = true;
return;
}
// Drop empty tanks
@@ -115,12 +111,15 @@ namespace Barotrauma
if (fuel == null)
{
RemoveSubObjective(ref goToObjective);
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, item.Identifiers, repairTool.Item.GetComponent<ItemContainer>(), objectiveManager));
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, item.Identifiers, repairTool.Item.GetComponent<ItemContainer>(), objectiveManager),
onCompleted: () => RemoveSubObjective(ref refuelObjective),
onAbandon: () => Abandon = true);
return;
}
}
if (character.CanInteractWith(Item, out _, checkLinked: false))
{
HumanAIController.FaceTarget(Item);
if (repairTool != null)
{
OperateRepairTool(deltaTime);
@@ -129,10 +128,10 @@ namespace Barotrauma
{
if (repairable.CurrentFixer != null && repairable.CurrentFixer != character)
{
// Someone else is repairing the target. Abandon the objective if the other is better at this then us.
abandon = repairable.DegreeOfSuccess(character) < repairable.DegreeOfSuccess(repairable.CurrentFixer);
// Someone else is repairing the target. Abandon the objective if the other is better at this than us.
Abandon = repairable.DegreeOfSuccess(character) < repairable.DegreeOfSuccess(repairable.CurrentFixer);
}
if (!abandon)
if (!Abandon)
{
if (character.SelectedConstruction != Item)
{
@@ -145,12 +144,15 @@ namespace Barotrauma
else if (Item.Condition < previousCondition)
{
// If the current condition is less than the previous condition, we can't complete the task, so let's abandon it. The item is probably deteriorating at a greater speed than we can repair it.
abandon = true;
character?.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, true), null, 0.0f, "cannotrepair", 10.0f);
Abandon = true;
}
}
if (abandon)
if (Abandon)
{
if (IsRepairing)
{
character?.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, true), null, 0.0f, "cannotrepair", 10.0f);
}
repairable.StopRepairing(character);
}
else
@@ -179,7 +181,14 @@ namespace Barotrauma
}
return objective;
},
onAbandon: () => character.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, true), null, 0.0f, "cannotrepair", 10.0f));
onAbandon: () =>
{
Abandon = true;
if (IsRepairing)
{
character.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, true), null, 0.0f, "cannotrepair", 10.0f);
}
});
}
}
@@ -14,9 +14,12 @@ namespace Barotrauma
/// </summary>
public bool RequireAdequateSkills;
public AIObjectiveRepairItems(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { }
public override bool AllowMultipleInstances => true;
public override bool IsDuplicate(AIObjective otherObjective) => otherObjective is AIObjectiveRepairItems repairItems && repairItems.RequireAdequateSkills == RequireAdequateSkills;
public override bool IsDuplicate<T>(T otherObjective) =>
(otherObjective as AIObjective) is AIObjectiveRepairItems repairObjective && repairObjective.RequireAdequateSkills == RequireAdequateSkills;
public AIObjectiveRepairItems(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { }
protected override void CreateObjectives()
{
@@ -28,7 +31,21 @@ namespace Barotrauma
{
objective = ObjectiveConstructor(item);
Objectives.Add(item, objective);
AddSubObjective(objective);
if (!subObjectives.Contains(objective))
{
subObjectives.Add(objective);
}
objective.Completed += () =>
{
Objectives.Remove(item);
OnObjectiveCompleted(objective, item);
};
objective.Abandoned += () =>
{
Objectives.Remove(item);
ignoreList.Add(item);
targetUpdateTimer = 0;
};
}
break;
}
@@ -40,7 +57,7 @@ namespace Barotrauma
if (!IsValidTarget(item, character)) { return false; }
if (item.CurrentHull.FireSources.Count > 0) { return false; }
// Don't repair items in rooms that have enemies inside.
if (Character.CharacterList.Any(c => c.CurrentHull == item.CurrentHull && !HumanAIController.IsFriendly(c))) { return false; }
if (Character.CharacterList.Any(c => c.CurrentHull == item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; }
if (!Objectives.ContainsKey(item))
{
if (item.Repairables.All(r => item.ConditionPercentage > r.ShowRepairUIThreshold)) { return false; }
@@ -52,7 +69,7 @@ namespace Barotrauma
return true;
}
protected override float TargetEvaluation() => Targets.Max(t => 100 - t.ConditionPercentage);
protected override float TargetEvaluation() => Targets.Max(t => character.SelectedConstruction == t && t.ConditionPercentage < 100 ? 100 : 100 - t.ConditionPercentage);
protected override IEnumerable<Item> GetList() => Item.ItemList;
protected override AIObjective ObjectiveConstructor(Item item)
@@ -10,12 +10,16 @@ namespace Barotrauma
{
public override string DebugTag => "rescue";
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
const float TreatmentDelay = 0.5f;
const float CloseEnoughToTreat = 150.0f;
private readonly Character targetCharacter;
private AIObjectiveGoTo goToObjective;
private AIObjectiveGetItem getItemObjective;
private float treatmentTimer;
private Hull safeHull;
@@ -24,94 +28,79 @@ namespace Barotrauma
{
if (targetCharacter == null)
{
string errorMsg = "Attempted to create a Rescue objective with no target!\n" + Environment.StackTrace;
string errorMsg = $"{character.Name}: Attempted to create a Rescue objective with no target!\n" + Environment.StackTrace;
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("AIObjectiveRescue:ctor:targetnull", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
abandon = true;
return;
}
if (targetCharacter == character)
{
// TODO: enable healing self too
abandon = true;
Abandon = true;
return;
}
this.targetCharacter = targetCharacter;
}
public override bool IsDuplicate(AIObjective otherObjective)
{
AIObjectiveRescue rescueObjective = otherObjective as AIObjectiveRescue;
return rescueObjective != null && rescueObjective.targetCharacter == targetCharacter;
}
protected override void Act(float deltaTime)
{
if (targetCharacter == null || targetCharacter.Removed)
if (character.LockHands || targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead)
{
Abandon = true;
return;
}
// Unconcious target is not in a safe place -> Move to a safe place first
if (targetCharacter.IsUnconscious && HumanAIController.GetHullSafety(targetCharacter.CurrentHull, targetCharacter) < HumanAIController.HULL_SAFETY_THRESHOLD)
if (targetCharacter != character)
{
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))
// Unconcious target is not in a safe place -> Move to a safe place first
if (targetCharacter.IsUnconscious && HumanAIController.GetHullSafety(targetCharacter.CurrentHull, targetCharacter) < HumanAIController.HULL_SAFETY_THRESHOLD)
{
if (character.SelectedCharacter != targetCharacter)
{
if (goToObjective != null && goToObjective.Target != 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))
{
goToObjective = null;
RemoveSubObjective(ref goToObjective);
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager) { CloseEnough = CloseEnoughToTreat },
onCompleted: () => RemoveSubObjective(ref goToObjective),
onAbandon: () => RemoveSubObjective(ref goToObjective));
}
else
{
character.SelectCharacter(targetCharacter);
}
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager));
}
else
{
character.SelectCharacter(targetCharacter);
}
}
else
{
// Drag the character into safety
if (goToObjective != null && goToObjective.Target == targetCharacter)
{
goToObjective = null;
}
if (safeHull == null)
{
var findSafety = objectiveManager.GetObjective<AIObjectiveFindSafety>();
if (findSafety == null)
// Drag the character into safety
if (safeHull == null)
{
// Ensure that we have the find safety objective (should always be the case)
findSafety = new AIObjectiveFindSafety(character, objectiveManager);
objectiveManager.AddObjective(findSafety);
safeHull = objectiveManager.GetObjective<AIObjectiveFindSafety>().FindBestHull(HumanAIController.VisibleHulls);
}
if (character.CurrentHull != safeHull)
{
RemoveSubObjective(ref goToObjective);
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(safeHull, character, objectiveManager),
onCompleted: () => RemoveSubObjective(ref goToObjective),
onAbandon: () => RemoveSubObjective(ref goToObjective));
}
safeHull = findSafety.FindBestHull(HumanAIController.VisibleHulls);
}
if (character.CurrentHull != safeHull)
{
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(safeHull, character, objectiveManager));
}
}
}
if (subObjectives.Any()) { return; }
if (!character.CanInteractWith(targetCharacter))
if (targetCharacter != character && !character.CanInteractWith(targetCharacter))
{
RemoveSubObjective(ref goToObjective);
// Go to the target and select it
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager));
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager) { CloseEnough = CloseEnoughToTreat },
onCompleted: () => RemoveSubObjective(ref goToObjective),
onAbandon: () => RemoveSubObjective(ref goToObjective));
}
else
{
// We can start applying treatment
if (character.SelectedCharacter != targetCharacter)
if (character != targetCharacter && character.SelectedCharacter != targetCharacter)
{
character.Speak(TextManager.GetWithVariables("DialogFoundWoundedTarget", new string[2] { "[targetname]", "[roomname]" },
new string[2] { targetCharacter.Name, targetCharacter.CurrentHull.DisplayName }, new bool[2] { false, true }),
@@ -123,61 +112,59 @@ namespace Barotrauma
}
}
// TODO: consider optimizing a bit
private readonly List<string> suitableItemIdentifiers = new List<string>();
private readonly List<string> itemNameList = new List<string>();
private Dictionary<string, float> currentTreatmentSuitabilities = new Dictionary<string, float>();
private void GiveTreatment(float deltaTime)
{
if (treatmentTimer > 0.0f)
{
treatmentTimer -= deltaTime;
return;
}
treatmentTimer = TreatmentDelay;
var allAfflictions = targetCharacter.CharacterHealth.GetAllAfflictions()
.Where(a => a.GetVitalityDecrease(targetCharacter.CharacterHealth) > 0)
.ToList();
//find which treatments are the most suitable to treat the character's current condition
targetCharacter.CharacterHealth.GetSuitableTreatments(currentTreatmentSuitabilities, normalize: false);
allAfflictions.Sort((a1, a2) =>
{
return Math.Sign(a2.GetVitalityDecrease(targetCharacter.CharacterHealth) - a1.GetVitalityDecrease(targetCharacter.CharacterHealth));
});
var allAfflictions = GetVitalityReducingAfflictions(targetCharacter).OrderByDescending(a => a.GetVitalityDecrease(targetCharacter.CharacterHealth));
//check if we already have a suitable treatment for any of the afflictions
foreach (Affliction affliction in allAfflictions)
{
foreach (KeyValuePair<string, float> treatmentSuitability in affliction.Prefab.TreatmentSuitability)
{
if (treatmentSuitability.Value > 0.0f)
if (currentTreatmentSuitabilities.ContainsKey(treatmentSuitability.Key) && currentTreatmentSuitabilities[treatmentSuitability.Key] > 0.0f)
{
Item matchingItem = character.Inventory.FindItemByIdentifier(treatmentSuitability.Key);
Item matchingItem = character.Inventory.FindItemByIdentifier(treatmentSuitability.Key, true);
if (matchingItem == null) { continue; }
ApplyTreatment(affliction, matchingItem);
//wait a bit longer after applying a treatment to wait for potential side-effects to manifest
treatmentTimer = TreatmentDelay * 4;
return;
}
}
}
float cprSuitability = targetCharacter.Oxygen < 0.0f ? -targetCharacter.Oxygen * 100.0f : 0.0f;
//didn't have any suitable treatments available, try to find some medical items
HashSet<string> suitableItemIdentifiers = new HashSet<string>();
foreach (Affliction affliction in allAfflictions)
if (currentTreatmentSuitabilities.Any(s => s.Value > cprSuitability))
{
foreach (KeyValuePair<string, float> treatmentSuitability in affliction.Prefab.TreatmentSuitability)
itemNameList.Clear();
suitableItemIdentifiers.Clear();
foreach (KeyValuePair<string, float> treatmentSuitability in currentTreatmentSuitabilities)
{
if (treatmentSuitability.Value > 0.0f)
if (treatmentSuitability.Value <= cprSuitability) { continue; }
if (MapEntityPrefab.Find(null, treatmentSuitability.Key, showErrorMessages: false) is ItemPrefab itemPrefab)
{
if (!Item.ItemList.Any(it => it.prefab.Identifier == treatmentSuitability.Key)) { continue; }
suitableItemIdentifiers.Add(treatmentSuitability.Key);
//only list the first 4 items
if (itemNameList.Count < 4)
{
itemNameList.Add(itemPrefab.Name);
}
}
}
}
if (suitableItemIdentifiers.Count > 0)
{
List<string> itemNameList = new List<string>();
foreach (string itemIdentifier in suitableItemIdentifiers)
{
if (MapEntityPrefab.Find(null, itemIdentifier, showErrorMessages: false) is ItemPrefab itemPrefab)
{
itemNameList.Add(itemPrefab.Name);
}
//only list the first 4 items
if (itemNameList.Count >= 4) { break; }
}
if (itemNameList.Count > 0)
{
string itemListStr = "";
@@ -189,18 +176,24 @@ namespace Barotrauma
{
itemListStr = string.Join(" or ", string.Join(", ", itemNameList.Take(itemNameList.Count - 1)), itemNameList.Last());
}
character.Speak(TextManager.GetWithVariables("DialogListRequiredTreatments", new string[2] { "[targetname]", "[treatmentlist]" },
new string[2] { targetCharacter.Name, itemListStr }, new bool[2] { false, true }),
null, 2.0f, "listrequiredtreatments" + targetCharacter.Name, 60.0f);
if (targetCharacter != character)
{
character.Speak(TextManager.GetWithVariables("DialogListRequiredTreatments", new string[2] { "[targetname]", "[treatmentlist]" },
new string[2] { targetCharacter.Name, itemListStr }, new bool[2] { false, true }),
null, 2.0f, "listrequiredtreatments" + targetCharacter.Name, 60.0f);
}
character.DeselectCharacter();
RemoveSubObjective(ref getItemObjective);
TryAddSubObjective(ref getItemObjective,
constructor: () => new AIObjectiveGetItem(character, suitableItemIdentifiers.ToArray(), objectiveManager, equip: true),
onCompleted: () => RemoveSubObjective(ref getItemObjective),
onAbandon: () => RemoveSubObjective(ref getItemObjective));
}
character.DeselectCharacter();
AddSubObjective(new AIObjectiveGetItem(character, suitableItemIdentifiers.ToArray(), objectiveManager, equip: true));
}
character.AnimController.Anim = AnimController.Animation.CPR;
}
private void ApplyTreatment(Affliction affliction, Item item)
{
var targetLimb = targetCharacter.CharacterHealth.GetAfflictionLimb(affliction);
@@ -224,43 +217,46 @@ namespace Barotrauma
}
}
public override bool IsCompleted()
protected override bool Check()
{
if (targetCharacter == null || targetCharacter.Removed)
if (character.LockHands || targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead)
{
abandon = true;
return true;
Abandon = true;
return false;
}
bool isCompleted = targetCharacter.Bleeding <= 0 && targetCharacter.Vitality / targetCharacter.MaxVitality > AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager);
if (isCompleted)
// 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);
if (isCompleted && targetCharacter != character)
{
character.Speak(TextManager.GetWithVariable("DialogTargetHealed", "[targetname]", targetCharacter.Name),
null, 1.0f, "targethealed" + targetCharacter.Name, 60.0f);
}
return isCompleted || targetCharacter.IsDead;
return isCompleted;
}
public override float GetPriority()
{
if (targetCharacter == null) { return 0; }
if (targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead)
if (targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead)
{
abandon = true;
return 0;
}
// Don't go into rooms that have enemies
if (Character.CharacterList.Any(c => c.CurrentHull == targetCharacter.CurrentHull && !HumanAIController.IsFriendly(c)))
{
abandon = true;
return 0;
}
// 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.5f, MathUtils.InverseLerp(0, 10000, dist));
float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, dist));
if (targetCharacter.CurrentHull == character.CurrentHull)
{
distanceFactor = 1;
}
float vitalityFactor = AIObjectiveRescueAll.GetVitalityFactor(targetCharacter);
float devotion = Math.Min(Priority, 10) / 100;
return MathHelper.Lerp(0, 100, MathHelper.Clamp(devotion + vitalityFactor * distanceFactor, 0, 1));
}
public static IEnumerable<Affliction> GetVitalityReducingAfflictions(Character character) => character.CharacterHealth.GetAllAfflictions(a => a.GetVitalityDecrease(character.CharacterHealth) > 0);
}
}
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
@@ -7,9 +8,10 @@ namespace Barotrauma
{
public override string DebugTag => "rescue all";
public override bool ForceRun => true;
public override bool IgnoreUnsafeHulls => true;
private const float vitalityThreshold = 0.8f;
private const float vitalityThresholdForOrders = 0.95f;
private const float vitalityThreshold = 80;
private const float vitalityThresholdForOrders = 95;
public static float GetVitalityThreshold(AIObjectiveManager manager)
{
if (manager == null)
@@ -25,15 +27,13 @@ namespace Barotrauma
public AIObjectiveRescueAll(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier) { }
public override bool IsDuplicate(AIObjective otherObjective) => otherObjective is AIObjectiveRescueAll;
protected override bool Filter(Character target) => IsValidTarget(target, character);
protected override IEnumerable<Character> GetList() => Character.CharacterList;
protected override float TargetEvaluation() => Targets.Max(t => GetVitalityFactor(t)) * 100;
protected override float TargetEvaluation() => Targets.Max(t => GetVitalityFactor(t));
public static float GetVitalityFactor(Character character) => (character.MaxVitality - character.Vitality) / character.MaxVitality;
public static float GetVitalityFactor(Character character) => Math.Min(character.HealthPercentage - character.Bleeding - character.Bloodloss - Math.Min(character.Oxygen, 0), 100);
protected override AIObjective ObjectiveConstructor(Character target)
=> new AIObjectiveRescue(character, target, objectiveManager, PriorityModifier);
@@ -47,16 +47,18 @@ namespace Barotrauma
if (!HumanAIController.IsFriendly(character, target)) { return false; }
if (character.AIController is HumanAIController humanAI)
{
if (target.Bleeding < 1 && target.Vitality / target.MaxVitality > GetVitalityThreshold(humanAI.ObjectiveManager)) { return false; }
if (GetVitalityFactor(target) > GetVitalityThreshold(humanAI.ObjectiveManager)) { return false; }
}
else
{
if (target.Bleeding < 1 && target.Vitality / target.MaxVitality > vitalityThreshold) { return false; }
if (GetVitalityFactor(target) > vitalityThreshold) { return false; }
}
if (target.Submarine == null || character.Submarine == null) { return false; }
if (target.Submarine.TeamID != character.Submarine.TeamID) { return false; }
if (target.CurrentHull == null) { return false; }
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, true)) { 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;
}
}
@@ -157,12 +157,13 @@ namespace Barotrauma
}
}
public SteeringPath FindPath(Vector2 start, Vector2 end, Submarine hostSub = null, string errorMsgStr = null, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null)
public SteeringPath FindPath(Vector2 start, Vector2 end, Submarine hostSub = null, string errorMsgStr = null, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null)
{
float closestDist = 0.0f;
PathNode startNode = null;
foreach (PathNode node in nodes)
{
if (nodeFilter != null && !nodeFilter(node)) { continue; }
if (startNodeFilter != null && !startNodeFilter(node)) { continue; }
Vector2 nodePos = node.Position;
if (hostSub != null)
@@ -220,6 +221,7 @@ namespace Barotrauma
PathNode endNode = null;
foreach (PathNode node in nodes)
{
if (nodeFilter != null && !nodeFilter(node)) { continue; }
if (endNodeFilter != null && !endNodeFilter(node)) { continue; }
Vector2 nodePos = node.Position;
if (hostSub != null)
@@ -264,7 +266,7 @@ namespace Barotrauma
return new SteeringPath(true);
}
var path = FindPath(startNode, endNode);
var path = FindPath(startNode, endNode, nodeFilter);
return path;
}
@@ -297,7 +299,7 @@ namespace Barotrauma
return FindPath(startNode, endNode);
}
private SteeringPath FindPath(PathNode start, PathNode end)
private SteeringPath FindPath(PathNode start, PathNode end, Func<PathNode, bool> filter = null)
{
if (start == end)
{
@@ -323,7 +325,8 @@ namespace Barotrauma
float dist = float.MaxValue;
foreach (PathNode node in nodes)
{
if (node.state != 1) continue;
if (filter != null && !filter(node)) { continue; }
if (node.state != 1) { continue; }
if (node.F < dist)
{
dist = node.F;
@@ -331,7 +334,7 @@ namespace Barotrauma
}
}
if (currNode == null || currNode == end) break;
if (currNode == null || currNode == end) { break; }
currNode.state = 2;
@@ -369,7 +372,7 @@ namespace Barotrauma
if (GetNodePenalty != null)
{
float? nodePenalty = GetNodePenalty(currNode, nextNode);
if (nodePenalty == null) continue;
if (nodePenalty == null) { continue; }
tempG += nodePenalty.Value;
}
@@ -388,7 +391,9 @@ namespace Barotrauma
if (end.state == 0 || end.Parent == null)
{
//DebugConsole.NewMessage("Pathfinding error: path not found", Color.DarkRed);
#if DEBUG
DebugConsole.NewMessage("Path not found", Color.Yellow);
#endif
return new SteeringPath(true);
}
@@ -15,13 +15,18 @@ namespace Barotrauma
protected ISteerable host;
private Vector2 steering;
private Vector2? avoidObstaclePos;
private float rayCastTimer;
private float wanderAngle;
protected Vector2 steering;
private float lastRayCastTime;
private bool avoidRayCastHit;
public Vector2 AvoidDir { get; private set; }
public Vector2 AvoidRayCastHitPosition { get; private set; }
public Vector2 AvoidLookAheadPos { get; private set; }
private float wanderAngle;
public float WanderAngle
{
get { return wanderAngle; }
@@ -45,9 +50,9 @@ namespace Barotrauma
steering += DoSteeringWander(weight);
}
public void SteeringAvoid(float deltaTime, float lookAheadDistance, float weight = 1, Vector2? heading = null)
public void SteeringAvoid(float deltaTime, float lookAheadDistance, float weight = 1)
{
steering += DoSteeringAvoid(deltaTime, lookAheadDistance, weight, heading);
steering += DoSteeringAvoid(deltaTime, lookAheadDistance, weight);
}
public void SteeringManual(float deltaTime, Vector2 velocity)
@@ -107,7 +112,7 @@ namespace Barotrauma
protected virtual Vector2 DoSteeringWander(float weight)
{
Vector2 circleCenter = (host.Steering == Vector2.Zero) ? Rand.Vector(weight) : host.Steering;
Vector2 circleCenter = (host.Steering == Vector2.Zero) ? Vector2.UnitY : host.Steering;
circleCenter = Vector2.Normalize(circleCenter) * CircleDistance;
Vector2 displacement = new Vector2(
@@ -135,70 +140,42 @@ namespace Barotrauma
{
return Vector2.Zero;
}
float maxDistance = lookAheadDistance;
if (rayCastTimer <= 0.0f)
if (Timing.TotalTime >= lastRayCastTime + RayCastInterval)
{
Vector2 ahead = host.SimPosition + Vector2.Normalize(host.Steering) * maxDistance;
rayCastTimer = RayCastInterval;
Body closestBody = Submarine.CheckVisibility(host.SimPosition, ahead);
if (closestBody == null)
avoidRayCastHit = false;
AvoidLookAheadPos = host.SimPosition + Vector2.Normalize(host.Steering) * maxDistance;
lastRayCastTime = (float)Timing.TotalTime;
Body closestBody = Submarine.CheckVisibility(host.SimPosition, AvoidLookAheadPos);
if (closestBody != null)
{
avoidObstaclePos = null;
return Vector2.Zero;
}
else
{
// TODO: Doesn't take items into account (like turrets)
if (closestBody.UserData is Structure closestStructure)
{
Vector2 obstaclePosition = Submarine.LastPickedPosition;
if (closestStructure.IsHorizontal)
{
obstaclePosition.Y = closestStructure.SimPosition.Y;
}
else
{
obstaclePosition.X = closestStructure.SimPosition.X;
}
avoidObstaclePos = obstaclePosition;
}
else
{
avoidObstaclePos = Submarine.LastPickedPosition;
}
avoidRayCastHit = true;
AvoidRayCastHitPosition = Submarine.LastPickedPosition;
AvoidDir = Submarine.LastPickedNormal;
//add a bit of randomness
AvoidDir = MathUtils.RotatePoint(AvoidDir, Rand.Range(-0.15f, 0.15f));
//wait a bit longer for the next raycast
lastRayCastTime += RayCastInterval;
}
}
else
if (AvoidDir.LengthSquared() < 0.0001f) { return Vector2.Zero; }
//if raycast hit nothing, lerp avoid dir to zero
if (!avoidRayCastHit)
{
rayCastTimer -= deltaTime;
AvoidDir -= Vector2.Normalize(AvoidDir) * deltaTime * 0.5f;
}
if (!avoidObstaclePos.HasValue)
{
return Vector2.Zero;
}
Vector2 diff = avoidObstaclePos.Value - host.SimPosition;
Vector2 diff = AvoidRayCastHitPosition - host.SimPosition;
float dist = diff.Length();
if (dist > maxDistance)
{
return Vector2.Zero;
}
if (heading.HasValue)
{
var f = heading ?? host.Steering;
// Avoid to left or right depending on the current heading
Vector2 relativeVector = Vector2.Normalize(diff) - Vector2.Normalize(f);
var dir = relativeVector.X > 0 ? diff.Right() : diff.Left();
float factor = 1.0f - Math.Min(dist / maxDistance, 1);
return dir * factor * weight;
}
else
{
// Doesn't work right because it effectively just slows down or reverses the movement, where as we'd like to go right or left to avoid the target.
// There's also another issue, which also affects going right or left: the raycast doesn't hit anything if we turn too much -> avoiding doesn't work well.
// Could probably "remember" the avoidance a bit longer so that the avoid steering is not immedieately disgarded, but kept for a while and reduced gradually?
return -diff * (1.0f - dist / maxDistance) * weight;
}
//> 0 when heading in the same direction as the obstacle, < 0 when away from it
float dot = MathHelper.Clamp(Vector2.Dot(diff / dist, host.Steering), 0.0f, 1.0f);
if (dot < 0) { return Vector2.Zero; }
return AvoidDir * dot * weight * MathHelper.Clamp(1.0f - dist / lookAheadDistance, 0.0f, 1.0f);
}
}
}
@@ -10,9 +10,9 @@ namespace Barotrauma
{
class SwarmBehavior
{
private float minDistFromClosest;
private float maxDistFromCenter;
private float cohesion;
private readonly float minDistFromClosest;
private readonly float maxDistFromCenter;
private readonly float cohesion;
public List<AICharacter> Members { get; private set; } = new List<AICharacter>();
public HashSet<AICharacter> ActiveMembers { get; private set; } = new HashSet<AICharacter>();
@@ -28,23 +28,29 @@ namespace Barotrauma
this.ai = ai;
minDistFromClosest = ConvertUnits.ToSimUnits(element.GetAttributeFloat("mindistfromclosest", 10.0f));
maxDistFromCenter = ConvertUnits.ToSimUnits(element.GetAttributeFloat("maxdistfromcenter", 1000.0f));
cohesion = element.GetAttributeFloat("cohesion", 0.1f);
cohesion = element.GetAttributeFloat("cohesion", 1) / 10;
}
public static void CreateSwarm(IEnumerable<AICharacter> swarm)
{
var aiControllers = new List<EnemyAIController>();
foreach (AICharacter character in swarm)
{
if (character.AIController is EnemyAIController enemyAI && enemyAI.SwarmBehavior != null)
{
enemyAI.SwarmBehavior.Members = swarm.ToList();
aiControllers.Add(enemyAI);
}
}
var filteredMembers = aiControllers.Select(m => m.Character as AICharacter).Where(m => m != null);
foreach (EnemyAIController ai in aiControllers)
{
ai.SwarmBehavior.Members = filteredMembers.ToList();
}
}
public void Refresh()
{
Members.RemoveAll(m => m.IsDead || m.Removed);
Members.RemoveAll(m => m.IsDead || m.Removed || m.AIController is EnemyAIController ai && ai.State == AIState.Flee);
foreach (var member in Members)
{
if (!member.AIController.Enabled && member.IsRemotePlayer || Character.Controlled == member || !((EnemyAIController)member.AIController).SwarmBehavior.IsActive)
@@ -61,7 +61,7 @@ namespace Barotrauma
}
}
public bool CanWalk => CanEnterSubmarine;
public bool CanWalk => RagdollParams.CanWalk;
public bool IsMovingBackwards => !InWater && Math.Sign(targetMovement.X) == -Math.Sign(Dir);
// TODO: define death anim duration in XML
@@ -132,25 +132,33 @@ namespace Barotrauma
{
if (Frozen) return;
if (MainLimb == null) { return; }
var mainLimb = MainLimb;
levitatingCollider = true;
if (!character.AllowInput)
if (!character.CanMove)
{
levitatingCollider = false;
Collider.FarseerBody.FixedRotation = false;
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
{
Collider.Enabled = false;
Collider.FarseerBody.FixedRotation = false;
Collider.LinearVelocity = MainLimb.LinearVelocity;
Collider.SetTransformIgnoreContacts(MainLimb.SimPosition, MainLimb.Rotation);
//reset pull joints to prevent the character from "hanging" mid-air if pull joints had been active when the character was still moving
//(except when dragging, then we need the pull joints)
if (!character.CanBeDragged || character.SelectedBy == null) { ResetPullJoints(); }
}
if (character.IsDead && deathAnimTimer < deathAnimDuration)
{
deathAnimTimer += deltaTime;
UpdateDying(deltaTime);
}
UpdateDying(deltaTime);
}
else if (!InWater && !CanWalk && character.AllowInput)
{
//cannot walk but on dry land -> wiggle around
UpdateDying(deltaTime);
}
return;
}
else
@@ -184,20 +192,22 @@ namespace Barotrauma
Collider.FarseerBody.FixedRotation = false;
UpdateSineAnim(deltaTime);
}
else if (CanEnterSubmarine && (currentHull != null || forceStanding) && CurrentGroundedParams != null)
else if (CanEnterSubmarine && (currentHull != null || forceStanding))
{
//rotate collider back upright
float standAngle = dir == Direction.Right ? CurrentGroundedParams.ColliderStandAngleInRadians : -CurrentGroundedParams.ColliderStandAngleInRadians;
if (Math.Abs(MathUtils.GetShortestAngle(Collider.Rotation, standAngle)) > 0.001f)
if (CurrentGroundedParams != null)
{
Collider.AngularVelocity = MathUtils.GetShortestAngle(Collider.Rotation, standAngle) * 60.0f;
Collider.FarseerBody.FixedRotation = false;
//rotate collider back upright
float standAngle = dir == Direction.Right ? CurrentGroundedParams.ColliderStandAngleInRadians : -CurrentGroundedParams.ColliderStandAngleInRadians;
if (Math.Abs(MathUtils.GetShortestAngle(Collider.Rotation, standAngle)) > 0.001f)
{
Collider.AngularVelocity = MathUtils.GetShortestAngle(Collider.Rotation, standAngle) * 60.0f;
Collider.FarseerBody.FixedRotation = false;
}
else
{
Collider.FarseerBody.FixedRotation = true;
}
}
else
{
Collider.FarseerBody.FixedRotation = true;
}
UpdateWalkAnim(deltaTime);
}
@@ -251,8 +261,9 @@ namespace Barotrauma
if (character.SelectedCharacter != null) DragCharacter(character.SelectedCharacter, deltaTime);
if (!CurrentFishAnimation.Flip || IsStuck) return;
if (character.AIController != null && !character.AIController.CanFlip) return;
if (!CurrentFishAnimation.Flip) { return; }
if (IsStuck) { return; }
if (character.AIController != null && !character.AIController.CanFlip) { return; }
flipCooldown -= deltaTime;
@@ -376,34 +387,34 @@ namespace Barotrauma
//limbs are disabled when simple physics is enabled, no need to move them
if (SimplePhysicsEnabled) { return; }
MainLimb.PullJointEnabled = true;
//MainLimb.PullJointWorldAnchorB = Collider.SimPosition;
var mainLimb = MainLimb;
mainLimb.PullJointEnabled = true;
//mainLimb.PullJointWorldAnchorB = Collider.SimPosition;
if (movement.LengthSquared() < 0.00001f)
{
WalkPos = MathHelper.SmoothStep(WalkPos, MathHelper.PiOver2, deltaTime * 5);
MainLimb.PullJointWorldAnchorB = Collider.SimPosition;
mainLimb.PullJointWorldAnchorB = Collider.SimPosition;
return;
}
Vector2 transformedMovement = reverse ? -movement : movement;
float movementAngle = MathUtils.VectorToAngle(transformedMovement) - MathHelper.PiOver2;
float mainLimbAngle = 0;
if (MainLimb.type == LimbType.Torso && TorsoAngle.HasValue)
if (mainLimb.type == LimbType.Torso && TorsoAngle.HasValue)
{
mainLimbAngle = TorsoAngle.Value;
}
else if (MainLimb.type == LimbType.Head && HeadAngle.HasValue)
else if (mainLimb.type == LimbType.Head && HeadAngle.HasValue)
{
mainLimbAngle = HeadAngle.Value;
}
mainLimbAngle *= Dir;
while (MainLimb.Rotation - (movementAngle + mainLimbAngle) > MathHelper.Pi)
while (mainLimb.Rotation - (movementAngle + mainLimbAngle) > MathHelper.Pi)
{
movementAngle += MathHelper.TwoPi;
}
while (MainLimb.Rotation - (movementAngle + mainLimbAngle) < -MathHelper.Pi)
while (mainLimb.Rotation - (movementAngle + mainLimbAngle) < -MathHelper.Pi)
{
movementAngle -= MathHelper.TwoPi;
}
@@ -416,7 +427,7 @@ namespace Barotrauma
Limb torso = GetLimb(LimbType.Torso);
if (torso != null)
{
SmoothRotateWithoutWrapping(torso, movementAngle + TorsoAngle.Value * Dir, MainLimb, TorsoTorque);
SmoothRotateWithoutWrapping(torso, movementAngle + TorsoAngle.Value * Dir, mainLimb, TorsoTorque);
}
}
if (HeadAngle.HasValue)
@@ -424,7 +435,7 @@ namespace Barotrauma
Limb head = GetLimb(LimbType.Head);
if (head != null)
{
SmoothRotateWithoutWrapping(head, movementAngle + HeadAngle.Value * Dir, MainLimb, HeadTorque);
SmoothRotateWithoutWrapping(head, movementAngle + HeadAngle.Value * Dir, mainLimb, HeadTorque);
}
}
if (TailAngle.HasValue)
@@ -432,7 +443,24 @@ namespace Barotrauma
Limb tail = GetLimb(LimbType.Tail);
if (tail != null)
{
SmoothRotateWithoutWrapping(tail, movementAngle + TailAngle.Value * Dir, MainLimb, TailTorque);
float? mainLimbTargetAngle = null;
if (mainLimb.type == LimbType.Torso)
{
mainLimbTargetAngle = TorsoAngle;
}
else if (mainLimb.type == LimbType.Head)
{
mainLimbTargetAngle = HeadAngle;
}
float torque = TailTorque;
float maxMultiplier = CurrentSwimParams.TailTorqueMultiplier;
if (mainLimbTargetAngle.HasValue && maxMultiplier > 1)
{
float diff = Math.Abs(mainLimb.Rotation - tail.Rotation);
float offset = Math.Abs(mainLimbTargetAngle.Value - TailAngle.Value);
torque *= MathHelper.Lerp(1, maxMultiplier, MathUtils.InverseLerp(0, MathHelper.PiOver2, diff - offset));
}
SmoothRotateWithoutWrapping(tail, movementAngle + TailAngle.Value * Dir, mainLimb, torque);
}
}
}
@@ -443,11 +471,11 @@ namespace Barotrauma
{
movementAngle = MathUtils.WrapAngleTwoPi(movementAngle - MathHelper.Pi);
}
if (MainLimb.type == LimbType.Head && HeadAngle.HasValue)
if (mainLimb.type == LimbType.Head && HeadAngle.HasValue)
{
Collider.SmoothRotate(HeadAngle.Value * Dir, CurrentSwimParams.SteerTorque);
}
else if (MainLimb.type == LimbType.Torso && TorsoAngle.HasValue)
else if (mainLimb.type == LimbType.Torso && TorsoAngle.HasValue)
{
Collider.SmoothRotate(TorsoAngle.Value * Dir, CurrentSwimParams.SteerTorque);
}
@@ -484,14 +512,14 @@ namespace Barotrauma
case LimbType.RightFoot:
if (CurrentSwimParams.FootAnglesInRadians.ContainsKey(limb.Params.ID))
{
SmoothRotateWithoutWrapping(limb, movementAngle + CurrentSwimParams.FootAnglesInRadians[limb.Params.ID] * Dir, MainLimb, FootTorque);
SmoothRotateWithoutWrapping(limb, movementAngle + CurrentSwimParams.FootAnglesInRadians[limb.Params.ID] * Dir, mainLimb, FootTorque);
}
break;
case LimbType.Tail:
if (waveLength > 0 && waveAmplitude > 0)
{
float waveRotation = (float)Math.Sin(WalkPos);
limb.body.ApplyTorque(waveRotation * limb.Mass * CurrentSwimParams.TailTorque * waveAmplitude);
limb.body.ApplyTorque(waveRotation * limb.Mass * waveAmplitude);
}
break;
}
@@ -499,25 +527,25 @@ namespace Barotrauma
for (int i = 0; i < Limbs.Length; i++)
{
if (Limbs[i].SteerForce <= 0.0f) continue;
if (Limbs[i].SteerForce <= 0.0f) { continue; }
if (!Collider.PhysEnabled) { continue; }
Vector2 pullPos = Limbs[i].PullJointWorldAnchorA;
Limbs[i].body.ApplyForce(movement * Limbs[i].SteerForce * Limbs[i].Mass, pullPos);
}
Vector2 mainLimbDiff = MainLimb.PullJointWorldAnchorB - MainLimb.SimPosition;
Vector2 mainLimbDiff = mainLimb.PullJointWorldAnchorB - mainLimb.SimPosition;
if (CurrentSwimParams.UseSineMovement)
{
MainLimb.PullJointWorldAnchorB = Vector2.SmoothStep(
MainLimb.PullJointWorldAnchorB,
mainLimb.PullJointWorldAnchorB = Vector2.SmoothStep(
mainLimb.PullJointWorldAnchorB,
Collider.SimPosition,
mainLimbDiff.LengthSquared() > 10.0f ? 1.0f : (float)Math.Abs(Math.Sin(WalkPos)));
}
else
{
//MainLimb.PullJointWorldAnchorB = Collider.SimPosition;
MainLimb.PullJointWorldAnchorB = Vector2.Lerp(
MainLimb.PullJointWorldAnchorB,
//mainLimb.PullJointWorldAnchorB = Collider.SimPosition;
mainLimb.PullJointWorldAnchorB = Vector2.Lerp(
mainLimb.PullJointWorldAnchorB,
Collider.SimPosition,
mainLimbDiff.LengthSquared() > 10.0f ? 1.0f : 0.5f);
}
@@ -527,7 +555,6 @@ namespace Barotrauma
void UpdateWalkAnim(float deltaTime)
{
if (CurrentGroundedParams == null) { return; }
movement = MathUtils.SmoothStep(movement, TargetMovement, 0.2f);
Collider.LinearVelocity = new Vector2(
@@ -540,12 +567,13 @@ namespace Barotrauma
Vector2 colliderBottom = GetColliderBottom();
float movementAngle = 0.0f;
float mainLimbAngle = (MainLimb.type == LimbType.Torso ? TorsoAngle ?? 0 : HeadAngle ?? 0) * Dir;
while (MainLimb.Rotation - (movementAngle + mainLimbAngle) > MathHelper.Pi)
var mainLimb = MainLimb;
float mainLimbAngle = (mainLimb.type == LimbType.Torso ? TorsoAngle ?? 0 : HeadAngle ?? 0) * Dir;
while (mainLimb.Rotation - (movementAngle + mainLimbAngle) > MathHelper.Pi)
{
movementAngle += MathHelper.TwoPi;
}
while (MainLimb.Rotation - (movementAngle + mainLimbAngle) < -MathHelper.Pi)
while (mainLimb.Rotation - (movementAngle + mainLimbAngle) < -MathHelper.Pi)
{
movementAngle -= MathHelper.TwoPi;
}
@@ -558,13 +586,13 @@ namespace Barotrauma
{
if (TorsoAngle.HasValue)
{
SmoothRotateWithoutWrapping(torso, movementAngle + TorsoAngle.Value * Dir, MainLimb, TorsoTorque);
SmoothRotateWithoutWrapping(torso, movementAngle + TorsoAngle.Value * Dir, mainLimb, TorsoTorque);
}
if (TorsoPosition.HasValue)
{
Vector2 pos = colliderBottom + new Vector2(0, TorsoPosition.Value + stepLift);
if (torso != MainLimb)
if (torso != mainLimb)
{
pos.X = torso.SimPosition.X;
}
@@ -580,13 +608,13 @@ namespace Barotrauma
{
if (HeadAngle.HasValue)
{
SmoothRotateWithoutWrapping(head, movementAngle + HeadAngle.Value * Dir, MainLimb, HeadTorque);
SmoothRotateWithoutWrapping(head, movementAngle + HeadAngle.Value * Dir, mainLimb, HeadTorque);
}
if (HeadPosition.HasValue)
{
Vector2 pos = colliderBottom + new Vector2(0, HeadPosition.Value + stepLift * CurrentGroundedParams.StepLiftHeadMultiplier);
if (head != MainLimb)
if (head != mainLimb)
{
pos.X = head.SimPosition.X;
}
@@ -602,12 +630,12 @@ namespace Barotrauma
var tail = GetLimb(LimbType.Tail);
if (tail != null)
{
SmoothRotateWithoutWrapping(tail, movementAngle + TailAngle.Value * Dir, MainLimb, TailTorque);
SmoothRotateWithoutWrapping(tail, movementAngle + TailAngle.Value * Dir, mainLimb, TailTorque);
}
}
float prevWalkPos = WalkPos;
WalkPos -= MainLimb.LinearVelocity.X * (CurrentAnimationParams.CycleSpeed / RagdollParams.JointScale / 100.0f);
WalkPos -= mainLimb.LinearVelocity.X * (CurrentAnimationParams.CycleSpeed / RagdollParams.JointScale / 100.0f);
Vector2 transformedStepSize = Vector2.Zero;
if (Math.Abs(TargetMovement.X) > 0.01f)
@@ -673,7 +701,7 @@ namespace Barotrauma
{
SmoothRotateWithoutWrapping(limb,
movementAngle + CurrentGroundedParams.FootAnglesInRadians[limb.Params.ID] * Dir,
MainLimb, FootTorque);
mainLimb, FootTorque);
}
break;
case LimbType.LeftLeg:
@@ -686,15 +714,16 @@ namespace Barotrauma
void UpdateDying(float deltaTime)
{
if (deathAnimDuration <= 0.0f) return;
if (deathAnimDuration <= 0.0f) { return; }
float noise = (PerlinNoise.GetPerlin(WalkPos * 0.002f, WalkPos * 0.003f) - 0.5f) * 5.0f;
float animStrength = (1.0f - deathAnimTimer / deathAnimDuration);
Limb head = GetLimb(LimbType.Head);
if (head != null && head.IsSevered) { return; }
Limb tail = GetLimb(LimbType.Tail);
if (head != null && !head.IsSevered) head.body.ApplyTorque((float)(Math.Sqrt(head.Mass) * Dir * Math.Sin(WalkPos)) * 30.0f * animStrength);
if (tail != null && !tail.IsSevered) tail.body.ApplyTorque((float)(Math.Sqrt(tail.Mass) * -Dir * Math.Sin(WalkPos)) * 30.0f * animStrength);
if (head != null && !head.IsSevered) head.body.ApplyTorque((float)(Math.Sqrt(head.Mass) * Dir * (Math.Sin(WalkPos) + noise)) * 30.0f * animStrength);
if (tail != null && !tail.IsSevered) tail.body.ApplyTorque((float)(Math.Sqrt(tail.Mass) * -Dir * (Math.Sin(WalkPos) + noise)) * 30.0f * animStrength);
WalkPos += deltaTime * 10.0f * animStrength;
@@ -753,12 +782,18 @@ namespace Barotrauma
base.Flip();
foreach (Limb l in Limbs)
{
if (!l.DoesFlip) continue;
l.body.SetTransform(l.SimPosition, -l.body.Rotation);
if (!l.DoesFlip) { continue; }
if (RagdollParams.IsSpritesheetOrientationHorizontal)
{
//horizontally aligned limbs need to be flipped 180 degrees
l.body.SetTransform(l.SimPosition, l.body.Rotation + MathHelper.Pi * Dir);
}
//no need to do anything when flipping vertically oriented limbs
//the sprite gets flipped horizontally, which does the job
}
}
private void Mirror()
public void Mirror(bool lerp = true)
{
Vector2 centerOfMass = GetCenterOfMass();
@@ -767,8 +802,20 @@ namespace Barotrauma
TrySetLimbPosition(l,
centerOfMass,
new Vector2(centerOfMass.X - (l.SimPosition.X - centerOfMass.X), l.SimPosition.Y),
true);
lerp);
l.body.PositionSmoothingFactor = 0.8f;
if (!l.DoesFlip) { continue; }
if (RagdollParams.IsSpritesheetOrientationHorizontal)
{
//horizontally oriented sprites can be mirrored by rotating 180 deg and inverting the angle
l.body.SetTransform(l.SimPosition, -(l.body.Rotation + MathHelper.Pi));
}
else
{
//vertically oriented limbs can be mirrored by inverting the angle (neutral angle is straight upwards)
l.body.SetTransform(l.SimPosition, -l.body.Rotation);
}
}
if (character.SelectedCharacter != null && CanDrag(character.SelectedCharacter))
{
@@ -343,7 +343,7 @@ namespace Barotrauma
deathAnimTimer = 0.0f;
}
if (!character.AllowInput)
if (!character.CanMove)
{
levitatingCollider = false;
Collider.FarseerBody.FixedRotation = false;
@@ -352,6 +352,9 @@ namespace Barotrauma
Collider.Enabled = false;
Collider.LinearVelocity = MainLimb.LinearVelocity;
Collider.SetTransformIgnoreContacts(MainLimb.SimPosition, MainLimb.Rotation);
//reset pull joints to prevent the character from "hanging" mid-air if pull joints had been active when the character was still moving
//(except when dragging, then we need the pull joints)
if (!character.CanBeDragged || character.SelectedBy == null) { ResetPullJoints(); }
}
return;
}
@@ -1082,9 +1085,7 @@ namespace Barotrauma
Limb rightFoot = GetLimb(LimbType.RightFoot);
Limb head = GetLimb(LimbType.Head);
Limb torso = GetLimb(LimbType.Torso);
Limb waist = GetLimb(LimbType.Waist);
Limb leftHand = GetLimb(LimbType.LeftHand);
Limb rightHand = GetLimb(LimbType.RightHand);
@@ -1107,10 +1108,6 @@ namespace Barotrauma
MoveLimb(head, new Vector2(ladderSimPos.X - 0.27f * Dir, bottomPos + WalkParams.HeadPosition), 10.5f);
MoveLimb(torso, new Vector2(ladderSimPos.X - 0.27f * Dir, bottomPos + WalkParams.TorsoPosition), 10.5f);
if (waist != null)
{
//MoveLimb(waist, new Vector2(ladderSimPos.X - 0.35f * Dir, Collider.SimPosition.Y + 0.6f - ColliderHeightFromFloor), 10.5f);
}
Collider.MoveToPos(new Vector2(ladderSimPos.X - 0.2f * Dir, Collider.SimPosition.Y), 10.5f);
@@ -1140,42 +1137,47 @@ namespace Barotrauma
Vector2 footPos = new Vector2(
handPos.X - Dir * 0.05f,
bottomPos + ColliderHeightFromFloor - stepHeight * 2.7f - ladderSimPos.Y);
if (slide)
{
MoveLimb(leftFoot, new Vector2(footPos.X, footPos.Y + ladderSimPos.Y), 15.5f, true);
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);
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);
//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 > -ConvertUnits.ToSimUnits(character.SelectedConstruction.Rect.Height))
{
if (slide)
{
MoveLimb(leftFoot, new Vector2(footPos.X, footPos.Y + ladderSimPos.Y), 15.5f, true);
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);
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 CLIENT
if (Math.Abs(leftFootPos - prevLeftFootPos) > stepHeight && leftFoot.LastImpactSoundTime < Timing.TotalTime - Limb.SoundInterval)
{
SoundPlayer.PlaySound("footstep_armor_heavy", leftFoot.WorldPosition, hullGuess: currentHull);
leftFoot.LastImpactSoundTime = (float)Timing.TotalTime;
}
if (Math.Abs(rightFootPos - prevRightFootPos) > stepHeight && rightFoot.LastImpactSoundTime < Timing.TotalTime - Limb.SoundInterval)
{
SoundPlayer.PlaySound("footstep_armor_heavy", rightFoot.WorldPosition, hullGuess: currentHull);
rightFoot.LastImpactSoundTime = (float)Timing.TotalTime;
}
if (Math.Abs(leftFootPos - prevLeftFootPos) > stepHeight && leftFoot.LastImpactSoundTime < Timing.TotalTime - Limb.SoundInterval)
{
SoundPlayer.PlaySound("footstep_armor_heavy", leftFoot.WorldPosition, hullGuess: currentHull);
leftFoot.LastImpactSoundTime = (float)Timing.TotalTime;
}
if (Math.Abs(rightFootPos - prevRightFootPos) > stepHeight && rightFoot.LastImpactSoundTime < Timing.TotalTime - Limb.SoundInterval)
{
SoundPlayer.PlaySound("footstep_armor_heavy", rightFoot.WorldPosition, hullGuess: currentHull);
rightFoot.LastImpactSoundTime = (float)Timing.TotalTime;
}
#endif
prevFootPos = footPos.Y;
}
prevFootPos = footPos.Y;
}
//apply torque to the legs to make the knees bend
Limb leftLeg = GetLimb(LimbType.LeftLeg);
Limb rightLeg = GetLimb(LimbType.RightLeg);
//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);
leftLeg.body.ApplyTorque(Dir * -8.0f);
rightLeg.body.ApplyTorque(Dir * -8.0f);
}
float movementFactor = (handPos.Y / stepHeight) * (float)Math.PI;
movementFactor = 0.8f + (float)Math.Abs(Math.Sin(movementFactor));
@@ -174,13 +174,16 @@ namespace Barotrauma
{
get
{
Limb torso = GetLimb(LimbType.Torso);
Limb head = GetLimb(LimbType.Head);
var mainLimb = torso ?? head;
Limb mainLimb = GetLimb(RagdollParams.MainLimb);
if (mainLimb == null)
{
//DebugConsole.ThrowError("No head or torso found. Using the first limb as the main limb.");
mainLimb = Limbs.FirstOrDefault();
Limb torso = GetLimb(LimbType.Torso);
Limb head = GetLimb(LimbType.Head);
mainLimb = torso ?? head;
if (mainLimb == null)
{
mainLimb = Limbs.FirstOrDefault();
}
}
return mainLimb;
}
@@ -261,10 +264,9 @@ namespace Barotrauma
public bool CanEnterSubmarine => RagdollParams.CanEnterSubmarine;
public bool CanAttackSubmarine => Limbs.Any(l => l.attack != null && l.attack.IsValidTarget(AttackTarget.Structure));
public float Dir
{
get { return ((dir == Direction.Left) ? -1.0f : 1.0f); }
}
public float Dir => dir == Direction.Left ? -1.0f : 1.0f;
public Direction Direction => dir;
public bool InWater
{
@@ -883,9 +885,8 @@ namespace Barotrauma
{
Collider.SetTransform(ConvertUnits.ToSimUnits(intersection), Collider.Rotation);
}
return;
}
return;
}
if (setSubmarine)
@@ -1105,8 +1106,11 @@ namespace Barotrauma
if (lowerHull != null) floorY = ConvertUnits.ToSimUnits(lowerHull.Rect.Y - lowerHull.Rect.Height);
}
}
if (HeadPosition.HasValue &&
Collider.SimPosition.Y < waterSurface && waterSurface - floorY > HeadPosition * 0.95f)
float standHeight =
HeadPosition.HasValue ? HeadPosition.Value :
TorsoPosition.HasValue ? TorsoPosition.Value :
Collider.GetMaxExtent() * 0.5f;
if (Collider.SimPosition.Y < waterSurface && waterSurface - floorY > standHeight * 0.95f)
{
inWater = true;
}
@@ -1453,7 +1457,7 @@ namespace Barotrauma
Vector2 rayEnd = rayStart - new Vector2(0.0f, height);
//var lowestLimb = FindLowestLimb();
Vector2 colliderBottomDisplay = ConvertUnits.ToDisplayUnits(GetColliderBottom());
float closestFraction = 1;
GameMain.World.RayCast((fixture, point, normal, fraction) =>
@@ -1466,6 +1470,7 @@ namespace Barotrauma
break;
case Physics.CollisionPlatform:
Structure platform = fixture.Body.UserData as Structure;
if (colliderBottomDisplay.Y < platform.Rect.Y - 16 && (targetMovement.Y <= 0.0f || Stairs != null)) return -1;
if (IgnorePlatforms && TargetMovement.Y < -0.5f || Collider.Position.Y < platform.Rect.Y) return -1;
break;
case Physics.CollisionWall:
@@ -1568,7 +1573,7 @@ namespace Barotrauma
protected void CheckDistFromCollider()
{
float allowedDist = Math.Max(Math.Max(Collider.radius, Collider.width), Collider.height) * 2.0f;
float allowedDist = Math.Max(Math.Max(Collider.radius, Collider.width), Collider.height) * 2.0f;
float resetDist = allowedDist * 5.0f;
Vector2 diff = Collider.SimPosition - MainLimb.SimPosition;
@@ -15,7 +15,9 @@ namespace Barotrauma
{
NotDefined,
Water,
Ground
Ground,
Inside,
Outside
}
public enum AttackTarget
@@ -70,7 +72,7 @@ namespace Barotrauma
partial class Attack : ISerializableEntity
{
[Serialize(AttackContext.NotDefined, true, description: "Is the attack used only in a specific condition?"), Editable]
[Serialize(AttackContext.NotDefined, true, description: "The attack will be used only in this context."), Editable]
public AttackContext Context { get; private set; }
[Serialize(AttackTarget.Any, true, description: "Does the attack target only specific targets?"), Editable]
@@ -198,7 +200,7 @@ namespace Barotrauma
/// </summary>
public List<PropertyConditional> Conditionals { get; private set; } = new List<PropertyConditional>();
private readonly List<StatusEffect> statusEffects;
private readonly List<StatusEffect> statusEffects = new List<StatusEffect>();
public void SetUser(Character user)
{
@@ -269,10 +271,6 @@ namespace Barotrauma
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "statuseffect":
if (statusEffects == null)
{
statusEffects = new List<StatusEffect>();
}
statusEffects.Add(StatusEffect.Load(subElement, parentDebugName));
break;
case "affliction":
@@ -378,27 +376,27 @@ namespace Barotrauma
{
effectType = ActionType.OnEating;
}
if (statusEffects == null) return attackResult;
foreach (StatusEffect effect in statusEffects)
{
// TODO: do we want to apply the effect at the world position or the entity positions in each cases? -> go through also other cases where status effects are applied
if (effect.HasTargetType(StatusEffect.TargetType.This))
{
effect.Apply(effectType, deltaTime, attacker, attacker);
effect.Apply(effectType, deltaTime, attacker, attacker, worldPosition);
}
if (target is Character)
if (targetCharacter != null)
{
if (effect.HasTargetType(StatusEffect.TargetType.Character))
{
effect.Apply(effectType, deltaTime, (Character)target, (Character)target);
effect.Apply(effectType, deltaTime, targetCharacter, targetCharacter);
}
if (effect.HasTargetType(StatusEffect.TargetType.Limb))
{
effect.Apply(effectType, deltaTime, (Character)target, attackResult.HitLimb);
effect.Apply(effectType, deltaTime, targetCharacter, attackResult.HitLimb);
}
if (effect.HasTargetType(StatusEffect.TargetType.AllLimbs))
{
effect.Apply(effectType, deltaTime, (Character)target, ((Character)target).AnimController.Limbs.Cast<ISerializableEntity>().ToList());
effect.Apply(effectType, deltaTime, targetCharacter, targetCharacter.AnimController.Limbs.Cast<ISerializableEntity>().ToList());
}
}
if (target is Entity entity)
@@ -434,7 +432,6 @@ namespace Barotrauma
var attackResult = targetLimb.character.ApplyAttack(attacker, worldPosition, this, deltaTime, playSound, targetLimb);
var effectType = attackResult.Damage > 0.0f ? ActionType.OnUse : ActionType.OnFailure;
if (statusEffects == null) return attackResult;
foreach (StatusEffect effect in statusEffects)
{
@@ -507,6 +504,43 @@ namespace Barotrauma
public bool IsValidContext(AttackContext context) => Context == context || Context == AttackContext.NotDefined;
public bool IsValidContext(IEnumerable<AttackContext> contexts)
{
foreach (var context in contexts)
{
switch (context)
{
case AttackContext.Ground:
if (Context == AttackContext.Water)
{
return false;
}
break;
case AttackContext.Water:
if (Context == AttackContext.Ground)
{
return false;
}
break;
case AttackContext.Inside:
if (Context == AttackContext.Outside)
{
return false;
}
break;
case AttackContext.Outside:
if (Context == AttackContext.Inside)
{
return false;
}
break;
default:
continue;
}
}
return true;
}
public bool IsValidTarget(AttackTarget targetType) => TargetType == AttackTarget.Any || TargetType == targetType;
public bool IsValidTarget(Entity target)
@@ -194,12 +194,16 @@ namespace Barotrauma
}
}
private string displayName;
public string DisplayName
{
get
{
return displayName != null && displayName.Length > 0 ? displayName : Name;
var displayName = Params.DisplayName;
if (string.IsNullOrWhiteSpace(displayName))
{
displayName = TextManager.Get($"Character.{SpeciesName}", returnNull: true);
}
return displayName ?? Name;
}
}
@@ -262,6 +266,16 @@ namespace Barotrauma
get { return !IsUnconscious && Stun <= 0.0f && !IsDead; }
}
public bool CanMove
{
get
{
if (!AllowInput) { return false; }
if (!AnimController.InWater && !AnimController.CanWalk) { return false; }
return true;
}
}
public bool CanInteract
{
get { return AllowInput && IsHumanoid && !LockHands && !Removed; }
@@ -703,7 +717,6 @@ namespace Barotrauma
var rootElement = doc.Root;
var mainElement = rootElement.IsOverride() ? rootElement.FirstElement() : rootElement;
InitProjSpecific(mainElement);
displayName = TextManager.Get($"Character.{speciesName}", true);
List<XElement> inventoryElements = new List<XElement>();
List<float> inventoryCommonness = new List<float>();
@@ -750,7 +763,7 @@ namespace Barotrauma
var matchingAffliction = AfflictionPrefab.List
.Where(p => p.AfflictionType == "huskinfection")
.Select(p => p as AfflictionPrefabHusk)
.FirstOrDefault(p => p.TargetSpecies.Contains(AfflictionHusk.GetNonHuskedSpeciesName(speciesName, p)));
.FirstOrDefault(p => p.TargetSpecies.Any(t => t.Equals(AfflictionHusk.GetNonHuskedSpeciesName(speciesName, p), StringComparison.InvariantCultureIgnoreCase)));
if (matchingAffliction == null)
{
DebugConsole.ThrowError("Cannot find a husk infection that matches this species! Please add the speciesnames as 'targets' in the husk affliction prefab definition!");
@@ -1237,7 +1250,7 @@ namespace Barotrauma
public void Control(float deltaTime, Camera cam)
{
ViewTarget = null;
if (!AllowInput) return;
if (!AllowInput) { return; }
if (Controlled == this || (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer))
{
@@ -1252,10 +1265,10 @@ namespace Barotrauma
SmoothedCursorPosition = cursorPosition - smoothedCursorDiff;
}
if (!(this is AICharacter) || Controlled == this || IsRemotePlayer)
bool playerControlled = !(this is AICharacter) || Controlled == this || IsRemotePlayer;
if (playerControlled)
{
Vector2 targetMovement = GetTargetMovement();
AnimController.TargetMovement = targetMovement;
AnimController.IgnorePlatforms = AnimController.TargetMovement.Y < -0.1f;
}
@@ -1265,7 +1278,8 @@ namespace Barotrauma
((HumanoidAnimController)AnimController).Crouching = IsKeyDown(InputType.Crouch);
}
if (AnimController.onGround &&
if (playerControlled &&
AnimController.onGround &&
!AnimController.InWater &&
AnimController.Anim != AnimController.Animation.UsingConstruction &&
AnimController.Anim != AnimController.Animation.CPR &&
@@ -1292,13 +1306,16 @@ namespace Barotrauma
{
if (GameMain.NetworkMember.IsServer)
{
if (dequeuedInput.HasFlag(InputNetFlags.FacingLeft))
if (playerControlled)
{
AnimController.TargetDir = Direction.Left;
}
else
{
AnimController.TargetDir = Direction.Right;
if (dequeuedInput.HasFlag(InputNetFlags.FacingLeft))
{
AnimController.TargetDir = Direction.Left;
}
else
{
AnimController.TargetDir = Direction.Right;
}
}
}
else if (GameMain.NetworkMember.IsClient && Controlled != this)
@@ -1327,8 +1344,8 @@ namespace Barotrauma
}
else if (IsKeyDown(InputType.Attack))
{
AttackContext currentContext = GetAttackContext();
var validLimbs = AnimController.Limbs.Where(l => !l.IsSevered && !l.IsStuck && l.attack != null && l.attack.IsValidContext(currentContext));
var currentContexts = GetAttackContexts();
var validLimbs = AnimController.Limbs.Where(l => !l.IsSevered && !l.IsStuck && l.attack != null && l.attack.IsValidContext(currentContexts));
var sortedLimbs = validLimbs.OrderBy(l => Vector2.DistanceSquared(ConvertUnits.ToDisplayUnits(l.SimPosition), cursorPosition));
// Select closest
var attackLimb = sortedLimbs.FirstOrDefault();
@@ -1457,14 +1474,7 @@ namespace Barotrauma
public bool CanSeeCharacter(Character target)
{
Limb seeingLimb = GetSeeingLimb();
foreach (var targetLimb in target.AnimController.Limbs)
{
if (CanSeeTarget(targetLimb, seeingLimb))
{
return true;
}
}
return false;
return target.AnimController.Limbs.Any(l => CanSeeTarget(l, seeingLimb));
}
private Limb GetSeeingLimb()
@@ -1546,8 +1556,7 @@ namespace Barotrauma
return (wall == null || !wall.CastShadow) && (door == null || door.IsOpen);
}
public bool HasItem(Item item, bool requireEquipped = false) =>
requireEquipped ? HasEquippedItem(item) : item.FindParentInventory(i => i.Owner == this) != null;
public bool HasItem(Item item, bool requireEquipped = false) => requireEquipped ? HasEquippedItem(item) : item.IsOwnedBy(this);
public bool HasEquippedItem(Item item)
{
@@ -1636,6 +1645,62 @@ namespace Barotrauma
return true;
}
private float _selectedItemPriority;
private Item _foundItem;
/// <summary>
/// Finds the closest item seeking by identifiers or tags from the world.
/// Ignores items that are outside or in another team's submarine or in a submarine that is not connected to this submarine.
/// Also ignores items that are taken by someone else.
/// The method is run in steps for performance reasons. So you'll have to provide the reference to the itemIndex.
/// Returns false while running and true when done.
/// </summary>
public bool FindItem(ref int itemIndex, out Item targetItem, IEnumerable<string> identifiers = null, bool ignoreBroken = true,
IEnumerable<Item> ignoredItems = null, IEnumerable<string> ignoredContainerIdentifiers = null,
Func<Item, bool> customPredicate = null, Func<Item, float> customPriorityFunction = null, float maxItemDistance = 10000)
{
if (itemIndex == 0)
{
_foundItem = null;
_selectedItemPriority = 0;
}
for (int i = 0; i < 10 && itemIndex < Item.ItemList.Count - 1; i++)
{
itemIndex++;
var item = Item.ItemList[itemIndex];
if (ignoredItems != null && ignoredItems.Contains(item)) { continue; }
if (item.Submarine == null) { continue; }
if (item.Submarine.TeamID != TeamID) { continue; }
if (Submarine != null && !Submarine.IsEntityFoundOnThisSub(item, true)) { continue; }
if (item.CurrentHull == null) { continue; }
if (ignoreBroken && item.Condition <= 0) { continue; }
if (customPredicate != null && !customPredicate(item)) { continue; }
if (identifiers != null && identifiers.None(id => item.Prefab.Identifier == id || item.HasTag(id))) { continue; }
if (ignoredContainerIdentifiers != null && item.Container != null)
{
if (ignoredContainerIdentifiers.Contains(item.ContainerIdentifier)) { continue; }
}
if (IsItemTakenBySomeoneElse(item)) { continue; }
float itemPriority = customPriorityFunction != null ? customPriorityFunction(item) : 1;
if (itemPriority <= 0) { continue; }
Item rootContainer = item.GetRootContainer();
Vector2 itemPos = (rootContainer ?? item).WorldPosition;
float yDist = Math.Abs(WorldPosition.Y - itemPos.Y);
yDist = yDist > 100 ? yDist * 5 : 0;
float dist = Math.Abs(WorldPosition.X - itemPos.X) + yDist;
float distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, maxItemDistance, dist));
itemPriority *= distanceFactor;
if (itemPriority > _selectedItemPriority)
{
_selectedItemPriority = itemPriority;
_foundItem = item;
}
}
targetItem = _foundItem;
return itemIndex >= Item.ItemList.Count - 1;
}
public bool IsItemTakenBySomeoneElse(Item item) => item.FindParentInventory(i => i.Owner != this && i.Owner is Character owner && !owner.IsDead && !owner.Removed) != null;
public bool CanInteractWith(Character c, float maxDist = 200.0f, bool checkVisibility = true)
{
if (c == this || Removed || !c.Enabled || !c.CanBeSelected) return false;
@@ -1949,7 +2014,7 @@ namespace Barotrauma
}
#endif
}
else if (IsKeyHit(InputType.Deselect) && SelectedConstruction != null)
else if (IsKeyHit(InputType.Deselect) && SelectedConstruction != null && SelectedConstruction.GetComponent<Ladder>() == null)
{
SelectedConstruction = null;
#if CLIENT
@@ -2516,16 +2581,19 @@ namespace Barotrauma
//character inside the sub received damage from a monster outside the sub
//can happen during normal gameplay if someone for example fires a ranged weapon from outside,
//the intention of this error message is to diagnose an issue with monsters being able to damage characters from outside
if (attacker?.AIController is EnemyAIController && Submarine != null && attacker.Submarine == null)
{
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,
errorMsg + "\n" + Environment.StackTrace);
#if DEBUG
DebugConsole.ThrowError(errorMsg);
#endif
}
// Disabled, because this happens every now and then when the monsters can get in and out of the sub.
// if (attacker?.AIController is EnemyAIController && Submarine != null && attacker.Submarine == null)
// {
// 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,
// errorMsg + "\n" + Environment.StackTrace);
//#if DEBUG
// DebugConsole.ThrowError(errorMsg);
//#endif
// }
if (attacker != null && attacker != this && GameMain.NetworkMember != null && !GameMain.NetworkMember.ServerSettings.AllowFriendlyFire)
{
@@ -2660,6 +2728,8 @@ namespace Barotrauma
return;
}
IsDead = true;
ApplyStatusEffects(ActionType.OnDeath, 1.0f);
AnimController.Frozen = false;
@@ -2691,8 +2761,6 @@ namespace Barotrauma
KillProjSpecific(causeOfDeath, causeOfDeathAffliction);
IsDead = true;
if (info != null) info.CauseOfDeath = CauseOfDeath;
AnimController.movement = Vector2.Zero;
AnimController.TargetMovement = Vector2.Zero;
@@ -2841,7 +2909,29 @@ namespace Barotrauma
}
}
public AttackContext GetAttackContext() => AnimController.CurrentAnimationParams.IsGroundedAnimation ? AttackContext.Ground : AttackContext.Water;
private HashSet<AttackContext> currentContexts = new HashSet<AttackContext>();
public IEnumerable<AttackContext> GetAttackContexts()
{
currentContexts.Clear();
if (AnimController.CurrentAnimationParams.IsGroundedAnimation)
{
currentContexts.Add(AttackContext.Ground);
}
else
{
currentContexts.Add(AttackContext.Water);
}
if (CurrentHull == null)
{
currentContexts.Add(AttackContext.Outside);
}
else
{
currentContexts.Add(AttackContext.Inside);
}
return currentContexts;
}
private readonly List<Hull> visibleHulls = new List<Hull>();
private readonly HashSet<Hull> tempList = new HashSet<Hull>();
@@ -2869,7 +2959,7 @@ namespace Barotrauma
}
}
}
visibleHulls.AddRange(CurrentHull.GetLinkedEntities<Hull>(tempList, filter: h =>
visibleHulls.AddRange(CurrentHull.GetLinkedEntities(tempList, filter: h =>
{
// Ignore adjacent hulls because they were already handled above
if (adjacentHulls.Contains(h))
@@ -2894,5 +2984,43 @@ namespace Barotrauma
}
return visibleHulls;
}
public Vector2 GetRelativeSimPosition(ISpatialEntity target, Vector2? worldPos = null)
{
Vector2 targetPos = target.SimPosition;
if (worldPos.HasValue)
{
Vector2 wp = worldPos.Value;
if (target.Submarine != null)
{
wp -= target.Submarine.Position;
}
targetPos = ConvertUnits.ToSimUnits(wp);
}
if (Submarine == null && target.Submarine != null)
{
if (AIController == null || !(AIController.SteeringManager is IndoorsSteeringManager))
{
// outside and targeting inside
// doesn't work with inside steering
targetPos += target.Submarine.SimPosition;
}
}
else if (Submarine != null && target.Submarine == null)
{
// inside and targeting outside
targetPos -= Submarine.SimPosition;
}
else if (Submarine != target.Submarine)
{
if (Submarine != null && target.Submarine != null)
{
// both inside, but in different subs
Vector2 diff = Submarine.SimPosition - target.Submarine.SimPosition;
targetPos -= diff;
}
}
return targetPos;
}
}
}
@@ -11,7 +11,7 @@ using System.Xml.Linq;
namespace Barotrauma
{
public enum Gender { None, Male, Female };
public enum Race { None, White, Black, Asian };
public enum Race { None, White, Black, Brown, Asian };
// TODO: Generating the HeadInfo could be simplified.
partial class CharacterInfo
@@ -33,8 +33,10 @@ namespace Barotrauma
{
_headSpriteId = (int)headSpriteRange.X;
}
GetSpriteSheetIndex();
}
}
public Vector2? SheetIndex { get; private set; }
public Vector2 headSpriteRange;
public Gender gender;
public Race race;
@@ -51,9 +53,16 @@ namespace Barotrauma
public HeadInfo() { }
public HeadInfo(int headId)
public HeadInfo(int headId, Gender gender, Race race, int hairIndex = 0, int beardIndex = 0, int moustacheIndex = 0, int faceAttachmentIndex = 0)
{
_headSpriteId = Math.Max(headId, 1);
this.gender = gender;
this.race = race;
HairIndex = hairIndex;
BeardIndex = beardIndex;
MoustacheIndex = moustacheIndex;
FaceAttachmentIndex = faceAttachmentIndex;
GetSpriteSheetIndex();
}
public void ResetAttachmentIndices()
@@ -63,6 +72,21 @@ namespace Barotrauma
MoustacheIndex = -1;
FaceAttachmentIndex = -1;
}
private void GetSpriteSheetIndex()
{
if (heads != null && heads.Any())
{
var matchingHead = heads.Keys.FirstOrDefault(h => h.Gender == gender && h.Race == race && h.ID == _headSpriteId);
if (matchingHead != null)
{
if (heads.TryGetValue(matchingHead, out Vector2 index))
{
SheetIndex = index;
}
}
}
}
}
private HeadInfo head;
@@ -86,6 +110,43 @@ namespace Barotrauma
}
}
public Dictionary<HeadPreset, Vector2> Heads
{
get
{
if (heads == null)
{
LoadHeadPresets();
}
return heads;
}
}
private static Dictionary<HeadPreset, Vector2> heads;
public class HeadPreset : ISerializableEntity
{
[Serialize(Race.None, false)]
public Race Race { get; private set; }
[Serialize(Gender.None, false)]
public Gender Gender { get; private set; }
[Serialize(0, false)]
public int ID { get; private set; }
[Serialize("0,0", false)]
public Vector2 SheetIndex { get; private set; }
public string Name => $"Head Preset {Race} {Gender} {ID}";
public Dictionary<string, SerializableProperty> SerializableProperties { get; private set; }
public HeadPreset(XElement element)
{
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
}
}
private static ushort idCounter;
public string Name;
@@ -158,6 +219,12 @@ namespace Barotrauma
{
LoadHeadSprite();
}
#if CLIENT
if (headSprite != null)
{
CalculateHeadPosition(headSprite);
}
#endif
return headSprite;
}
private set
@@ -170,6 +237,8 @@ namespace Barotrauma
}
}
public bool OmitJobInPortraitClothing;
private Sprite portrait;
public Sprite Portrait
{
@@ -223,7 +292,7 @@ namespace Barotrauma
{
if (attachmentSprites == null)
{
LoadAttachmentSprites();
LoadAttachmentSprites(OmitJobInPortraitClothing);
}
return attachmentSprites;
}
@@ -350,7 +419,7 @@ namespace Barotrauma
public bool IsAttachmentsLoaded => HairIndex > -1 && BeardIndex > -1 && MoustacheIndex > -1 && FaceAttachmentIndex > -1;
// Used for creating the data
public CharacterInfo(string speciesName, string name = "", JobPrefab jobPrefab = null, string ragdollFileName = null)
public CharacterInfo(string speciesName, string name = "", JobPrefab jobPrefab = null, string ragdollFileName = null, int variant = 0)
{
if (speciesName.EndsWith(".xml", StringComparison.OrdinalIgnoreCase))
{
@@ -372,7 +441,7 @@ namespace Barotrauma
Head.race = GetRandomRace();
CalculateHeadSpriteRange();
Head.HeadSpriteId = GetRandomHeadID();
Job = (jobPrefab == null) ? Job.Random(Rand.RandSync.Server) : new Job(jobPrefab);
Job = (jobPrefab == null) ? Job.Random(Rand.RandSync.Server) : new Job(jobPrefab, variant);
if (!string.IsNullOrEmpty(name))
{
Name = name;
@@ -534,11 +603,38 @@ namespace Barotrauma
Enum.TryParse(w.GetAttributeString("race", "None"), true, out Race r) && r == Head.race);
}
private void LoadHeadPresets()
{
if (CharacterConfigElement == null) { return; }
heads = new Dictionary<HeadPreset, Vector2>();
var headsElement = CharacterConfigElement.GetChildElement("heads");
if (headsElement != null)
{
foreach (var head in headsElement.GetChildElements("head"))
{
var preset = new HeadPreset(head);
heads.Add(preset, preset.SheetIndex);
}
}
}
private void CalculateHeadSpriteRange()
{
if (CharacterConfigElement == null) { return; }
Head.headSpriteRange = CharacterConfigElement.GetAttributeVector2("headidrange", Vector2.Zero);
// If range is defined, we use it as it is
// If the range is defined, we use it as it is
if (Head.headSpriteRange != Vector2.Zero) { return; }
if (heads == null)
{
LoadHeadPresets();
}
// If there are any head presets defined, use them.
if (heads.Any())
{
var ids = heads.Keys.Where(h => h.Race == Race && h.Gender == Gender).Select(w => w.ID);
ids = ids.OrderBy(id => id);
Head.headSpriteRange = new Vector2(ids.First(), ids.Last());
}
// Else we calculate the range from the wearables.
if (Head.headSpriteRange == Vector2.Zero)
{
@@ -580,23 +676,13 @@ namespace Barotrauma
{
gender = Gender.None;
}
head = new HeadInfo(headID)
{
race = race,
gender = gender,
HairIndex = hairIndex,
BeardIndex = beardIndex,
MoustacheIndex = moustacheIndex,
FaceAttachmentIndex = faceAttachmentIndex
};
head = new HeadInfo(headID, gender, race, hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex);
CalculateHeadSpriteRange();
ReloadHeadAttachments();
}
public void LoadHeadSprite()
{
// TODO: use ragdollparams instead?
foreach (XElement limbElement in Ragdoll.MainElement.Elements())
{
if (limbElement.GetAttributeString("type", "").ToLowerInvariant() != "head") { continue; }
@@ -649,7 +735,8 @@ namespace Barotrauma
{
if (hairs == null)
{
hairs = AddEmpty(FilterByTypeAndHeadID(FilterElementsByGenderAndRace(wearables), WearableType.Hair), WearableType.Hair);
float commonness = Gender == Gender.Female ? 0.05f : 0.2f;
hairs = AddEmpty(FilterByTypeAndHeadID(FilterElementsByGenderAndRace(wearables), WearableType.Hair), WearableType.Hair, commonness);
}
if (beards == null)
{
@@ -701,10 +788,10 @@ namespace Barotrauma
Head.FaceAttachmentIndex = faceAttachments.IndexOf(Head.FaceAttachment);
}
List<XElement> AddEmpty(IEnumerable<XElement> elements, WearableType type)
List<XElement> AddEmpty(IEnumerable<XElement> elements, WearableType type, float commonness = 1)
{
// Let's add an empty element so that there's a chance that we don't get any actual element -> allows bald and beardless guys, for example.
var emptyElement = new XElement("EmptyWearable", type.ToString());
var emptyElement = new XElement("EmptyWearable", type.ToString(), new XAttribute("commonness", commonness));
var list = new List<XElement>() { emptyElement };
list.AddRange(elements);
return list;
@@ -743,7 +830,7 @@ namespace Barotrauma
}
}
partial void LoadAttachmentSprites();
partial void LoadAttachmentSprites(bool omitJob);
// TODO: change the formula so that it's not linear and so that it takes into account the usefulness of the skill
// -> give a weight to each skill, because some are much more valuable than others?
@@ -243,7 +243,8 @@ namespace Barotrauma
XElement sourceElement = isOverride ? element.FirstElement() : element;
string elementName = sourceElement.Name.ToString().ToLowerInvariant();
string identifier = sourceElement.GetAttributeString("identifier", null);
if (!elementName.Equals("cprsettings", StringComparison.OrdinalIgnoreCase))
if (!elementName.Equals("cprsettings", StringComparison.OrdinalIgnoreCase) &&
!elementName.Equals("damageoverlay", StringComparison.OrdinalIgnoreCase))
{
if (string.IsNullOrWhiteSpace(identifier))
{
@@ -265,16 +266,38 @@ namespace Barotrauma
}
}
}
string type = sourceElement.GetAttributeString("type", null);
if (sourceElement.Name.ToString().ToLowerInvariant() == "cprsettings")
string type = sourceElement.GetAttributeString("type", "");
switch (sourceElement.Name.ToString().ToLowerInvariant())
{
//backwards compatibility
type = "cprsettings";
case "cprsettings":
type = "cprsettings";
break;
case "damageoverlay":
type = "damageoverlay";
break;
}
AfflictionPrefab prefab = null;
switch (type)
{
case "damageoverlay":
#if CLIENT
if (CharacterHealth.DamageOverlay != null)
{
if (isOverride)
{
DebugConsole.NewMessage($"Overriding damage overlay with '{filePath}'", Color.Yellow);
}
else
{
DebugConsole.ThrowError($"Error in '{filePath}': damage overlay already loaded. Add <override></override> tags as the parent of the custom damage overlay sprite to allow overriding the vanilla one.");
break;
}
}
CharacterHealth.DamageOverlay?.Remove();
CharacterHealth.DamageOverlay = new Sprite(element);
#endif
break;
case "bleeding":
prefab = new AfflictionPrefab(sourceElement, typeof(AfflictionBleeding));
break;
@@ -781,6 +781,54 @@ namespace Barotrauma
return allAfflictions;
}
/// <summary>
/// Get the identifiers of the items that can be used to treat the character. Takes into account all the afflictions the character has,
/// and negative treatment suitabilities (e.g. a medicine that causes oxygen loss may not be suitable if the character is already suffocating)
/// </summary>
/// <param name="treatmentSuitability">A dictionary where the key is the identifier of the item and the value the suitability</param>
/// <param name="normalize">If true, the suitability values are normalized between 0 and 1. If not, they're arbitrary values defined in the medical item XML, where negative values are unsuitable, and positive ones suitable.</param>
/// <param name="randomization">Amount of randomization to apply to the values (0 = the values are accurate, 1 = the values are completely random)</param>
public void GetSuitableTreatments(Dictionary<string, float> treatmentSuitability, bool normalize, float randomization = 0.0f)
{
//key = item identifier
//float = suitability
treatmentSuitability.Clear();
float minSuitability = -10, maxSuitability = 10;
foreach (Affliction affliction in GetAllAfflictions())
{
foreach (KeyValuePair<string, float> treatment in affliction.Prefab.TreatmentSuitability)
{
if (!treatmentSuitability.ContainsKey(treatment.Key))
{
treatmentSuitability[treatment.Key] = treatment.Value * affliction.Strength;
}
else
{
treatmentSuitability[treatment.Key] += treatment.Value * affliction.Strength;
}
minSuitability = Math.Min(treatmentSuitability[treatment.Key], minSuitability);
maxSuitability = Math.Max(treatmentSuitability[treatment.Key], maxSuitability);
}
}
//normalize the suitabilities to a range of 0 to 1
if (normalize)
{
foreach (string treatment in treatmentSuitability.Keys.ToList())
{
treatmentSuitability[treatment] = (treatmentSuitability[treatment] - minSuitability) / (maxSuitability - minSuitability);
treatmentSuitability[treatment] = MathHelper.Lerp(treatmentSuitability[treatment], Rand.Range(0.0f, 1.0f), randomization);
}
}
else
{
foreach (string treatment in treatmentSuitability.Keys.ToList())
{
treatmentSuitability[treatment] += Rand.Range(-100.0f, 100.0f) * randomization;
}
}
}
public void ServerWrite(IWriteMessage msg)
{
List<Affliction> activeAfflictions = afflictions.FindAll(a => a.Strength > 0.0f && a.Strength >= a.Prefab.ActivationThreshold);
@@ -36,9 +36,12 @@ namespace Barotrauma
get { return skills.Values.ToList(); }
}
public Job(JobPrefab jobPrefab)
public int Variant;
public Job(JobPrefab jobPrefab, int variant = 0)
{
prefab = jobPrefab;
Variant = variant;
skills = new Dictionary<string, Skill>();
foreach (SkillPrefab skillPrefab in prefab.Skills)
@@ -156,6 +159,25 @@ namespace Barotrauma
character.Inventory.TryPutItem(item, null, item.AllowedSlots);
}
Wearable wearable = ((List<ItemComponent>)item.Components)?.Find(c => c is Wearable) as Wearable;
if (wearable != null)
{
if (Variant > 0 && Variant <= wearable.Variants)
{
wearable.Variant = Variant;
}
else
{
wearable.Variant = wearable.Variant; //force server event
if (wearable.Variants > 0 && Variant == 0)
{
//set variant to the same as the wearable to get the rest of the character's gear
//to use the same variant (if possible)
Variant = wearable.Variant;
}
}
}
if (item.Prefab.Identifier == "idcard" && spawnPoint != null)
{
foreach (string s in spawnPoint.IdCardTags)
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Xml.Linq;
using Barotrauma.Extensions;
using System.Linq;
using System.IO;
namespace Barotrauma
{
@@ -31,6 +32,8 @@ namespace Barotrauma
partial class JobPrefab
{
public static Dictionary<string, JobPrefab> List;
public static XElement NoJobElement;
public static JobPrefab Get(string identifier)
{
if (List == null)
@@ -146,8 +149,11 @@ namespace Barotrauma
private set;
}
public XElement Element { get; private set; }
public XElement ClothingElement { get; private set; }
public XElement PreviewElement { get; private set; }
public JobPrefab(XElement element)
{
SerializableProperty.DeserializeProperties(this, element);
@@ -155,6 +161,8 @@ namespace Barotrauma
Description = TextManager.Get("JobDescription." + Identifier);
Identifier = Identifier.ToLowerInvariant();
Element = element;
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
@@ -220,7 +228,71 @@ namespace Barotrauma
{
ClothingElement = element.Element("portraitclothing");
}
PreviewElement = element.Element("PreviewSprites");
if (PreviewElement == null)
{
PreviewElement = element.Element("previewsprites");
}
}
public class OutfitPreview
{
/// <summary>
/// Pair.First = sprite, Pair.Second = draw offset
/// </summary>
public readonly List<Pair<Sprite, Vector2>> Sprites;
public OutfitPreview()
{
Sprites = new List<Pair<Sprite, Vector2>>();
}
public void AddSprite(Sprite sprite, Vector2 drawOffset)
{
Sprites.Add(new Pair<Sprite, Vector2>(sprite, drawOffset));
}
}
public List<OutfitPreview> GetJobOutfitSprites(Gender gender, out Vector2 dimensions)
{
List<OutfitPreview> outfitPreviews = new List<OutfitPreview>();
dimensions = PreviewElement.GetAttributeVector2("dims", Vector2.One);
if (PreviewElement == null) { return outfitPreviews; }
var equipIdentifiers = Element.Elements("Items").Elements().Where(e => e.GetAttributeBool("outfit", false)).Select(e => e.GetAttributeString("identifier", ""));
var children = PreviewElement.Elements().ToList();
var outfitPrefab = MapEntityPrefab.List.Find(me => me is ItemPrefab itemPrefab && equipIdentifiers.Contains(itemPrefab.Identifier)) as ItemPrefab;
if (outfitPrefab == null) { return null; }
var wearables = outfitPrefab.ConfigElement.Elements("Wearable");
if (!wearables.Any()) { return null; }
int variantCount = wearables.First().GetAttributeInt("variants", 1);
for (int i = 0; i < variantCount; i++)
{
var outfitPreview = new OutfitPreview();
for (int n = 0; n < children.Count; n++)
{
XElement spriteElement = children[n];
string spriteTexture = spriteElement.GetAttributeString("texture", "").Replace("[GENDER]", (gender == Gender.Female) ? "female" : "male");
string textureVariant = spriteTexture.Replace("[VARIANT]", (i + 1).ToString());
if (!File.Exists(textureVariant))
{
textureVariant = spriteTexture.Replace("[VARIANT]", "1");
}
var torsoSprite = new Sprite(spriteElement, path: "", file: textureVariant);
torsoSprite.size = new Vector2(torsoSprite.SourceRect.Width, torsoSprite.SourceRect.Height);
outfitPreview.AddSprite(torsoSprite, children[n].GetAttributeVector2("offset", Vector2.Zero));
}
outfitPreviews.Add(outfitPreview);
}
return outfitPreviews;
}
public static JobPrefab Random(Rand.RandSync sync = Rand.RandSync.Unsynced) => List.Values.GetRandom(sync);
@@ -239,6 +311,7 @@ namespace Barotrauma
}
foreach (XElement element in mainElement.Elements())
{
if (element.Name.ToString().ToLowerInvariant() == "nojob") { continue; }
if (element.IsOverride())
{
var job = new JobPrefab(element.FirstElement());
@@ -262,6 +335,8 @@ namespace Barotrauma
}
}
}
NoJobElement = NoJobElement ?? mainElement.Element("NoJob");
NoJobElement = NoJobElement ?? mainElement.Element("nojob");
}
}
}
@@ -435,19 +435,12 @@ namespace Barotrauma
//sector 360 degrees or more -> always hits
if (Math.Abs(armorSector.Y - armorSector.X) >= MathHelper.TwoPi) { return true; }
float rotation = body.TransformedRotation;
float offset = (MathHelper.PiOver2 - GetArmorSectorRotationOffset(armorSector)) * Dir;
float offset = (MathHelper.PiOver2 - MathUtils.GetMidAngle(armorSector.X, armorSector.Y)) * Dir;
float hitAngle = VectorExtensions.Angle(VectorExtensions.Forward(rotation + offset), SimPosition - simPosition);
float sectorSize = GetArmorSectorSize(armorSector);
return hitAngle < sectorSize / 2;
}
protected float GetArmorSectorRotationOffset(Vector2 armorSector)
{
float midAngle = MathUtils.GetMidAngle(armorSector.X, armorSector.Y);
float spritesheetOrientation = Params.GetSpriteOrientation();
return midAngle + spritesheetOrientation;
}
protected float GetArmorSectorSize(Vector2 armorSector)
{
return Math.Abs(armorSector.X - armorSector.Y);
@@ -140,7 +140,7 @@ namespace Barotrauma
abstract class FishSwimParams : SwimParams, IFishAnimation
{
[Serialize(false, true, description: "TODO"), Editable]
[Serialize(false, true, description: "Instead of linear movement (default), use a wave-like movement. Note: WaveAmplitude and WaveLength don't have any effect on this. It's synced with the movement speed."), Editable]
public bool UseSineMovement { get; set; }
[Editable, Serialize(true, true, description: "Should the character be flipped depending on which direction it faces. Should usually be enabled on all characters that have distinctive upper and lower sides.")]
@@ -149,7 +149,7 @@ namespace Barotrauma
[Editable, Serialize(true, true, description: "If enabled, the character will simply be mirrored horizontally when it wants to turn around. If disabled, it will rotate itself to face the other direction.")]
public bool Mirror { get; set; }
[Serialize(1f, true), Editable]
[Serialize(5f, true), Editable]
public float WaveAmplitude { get; set; }
[Serialize(10.0f, true), Editable]
@@ -167,6 +167,9 @@ namespace Barotrauma
[Serialize(50.0f, true, description: "How much torque is used to rotate the tail to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
public float TailTorque { get; set; }
[Serialize(1f, true, description: "Multiplier applied based on the angle difference between the tail and the main limb. Increasing the value prevents snake-like characters from getting tangled on themselves. Default = 1 (no boost)"), Editable(MinValueFloat = 1, MaxValueFloat = 100)]
public float TailTorqueMultiplier { get; set; }
[Serialize(25.0f, true, description: "How much torque is used to rotate the feet to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
public float FootTorque { get; set; }
@@ -19,6 +19,9 @@ namespace Barotrauma
[Serialize("", true), Editable]
public string SpeciesName { get; private set; }
[Serialize("", true, description: "If the display name is not defined, the game first tries to find the translated name. If that is not found, the species name will be used."), Editable]
public string DisplayName { get; private set; }
[Serialize("", true, description: "If defined, different species of the same group are considered like the characters of the same species by the AI."), Editable]
public string Group { get; private set; }
@@ -46,6 +49,7 @@ namespace Barotrauma
public readonly List<SoundParams> Sounds = new List<SoundParams>();
public readonly List<ParticleParams> BloodEmitters = new List<ParticleParams>();
public readonly List<ParticleParams> GibEmitters = new List<ParticleParams>();
public readonly List<ParticleParams> DamageEmitters = new List<ParticleParams>();
public readonly List<InventoryParams> Inventories = new List<InventoryParams>();
public HealthParams Health { get; private set; }
public AIParams AI { get; private set; }
@@ -124,6 +128,12 @@ namespace Barotrauma
GibEmitters.Add(emitter);
SubParams.Add(emitter);
}
foreach (var element in MainElement.GetChildElements("damageemitter"))
{
var emitter = new ParticleParams(element, this);
GibEmitters.Add(emitter);
SubParams.Add(emitter);
}
foreach (var soundElement in MainElement.GetChildElements("sound"))
{
var sound = new SoundParams(soundElement, this);
@@ -193,6 +203,7 @@ namespace Barotrauma
public void AddBloodEmitter() => AddEmitter("bloodemitter");
public void AddGibEmitter() => AddEmitter("gibemitter");
public void AddDamageEmitter() => AddEmitter("damageemitter");
private void AddEmitter(string type)
{
@@ -204,6 +215,9 @@ namespace Barotrauma
case "bloodemitter":
TryAddSubParam(new XElement(type), (e, c) => new ParticleParams(e, c), out _, BloodEmitters);
break;
case "damageemitter":
TryAddSubParam(new XElement(type), (e, c) => new ParticleParams(e, c), out _, DamageEmitters);
break;
default: throw new NotImplementedException(type);
}
}
@@ -211,6 +225,7 @@ namespace Barotrauma
public bool RemoveSound(SoundParams soundParams) => RemoveSubParam(soundParams);
public bool RemoveBloodEmitter(ParticleParams emitter) => RemoveSubParam(emitter, BloodEmitters);
public bool RemoveGibEmitter(ParticleParams emitter) => RemoveSubParam(emitter, GibEmitters);
public bool RemoveDamageEmitter(ParticleParams emitter) => RemoveSubParam(emitter, DamageEmitters);
public bool RemoveInventory(InventoryParams inventory) => RemoveSubParam(inventory, Inventories);
protected bool RemoveSubParam<T>(T subParam, IList<T> collection = null) where T : SubParam
@@ -333,16 +348,16 @@ namespace Barotrauma
[Serialize(false, true)]
public bool UseHealthWindow { get; set; }
[Serialize(0f, true, description: "How easily the character heals from the bleeding wounds. Default 0 (no extra healing)."), Editable(MinValueFloat = 0, MaxValueFloat = 10)]
[Serialize(0f, true, description: "How easily the character heals from the bleeding wounds. Default 0 (no extra healing)."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
public float BleedingReduction { get; private set; }
[Serialize(0f, true, description: "How easily the character heals from the burn wounds. Default 0 (no extra healing)."), Editable(MinValueFloat = 0, MaxValueFloat = 10)]
[Serialize(0f, true, description: "How easily the character heals from the burn wounds. Default 0 (no extra healing)."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
public float BurnReduction { get; private set; }
[Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 10)]
[Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
public float ConstantHealthRegeneration { get; private set; }
[Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 10)]
[Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
public float HealthRegenerationWhenEating { get; private set; }
// TODO: limbhealths, sprite?
@@ -411,10 +426,10 @@ namespace Barotrauma
[Serialize(1.0f, true, description: "Affects how far the character can hear the targets. Used as a multiplier."), Editable(minValue: 0f, maxValue: 10f)]
public float Hearing { get; private set; }
[Serialize(100f, true, description: "How much the target priority increase when the character takes damage? Additive."), Editable(minValue: -1000f, maxValue: 1000f)]
[Serialize(100f, true, description: "How much the targeting priority increases each time the character takes damage. Works like the greed value, described above. The default value is 100."), Editable(minValue: -1000f, maxValue: 1000f)]
public float AggressionHurt { get; private set; }
[Serialize(10f, true, description: "How much the target priority increase when the character takes damage? Additive."), Editable(minValue: 0f, maxValue: 1000f)]
[Serialize(10f, true, description: "How much the targeting priority increases each time the character does damage to the target. The actual priority adjustment is calculated based on the damage percentage multiplied by the greed value. The default value is 10, which means the priority will increase by 1 every time the character does damage 10% of the target's current health. If the damage is 50%, then the priority increase is 5."), Editable(minValue: 0f, maxValue: 1000f)]
public float AggressionGreed { get; private set; }
[Serialize(0f, true, description: "If the health drops below this threshold, the character flees. In percentages."), Editable(minValue: 0f, maxValue: 100f)]
@@ -423,8 +438,8 @@ namespace Barotrauma
[Serialize(false, true, description: "Does the character attack ONLY when provoked?"), Editable()]
public bool AttackOnlyWhenProvoked { get; private set; }
[Serialize(true, true, description: "When true, the character retaliates quickly when it's taking damage. Enabled by default."), Editable]
public bool RetaliateWhenTakingDamage { get; private set; }
[Serialize(true, true, description: "The character will flee for a brief moment when being shot at if not performing an attack."), Editable]
public bool AvoidGunfire { get; private set; }
[Serialize(false, true, description: "Does the character try to break inside the sub?"), Editable()]
public bool AggressiveBoarding { get; private set; }
@@ -34,9 +34,19 @@ namespace Barotrauma
[Serialize("", true, description: "Default path for the limb sprite textures. Used only if the limb specific path for the limb is not defined"), Editable]
public string Texture { get; set; }
[Serialize(0f, true, description: "The orientation of the sprites as drawn on the sprite sheet. Can be overridden by setting a value for Limb's 'Sprite Orientation'. Used mainly for animations and widgets."), Editable(-360, 360)]
[Serialize(0.0f, true, description: "The orientation of the sprites as drawn on the sprite sheet. Can be overridden by setting a value for Limb's 'Sprite Orientation'. Used mainly for animations and widgets."), Editable(-360, 360)]
public float SpritesheetOrientation { get; set; }
public bool IsSpritesheetOrientationHorizontal
{
get
{
return
(SpritesheetOrientation > 45.0f && SpritesheetOrientation < 135.0f) ||
(SpritesheetOrientation > 255.0f && SpritesheetOrientation < 315.0f);
}
}
private float limbScale;
[Serialize(1.0f, true), Editable(MIN_SCALE, MAX_SCALE, DecimalCount = 3)]
public float LimbScale { get { return limbScale; } set { limbScale = MathHelper.Clamp(value, MIN_SCALE, MAX_SCALE); } }
@@ -55,12 +65,18 @@ namespace Barotrauma
[Serialize(50f, true, description: "How much impact is required before the character takes impact damage?"), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
public float ImpactTolerance { get; set; }
[Serialize(true, true, description: "Can the creature enter submarine and walk when there is no water? Creatures that cannot enter submarines, always collide with it, even when there is a gap."), Editable()]
[Serialize(true, true, description: "Can the creature enter submarine. Creatures that cannot enter submarines, always collide with it, even when there is a gap."), Editable()]
public bool CanEnterSubmarine { get; set; }
[Serialize(true, true), Editable]
public bool CanWalk { get; set; }
[Serialize(true, true, description: "Can the character be dragged around by other creatures?"), Editable()]
public bool Draggable { get; set; }
[Serialize(LimbType.Torso, true), Editable]
public LimbType MainLimb { get; set; }
private static Dictionary<string, Dictionary<string, RagdollParams>> allRagdolls = new Dictionary<string, Dictionary<string, RagdollParams>>();
public List<ColliderParams> Colliders { get; private set; } = new List<ColliderParams>();
@@ -513,6 +529,9 @@ namespace Barotrauma
[Serialize(float.NaN, true, description: "The orientation of the sprite as drawn on the sprite sheet. Overrides the value defined in the Ragdoll settings. Used mainly for animations and widgets."), Editable(-360, 360)]
public float SpriteOrientation { get; set; }
/// <summary>
/// The orientation of the sprite as drawn on the sprite sheet (in radians).
/// </summary>
public float GetSpriteOrientation() => MathHelper.ToRadians(float.IsNaN(SpriteOrientation) ? Ragdoll.SpritesheetOrientation : SpriteOrientation);
[Serialize(true, true, description: "Does the limb flip when the character flips?"), Editable()]
@@ -527,7 +546,7 @@ namespace Barotrauma
[Serialize(false, true, description: "Disable drawing for this limb."), Editable()]
public bool Hide { get; set; }
[Serialize(1f, true, description: "Higher values make AI characters prefer attacking this limb."), Editable()]
[Serialize(1f, true, description: "Higher values make AI characters prefer attacking this limb."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 10)]
public float AttackPriority { get; set; }
[Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
@@ -216,6 +216,8 @@ namespace Barotrauma
}
}
public bool NeedsRestart;
public override string ToString()
{
return Name;
@@ -79,7 +79,7 @@ namespace Barotrauma
}
}
private static Queue<ColoredText> queuedMessages = new Queue<ColoredText>();
private static readonly Queue<ColoredText> queuedMessages = new Queue<ColoredText>();
static partial void ShowHelpMessage(Command command);
@@ -90,7 +90,7 @@ namespace Barotrauma
public delegate void QuestionCallback(string answer);
private static QuestionCallback activeQuestionCallback;
private static List<Command> commands = new List<Command>();
private static readonly List<Command> commands = new List<Command>();
public static List<Command> Commands
{
get { return commands; }
@@ -100,12 +100,12 @@ namespace Barotrauma
private static int currentAutoCompletedIndex;
//used for keeping track of the message entered when pressing up/down
static int selectedIndex;
private static int selectedIndex;
public static bool CheatsEnabled;
private static List<ColoredText> unsavedMessages = new List<ColoredText>();
private static int messagesPerFile = 5000;
private static readonly List<ColoredText> unsavedMessages = new List<ColoredText>();
private static readonly int messagesPerFile = 5000;
public const string SavePath = "ConsoleLogs";
private static void AssignOnExecute(string names, Action<string[]> onExecute)
@@ -113,7 +113,7 @@ namespace Barotrauma
var matchingCommand = commands.Find(c => c.names.Intersect(names.Split('|')).Count() > 0);
if (matchingCommand == null)
{
throw new Exception("AssignOnExecute failed. Command matching the name(s) \""+names+"\" not found.");
throw new Exception("AssignOnExecute failed. Command matching the name(s) \"" + names + "\" not found.");
}
else
{
@@ -165,8 +165,7 @@ namespace Barotrauma
NewMessage("***************", Color.Cyan);
foreach (MapEntityPrefab ep in MapEntityPrefab.List)
{
var itemPrefab = ep as ItemPrefab;
if (itemPrefab == null || itemPrefab.Name == null) continue;
if (!(ep is ItemPrefab itemPrefab) || itemPrefab.Name == null) continue;
string text = $"- {itemPrefab.Name}";
if (itemPrefab.Tags.Any())
{
@@ -284,6 +283,8 @@ namespace Barotrauma
NewMessage("Enemy AI enabled", Color.Green);
}, isCheat: true));
commands.Add(new Command("starttraitormissionimmediately", "starttraitormissionimmediately: Skip the initial delay of the traitor mission and start one immediately.", null));
commands.Add(new Command("botcount", "botcount [x]: Set the number of bots in the crew in multiplayer.", null));
commands.Add(new Command("botspawnmode", "botspawnmode [fill/normal]: Set how bots are spawned in the multiplayer.", null));
@@ -335,18 +336,19 @@ namespace Barotrauma
commands.Add(new Command("kick", "kick [name]: Kick a player out of the server.", (string[] args) =>
{
if (GameMain.NetworkMember == null || args.Length == 0) return;
if (GameMain.NetworkMember == null || args.Length == 0) { return; }
string playerName = string.Join(" ", args);
ShowQuestionPrompt("Reason for kicking \"" + playerName + "\"?", (reason) =>
ShowQuestionPrompt("Reason for kicking \"" + playerName + "\"? (Enter c to cancel)", (reason) =>
{
if (reason == "c" || reason == "C") { return; }
GameMain.NetworkMember.KickPlayer(playerName, reason);
});
},
() =>
{
if (GameMain.NetworkMember == null) return null;
if (GameMain.NetworkMember == null) { return null; }
return new string[][]
{
@@ -366,8 +368,9 @@ namespace Barotrauma
return;
}
ShowQuestionPrompt("Reason for kicking \"" + client.Name + "\"?", (reason) =>
ShowQuestionPrompt("Reason for kicking \"" + client.Name + "\"? (Enter c to cancel)", (reason) =>
{
if (reason == "c" || reason == "C") { return; }
GameMain.NetworkMember.KickPlayer(client.Name, reason);
});
}));
@@ -377,10 +380,12 @@ namespace Barotrauma
if (GameMain.NetworkMember == null || args.Length == 0) return;
string clientName = string.Join(" ", args);
ShowQuestionPrompt("Reason for banning \"" + clientName + "\"?", (reason) =>
ShowQuestionPrompt("Reason for banning \"" + clientName + "\"? (Enter c to cancel)", (reason) =>
{
ShowQuestionPrompt("Enter the duration of the ban (leave empty to ban permanently, or use the format \"[days] d [hours] h\")", (duration) =>
if (reason == "c" || reason == "C") { return; }
ShowQuestionPrompt("Enter the duration of the ban (leave empty to ban permanently, or use the format \"[days] d [hours] h\") (Enter c to cancel)", (duration) =>
{
if (duration == "c" || duration == "C") { return; }
TimeSpan? banDuration = null;
if (!string.IsNullOrWhiteSpace(duration))
{
@@ -418,10 +423,12 @@ namespace Barotrauma
return;
}
ShowQuestionPrompt("Reason for banning \"" + client.Name + "\"?", (reason) =>
ShowQuestionPrompt("Reason for banning \"" + client.Name + "\"? (Enter c to cancel)", (reason) =>
{
ShowQuestionPrompt("Enter the duration of the ban (leave empty to ban permanently, or use the format \"[days] d [hours] h\")", (duration) =>
if (reason == "c" || reason == "C") { return; }
ShowQuestionPrompt("Enter the duration of the ban (leave empty to ban permanently, or use the format \"[days] d [hours] h\") (c to cancel)", (duration) =>
{
if (duration == "c" || duration == "C") { return; }
TimeSpan? banDuration = null;
if (!string.IsNullOrWhiteSpace(duration))
{
@@ -874,8 +881,7 @@ namespace Barotrauma
commands.Add(new Command("campaigninfo|campaignstatus", "campaigninfo: Display information about the state of the currently active campaign.", (string[] args) =>
{
var campaign = GameMain.GameSession?.GameMode as CampaignMode;
if (campaign == null)
if (!(GameMain.GameSession?.GameMode is CampaignMode campaign))
{
ThrowError("No campaign active!");
return;
@@ -886,8 +892,7 @@ namespace Barotrauma
commands.Add(new Command("campaigndestination|setcampaigndestination", "campaigndestination [index]: Set the location to head towards in the currently active campaign.", (string[] args) =>
{
var campaign = GameMain.GameSession?.GameMode as CampaignMode;
if (campaign == null)
if (!(GameMain.GameSession?.GameMode is CampaignMode campaign))
{
ThrowError("No campaign active!");
return;
@@ -938,7 +943,6 @@ namespace Barotrauma
NewMessage((GameSettings.VerboseLogging ? "Enabled" : "Disabled") + " verbose logging.", Color.White);
}, isCheat: false));
commands.Add(new Command("calculatehashes", "calculatehashes [content package name]: Show the MD5 hashes of the files in the selected content package. If the name parameter is omitted, the first content package is selected.", (string[] args) =>
{
if (args.Length > 0)
@@ -967,7 +971,21 @@ namespace Barotrauma
};
}));
#if DEBUG
commands.Add(new Command("debugai", "", onExecute: (string[] args) =>
{
var commands = new List<KeyValuePair<string, string[]>>()
{
new KeyValuePair<string, string[]>("debugdraw", new string[]{ "true" }),
new KeyValuePair<string, string[]>("los", new string[]{ "false" }),
new KeyValuePair<string, string[]>("lights", new string[]{ "false" }),
new KeyValuePair<string, string[]>("freecam", new string[0]),
};
foreach (var command in commands)
{
Commands.Find(c => c.names.Any(n => n.Equals(command.Key, StringComparison.OrdinalIgnoreCase)))?.Execute(command.Value);
}
}));
commands.Add(new Command("simulatedlatency", "simulatedlatency [minimumlatencyseconds] [randomlatencyseconds]: applies a simulated latency to network messages. Useful for simulating real network conditions when testing the multiplayer locally.", (string[] args) =>
{
if (args.Count() < 2 || (GameMain.NetworkMember == null)) return;
@@ -1039,7 +1057,6 @@ namespace Barotrauma
#endif
NewMessage("Set packet duplication to " + (int)(duplicates * 100) + "%.", Color.White);
}));
#endif
//"dummy commands" that only exist so that the server can give clients permissions to use them
//TODO: alphabetical order?
@@ -1170,16 +1187,6 @@ namespace Barotrauma
}
}
private static string AutoCompleteStr(string str, IEnumerable<string> validStrings)
{
if (string.IsNullOrEmpty(str)) return str;
foreach (string validStr in validStrings)
{
if (validStr.Length > str.Length && validStr.Substring(0, str.Length) == str) return validStr;
}
return str;
}
public static void ResetAutoComplete()
{
currentAutoCompletedCommand = "";
@@ -1197,7 +1204,7 @@ namespace Barotrauma
{
selectedIndex += direction;
if (selectedIndex < 0) selectedIndex = Messages.Count - 1;
selectedIndex = selectedIndex % Messages.Count;
selectedIndex %= Messages.Count;
if (++i >= Messages.Count) break;
} while (!Messages[selectedIndex].IsCommand || Messages[selectedIndex].Text == currentText);
@@ -1257,13 +1264,6 @@ namespace Barotrauma
return;
}
#if !DEBUG
if (!IsCommandPermitted(splitCommand[0].ToLowerInvariant(), GameMain.Client))
{
ThrowError("You're not permitted to use the command \"" + splitCommand[0].ToLowerInvariant() + "\"!");
return;
}
#endif
}
#endif
@@ -1509,9 +1509,7 @@ namespace Barotrauma
public static void NewMessage(string msg, Color color, bool isCommand = false)
{
if (string.IsNullOrEmpty((msg))) return;
var newMsg = new ColoredText(msg, color, isCommand);
if (string.IsNullOrEmpty(msg)) { return; }
lock (queuedMessages)
{
@@ -10,6 +10,7 @@ namespace Barotrauma
{
public static readonly List<EventManagerSettings> List = new List<EventManagerSettings>();
public readonly string Identifier;
public readonly string Name;
//How much the event threshold increases per second. 0.0005f = 0.03f per minute
@@ -48,28 +49,30 @@ namespace Barotrauma
foreach (XElement subElement in mainElement.Elements())
{
var element = subElement.IsOverride() ? subElement.FirstElement() : subElement;
string name = element.Name.ToString();
var duplicate = List.FirstOrDefault(e => e.Name.ToString().Equals(name, StringComparison.OrdinalIgnoreCase));
string identifier = element.Name.ToString();
var duplicate = List.FirstOrDefault(e => e.Identifier.ToString().Equals(identifier, StringComparison.OrdinalIgnoreCase));
if (duplicate != null)
{
if (allowOverriding || subElement.IsOverride())
{
DebugConsole.NewMessage($"Overriding the existing preset '{name}' in the event manager settings using the file '{file}'", Color.Yellow);
DebugConsole.NewMessage($"Overriding the existing preset '{identifier}' in the event manager settings using the file '{file}'", Color.Yellow);
List.Remove(duplicate);
}
else
{
DebugConsole.ThrowError($"Error in '{file}': Another element with the name '{name}' found! Each element must have a unique name. Use <override></override> tags if you want to override an existing preset.");
DebugConsole.ThrowError($"Error in '{file}': Another element with the name '{identifier}' found! Each element must have a unique name. Use <override></override> tags if you want to override an existing preset.");
continue;
}
}
List.Add(new EventManagerSettings(element));
}
List.Sort((x, y) => { return Math.Sign((x.MinLevelDifficulty + x.MaxLevelDifficulty) / 2.0f - (y.MinLevelDifficulty + y.MaxLevelDifficulty) / 2.0f); });
}
public EventManagerSettings(XElement element)
{
Name = element.Name.ToString();
Identifier = element.Name.ToString();
Name = TextManager.Get("difficulty." + Identifier, returnNull: true) ?? Identifier;
EventThresholdIncrease = element.GetAttributeFloat("EventThresholdIncrease", 0.0005f);
DefaultEventThreshold = element.GetAttributeFloat("DefaultEventThreshold", 0.2f);
EventCooldown = element.GetAttributeFloat("EventCooldown", 360.0f);
@@ -9,7 +9,25 @@ namespace Barotrauma
{
public readonly MissionPrefab Prefab;
protected bool completed;
protected int state;
public int State
{
get { return state; }
protected set
{
if (state != value)
{
state = value;
#if SERVER
GameMain.Server?.UpdateMissionState(state);
#endif
ShowMessage(State);
}
}
}
protected bool IsClient => GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient;
public readonly List<string> Headers;
public readonly List<string> Messages;
@@ -107,17 +125,13 @@ namespace Barotrauma
public static Mission LoadRandom(Location[] locations, MTRandom rand, bool requireCorrectLocationType, MissionType missionType, bool isSinglePlayer = false)
{
List<MissionPrefab> allowedMissions = new List<MissionPrefab>();
if (missionType == MissionType.Random)
{
allowedMissions.AddRange(MissionPrefab.List);
}
else if (missionType == MissionType.None)
if (missionType == MissionType.None)
{
return null;
}
else
{
allowedMissions = MissionPrefab.List.FindAll(m => m.type == missionType);
allowedMissions.AddRange(MissionPrefab.List.Where(m => ((int)(missionType & m.type)) != 0));
}
allowedMissions.RemoveAll(m => isSinglePlayer ? m.MultiplayerOnly : m.SingleplayerOnly);
@@ -6,14 +6,15 @@ using Microsoft.Xna.Framework;
namespace Barotrauma
{
[Flags]
public enum MissionType
{
Random,
None,
Salvage,
Monster,
Cargo,
Combat
None = 0x0,
Salvage = 0x1,
Monster = 0x2,
Cargo = 0x4,
Combat = 0x8,
All = 0xf
}
partial class MissionPrefab
@@ -155,11 +156,6 @@ namespace Barotrauma
DebugConsole.ThrowError("Error in mission prefab \"" + Name + "\" - \"" + missionTypeName + "\" is not a valid mission type.");
return;
}
if (type == MissionType.Random)
{
DebugConsole.ThrowError("Error in mission prefab \"" + Name + "\" - mission type cannot be random.");
return;
}
if (type == MissionType.None)
{
DebugConsole.ThrowError("Error in mission prefab \"" + Name + "\" - mission type cannot be none.");
@@ -1,34 +1,41 @@
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
using System;
namespace Barotrauma
{
class MonsterMission : Mission
{
private string monsterFile;
private int state;
private int monsterCount;
private readonly string monsterFile;
private readonly int monsterCount;
private readonly HashSet<Tuple<string, int>> monsterFiles = new HashSet<Tuple<string, int>>();
private readonly List<Character> monsters = new List<Character>();
private readonly List<Vector2> sonarPositions = new List<Vector2>();
public override IEnumerable<Vector2> SonarPositions
{
get
{
return sonarPositions;
}
}
public override IEnumerable<Vector2> SonarPositions => sonarPositions;
public MonsterMission(MissionPrefab prefab, Location[] locations)
: base(prefab, locations)
{
monsterFile = prefab.ConfigElement.GetAttributeString("monsterfile", "");
monsterFile = prefab.ConfigElement.GetAttributeString("monsterfile", null);
monsterCount = prefab.ConfigElement.GetAttributeInt("monstercount", 1);
foreach (var monsterElement in prefab.ConfigElement.GetChildElements("monster"))
{
string monster = monsterElement.GetAttributeString("character", string.Empty);
if (monsterFile == null)
{
monsterFile = monster;
}
int defaultCount = monsterElement.GetAttributeInt("count", -1);
if (defaultCount < 0)
{
defaultCount = monsterElement.GetAttributeInt("amount", 1);
}
int min = monsterElement.GetAttributeInt("min", defaultCount);
int max = Math.Max(min, monsterElement.GetAttributeInt("max", defaultCount));
monsterFiles.Add(new Tuple<string, int>(monster, Rand.Range(min, max + 1, Rand.RandSync.Server)));
}
description = description.Replace("[monster]",
TextManager.Get("character." + System.IO.Path.GetFileNameWithoutExtension(monsterFile)));
}
@@ -37,11 +44,22 @@ namespace Barotrauma
{
Level.Loaded.TryGetInterestingPosition(true, Level.PositionType.MainPath, Level.Loaded.Size.X * 0.3f, out Vector2 spawnPos);
bool isClient = GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient;
for (int i = 0; i < monsterCount; i++)
bool isClient = IsClient;
if (!string.IsNullOrEmpty(monsterFile))
{
monsters.Add(Character.Create(monsterFile, spawnPos, ToolBox.RandomSeed(8), null, isClient, true, false));
for (int i = 0; i < monsterCount; i++)
{
monsters.Add(Character.Create(monsterFile, spawnPos, ToolBox.RandomSeed(8), null, isClient, true, false));
}
}
foreach (var monster in monsterFiles)
{
for (int i = 0; i < monster.Item2; i++)
{
monsters.Add(Character.Create(monster.Item1, spawnPos, ToolBox.RandomSeed(8), null, isClient, true, false));
}
}
monsters.ForEach(m => m.Enabled = false);
SwarmBehavior.CreateSwarm(monsters.Cast<AICharacter>());
sonarPositions.Add(spawnPos);
@@ -49,40 +67,35 @@ namespace Barotrauma
public override void Update(float deltaTime)
{
switch (state)
switch (State)
{
case 0:
sonarPositions.Clear();
var activeMonsters = monsters.Where(m => m != null && !m.Removed && !m.IsDead);
if (activeMonsters.Any())
foreach (var monster in monsters)
{
Vector2 centerOfMass = Vector2.Zero;
foreach (var monster in activeMonsters)
if (monster.Removed || monster.IsDead) { continue; }
//don't add another label if there's another monster roughly at the same spot
if (sonarPositions.All(p => Vector2.DistanceSquared(p, monster.Position) > 1000.0f * 1000.0f))
{
//don't add another label if there's another monster roughly at the same spot
if (sonarPositions.All(p => Vector2.DistanceSquared(p, monster.Position) > 1000.0f * 1000.0f))
{
sonarPositions.Add(monster.Position);
}
sonarPositions.Add(monster.Position);
}
}
if (activeMonsters.Any()) { return; }
ShowMessage(state);
state = 1;
if (!IsClient && monsters.All(m => IsEliminated(m)))
{
State = 1;
}
break;
}
}
public override void End()
{
if (!monsters.All(m => m.Removed || m.IsDead)) { return; }
if (State < 1) { return; }
GiveReward();
completed = true;
}
public bool IsEliminated(Character enemy) => enemy.Removed || enemy.IsDead || enemy.AIController is EnemyAIController ai && ai.State == AIState.Flee;
}
}
@@ -14,13 +14,11 @@ namespace Barotrauma
private Level.PositionType spawnPositionType;
private int state;
public override IEnumerable<Vector2> SonarPositions
{
get
{
if (state > 0 )
if (State > 0 )
{
Enumerable.Empty<Vector2>();
}
@@ -87,34 +85,27 @@ namespace Barotrauma
public override void Update(float deltaTime)
{
switch (state)
if (IsClient) { return; }
switch (State)
{
case 0:
//item.body.LinearVelocity = Vector2.Zero;
if (item.ParentInventory != null) item.body.FarseerBody.IsKinematic = false;
if (item.CurrentHull?.Submarine == null) return;
ShowMessage(state);
state = 1;
if (item.ParentInventory != null) { item.body.FarseerBody.IsKinematic = false; }
if (item.CurrentHull?.Submarine == null) { return; }
State = 1;
break;
case 1:
if (!Submarine.MainSub.AtEndPosition && !Submarine.MainSub.AtStartPosition) return;
ShowMessage(state);
state = 2;
if (!Submarine.MainSub.AtEndPosition && !Submarine.MainSub.AtStartPosition) { return; }
State = 2;
break;
}
}
public override void End()
{
if (item.CurrentHull?.Submarine == null || !item.CurrentHull.Submarine.AtEndPosition || item.Removed) return;
if (item.CurrentHull?.Submarine == null || !item.CurrentHull.Submarine.AtEndPosition || item.Removed) { return; }
item.Remove();
GiveReward();
completed = true;
}
}
@@ -110,5 +110,60 @@ namespace Barotrauma
}
}
}
public static ICollection<string> ParseCommaSeparatedStringToCollection(string input, ICollection<string> texts = null, bool convertToLowerInvariant = true)
{
if (texts == null)
{
texts = new HashSet<string>();
}
else
{
texts.Clear();
}
if (!string.IsNullOrWhiteSpace(input))
{
foreach (string value in input.Split(','))
{
if (string.IsNullOrWhiteSpace(value)) { continue; }
if (convertToLowerInvariant)
{
texts.Add(value.ToLowerInvariant());
}
else
{
texts.Add(value);
}
}
}
return texts;
}
public static ICollection<string> ParseSeparatedStringToCollection(string input, string[] separators, ICollection<string> texts = null, bool convertToLowerInvariant = true)
{
if (texts == null)
{
texts = new HashSet<string>();
}
else
{
texts.Clear();
}
if (!string.IsNullOrWhiteSpace(input))
{
foreach (string value in input.Split(separators, StringSplitOptions.RemoveEmptyEntries))
{
if (convertToLowerInvariant)
{
texts.Add(value.ToLowerInvariant());
}
else
{
texts.Add(value);
}
}
}
return texts;
}
}
}
@@ -83,8 +83,16 @@ namespace Barotrauma
/// </summary>
public static void AddErrorEventOnce(string identifier, EGAErrorSeverity errorSeverity, string message)
{
if (!GameSettings.SendUserStatistics) return;
if (sentEventIdentifiers.Contains(identifier)) return;
if (!GameSettings.SendUserStatistics) { return; }
if (sentEventIdentifiers.Contains(identifier)) { return; }
if (GameMain.SelectedPackages != null)
{
if (GameMain.VanillaContent == null || GameMain.SelectedPackages.Any(p => p.HasMultiplayerIncompatibleContent && p != GameMain.VanillaContent))
{
message = "[MODDED] " + message;
}
}
GameAnalytics.AddErrorEvent(errorSeverity, message);
sentEventIdentifiers.Add(identifier);
@@ -104,6 +104,7 @@ namespace Barotrauma
{
if (c.Character?.Info != null && !c.Character.IsDead)
{
c.CharacterInfo = c.Character.Info;
characterData.Add(new CharacterCampaignData(c));
}
}
@@ -51,6 +51,8 @@ namespace Barotrauma
public bool VoipAttenuationEnabled { get; set; }
public bool UseDirectionalVoiceChat { get; set; }
public IList<string> CaptureDeviceNames;
public enum VoiceMode
{
Disabled,
@@ -69,7 +71,7 @@ namespace Barotrauma
private LosMode losMode;
public List<string> jobPreferences;
public List<Pair<string, int>> jobPreferences;
private bool useSteamMatchmaking;
private bool requireSteamAuthentication;
@@ -119,7 +121,7 @@ namespace Barotrauma
}
}
public List<string> JobPreferences
public List<Pair<string, int>> JobPreferences
{
get { return jobPreferences; }
set { jobPreferences = value; }
@@ -211,12 +213,13 @@ namespace Barotrauma
}
}
public const float MaxMicrophoneVolume = 10.0f;
public float MicrophoneVolume
{
get { return microphoneVolume; }
set
{
microphoneVolume = MathHelper.Clamp(value, 0.2f, 10.0f);
microphoneVolume = MathHelper.Clamp(value, 0.2f, MaxMicrophoneVolume);
}
}
public string Language
@@ -232,6 +235,7 @@ namespace Barotrauma
if (!SelectedContentPackages.Contains(contentPackage))
{
SelectedContentPackages.Add(contentPackage);
contentPackage.NeedsRestart |= contentPackage.HasMultiplayerIncompatibleContent;
ContentPackage.SortContentPackages();
}
}
@@ -241,6 +245,7 @@ namespace Barotrauma
if (SelectedContentPackages.Contains(contentPackage))
{
SelectedContentPackages.Remove(contentPackage);
contentPackage.NeedsRestart |= contentPackage.HasMultiplayerIncompatibleContent;
ContentPackage.SortContentPackages();
}
}
@@ -442,11 +447,7 @@ namespace Barotrauma
GraphicsHeight = 768;
MasterServerUrl = "";
SelectContentPackage(ContentPackage.List.Any() ? ContentPackage.List[0] : new ContentPackage(""));
jobPreferences = new List<string>();
foreach (string job in JobPrefab.List.Keys)
{
jobPreferences.Add(job);
}
jobPreferences = new List<Pair<string, int>>();
return;
}
@@ -570,9 +571,12 @@ namespace Barotrauma
var gameplay = new XElement("gameplay");
var jobPreferences = new XElement("jobpreferences");
foreach (string jobName in JobPreferences)
foreach (Pair<string, int> job in JobPreferences)
{
jobPreferences.Add(new XElement("job", new XAttribute("identifier", jobName)));
XElement jobElement = new XElement("job");
jobElement.Add(new XAttribute("identifier", job.First));
jobElement.Add(new XAttribute("variant", job.Second));
jobPreferences.Add(jobElement);
}
gameplay.Add(jobPreferences);
doc.Root.Add(gameplay);
@@ -890,9 +894,12 @@ namespace Barotrauma
var gameplay = new XElement("gameplay");
var jobPreferences = new XElement("jobpreferences");
foreach (string jobName in JobPreferences)
foreach (Pair<string, int> job in JobPreferences)
{
jobPreferences.Add(new XElement("job", new XAttribute("identifier", jobName)));
XElement jobElement = new XElement("job");
jobElement.Add(new XAttribute("identifier", job.First));
jobElement.Add(new XAttribute("variant", job.Second));
jobPreferences.Add(jobElement);
}
gameplay.Add(jobPreferences);
doc.Root.Add(gameplay);
@@ -972,14 +979,19 @@ namespace Barotrauma
CampaignDisclaimerShown = doc.Root.GetAttributeBool("campaigndisclaimershown", CampaignDisclaimerShown);
EditorDisclaimerShown = doc.Root.GetAttributeBool("editordisclaimershown", EditorDisclaimerShown);
XElement gameplayElement = doc.Root.Element("gameplay");
jobPreferences = new List<Pair<string, int>>();
if (gameplayElement != null)
{
jobPreferences = new List<string>();
foreach (XElement ele in gameplayElement.Element("jobpreferences").Elements("job"))
var preferencesElement = gameplayElement.Element("jobpreferences");
if (preferencesElement != null)
{
string jobIdentifier = ele.GetAttributeString("identifier", "");
if (string.IsNullOrEmpty(jobIdentifier)) continue;
jobPreferences.Add(jobIdentifier);
foreach (XElement ele in preferencesElement.Elements("job"))
{
string jobIdentifier = ele.GetAttributeString("identifier", "");
int outfitVariant = ele.GetAttributeInt("variant", 1);
if (string.IsNullOrEmpty(jobIdentifier)) continue;
jobPreferences.Add(new Pair<string, int>(jobIdentifier, outfitVariant));
}
}
}
@@ -20,9 +20,9 @@ namespace Barotrauma.Items.Components
private bool isOpen;
private float openState;
private Sprite doorSprite, weldedSprite, brokenSprite;
private bool scaleBrokenSprite, fadeBrokenSprite;
private bool autoOrientGap;
private readonly Sprite doorSprite, weldedSprite, brokenSprite;
private readonly bool scaleBrokenSprite, fadeBrokenSprite;
private readonly bool autoOrientGap;
private bool isStuck;
public bool IsStuck => isStuck;
@@ -221,8 +221,8 @@ namespace Barotrauma.Items.Components
#endif
}
private string accessDeniedTxt = TextManager.Get("AccessDenied");
private string cannotOpenText = TextManager.Get("DoorMsgCannotOpen");
private readonly string accessDeniedTxt = TextManager.Get("AccessDenied");
private readonly string cannotOpenText = TextManager.Get("DoorMsgCannotOpen");
private bool hasValidIdCard;
public override bool HasRequiredItems(Character character, bool addMessage, string msg = null)
{
@@ -272,14 +272,15 @@ namespace Barotrauma.Items.Components
ToggleState(ActionType.OnUse);
PickingTime = originalPickingTime;
}
else if (hasRequiredItems)
{
#if CLIENT
else if (hasRequiredItems && character != null && character == Character.Controlled)
{
GUI.AddMessage(accessDeniedTxt, Color.Red);
#endif
}
#endif
}
return item.Condition <= RepairThreshold;
return false;
}
public override void Update(float deltaTime, Camera cam)
@@ -340,6 +341,13 @@ namespace Barotrauma.Items.Components
if (!Impassable)
{
Body.FarseerBody.IsSensor = false;
var ce = Body.FarseerBody.ContactList;
while (ce != null && ce.Contact != null)
{
ce.Contact.Enabled = false;
ce = ce.Next;
}
PushCharactersAway();
}
#if CLIENT
UpdateConvexHulls();
@@ -354,6 +362,12 @@ namespace Barotrauma.Items.Components
if (!Impassable)
{
Body.FarseerBody.IsSensor = true;
var ce = Body.FarseerBody.ContactList;
while (ce != null && ce.Contact != null)
{
ce.Contact.Enabled = false;
ce = ce.Next;
}
}
linkedGap.Open = 1.0f;
IsOpen = false;
@@ -413,15 +427,14 @@ namespace Barotrauma.Items.Components
//otherwise the gap will be removed twice and cause console warnings
if (!Submarine.Unloading)
{
if (linkedGap != null) linkedGap.Remove();
linkedGap?.Remove();
}
doorSprite.Remove();
if (weldedSprite != null) weldedSprite.Remove();
doorSprite?.Remove();
weldedSprite?.Remove();
#if CLIENT
if (convexHull != null) convexHull.Remove();
if (convexHull2 != null) convexHull2.Remove();
convexHull?.Remove();
convexHull2?.Remove();
#endif
}
@@ -474,7 +487,6 @@ namespace Barotrauma.Items.Components
private bool PushBodyOutOfDoorway(Character c, PhysicsBody body, int dir, Vector2 doorRectSimPos, Vector2 doorRectSimSize)
{
float diff = 0.0f;
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 + ")");
@@ -484,7 +496,8 @@ namespace Barotrauma.Items.Components
" Remoteplayer: " + c.IsRemotePlayer);
return false;
}
float diff;
if (IsHorizontal)
{
if (body.SimPosition.X < doorRectSimPos.X || body.SimPosition.X > doorRectSimPos.X + doorRectSimSize.X) { return false; }
@@ -131,7 +131,7 @@ namespace Barotrauma.Items.Components
{
if (charging)
{
if (voltage > minVoltage || powerConsumption <= 0.0f)
if (Voltage > MinVoltage)
{
Discharge();
}
@@ -142,8 +142,6 @@ namespace Barotrauma.Items.Components
{
IsActive = false;
}
voltage = 0.0f;
}
public override void UpdateBroken(float deltaTime, Camera cam)
@@ -455,6 +453,7 @@ namespace Barotrauma.Items.Components
protected override void RemoveComponentSpecific()
{
base.RemoveComponentSpecific();
list.Remove(this);
}
}
@@ -242,9 +242,19 @@ namespace Barotrauma.Items.Components
User = null;
}
//ignore collision if there's a wall between the user and the weapon to prevent hitting through walls
if (Submarine.PickBody(User.AnimController.AimSourceSimPos,
item.SimPosition,
collisionCategory: Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking,
allowInsideFixture: true) != null)
{
return false;
}
Character targetCharacter = null;
Limb targetLimb = null;
Structure targetStructure = null;
Item targetItem = null;
attack?.SetUser(User);
@@ -292,6 +302,19 @@ namespace Barotrauma.Items.Components
}
hitTargets.Add(targetStructure);
}
else if (f2.Body.UserData is Item)
{
targetItem = (Item)f2.Body.UserData;
if (AllowHitMultiple)
{
if (hitTargets.Contains(targetItem)) { return true; }
}
else
{
if (hitTargets.Any(t => t is Item)) { return true; }
}
hitTargets.Add(targetItem);
}
else
{
return false;
@@ -313,6 +336,10 @@ namespace Barotrauma.Items.Components
{
attack.DoDamage(User, targetStructure, item.WorldPosition, 1.0f);
}
else if (targetItem != null && targetItem.Prefab.DamagedByMeleeWeapons)
{
attack.DoDamage(User, targetItem, item.WorldPosition, 1.0f);
}
else
{
return false;
@@ -127,6 +127,7 @@ namespace Barotrauma.Items.Components
if (activeTimer <= 0.0f) IsActive = false;
}
private List<Body> ignoredBodies = new List<Body>();
public override bool Use(float deltaTime, Character character = null)
{
if (character == null || character.Removed) return false;
@@ -185,7 +186,7 @@ namespace Barotrauma.Items.Components
(float)Math.Cos(angle),
(float)Math.Sin(angle)) * Range * item.body.Dir);
List<Body> ignoredBodies = new List<Body>();
ignoredBodies.Clear();
foreach (Limb limb in character.AnimController.Limbs)
{
if (Rand.Range(0.0f, 0.5f) > degreeOfSuccess) continue;
@@ -438,27 +439,48 @@ namespace Barotrauma.Items.Components
private float sinTime;
public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
{
if (!(objective.OperateTarget is Gap leak)) return true;
Vector2 fromItemToLeak = leak.WorldPosition - item.WorldPosition;
float dist = fromItemToLeak.Length();
if (!(objective.OperateTarget is Gap leak)) { return true; }
if (leak.Submarine == null) { return true; }
Vector2 fromCharacterToLeak = leak.WorldPosition - character.WorldPosition;
float dist = fromCharacterToLeak.Length();
float reach = Range + ConvertUnits.ToDisplayUnits(((HumanoidAnimController)character.AnimController).ArmLength);
//too far away -> consider this done and hope the AI is smart enough to move closer
if (dist > Range * 3.0f) { return true; }
// TODO: use the collider size?
if (!character.AnimController.InWater && character.AnimController is HumanoidAnimController &&
Math.Abs(fromItemToLeak.X) < 100.0f && fromItemToLeak.Y < 0.0f && fromItemToLeak.Y > -150.0f)
{
((HumanoidAnimController)character.AnimController).Crouching = true;
}
if (dist > reach * 2) { return true; }
character.AIController.SteeringManager.Reset();
//steer closer if almost in range
if (dist > Range)
if (dist > reach)
{
Vector2 standPos = new Vector2(Math.Sign(-fromItemToLeak.X), Math.Sign(-fromItemToLeak.Y)) / 2;
if (!character.AnimController.InWater)
if (character.AnimController.InWater)
{
if (character.AIController.SteeringManager is IndoorsSteeringManager indoorSteering)
{
// Swimming inside the sub
if (indoorSteering.CurrentPath != null && !indoorSteering.IsPathDirty && indoorSteering.CurrentPath.Unreachable)
{
Vector2 dir = Vector2.Normalize(fromCharacterToLeak);
character.AIController.SteeringManager.SteeringManual(deltaTime, dir);
}
else
{
character.AIController.SteeringManager.SteeringSeek(character.GetRelativeSimPosition(leak));
}
}
else
{
// Swimming outside the sub
character.AIController.SteeringManager.SteeringSeek(character.GetRelativeSimPosition(leak));
}
}
else
{
// TODO: use the collider size?
if (!character.AnimController.InWater && character.AnimController is HumanoidAnimController &&
Math.Abs(fromCharacterToLeak.X) < 100.0f && fromCharacterToLeak.Y < 0.0f && fromCharacterToLeak.Y > -150.0f)
{
((HumanoidAnimController)character.AnimController).Crouching = true;
}
Vector2 standPos = new Vector2(Math.Sign(-fromCharacterToLeak.X), Math.Sign(-fromCharacterToLeak.Y)) / 2;
if (leak.IsHorizontal)
{
standPos.X *= 2;
@@ -468,43 +490,40 @@ namespace Barotrauma.Items.Components
{
standPos.X = 0;
}
}
if (character.AIController.SteeringManager is IndoorsSteeringManager indoorSteering)
{
if (indoorSteering.CurrentPath != null && !indoorSteering.IsPathDirty && indoorSteering.CurrentPath.Unreachable)
{
Vector2 dir = Vector2.Normalize(standPos - character.WorldPosition);
character.AIController.SteeringManager.SteeringManual(deltaTime, dir / 2);
}
else
{
character.AIController.SteeringManager.SteeringSeek(standPos);
}
}
else
{
character.AIController.SteeringManager.SteeringSeek(standPos);
}
}
else
{
if (dist < Range / 2)
if (dist < reach / 2)
{
// Too close -> steer away
character.AIController.SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(character.SimPosition - leak.SimPosition) / 2);
character.AIController.SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(character.SimPosition - leak.SimPosition));
}
else if (dist <= Range)
else if (dist <= reach)
{
// In range
character.AIController.SteeringManager.Reset();
}
else
{
return false;
character.CursorPosition = leak.Position;
character.CursorPosition += VectorExtensions.Forward(Item.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, dist / 2);
if (character.AnimController.InWater)
{
var torso = character.AnimController.GetLimb(LimbType.Torso);
// Turn facing the target when not moving (handled in the animcontroller if not moving)
Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition);
Vector2 diff = (mousePos - torso.SimPosition) * character.AnimController.Dir;
float newRotation = MathUtils.VectorToAngle(diff);
character.AnimController.Collider.SmoothRotate(newRotation, 5.0f);
if (VectorExtensions.Angle(VectorExtensions.Forward(torso.body.TransformedRotation), fromCharacterToLeak) < MathHelper.PiOver4)
{
// Swim past
Vector2 moveDir = leak.IsHorizontal ? Vector2.UnitY : Vector2.UnitX;
moveDir *= character.AnimController.Dir;
character.AIController.SteeringManager.SteeringManual(deltaTime, moveDir);
}
}
}
}
sinTime += deltaTime;
character.CursorPosition = leak.Position + VectorExtensions.Forward(Item.body.TransformedRotation + (float)Math.Sin(sinTime), dist);
if (item.RequireAimToUse)
{
bool isOperatingButtons = false;
@@ -520,13 +539,33 @@ namespace Barotrauma.Items.Components
{
character.SetInput(InputType.Aim, false, true);
}
bool isAiming = false;
var holdable = item.GetComponent<Holdable>();
if (holdable != null)
{
isAiming = holdable.ControlPose;
}
sinTime = isAiming ? sinTime + deltaTime * 5 : 0;
}
// Press the trigger only when the tool is approximately facing the target.
Vector2 fromItemToLeak = leak.WorldPosition - item.WorldPosition;
var angle = VectorExtensions.Angle(VectorExtensions.Forward(item.body.TransformedRotation), fromItemToLeak);
if (angle < MathHelper.PiOver4)
{
character.SetInput(InputType.Shoot, false, true);
Use(deltaTime, character);
// Check that we don't hit any friendlies
if (Submarine.PickBodies(item.SimPosition, leak.SimPosition, collisionCategory: Physics.CollisionCharacter).None(hit =>
{
if (hit.UserData is Character c)
{
if (c == character) { return false; }
return HumanAIController.IsFriendly(character, c);
}
return false;
}))
{
character.SetInput(InputType.Shoot, false, true);
Use(deltaTime, character);
}
}
bool leakFixed = (leak.Open <= 0.0f || leak.Removed) &&
@@ -534,7 +573,6 @@ namespace Barotrauma.Items.Components
if (leakFixed && leak.FlowTargetHull != null)
{
sinTime = 0;
if (!leak.FlowTargetHull.ConnectedGaps.Any(g => !g.IsRoomToRoom && g.Open > 0.0f))
{
@@ -44,8 +44,9 @@ namespace Barotrauma.Items.Components
public bool WasUsed;
public readonly Dictionary<ActionType, List<StatusEffect>> statusEffectLists;
public Dictionary<RelatedItem.RelationType, List<RelatedItem>> requiredItems;
public readonly List<RelatedItem> DisabledRequiredItems = new List<RelatedItem>();
public List<Skill> requiredSkills;
@@ -271,19 +272,7 @@ namespace Barotrauma.Items.Components
break;
case "requireditem":
case "requireditems":
RelatedItem ri = RelatedItem.Load(subElement, item.Name);
if (ri != null)
{
if (!requiredItems.ContainsKey(ri.Type))
{
requiredItems.Add(ri.Type, new List<RelatedItem>());
}
requiredItems[ri.Type].Add(ri);
}
else
{
DebugConsole.ThrowError("Error in item config \"" + item.ConfigFile + "\" - component " + GetType().ToString() + " requires an item with no identifiers.");
}
SetRequiredItems(subElement);
break;
case "requiredskill":
case "requiredskills":
@@ -323,6 +312,34 @@ namespace Barotrauma.Items.Components
}
}
public void SetRequiredItems(XElement element)
{
bool returnEmpty = false;
#if CLIENT
returnEmpty = Screen.Selected == GameMain.SubEditorScreen;
#endif
RelatedItem ri = RelatedItem.Load(element, returnEmpty, item.Name);
if (ri != null)
{
if (ri.Identifiers.Length == 0)
{
DisabledRequiredItems.Add(ri);
}
else
{
if (!requiredItems.ContainsKey(ri.Type))
{
requiredItems.Add(ri.Type, new List<RelatedItem>());
}
requiredItems[ri.Type].Add(ri);
}
}
else
{
DebugConsole.ThrowError("Error in item config \"" + item.ConfigFile + "\" - component " + GetType().ToString() + " requires an item with no identifiers.");
}
}
public virtual void Move(Vector2 amount) { }
/// <summary>a Character has picked the item</summary>
@@ -762,6 +779,12 @@ namespace Barotrauma.Items.Components
componentElement.Add(newElement);
}
}
foreach (RelatedItem ri in DisabledRequiredItems)
{
XElement newElement = new XElement("requireditem");
ri.Save(newElement);
componentElement.Add(newElement);
}
SerializableProperty.SerializeProperties(this, componentElement);
@@ -783,12 +806,16 @@ namespace Barotrauma.Items.Components
var prevRequiredItems = new Dictionary<RelatedItem.RelationType, List<RelatedItem>>(requiredItems);
requiredItems.Clear();
bool returnEmptyRequirements = false;
#if CLIENT
returnEmptyRequirements = Screen.Selected == GameMain.SubEditorScreen;
#endif
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "requireditem":
RelatedItem newRequiredItem = RelatedItem.Load(subElement, item.Name);
RelatedItem newRequiredItem = RelatedItem.Load(subElement, returnEmptyRequirements, item.Name);
if (newRequiredItem == null) continue;
var prevRequiredItem = prevRequiredItems.ContainsKey(newRequiredItem.Type) ?
@@ -3,6 +3,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Extensions;
namespace Barotrauma.Items.Components
{
@@ -54,20 +55,44 @@ namespace Barotrauma.Items.Components
[Serialize(5, false, description: "How many inventory slots the inventory has per row.")]
public int SlotsPerRow { get; set; }
public List<RelatedItem> ContainableItems { get; private set; }
private HashSet<string> containableRestrictions = new HashSet<string>();
[Editable, Serialize("", true, description: "Define items (by identifiers or tags) that bots should place inside this container. If empty, no restrictions are applied.")]
public string ContainableRestrictions
{
get { return string.Join(",", containableRestrictions); }
set
{
StringFormatter.ParseCommaSeparatedStringToCollection(value, containableRestrictions);
}
}
public bool ShouldBeContained(string[] identifiersOrTags, out bool isRestrictionsDefined)
{
isRestrictionsDefined = containableRestrictions.Any();
if (!isRestrictionsDefined) { return true; }
return identifiersOrTags.Any(id => containableRestrictions.Any(r => r == id));
}
public bool ShouldBeContained(Item item, out bool isRestrictionsDefined)
{
isRestrictionsDefined = containableRestrictions.Any();
if (!isRestrictionsDefined) { return true; }
return containableRestrictions.Any(id => item.Prefab.Identifier == id || item.HasTag(id));
}
public List<RelatedItem> ContainableItems { get; private set; } = new List<RelatedItem>();
public ItemContainer(Item item, XElement element)
: base (item, element)
{
Inventory = new ItemInventory(item, this, capacity, SlotsPerRow);
ContainableItems = new List<RelatedItem>();
Inventory = new ItemInventory(item, this, capacity, SlotsPerRow);
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "containable":
RelatedItem containable = RelatedItem.Load(subElement, item.Name);
RelatedItem containable = RelatedItem.Load(subElement, returnEmpty: false, parentDebugName: item.Name);
if (containable == null)
{
DebugConsole.ThrowError("Error in item config \"" + item.ConfigFile + "\" - containable with no identifiers.");
@@ -34,6 +34,7 @@ namespace Barotrauma.Items.Components
public override void OnItemLoaded()
{
base.OnItemLoaded();
var containers = item.GetComponents<ItemContainer>().ToList();
if (containers.Count < 2)
{
@@ -59,7 +60,7 @@ namespace Barotrauma.Items.Components
return;
}
hasPower = voltage >= minVoltage;
hasPower = Voltage >= MinVoltage;
if (!hasPower) { return; }
var repairable = item.GetComponent<Repairable>();
@@ -70,10 +71,8 @@ namespace Barotrauma.Items.Components
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
if (powerConsumption == 0.0f) { voltage = 1.0f; }
progressTimer += deltaTime * voltage;
Voltage -= deltaTime * 10.0f;
if (powerConsumption <= 0.0f) { Voltage = 1.0f; }
progressTimer += deltaTime * Voltage;
var targetItem = inputContainer.Inventory.Items.LastOrDefault(i => i != null);
if (targetItem == null) { return; }
@@ -99,7 +98,7 @@ namespace Barotrauma.Items.Components
float condition = deconstructProduct.CopyCondition ?
percentageHealth * itemPrefab.Health :
itemPrefab.Health * deconstructProduct.OutCondition;
//container full, drop the items outside the deconstructor
if (emptySlots <= 0)
{
@@ -111,7 +110,7 @@ namespace Barotrauma.Items.Components
emptySlots--;
}
}
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
{
if (targetItem.Prefab.DeconstructItems.Any())
@@ -149,8 +148,6 @@ namespace Barotrauma.Items.Components
progressState = 0.0f;
}
}
voltage -= deltaTime * 10.0f;
}
private void PutItemsToLinkedContainer()
@@ -50,7 +50,7 @@ namespace Barotrauma.Items.Components
public float CurrentVolume
{
get { return Math.Abs((force / 100.0f) * (minVoltage <= 0.0f ? 1.0f : Math.Min(prevVoltage / minVoltage, 1.0f))); }
get { return Math.Abs((force / 100.0f) * (MinVoltage <= 0.0f ? 1.0f : Math.Min(prevVoltage / MinVoltage, 1.0f))); }
}
public Engine(Item item, XElement element)
@@ -83,15 +83,15 @@ namespace Barotrauma.Items.Components
//pumps consume more power when in a bad condition
currPowerConsumption *= MathHelper.Lerp(2.0f, 1.0f, item.Condition / item.MaxCondition);
if (powerConsumption == 0.0f) voltage = 1.0f;
if (powerConsumption == 0.0f) { Voltage = 1.0f; }
prevVoltage = voltage;
hasPower = voltage > minVoltage;
prevVoltage = Voltage;
hasPower = Voltage > MinVoltage;
Force = MathHelper.Lerp(force, (voltage < minVoltage) ? 0.0f : targetForce, 0.1f);
Force = MathHelper.Lerp(force, (Voltage < MinVoltage) ? 0.0f : targetForce, 0.1f);
if (Math.Abs(Force) > 1.0f)
{
Vector2 currForce = new Vector2((force / 10.0f) * maxForce * Math.Min(voltage / minVoltage, 1.0f), 0.0f);
Vector2 currForce = new Vector2((force / 10.0f) * maxForce * Math.Min(Voltage / MinVoltage, 1.0f), 0.0f);
//less effective when in a bad condition
currForce *= MathHelper.Lerp(0.5f, 2.0f, item.Condition / item.MaxCondition);
@@ -119,8 +119,6 @@ namespace Barotrauma.Items.Components
}
#endif
}
voltage -= deltaTime;
}
private void UpdatePropellerDamage(float deltaTime)
@@ -172,5 +170,16 @@ namespace Barotrauma.Items.Components
}
}
}
public override XElement Save(XElement parentElement)
{
Vector2 prevPropellerPos = PropellerPos;
//undo flipping before saving
if (item.FlippedX) { PropellerPos = new Vector2(-PropellerPos.X, PropellerPos.Y); }
if (item.FlippedY) { PropellerPos = new Vector2(PropellerPos.X, -PropellerPos.Y); }
XElement element = base.Save(parentElement);
PropellerPos = prevPropellerPos;
return element;
}
}
}
@@ -70,6 +70,7 @@ namespace Barotrauma.Items.Components
public override void OnItemLoaded()
{
base.OnItemLoaded();
var containers = item.GetComponents<ItemContainer>().ToList();
if (containers.Count < 2)
{
@@ -199,7 +200,7 @@ namespace Barotrauma.Items.Components
progressState = fabricatedItem == null ? 0.0f : (requiredTime - timeUntilReady) / requiredTime;
hasPower = voltage >= minVoltage;
hasPower = Voltage >= MinVoltage;
if (!hasPower) { return; }
var repairable = item.GetComponent<Repairable>();
@@ -210,10 +211,9 @@ namespace Barotrauma.Items.Components
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
if (powerConsumption <= 0) { voltage = 1.0f; }
if (powerConsumption <= 0) { Voltage = 1.0f; }
timeUntilReady -= deltaTime * voltage;
voltage -= deltaTime * 10.0f;
timeUntilReady -= deltaTime * Voltage;
if (timeUntilReady > 0.0f) { return; }
@@ -75,13 +75,11 @@ namespace Barotrauma.Items.Components
currPowerConsumption = powerConsumption;
currPowerConsumption *= MathHelper.Lerp(2.0f, 1.0f, item.Condition / item.MaxCondition);
hasPower = voltage > minVoltage;
hasPower = Voltage > MinVoltage;
if (hasPower)
{
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
}
voltage -= deltaTime;
}
public override bool Pick(Character picker)
@@ -46,12 +46,12 @@ namespace Barotrauma.Items.Components
if (powerConsumption <= 0.0f)
{
voltage = 1.0f;
Voltage = 1.0f;
}
if (item.CurrentHull == null) return;
if (voltage < minVoltage)
if (Voltage < MinVoltage)
{
powerDownTimer += deltaTime;
return;
@@ -61,7 +61,7 @@ namespace Barotrauma.Items.Components
powerDownTimer = 0.0f;
}
CurrFlow = Math.Min(voltage, 1.0f) * generatedAmount * 100.0f;
CurrFlow = Math.Min(Voltage, 1.0f) * generatedAmount * 100.0f;
//less effective when in bad condition
float conditionMult = item.Condition / item.MaxCondition;
@@ -71,8 +71,6 @@ namespace Barotrauma.Items.Components
CurrFlow *= conditionMult * conditionMult;
UpdateVents(CurrFlow);
voltage -= deltaTime;
}
public override void UpdateBroken(float deltaTime, Camera cam)
@@ -76,7 +76,7 @@ namespace Barotrauma.Items.Components
//pumps consume more power when in a bad condition
currPowerConsumption *= MathHelper.Lerp(2.0f, 1.0f, item.Condition / item.MaxCondition);
if (voltage < minVoltage) { return; }
if (Voltage < MinVoltage) { return; }
UpdateProjSpecific(deltaTime);
@@ -86,7 +86,7 @@ namespace Barotrauma.Items.Components
if (item.CurrentHull == null) { return; }
float powerFactor = currPowerConsumption <= 0.0f ? 1.0f : voltage;
float powerFactor = currPowerConsumption <= 0.0f ? 1.0f : Voltage;
currFlow = flowPercentage / 100.0f * maxFlow * powerFactor;
//less effective when in a bad condition
@@ -94,8 +94,6 @@ namespace Barotrauma.Items.Components
item.CurrentHull.WaterVolume += currFlow;
if (item.CurrentHull.WaterVolume > item.CurrentHull.Volume) { item.CurrentHull.Pressure += 0.5f; }
voltage -= deltaTime;
}
partial void UpdateProjSpecific(float deltaTime);
@@ -124,7 +122,7 @@ namespace Barotrauma.Items.Components
{
if (float.TryParse(signal, NumberStyles.Any, CultureInfo.InvariantCulture, out float tempTarget))
{
targetLevel = MathHelper.Clamp((tempTarget + 100.0f) / 2.0f, 0.0f, 100.0f);
targetLevel = MathHelper.Clamp(tempTarget + 50.0f, 0.0f, 100.0f);
controlLockTimer = 0.1f;
}
}
@@ -4,6 +4,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Extensions;
namespace Barotrauma.Items.Components
{
@@ -35,6 +36,7 @@ namespace Barotrauma.Items.Components
private float maxPowerOutput;
private Queue<float> loadQueue = new Queue<float>();
private float load;
private bool unsentChanges;
@@ -165,7 +167,10 @@ namespace Barotrauma.Items.Components
private float prevAvailableFuel;
public float AvailableFuel { get; set; }
private readonly string[] fuelTags = new string[1] { "reactorfuel" };
public Reactor(Item item, XElement element)
: base(item, element)
{
@@ -267,8 +272,7 @@ namespace Barotrauma.Items.Components
{
UpdateAutoTemp(2.0f, deltaTime);
}
load = 0.0f;
float currentLoad = 0.0f;
List<Connection> connections = item.Connections;
if (connections != null && connections.Count > 0)
{
@@ -284,13 +288,20 @@ namespace Barotrauma.Items.Components
//calculate how much external power there is in the grid
//(power coming from somewhere else than this reactor, e.g. batteries)
float externalPower = Math.Max(CurrPowerConsumption - pt.CurrPowerConsumption, 0);
float externalPower = Math.Max(CurrPowerConsumption - pt.CurrPowerConsumption, 0) * 0.95f;
//reduce the external power from the load to prevent overloading the grid
load = Math.Max(load, pt.PowerLoad - externalPower);
currentLoad = Math.Max(currentLoad, pt.PowerLoad - externalPower);
}
}
}
loadQueue.Enqueue(currentLoad);
while (loadQueue.Count() > 60.0f)
{
load = loadQueue.Average();
loadQueue.Dequeue();
}
if (fissionRate > 0.0f)
{
foreach (Item item in item.ContainedItems)
@@ -505,6 +516,19 @@ namespace Barotrauma.Items.Components
return picker != null;
}
private int itemIndex;
private List<Item> ignoredContainers = new List<Item>();
private bool FindSuitableContainer(Character character, Func<Item, float> priority, out Item suitableContainer)
{
suitableContainer = null;
if (character.FindItem(ref itemIndex, out Item targetContainer, ignoredItems: ignoredContainers, customPriorityFunction: priority))
{
suitableContainer = targetContainer;
return true;
}
return false;
}
public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return false; }
@@ -516,13 +540,56 @@ namespace Barotrauma.Items.Components
//characters with insufficient skill levels don't refuel the reactor
if (degreeOfSuccess > 0.2f)
{
//remove used-up fuel from the reactor
var containedItems = item.ContainedItems;
foreach (Item item in containedItems)
if (objective.SubObjectives.None())
{
if (item != null && item.Condition <= 0.0f)
var containedItems = item.ContainedItems;
foreach (Item fuelRod in containedItems)
{
item.Drop(character);
if (fuelRod != null && fuelRod.Condition <= 0.0f)
{
if (!FindSuitableContainer(character,
i =>
{
var container = i.GetComponent<ItemContainer>();
if (container == null) { return 0; }
if (container.Inventory.IsFull()) { return 0; }
if (container.ShouldBeContained(fuelRod, out bool isRestrictionsDefined))
{
if (isRestrictionsDefined)
{
return 3;
}
else
{
if (fuelRod.Prefab.IsContainerPreferred(container, out bool isPreferencesDefined))
{
return isPreferencesDefined ? 2 : 1;
}
else
{
return isPreferencesDefined ? 0 : 1;
}
}
}
else
{
return 0;
}
}, out Item targetContainer))
{
return false;
}
var decontainObjective = new AIObjectiveDecontainItem(character, fuelRod, item.GetComponent<ItemContainer>(), objective.objectiveManager, targetContainer?.GetComponent<ItemContainer>());
decontainObjective.Abandoned += () =>
{
itemIndex = 0;
if (targetContainer != null)
{
ignoredContainers.Add(targetContainer);
}
};
objective.AddSubObjectiveInQueue(decontainObjective);
}
}
}
@@ -535,31 +602,33 @@ namespace Barotrauma.Items.Components
//load more fuel if the current maximum output is only 50% of the current load
if (NeedMoreFuel(minimumOutputRatio: 0.5f))
{
var containFuelObjective = new AIObjectiveContainItem(character, new string[] { "fuelrod", "reactorfuel" }, item.GetComponent<ItemContainer>(), objective.objectiveManager)
{
targetItemCount = item.ContainedItems.Count(i => i != null && i.Prefab.Identifier == "fuelrod" || i.HasTag("reactorfuel")) + 1,
GetItemPriority = (Item fuelItem) =>
{
if (fuelItem.ParentInventory?.Owner is Item)
{
//don't take fuel from other reactors
if (((Item)fuelItem.ParentInventory.Owner).GetComponent<Reactor>() != null) return 0.0f;
}
return 1.0f;
}
};
objective.AddSubObjective(containFuelObjective);
character?.Speak(TextManager.Get("DialogReactorFuel"), null, 0.0f, "reactorfuel", 30.0f);
aiUpdateTimer = AIUpdateInterval;
if (objective.SubObjectives.None())
{
var containFuelObjective = new AIObjectiveContainItem(character, fuelTags, item.GetComponent<ItemContainer>(), objective.objectiveManager)
{
targetItemCount = item.ContainedItems.Count(i => i != null && fuelTags.Any(t => i.Prefab.Identifier == t || i.HasTag(t))) + 1,
GetItemPriority = (Item fuelItem) =>
{
if (fuelItem.ParentInventory?.Owner is Item)
{
//don't take fuel from other reactors
if (((Item)fuelItem.ParentInventory.Owner).GetComponent<Reactor>() != null) return 0.0f;
}
return 1.0f;
}
};
containFuelObjective.Abandoned += () => objective.Abandon = true;
objective.AddSubObjective(containFuelObjective);
character?.Speak(TextManager.Get("DialogReactorFuel"), null, 0.0f, "reactorfuel", 30.0f);
}
return false;
}
else if (TooMuchFuel())
{
foreach (Item item in item.ContainedItems)
{
if (item != null && item.HasTag("reactorfuel"))
if (item != null && fuelTags.Any(t => item.Prefab.Identifier == t || item.HasTag(t)))
{
if (!character.Inventory.TryPutItem(item, character, allowedSlots: item.AllowedSlots))
{
@@ -577,22 +646,28 @@ namespace Barotrauma.Items.Components
}
LastUser = lastAIUser = character;
bool prevAutoTemp = autoTemp;
bool prevShutDown = shutDown;
float prevFissionRate = targetFissionRate;
float prevTurbineOutput = targetTurbineOutput;
switch (objective.Option.ToLowerInvariant())
{
case "powerup":
shutDown = false;
//characters with insufficient skill levels simply set the autotemp on instead of trying to adjust the temperature manually
if (degreeOfSuccess < 0.5f)
if (objective.Override || !autoTemp)
{
if (!autoTemp) unsentChanges = true;
AutoTemp = true;
}
else
{
AutoTemp = false;
unsentChanges = true;
UpdateAutoTemp(MathHelper.Lerp(0.5f, 2.0f, degreeOfSuccess), 1.0f);
//characters with insufficient skill levels simply set the autotemp on instead of trying to adjust the temperature manually
if (degreeOfSuccess < 0.5f)
{
AutoTemp = true;
}
else
{
AutoTemp = false;
UpdateAutoTemp(MathHelper.Lerp(0.5f, 2.0f, degreeOfSuccess), 1.0f);
}
}
#if CLIENT
onOffSwitch.BarScroll = 0.0f;
@@ -604,11 +679,6 @@ namespace Barotrauma.Items.Components
#if CLIENT
onOffSwitch.BarScroll = 1.0f;
#endif
if (AutoTemp || !shutDown || targetFissionRate > 0.0f || targetTurbineOutput > 0.0f)
{
unsentChanges = true;
}
AutoTemp = false;
shutDown = true;
targetFissionRate = 0.0f;
@@ -616,6 +686,14 @@ namespace Barotrauma.Items.Components
break;
}
if (autoTemp != prevAutoTemp ||
prevShutDown != shutDown ||
Math.Abs(prevFissionRate - targetFissionRate) > 1.0f ||
Math.Abs(prevTurbineOutput - targetTurbineOutput) > 1.0f)
{
unsentChanges = true;
}
aiUpdateTimer = AIUpdateInterval;
return false;
@@ -162,7 +162,7 @@ namespace Barotrauma.Items.Components
if (currentMode == Mode.Active)
{
if ((voltage >= minVoltage || powerConsumption <= 0.0f) &&
if ((Voltage >= MinVoltage) &&
(!UseTransducers || connectedTransducers.Count > 0))
{
if (currentPingIndex != -1)
@@ -201,7 +201,6 @@ namespace Barotrauma.Items.Components
{
item.AiTarget.SectorDegrees = 360.0f;
}
currentPingIndex = -1;
aiPingCheckPending = false;
}
}
@@ -235,6 +234,7 @@ namespace Barotrauma.Items.Components
protected override void RemoveComponentSpecific()
{
base.RemoveComponentSpecific();
sonarBlip?.Remove();
pingCircle?.Remove();
directionalPingCircle?.Remove();
@@ -247,6 +247,7 @@ namespace Barotrauma.Items.Components
{
if (currentMode == Mode.Passive || !aiPingCheckPending) return false;
// TODO: Don't create new collections here
Dictionary<string, List<Character>> targetGroups = new Dictionary<string, List<Character>>();
foreach (Character c in Character.CharacterList)
@@ -20,7 +20,7 @@ namespace Barotrauma.Items.Components
{
UpdateOnActiveEffects(deltaTime);
if (voltage >= minVoltage || PowerConsumption <= 0.0f)
if (Voltage >= MinVoltage)
{
sendSignalTimer += deltaTime;
if (sendSignalTimer > SendSignalInterval)
@@ -29,8 +29,6 @@ namespace Barotrauma.Items.Components
sendSignalTimer = SendSignalInterval;
}
}
voltage = 0.0f;
}
}
}
@@ -15,6 +15,13 @@ namespace Barotrauma.Items.Components
private const float AutopilotRayCastInterval = 0.5f;
private const float RecalculatePathInterval = 5.0f;
private const float AutopilotMinDistToPathNode = 30.0f;
private const float AutoPilotSteeringLerp = 0.1f;
private const float AutoPilotMaxSpeed = 0.5f;
private const float AIPilotMaxSpeed = 1.0f;
private Vector2 currVelocity;
private Vector2 targetVelocity;
@@ -162,6 +169,7 @@ namespace Barotrauma.Items.Components
public override void OnItemLoaded()
{
base.OnItemLoaded();
sonar = item.GetComponent<Sonar>();
}
@@ -209,7 +217,7 @@ namespace Barotrauma.Items.Components
currPowerConsumption = powerConsumption;
if (voltage < minVoltage && currPowerConsumption > 0.0f) { return; }
if (Voltage < MinVoltage) { return; }
if (user != null && user.Removed)
{
@@ -221,6 +229,12 @@ namespace Barotrauma.Items.Components
if (autoPilot)
{
UpdateAutoPilot(deltaTime);
float userSkill = 0.0f;
if (user != null && (user.SelectedConstruction == item || item.linkedTo.Contains(user.SelectedConstruction)))
{
userSkill = user.GetSkillLevel("helm") / 100.0f;
}
targetVelocity = targetVelocity.ClampLength(MathHelper.Lerp(AutoPilotMaxSpeed, AIPilotMaxSpeed, userSkill) * 100.0f);
}
else
{
@@ -253,8 +267,6 @@ namespace Barotrauma.Items.Components
targetLevel += (neutralBallastLevel - 0.5f) * 100.0f;
item.SendSignal(0, targetLevel.ToString(CultureInfo.InvariantCulture), "velocity_y_out", null);
voltage -= deltaTime;
}
private void UpdateAutoPilot(float deltaTime)
@@ -262,7 +274,8 @@ namespace Barotrauma.Items.Components
if (controlledSub == null) return;
if (posToMaintain != null)
{
SteerTowardsPosition((Vector2)posToMaintain);
Vector2 steeringVel = GetSteeringVelocity((Vector2)posToMaintain);
TargetVelocity = Vector2.Lerp(TargetVelocity, steeringVel, AutoPilotSteeringLerp);
return;
}
@@ -284,7 +297,7 @@ namespace Barotrauma.Items.Components
//if the node is close enough, check if it's visible
float lengthSqr = diff.LengthSquared();
if (lengthSqr > 0.001f && lengthSqr < 500.0f)
if (lengthSqr > 0.001f && lengthSqr < AutopilotMinDistToPathNode * AutopilotMinDistToPathNode)
{
diff = Vector2.Normalize(diff);
@@ -298,11 +311,11 @@ namespace Barotrauma.Items.Components
Vector2 cornerPos =
new Vector2(controlledSub.Borders.Width * x, controlledSub.Borders.Height * y) / 2.0f;
cornerPos = ConvertUnits.ToSimUnits(cornerPos * 1.2f + controlledSub.WorldPosition);
cornerPos = ConvertUnits.ToSimUnits(cornerPos * 1.1f + controlledSub.WorldPosition);
float dist = Vector2.Distance(cornerPos, steeringPath.NextNode.SimPosition);
if (Submarine.PickBody(cornerPos, cornerPos + diff * dist, null, Physics.CollisionLevel) == null) continue;
if (Submarine.PickBody(cornerPos, cornerPos + diff * dist, null, Physics.CollisionLevel) == null) { continue; }
nextVisible = false;
x = 2;
@@ -313,19 +326,18 @@ namespace Barotrauma.Items.Components
if (nextVisible) steeringPath.SkipToNextNode();
}
autopilotRayCastTimer = AutopilotRayCastInterval;
}
Vector2 newVelocity = Vector2.Zero;
if (steeringPath.CurrentNode != null)
{
SteerTowardsPosition(steeringPath.CurrentNode.WorldPosition);
newVelocity = GetSteeringVelocity(steeringPath.CurrentNode.WorldPosition);
}
Vector2 avoidDist = new Vector2(
Math.Max(1000.0f * Math.Abs(controlledSub.Velocity.X), controlledSub.Borders.Width * 1.5f),
Math.Max(1000.0f * Math.Abs(controlledSub.Velocity.Y), controlledSub.Borders.Height * 1.5f));
Math.Max(1000.0f * Math.Abs(controlledSub.Velocity.X), controlledSub.Borders.Width * 0.75f),
Math.Max(1000.0f * Math.Abs(controlledSub.Velocity.Y), controlledSub.Borders.Height * 0.75f));
float avoidRadius = avoidDist.Length();
@@ -356,22 +368,22 @@ namespace Barotrauma.Items.Components
0.0f : Vector2.Dot(controlledSub.Velocity, -normalizedDiff);
//not heading towards the wall -> ignore
if (dot < 0.5)
if (dot < 1.0)
{
debugDrawObstacles.Add(new ObstacleDebugInfo(edge, intersection, dot, Vector2.Zero, cell.Translation));
continue;
}
Vector2 change = (normalizedDiff * Math.Max((avoidRadius - diff.Length()), 0.0f)) / avoidRadius;
newAvoidStrength += change * dot;
debugDrawObstacles.Add(new ObstacleDebugInfo(edge, intersection, dot, change * dot, cell.Translation));
Vector2 change = (normalizedDiff * Math.Max((avoidRadius - diff.Length()), 0.0f)) / avoidRadius;
if (change.LengthSquared() < 0.001f) { continue; }
newAvoidStrength += change * (dot - 1.0f);
debugDrawObstacles.Add(new ObstacleDebugInfo(edge, intersection, dot - 1.0f, change * (dot - 1.0f), cell.Translation));
}
}
}
avoidStrength = Vector2.Lerp(avoidStrength, newAvoidStrength, deltaTime * 10.0f);
targetVelocity += avoidStrength * 100.0f;
TargetVelocity = Vector2.Lerp(TargetVelocity, newVelocity + avoidStrength * 100.0f, AutoPilotSteeringLerp);
//steer away from other subs
foreach (Submarine sub in Submarine.Loaded)
@@ -447,21 +459,21 @@ namespace Barotrauma.Items.Components
UpdatePath();
}
}
private void SteerTowardsPosition(Vector2 worldPosition)
private Vector2 GetSteeringVelocity(Vector2 worldPosition)
{
float prediction = 10.0f;
float prediction = 2.0f;
Vector2 futurePosition = ConvertUnits.ToDisplayUnits(controlledSub.Velocity) * prediction;
Vector2 targetSpeed = ((worldPosition - controlledSub.WorldPosition) - futurePosition);
if (targetSpeed.Length() > 500.0f)
if (targetSpeed.LengthSquared() > 500.0f * 500.0f)
{
targetSpeed = Vector2.Normalize(targetSpeed);
TargetVelocity = targetSpeed * 100.0f;
return Vector2.Normalize(targetSpeed) * 100.0f;
}
else
{
TargetVelocity = targetSpeed / 5.0f;
return targetSpeed / 5.0f;
}
}
@@ -471,43 +483,53 @@ namespace Barotrauma.Items.Components
{
character.Speak(TextManager.Get("DialogSteeringTaken"), null, 0.0f, "steeringtaken", 10.0f);
}
user = character;
if (!AutoPilot)
{
unsentChanges = true;
AutoPilot = true;
}
switch (objective.Option.ToLowerInvariant())
{
case "maintainposition":
if (!posToMaintain.HasValue)
if (objective.Override)
{
unsentChanges = true;
posToMaintain = controlledSub != null ?
controlledSub.WorldPosition :
item.Submarine == null ? item.WorldPosition : item.Submarine.WorldPosition;
if (!MaintainPos)
{
unsentChanges = true;
MaintainPos = true;
}
if (!posToMaintain.HasValue)
{
unsentChanges = true;
posToMaintain = controlledSub != null ?
controlledSub.WorldPosition :
item.Submarine == null ? item.WorldPosition : item.Submarine.WorldPosition;
}
}
if (!AutoPilot || !MaintainPos) unsentChanges = true;
AutoPilot = true;
MaintainPos = true;
break;
case "navigateback":
if (!AutoPilot || MaintainPos || LevelEndSelected || !LevelStartSelected)
if (objective.Override)
{
unsentChanges = true;
if (MaintainPos || LevelEndSelected || !LevelStartSelected)
{
unsentChanges = true;
}
SetDestinationLevelStart();
}
SetDestinationLevelStart();
break;
case "navigatetodestination":
if (!AutoPilot || MaintainPos || !LevelEndSelected || LevelStartSelected)
if (objective.Override)
{
unsentChanges = true;
if (MaintainPos || !LevelEndSelected || LevelStartSelected)
{
unsentChanges = true;
}
SetDestinationLevelEnd();
}
SetDestinationLevelEnd();
break;
}
sonar?.AIOperate(deltaTime, character, objective);
return false;
}
@@ -14,7 +14,7 @@ namespace Barotrauma.Items.Components
private float charge;
private float rechargeVoltage;
//private float rechargeVoltage;
//how fast the battery can be recharged
private float maxRechargeSpeed;
@@ -28,10 +28,7 @@ namespace Barotrauma.Items.Components
protected Vector2 indicatorPosition, indicatorSize;
protected bool isHorizontal;
//a list of powered devices connected directly to this item
private readonly List<Pair<Powered, Connection>> directlyConnected = new List<Pair<Powered, Connection>>(10);
public float CurrPowerOutput
{
get;
@@ -107,12 +104,18 @@ namespace Barotrauma.Items.Components
if (!MathUtils.IsValid(value)) return;
rechargeSpeed = MathHelper.Clamp(value, 0.0f, maxRechargeSpeed);
rechargeSpeed = MathUtils.RoundTowardsClosest(rechargeSpeed, Math.Max(maxRechargeSpeed * 0.1f, 1.0f));
if (isRunning)
{
HasBeenTuned = true;
}
}
}
public float RechargeRatio => RechargeSpeed / MaxRechargeSpeed;
public const float aiRechargeTargetRatio = 0.5f;
private bool isRunning;
public bool HasBeenTuned { get; private set; }
public PowerContainer(Item item, XElement element)
: base(item, element)
@@ -131,14 +134,13 @@ namespace Barotrauma.Items.Components
public override void Update(float deltaTime, Camera cam)
{
isRunning = true;
float chargeRatio = charge / capacity;
float gridPower = 0.0f;
float gridLoad = 0.0f;
directlyConnected.Clear();
foreach (Connection c in item.Connections)
{
if (c.Name == "power_in") continue;
if (!c.IsPower || !c.IsOutput) { continue; }
foreach (Connection c2 in c.Recipients)
{
if (c2.Item.Condition <= 0.0f) { continue; }
@@ -149,15 +151,13 @@ namespace Barotrauma.Items.Components
foreach (Powered powered in c2.Item.GetComponents<Powered>())
{
if (!powered.IsActive) continue;
directlyConnected.Add(new Pair<Powered, Connection>(powered, c2));
gridLoad += powered.CurrPowerConsumption;
}
continue;
}
if (!pt.IsActive || !pt.CanTransfer) { continue; }
gridLoad += pt.PowerLoad;
gridPower -= pt.CurrPowerConsumption;
gridLoad += pt.PowerLoad;
}
}
@@ -168,66 +168,51 @@ namespace Barotrauma.Items.Components
if (charge >= capacity)
{
rechargeVoltage = 0.0f;
//rechargeVoltage = 0.0f;
charge = capacity;
CurrPowerConsumption = 0.0f;
}
else
{
currPowerConsumption = MathHelper.Lerp(currPowerConsumption, rechargeSpeed, 0.05f);
Charge += currPowerConsumption * rechargeVoltage / 3600.0f;
Charge += currPowerConsumption * Voltage / 3600.0f;
}
//provide power to the grid
if (gridLoad > 0.0f)
if (charge <= 0.0f)
{
if (charge <= 0.0f)
{
CurrPowerOutput = 0.0f;
charge = 0.0f;
return;
}
if (gridPower < gridLoad)
{
//output starts dropping when the charge is less than 10%
float maxOutputRatio = 1.0f;
if (chargeRatio < 0.1f)
{
maxOutputRatio = Math.Max(chargeRatio * 10.0f, 0.0f);
}
CurrPowerOutput = MathHelper.Lerp(
CurrPowerOutput,
Math.Min(MaxOutPut * maxOutputRatio, gridLoad),
deltaTime * 10.0f);
}
else
{
CurrPowerOutput = MathHelper.Lerp(CurrPowerOutput, 0.0f, deltaTime * 10.0f);
}
Charge -= CurrPowerOutput / 3600.0f;
CurrPowerOutput = 0.0f;
charge = 0.0f;
return;
}
item.SendSignal(0, ((int)Charge).ToString(), "charge", null);
item.SendSignal(0, ((int)((Charge / capacity) * 100)).ToString(), "charge_%", null);
item.SendSignal(0, ((int)((RechargeSpeed / maxRechargeSpeed) * 100)).ToString(), "charge_rate", null);
foreach (Pair<Powered, Connection> connected in directlyConnected)
//output starts dropping when the charge is less than 10%
float maxOutputRatio = 1.0f;
if (chargeRatio < 0.1f)
{
connected.First.ReceiveSignal(0, "", connected.Second, source: item, sender: null,
power: gridLoad <= 0.0f ? 1.0f : CurrPowerOutput / gridLoad);
maxOutputRatio = Math.Max(chargeRatio * 10.0f, 0.0f);
}
rechargeVoltage = 0.0f;
CurrPowerOutput += (gridLoad - gridPower) * deltaTime;
float maxOutput = Math.Min(MaxOutPut * maxOutputRatio, gridLoad);
CurrPowerOutput = MathHelper.Clamp(CurrPowerOutput, 0.0f, maxOutput);
Charge -= CurrPowerOutput / 3600.0f;
item.SendSignal(0, ((int)Math.Round(Charge)).ToString(), "charge", null);
item.SendSignal(0, ((int)Math.Round((Charge / capacity) * 100)).ToString(), "charge_%", null);
item.SendSignal(0, ((int)Math.Round((RechargeSpeed / maxRechargeSpeed) * 100)).ToString(), "charge_rate", null);
}
public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
{
#if CLIENT
if (GameMain.Client != null) return false;
#endif
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return false; }
if (objective.Override)
{
HasBeenTuned = false;
}
if (HasBeenTuned) { return true; }
if (string.IsNullOrEmpty(objective.Option) || objective.Option.ToLowerInvariant() == "charge")
{
@@ -274,6 +259,8 @@ namespace Barotrauma.Items.Components
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power, float signalStrength = 1.0f)
{
if (connection.IsPower) { return; }
if (connection.Name == "set_rate")
{
if (float.TryParse(signal, NumberStyles.Any, CultureInfo.InvariantCulture, out float tempSpeed))
@@ -290,12 +277,6 @@ namespace Barotrauma.Items.Components
#endif
}
}
if (!connection.IsPower) { return; }
if (connection.Name == "power_in")
{
rechargeVoltage = Math.Min(power, 1.0f);
}
}
}
}
@@ -8,37 +8,21 @@ namespace Barotrauma.Items.Components
{
partial class PowerTransfer : Powered
{
private static float fullPower;
private static float fullLoad;
public List<Connection> PowerConnections { get; private set; }
private int updateCount;
//affects how fast changes in power/load are carried over the grid
static float inertia = 5.0f;
private static HashSet<Powered> connectedList = new HashSet<Powered>();
private List<Connection> powerConnections;
public List<Connection> PowerConnections
{
get
{
return powerConnections;
}
}
private Dictionary<Connection, bool> connectionDirty = new Dictionary<Connection, bool>();
private readonly Dictionary<Connection, bool> connectionDirty = new Dictionary<Connection, bool>();
//a list of connections a given connection is connected to, either directly or via other power transfer components
private Dictionary<Connection, HashSet<Connection>> connectedRecipients = new Dictionary<Connection, HashSet<Connection>>();
private readonly Dictionary<Connection, HashSet<Connection>> connectedRecipients = new Dictionary<Connection, HashSet<Connection>>();
private float powerLoad;
protected float powerLoad;
private bool isBroken;
protected bool isBroken;
public float PowerLoad
{
get { return powerLoad; }
set { powerLoad = value; }
}
[Editable, Serialize(true, true, description: "Can the item be damaged if too much power is supplied to the power grid.")]
@@ -145,97 +129,43 @@ namespace Barotrauma.Items.Components
SetAllConnectionsDirty();
isBroken = false;
}
if (updateCount > 0)
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
//if the item can't be fixed, don't allow it to break
if (!item.Repairables.Any() || !CanBeOverloaded) { return; }
float maxOverVoltage = Math.Max(OverloadVoltage, 1.0f);
Overload = -currPowerConsumption > Math.Max(powerLoad, 200.0f) * maxOverVoltage;
if (Overload && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer))
{
//this junction box has already been updated this frame
updateCount--;
return;
}
//damage the item if voltage is too high (except if running as a client)
float prevCondition = item.Condition;
item.Condition -= deltaTime * 10.0f;
Overload = false;
//reset and recalculate the power generated/consumed
//by the constructions connected to the grid
fullPower = 0.0f;
fullLoad = 0.0f;
connectedList.Clear();
updateCount = 0;
CheckJunctions(deltaTime);
foreach (Powered p in connectedList)
{
PowerTransfer pt = p as PowerTransfer;
if (pt == null || pt.updateCount == 0) { continue; }
if (pt is RelayComponent != this is RelayComponent) { continue; }
pt.Overload = false;
pt.powerLoad += (fullLoad - pt.powerLoad) / inertia;
pt.currPowerConsumption += (-fullPower - pt.currPowerConsumption) / inertia;
float voltage = fullPower / Math.Max(fullLoad, 1.0f);
if (this is RelayComponent)
{
pt.currPowerConsumption = Math.Max(-fullLoad, pt.currPowerConsumption);
voltage = Math.Min(voltage, 1.0f);
}
pt.Item.SendSignal(0, "", "power", null, voltage);
pt.Item.SendSignal(0, "", "power_out", null, voltage);
//items in a bad condition are more sensitive to overvoltage
float maxOverVoltage = MathHelper.Lerp(OverloadVoltage * 0.75f, OverloadVoltage, pt.item.Condition / pt.item.MaxCondition);
maxOverVoltage = Math.Max(OverloadVoltage, 1.0f);
//if the item can't be fixed, don't allow it to break
if (!pt.item.Repairables.Any() || !pt.CanBeOverloaded) { continue; }
//relays don't blow up if the power is higher than load, only if the output is high enough
//(i.e. enough power passing through the relay)
if (pt is RelayComponent) { continue; }
if (-pt.currPowerConsumption < Math.Max(pt.powerLoad, 200.0f) * maxOverVoltage) { continue; }
pt.Overload = true;
#if CLIENT
//damage the item if voltage is too high
//(except if running as a client)
if (GameMain.Client != null) { continue; }
#endif
float prevCondition = pt.item.Condition;
pt.item.Condition -= deltaTime * 10.0f;
if (pt.item.Condition <= 0.0f && prevCondition > 0.0f)
if (item.Condition <= 0.0f && prevCondition > 0.0f)
{
#if CLIENT
SoundPlayer.PlaySound("zap", item.WorldPosition, hullGuess: pt.item.CurrentHull);
SoundPlayer.PlaySound("zap", item.WorldPosition, hullGuess: item.CurrentHull);
Vector2 baseVel = Rand.Vector(300.0f);
for (int i = 0; i < 10; i++)
{
var particle = GameMain.ParticleManager.CreateParticle("spark", pt.item.WorldPosition,
baseVel + Rand.Vector(100.0f), 0.0f, pt.item.CurrentHull);
var particle = GameMain.ParticleManager.CreateParticle("spark", item.WorldPosition,
baseVel + Rand.Vector(100.0f), 0.0f, item.CurrentHull);
if (particle != null) particle.Size *= Rand.Range(0.5f, 1.0f);
}
#endif
float currentIntensity = GameMain.GameSession?.EventManager != null ?
float currentIntensity = GameMain.GameSession?.EventManager != null ?
GameMain.GameSession.EventManager.CurrentIntensity : 0.5f;
//higher probability for fires if the current intensity is low
if (pt.FireProbability > 0.0f &&
Rand.Range(0.0f, 1.0f) < MathHelper.Lerp(pt.FireProbability, pt.FireProbability * 0.1f, currentIntensity))
if (FireProbability > 0.0f &&
Rand.Range(0.0f, 1.0f) < MathHelper.Lerp(FireProbability, FireProbability * 0.1f, currentIntensity))
{
new FireSource(pt.item.WorldPosition);
new FireSource(item.WorldPosition);
}
}
}
updateCount = 0;
}
public override bool Pick(Character picker)
@@ -243,7 +173,7 @@ namespace Barotrauma.Items.Components
return picker != null;
}
private void RefreshConnections()
protected void RefreshConnections()
{
var connections = item.Connections;
foreach (Connection c in connections)
@@ -317,102 +247,6 @@ namespace Barotrauma.Items.Components
}
}
//a recursive function that goes through all the junctions and adds up
//all the generated/consumed power of the constructions connected to the grid
private void CheckJunctions(float deltaTime, bool increaseUpdateCount = true, float clampPower = float.MaxValue, float clampLoad = float.MaxValue)
{
if (increaseUpdateCount)
{
updateCount = 1;
}
connectedList.Add(this);
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
//float maxPower = this is RelayComponent relayComponent ? relayComponent.MaxPower : float.PositiveInfinity;
RelayComponent thisRelayComponent = this as RelayComponent;
if (thisRelayComponent != null)
{
clampPower = Math.Min(Math.Min(clampPower, thisRelayComponent.MaxPower), powerLoad);
clampLoad = Math.Min(clampLoad, thisRelayComponent.MaxPower);
}
foreach (Connection c in PowerConnections)
{
var recipients = c.Recipients;
foreach (Connection recipient in recipients)
{
if (recipient?.Item == null || !recipient.IsPower) { continue; }
Item it = recipient.Item;
if (it.Condition <= 0.0f) { continue; }
foreach (ItemComponent ic in it.Components)
{
if (!(ic is Powered powered) || !powered.IsActive) { continue; }
if (connectedList.Contains(powered)) { continue; }
if (powered is PowerTransfer powerTransfer)
{
RelayComponent otherRelayComponent = powerTransfer as RelayComponent;
if ((thisRelayComponent == null) == (otherRelayComponent == null))
{
if (!powerTransfer.CanTransfer) { continue; }
powerTransfer.CheckJunctions(deltaTime, increaseUpdateCount, clampPower, clampLoad);
}
else
{
if (!powerTransfer.CanTransfer) continue;
float maxPowerIn = (thisRelayComponent != null && c.IsOutput) ? 0.0f : clampPower;
float maxPowerOut = (thisRelayComponent != null && !c.IsOutput) ? 0.0f : clampLoad;
if (maxPowerIn > 0.0f || maxPowerOut > 0.0f)
{
powerTransfer.CheckJunctions(deltaTime, false, maxPowerIn, maxPowerOut);
}
}
continue;
}
float addLoad = 0.0f;
float addPower = 0.0f;
if (powered is PowerContainer powerContainer)
{
if (recipient.Name == "power_in")
{
addLoad = powerContainer.CurrPowerConsumption;
}
else
{
addPower = powerContainer.CurrPowerOutput;
}
}
else
{
connectedList.Add(powered);
//positive power consumption = the construction requires power -> increase load
if (powered.CurrPowerConsumption > 0.0f)
{
addLoad = powered.CurrPowerConsumption;
}
else if (powered.CurrPowerConsumption < 0.0f)
//negative power consumption = the construction is a
//generator/battery or another junction box
{
addPower -= powered.CurrPowerConsumption;
}
}
if (addPower + fullPower > clampPower) { addPower -= (addPower + fullPower) - clampPower; };
if (addPower > 0) { fullPower += addPower; }
if (addLoad + fullLoad > clampLoad) { addLoad -= (addLoad + fullLoad) - clampLoad; };
if (addLoad > 0) { fullLoad += addLoad; }
}
}
}
}
public void SetAllConnectionsDirty()
{
if (item.Connections == null) return;
@@ -431,8 +265,9 @@ namespace Barotrauma.Items.Components
public override void OnItemLoaded()
{
base.OnItemLoaded();
var connections = Item.Connections;
powerConnections = connections == null ? new List<Connection>() : connections.FindAll(c => c.IsPower);
PowerConnections = connections == null ? new List<Connection>() : connections.FindAll(c => c.IsPower);
if (connections == null)
{
IsActive = false;
@@ -440,33 +275,45 @@ namespace Barotrauma.Items.Components
}
SetAllConnectionsDirty();
}
public override void ReceivePowerProbeSignal(Connection connection, Item source, float power)
{
//we've already received this signal
if (lastPowerProbeRecipients.Contains(this)) { return; }
lastPowerProbeRecipients.Add(this);
if (power < 0.0f)
{
powerLoad -= power;
}
else
{
currPowerConsumption -= power;
}
powerOut?.SendPowerProbeSignal(source, power);
}
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power, float signalStrength = 1.0f)
{
if (connection.IsPower) return;
base.ReceiveSignal(stepsTaken, signal, connection, source, sender, power);
if (!connectedRecipients.ContainsKey(connection)) return;
if (item.Condition <= 0.0f || connection.IsPower) { return; }
if (!connectedRecipients.ContainsKey(connection)) { return; }
if (connection.Name.Length > 5 && connection.Name.Substring(0, 6) == "signal")
{
foreach (Connection recipient in connectedRecipients[connection])
{
if (recipient.Item == item || recipient.Item == source) continue;
if (recipient.Item == item || recipient.Item == source) { continue; }
foreach (ItemComponent ic in recipient.Item.Components)
{
//powertransfer components don't need to receive the signal in the pass-through signal connections
//because we relay it straight to the connected items without going through the whole chain of junction boxes
if (ic is PowerTransfer && connection.Name.Contains("signal")) continue;
if (ic is PowerTransfer && connection.Name.Contains("signal")) { continue; }
ic.ReceiveSignal(stepsTaken, signal, recipient, source, sender, 0.0f, signalStrength);
}
bool broken = recipient.Item.Condition <= 0.0f;
foreach (StatusEffect effect in recipient.Effects)
{
if (broken && effect.type != ActionType.OnBroken) continue;
recipient.Item.ApplyStatusEffect(effect, ActionType.OnUse, 1.0f, null, null, false, false);
}
}
@@ -1,6 +1,7 @@
using System;
using System.Xml.Linq;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
#if CLIENT
using Barotrauma.Sounds;
#endif
@@ -9,25 +10,47 @@ namespace Barotrauma.Items.Components
{
partial class Powered : ItemComponent
{
//the amount of power CURRENTLY consumed by the item
//negative values mean that the item is providing power to connected items
private static float updateTimer;
protected static float UpdateInterval = 0.2f;
/// <summary>
/// List of all powered ItemComponents
/// </summary>
private static readonly List<Powered> poweredList = new List<Powered>();
/// <summary>
/// Items that have already received the "probe signal" that's used to distribute power and load across the grid
/// </summary>
protected static HashSet<PowerTransfer> lastPowerProbeRecipients = new HashSet<PowerTransfer>();
/// <summary>
/// The amount of power currently consumed by the item. Negative values mean that the item is providing power to connected items
/// </summary>
protected float currPowerConsumption;
//current voltage of the item (load / power)
protected float voltage;
/// <summary>
/// Current voltage of the item (load / power)
/// </summary>
private float voltage;
//the minimum voltage required for the item to work
protected float minVoltage;
/// <summary>
/// The minimum voltage required for the item to work
/// </summary>
private float minVoltage;
//the maximum amount of power the item can draw from connected items
/// <summary>
/// The maximum amount of power the item can draw from connected items
/// </summary>
protected float powerConsumption;
protected Connection powerIn, powerOut;
[Editable, Serialize(0.5f, true, description: "The minimum voltage required for the device to function. " +
"The voltage is calculated as power / powerconsumption, meaning that a device " +
"with a power consumption of 1000 kW would need at least 500 kW of power to work if the minimum voltage is set to 0.5.")]
public float MinVoltage
{
get { return minVoltage; }
get { return powerConsumption <= 0.0f ? 0.0f : minVoltage; }
set { minVoltage = value; }
}
@@ -76,34 +99,32 @@ namespace Barotrauma.Items.Components
public Powered(Item item, XElement element)
: base(item, element)
{
poweredList.Add(this);
InitProjectSpecific(element);
}
partial void InitProjectSpecific(XElement element);
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0, float signalStrength = 1.0f)
{
if (currPowerConsumption == 0.0f) voltage = 0.0f;
if (connection.IsPower) voltage = Math.Max(0.0f, power);
}
protected void UpdateOnActiveEffects(float deltaTime)
{
if (currPowerConsumption == 0.0f)
if (currPowerConsumption <= 0.0f)
{
//if the item consumes no power, ignore the voltage requirement and
//apply OnActive statuseffects as long as this component is active
if (powerConsumption == 0.0f)
if (powerConsumption <= 0.0f)
{
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
}
return;
}
#if CLIENT
if (voltage > minVoltage)
{
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
}
#if CLIENT
if (voltage > minVoltage)
{
if (!powerOnSoundPlayed && powerOnSound != null)
{
SoundPlayer.PlaySound(powerOnSound.Sound, item.WorldPosition, powerOnSound.Volume, powerOnSound.Range, item.CurrentHull);
@@ -114,21 +135,160 @@ namespace Barotrauma.Items.Components
{
powerOnSoundPlayed = false;
}
#else
if (voltage > minVoltage)
{
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
}
#endif
}
public override void Update(float deltaTime, Camera cam)
{
UpdateOnActiveEffects(deltaTime);
voltage = 0.0f;
}
public override void OnItemLoaded()
{
if (item.Connections == null) { return; }
foreach (Connection c in item.Connections)
{
if (!c.IsPower) { continue; }
if (this is PowerTransfer pt)
{
if (c.Name == "power_in")
{
powerIn = c;
}
else if (c.Name == "power_out")
{
powerOut = c;
}
else if (c.Name == "power")
{
powerIn = powerOut = c;
}
}
else
{
if (c.IsOutput)
{
if (c.Name == "power_in")
{
#if DEBUG
DebugConsole.ThrowError($"Item \"{item.Name}\" has a power output connection called power_in. If the item is supposed to receive power through the connection, change it to an input connection.");
#else
DebugConsole.NewMessage($"Item \"{item.Name}\" has a power output connection called power_in. If the item is supposed to receive power through the connection, change it to an input connection.", Color.Orange);
#endif
}
powerOut = c;
}
else
{
if (c.Name == "power_out")
{
#if DEBUG
DebugConsole.ThrowError($"Item \"{item.Name}\" has a power input connection called power_out. If the item is supposed to output power through the connection, change it to an output connection.");
#else
DebugConsole.NewMessage($"Item \"{item.Name}\" has a power input connection called power_out. If the item is supposed to output power through the connection, change it to an output connection.", Color.Orange);
#endif
}
powerIn = c;
}
}
}
}
public virtual void ReceivePowerProbeSignal(Connection connection, Item source, float power) { }
public static void UpdatePower(float deltaTime)
{
if (updateTimer > 0.0f)
{
updateTimer -= deltaTime;
return;
}
updateTimer = UpdateInterval;
//reset power first
foreach (Powered powered in poweredList)
{
if (powered is PowerTransfer pt)
{
powered.CurrPowerConsumption = 0.0f;
pt.PowerLoad = 0.0f;
if (pt is RelayComponent relay)
{
relay.DisplayLoad = 0.0f;
}
}
//only reset voltage if the item has a power connector
//(other items, such as handheld devices, get power through other means and shouldn't be updated here)
if (powered.powerIn != null || powered.powerOut != null) { powered.voltage = 0.0f; }
}
//go through all the devices that are consuming/providing power
//and send out a "probe signal" which the PowerTransfer components use to add up the grid power/load
foreach (Powered powered in poweredList)
{
if (powered is PowerTransfer) { continue; }
if (powered.currPowerConsumption > 0.0f)
{
//consuming power
lastPowerProbeRecipients.Clear();
powered.powerIn?.SendPowerProbeSignal(powered.item, -powered.currPowerConsumption);
}
}
foreach (Powered powered in poweredList)
{
if (powered is PowerTransfer) { continue; }
else if (powered.currPowerConsumption < 0.0f)
{
//providing power
lastPowerProbeRecipients.Clear();
powered.powerOut?.SendPowerProbeSignal(powered.item, -powered.currPowerConsumption);
}
if (powered is PowerContainer pc)
{
if (pc.CurrPowerOutput <= 0.0f) { continue; }
//providing power
lastPowerProbeRecipients.Clear();
powered.powerOut?.SendPowerProbeSignal(powered.item, pc.CurrPowerOutput);
}
}
//go through powered items and calculate their current voltage
foreach (Powered powered in poweredList)
{
if (powered is PowerTransfer pt1 || (pt1 = powered.Item.GetComponent<PowerTransfer>()) != null)
{
powered.voltage = -pt1.CurrPowerConsumption / Math.Max(pt1.PowerLoad, 1.0f);
continue;
}
if (powered.powerConsumption <= 0.0f && !(powered is PowerContainer))
{
powered.voltage = 1.0f;
continue;
}
if (powered.powerIn == null) { continue; }
foreach (Connection powerSource in powered.powerIn.Recipients)
{
if (!powerSource.IsPower || !powerSource.IsOutput) { continue; }
var pt = powerSource.Item.GetComponent<PowerTransfer>();
if (pt != null)
{
float voltage = -pt.CurrPowerConsumption / Math.Max(pt.PowerLoad, 1.0f);
powered.voltage = Math.Max(powered.voltage, voltage);
continue;
}
var pc = powerSource.Item.GetComponent<PowerContainer>();
if (pc != null)
{
float voltage = -pc.CurrPowerOutput / Math.Max(powered.CurrPowerConsumption, 1.0f);
powered.voltage += voltage;
}
}
}
}
protected override void RemoveComponentSpecific()
{
poweredList.Remove(this);
}
}
}
@@ -416,9 +416,8 @@ namespace Barotrauma.Items.Components
if (IgnoredBodies.Contains(target.Body)) { return false; }
if (target.UserData is Item) { return false; }
if (target.CollisionCategories == Physics.CollisionCharacter && !(target.Body.UserData is Limb))
//ignore character colliders (the projectile only hits limbs)
if (target.CollisionCategories == Physics.CollisionCharacter && target.Body.UserData is Character)
{
return false;
}
@@ -445,17 +444,52 @@ namespace Barotrauma.Items.Components
if (attack != null) { attackResult = attack.DoDamageToLimb(User, limb, item.WorldPosition, 1.0f); }
if (limb.character != null) { character = limb.character; }
}
else if (target.Body.UserData is Structure structure)
else if (target.Body.UserData is Item targetItem)
{
if (attack != null) { attackResult = attack.DoDamage(User, structure, item.WorldPosition, 1.0f); }
if (attack != null && targetItem.Prefab.DamagedByProjectiles)
{
attackResult = attack.DoDamage(User, targetItem, item.WorldPosition, 1.0f);
}
}
else if (target.Body.UserData is IDamageable damageable)
{
if (attack != null) { attackResult = attack.DoDamage(User, damageable, item.WorldPosition, 1.0f); }
}
if (character != null) { character.LastDamageSource = item; }
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
{
ApplyStatusEffects(ActionType.OnUse, 1.0f, character, target.Body.UserData as Limb, user: user);
ApplyStatusEffects(ActionType.OnImpact, 1.0f, character, target.Body.UserData as Limb, user: user);
if (target.Body.UserData is Limb targetLimb)
{
ApplyStatusEffects(ActionType.OnUse, 1.0f, character, targetLimb, user: user);
ApplyStatusEffects(ActionType.OnImpact, 1.0f, character, targetLimb, user: user);
var attack = targetLimb.attack;
if (attack != null)
{
// Apply the status effects defined in the limb's attack that was hit
foreach (var effect in attack.StatusEffects)
{
if (effect.type == ActionType.OnImpact)
{
//effect.Apply(effect.type, 1.0f, targetLimb.character, targetLimb.character, targetLimb.WorldPosition);
if (effect.HasTargetType(StatusEffect.TargetType.This))
{
effect.Apply(effect.type, 1.0f, targetLimb.character, targetLimb.character, targetLimb.WorldPosition);
}
if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
var targets = new List<ISerializableEntity>();
effect.GetNearbyTargets(targetLimb.WorldPosition, targets);
effect.Apply(ActionType.OnActive, 1.0f, targetLimb.character, targets);
}
}
}
}
}
#if SERVER
if (GameMain.NetworkMember.IsServer)
{
@@ -92,9 +92,7 @@ namespace Barotrauma.Items.Components
foreach (XElement connectionElement in subElement.Elements())
{
if (connectionElement.Name.ToString() != element.Name.ToString()) { continue; }
string prefabConnectionName = element.GetAttributeString("name", IsOutput ? "output" : "input");
string prefabConnectionName = element.GetAttributeString("name", null);
if (prefabConnectionName == Name)
{
displayNameTag = connectionElement.GetAttributeString("displayname", "");
@@ -245,31 +243,38 @@ namespace Barotrauma.Items.Components
{
for (int i = 0; i < MaxLinked; i++)
{
if (wires[i] == null) continue;
if (wires[i] == null) { continue; }
Connection recipient = wires[i].OtherConnection(this);
if (recipient == null) continue;
if (recipient.item == this.item || recipient.item == source) continue;
if (recipient == null) { continue; }
if (recipient.item == this.item || recipient.item == source) { continue; }
if (source != null && !source.LastSentSignalRecipients.Contains(recipient.item))
{
source.LastSentSignalRecipients.Add(recipient.item);
}
source?.LastSentSignalRecipients.Add(recipient.item);
foreach (ItemComponent ic in recipient.item.Components)
{
ic.ReceiveSignal(stepsTaken, signal, recipient, source, sender, power, signalStrength);
}
bool broken = recipient.Item.Condition <= 0.0f;
foreach (StatusEffect effect in recipient.Effects)
{
if (broken && effect.type != ActionType.OnBroken) continue;
recipient.Item.ApplyStatusEffect(effect, ActionType.OnUse, (float)Timing.Step, null, null, false, false);
}
}
}
public void SendPowerProbeSignal(Item source, float power)
{
for (int i = 0; i < MaxLinked; i++)
{
if (wires[i] == null) { continue; }
Connection recipient = wires[i].OtherConnection(this);
if (recipient == null) { continue; }
recipient.item.GetComponent<Powered>()?.ReceivePowerProbeSignal(recipient, source, power);
}
}
public void ClearConnections()
{
for (int i = 0; i < MaxLinked; i++)
@@ -0,0 +1,57 @@
using System;
using System.Globalization;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
class FunctionComponent : ItemComponent
{
public enum FunctionType
{
Round,
Ceil,
Floor,
Factorial
}
[Serialize(FunctionType.Round, false, description: "Which kind of function to run the input through.")]
public FunctionType Function
{
get; set;
}
public FunctionComponent(Item item, XElement element)
: base(item, element)
{
IsActive = true;
}
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0, float signalStrength = 1)
{
float.TryParse(signal, out float value);
switch (Function)
{
case FunctionType.Round:
item.SendSignal(0, Math.Round(value).ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
break;
case FunctionType.Ceil:
item.SendSignal(0, Math.Ceiling(value).ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
break;
case FunctionType.Floor:
item.SendSignal(0, Math.Floor(value).ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
break;
case FunctionType.Factorial:
int intVal = (int)Math.Min(value, 20);
ulong factorial = 1;
for (int i = intVal; i > 0; i--)
{
factorial *= (ulong)i;
}
item.SendSignal(0, factorial.ToString(), "signal_out", null);
break;
default:
throw new NotImplementedException($"Function {Function} has not been implemented.");
}
}
}
}
@@ -216,19 +216,12 @@ namespace Barotrauma.Items.Components
#endif
}
if (powerConsumption == 0.0f)
{
voltage = 1.0f;
}
else
{
currPowerConsumption = powerConsumption;
}
currPowerConsumption = powerConsumption;
if (Rand.Range(0.0f, 1.0f) < 0.05f && voltage < Rand.Range(0.0f, minVoltage))
if (Rand.Range(0.0f, 1.0f) < 0.05f && Voltage < Rand.Range(0.0f, MinVoltage))
{
#if CLIENT
if (voltage > 0.1f)
if (Voltage > 0.1f)
{
SoundPlayer.PlaySound("zap", item.WorldPosition, hullGuess: item.CurrentHull);
}
@@ -237,7 +230,7 @@ namespace Barotrauma.Items.Components
}
else
{
lightBrightness = MathHelper.Lerp(lightBrightness, Math.Min(voltage, 1.0f), 0.1f);
lightBrightness = MathHelper.Lerp(lightBrightness, Math.Min(Voltage, 1.0f), 0.1f);
}
if (blinkFrequency > 0.0f)
@@ -262,8 +255,6 @@ namespace Barotrauma.Items.Components
{
UpdateAITarget(item.AiTarget);
}
voltage -= deltaTime;
}
#if CLIENT
@@ -0,0 +1,41 @@
using System.Globalization;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
class ModuloComponent : ItemComponent
{
private float modulus;
[InGameEditable, Serialize(1.0f, false, description: "The modulus of the operation. Must be non-zero.")]
public float Modulus
{
get { return modulus; }
set
{
modulus = MathUtils.NearlyEqual(value, 0.0f) ? 1.0f : value;
}
}
public ModuloComponent(Item item, XElement element) : base(item, element)
{
IsActive = true;
}
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0, float signalStrength = 1)
{
switch (connection.Name)
{
case "set_modulus":
case "modulus":
float.TryParse(signal, out float newModulus);
Modulus = newModulus;
break;
case "signal_in":
float.TryParse(signal, out float value);
item.SendSignal(0, (value % modulus).ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
break;
}
}
}
}
@@ -25,6 +25,14 @@ namespace Barotrauma.Items.Components
set;
}
[Editable, Serialize(false, true, description: "Should the sensor ignore the bodies of dead characters?")]
public bool IgnoreDead
{
get;
set;
}
[InGameEditable, Serialize(0.0f, true, description: "Horizontal detection range.")]
public float RangeX
{
@@ -109,6 +117,7 @@ namespace Barotrauma.Items.Components
foreach (Character c in Character.CharacterList)
{
if (IgnoreDead && c.IsDead) { continue; }
if (OnlyHumans && !c.IsHuman) { continue; }
//do a rough check based on the position of the character's collider first
@@ -138,5 +147,15 @@ namespace Barotrauma.Items.Components
{
detectOffset.Y = -detectOffset.Y;
}
public override XElement Save(XElement parentElement)
{
Vector2 prevDetectOffset = detectOffset;
//undo flipping before saving
if (item.FlippedX) { detectOffset.X = -detectOffset.X; }
if (item.FlippedY) { detectOffset.Y = -detectOffset.Y; }
XElement element = base.Save(parentElement);
detectOffset = prevDetectOffset;
return element;
}
}
}
@@ -1,4 +1,5 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Xml.Linq;
@@ -8,9 +9,11 @@ namespace Barotrauma.Items.Components
class RelayComponent : PowerTransfer, IServerSerializable
{
private float maxPower;
private bool isOn;
private float throttlePowerOutput;
private static readonly Dictionary<string, string> connectionPairs = new Dictionary<string, string>
{
{ "power_in", "power_out"},
@@ -21,6 +24,7 @@ namespace Barotrauma.Items.Components
{ "signal_in4", "signal_out4" },
{ "signal_in5", "signal_out5" }
};
public float DisplayLoad { get; set; }
[Editable, Serialize(1000.0f, true, description: "The maximum amount of power that can pass through the item.")]
public float MaxPower
@@ -31,7 +35,7 @@ namespace Barotrauma.Items.Components
maxPower = Math.Max(0.0f, value);
}
}
[Editable, Serialize(false, true, description: "Can the relay currently pass power and signals through it.")]
public bool IsOn
{
@@ -49,18 +53,46 @@ namespace Barotrauma.Items.Components
}
}
}
public RelayComponent(Item item, XElement element)
: base (item, element)
: base(item, element)
{
IsActive = true;
}
throttlePowerOutput = MaxPower;
}
public override void Update(float deltaTime, Camera cam)
{
base.Update(deltaTime, cam);
RefreshConnections();
item.SendSignal(0, IsOn ? "1" : "0", "state_out", null);
if (!CanTransfer) { Voltage = 0.0f; return; }
if (isBroken)
{
SetAllConnectionsDirty();
isBroken = false;
}
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
if (powerOut != null)
{
bool overloaded = false;
foreach (Connection recipient in powerOut.Recipients)
{
var pt = recipient.Item.GetComponent<PowerTransfer>();
if (pt != null)
{
float overload = -pt.CurrPowerConsumption - pt.PowerLoad;
throttlePowerOutput += overload * deltaTime * 0.5f;
overloaded = overload > 1.0f;
}
}
throttlePowerOutput = overloaded ?
MathHelper.Clamp(throttlePowerOutput, 0.0f, MaxPower):
Math.Max(throttlePowerOutput - MaxPower * 0.1f * deltaTime, 0.0f);
}
if (Math.Min(-currPowerConsumption, PowerLoad) > maxPower && CanBeOverloaded)
{
@@ -68,9 +100,56 @@ namespace Barotrauma.Items.Components
}
}
public override void ReceivePowerProbeSignal(Connection connection, Item source, float power)
{
if (!IsOn) { return; }
//we've already received this signal
if (lastPowerProbeRecipients.Contains(this)) { return; }
lastPowerProbeRecipients.Add(this);
if (power < 0.0f)
{
if (!connection.IsOutput || powerIn == null) { return; }
//power being drawn from the power_out connection
DisplayLoad -= Math.Min(power, 0.0f);
powerLoad -= Math.Min(power + throttlePowerOutput, 0.0f);
//pass the load to items connected to the input
powerIn.SendPowerProbeSignal(source, Math.Max(power, -MaxPower));
}
else
{
if (connection.IsOutput || powerOut == null) { return; }
//power being supplied to the power_in connection
if (currPowerConsumption - power < -MaxPower)
{
power += MaxPower + (currPowerConsumption - power);
}
currPowerConsumption -= power;
foreach (Connection recipient in powerOut.Recipients)
{
if (!recipient.IsPower) { continue; }
var powered = recipient.Item.GetComponent<Powered>();
if (powered == null) { continue; }
float load = powered.CurrPowerConsumption;
var powerTransfer = powered as PowerTransfer;
if (powerTransfer != null) { load = powerTransfer.PowerLoad; }
float powerOut = power * (load / Math.Max(powerLoad + throttlePowerOutput, 0.01f));
powered.ReceivePowerProbeSignal(recipient, source, Math.Min(powerOut, power));
}
}
}
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f)
{
if (connection.IsPower || item.Condition <= 0.0f) { return; }
if (item.Condition <= 0.0f || connection.IsPower) { return; }
if (connectionPairs.TryGetValue(connection.Name, out string outConnection))
{
@@ -0,0 +1,108 @@
using Microsoft.Xna.Framework;
using System;
using System.Globalization;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
class TrigonometricFunctionComponent : ItemComponent
{
public enum FunctionType
{
Sin,
Cos,
Tan,
Asin,
Acos,
Atan,
}
protected float[] receivedSignal = new float[2];
[Serialize(FunctionType.Sin, false, description: "Which kind of function to run the input through.")]
public FunctionType Function
{
get; set;
}
[InGameEditable, Serialize(false, true, description: "If set to true, the trigonometric function uses radians instead of degrees.")]
public bool UseRadians
{
get; set;
}
public TrigonometricFunctionComponent(Item item, XElement element)
: base(item, element)
{
IsActive = true;
}
public override void Update(float deltaTime, Camera cam)
{
//reset received signals
receivedSignal[0] = float.NaN;
receivedSignal[1] = float.NaN;
}
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0, float signalStrength = 1)
{
float.TryParse(signal, out float value);
switch (Function)
{
case FunctionType.Sin:
if (!UseRadians) { value = MathHelper.ToRadians(value); }
item.SendSignal(0, ((float)Math.Sin(value)).ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
break;
case FunctionType.Cos:
if (!UseRadians) { value = MathHelper.ToRadians(value); }
item.SendSignal(0, ((float)Math.Cos(value)).ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
break;
case FunctionType.Tan:
if (!UseRadians) { value = MathHelper.ToRadians(value); }
item.SendSignal(0, ((float)Math.Tan(value)).ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
break;
case FunctionType.Asin:
{
float angle = (float)Math.Asin(value);
if (!UseRadians) { angle = MathHelper.ToDegrees(angle); }
item.SendSignal(0, angle.ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
}
break;
case FunctionType.Acos:
{
float angle = (float)Math.Acos(value);
if (!UseRadians) { angle = MathHelper.ToDegrees(angle); }
item.SendSignal(0, angle.ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
}
break;
case FunctionType.Atan:
if (connection.Name == "signal_in_x")
{
float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out receivedSignal[0]);
}
else if (connection.Name == "signal_in_y")
{
float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out receivedSignal[1]);
if (!float.IsNaN(receivedSignal[0]) && !float.IsNaN(receivedSignal[1]))
{
float angle = (float)Math.Atan2(receivedSignal[1], receivedSignal[0]);
if (!UseRadians) { angle = MathHelper.ToDegrees(angle); }
item.SendSignal(0, angle.ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
}
}
else
{
float angle = (float)Math.Atan(value);
if (!UseRadians) { angle = MathHelper.ToDegrees(angle); }
item.SendSignal(0, angle.ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
}
break;
default:
throw new NotImplementedException($"Function {Function} has not been implemented.");
}
}
}
}
@@ -22,7 +22,7 @@ namespace Barotrauma.Items.Components
[Serialize(Character.TeamType.None, false, description: "WiFi components can only communicate with components that have the same Team ID.")]
public Character.TeamType TeamID { get; set; }
[Serialize(20000.0f, false, description: "How close the recipient has to be to receive a signal from this WiFi component.")]
[Editable, Serialize(20000.0f, false, description: "How close the recipient has to be to receive a signal from this WiFi component.")]
public float Range
{
get { return range; }
@@ -174,8 +174,6 @@ namespace Barotrauma.Items.Components
protected override void RemoveComponentSpecific()
{
base.RemoveComponentSpecific();
list.Remove(this);
}
}
@@ -83,22 +83,16 @@ namespace Barotrauma.Items.Components
public Wire(Item item, XElement element)
: base(item, element)
{
#if CLIENT
if (wireSprite == null)
{
wireSprite = new Sprite("Content/Items/wireHorizontal.png", new Vector2(0.5f, 0.5f))
{
Depth = 0.85f
};
}
#endif
nodes = new List<Vector2>();
sections = new List<WireSection>();
connections = new Connection[2];
IsActive = false;
InitProjSpecific(element);
}
partial void InitProjSpecific(XElement element);
public Connection OtherConnection(Connection connection)
{
if (connection == connections[0]) { return connections[1]; }
@@ -728,6 +722,11 @@ namespace Barotrauma.Items.Components
{
ClearConnections();
base.RemoveComponentSpecific();
#if CLIENT
overrideSprite?.Remove();
overrideSprite = null;
wireSprite = null;
#endif
}
public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime)
@@ -192,6 +192,7 @@ namespace Barotrauma.Items.Components
public override void OnItemLoaded()
{
base.OnItemLoaded();
var lightComponents = item.GetComponents<LightComponent>();
if (lightComponents != null && lightComponents.Count() > 0)
{
@@ -325,20 +326,24 @@ namespace Barotrauma.Items.Components
failedLaunchAttempts = 0;
var batteries = item.GetConnectedComponents<PowerContainer>();
float availablePower = 0.0f;
foreach (PowerContainer battery in batteries)
float neededPower = powerConsumption;
while (neededPower > 0.0001f && batteries.Count > 0)
{
float batteryPower = Math.Min(battery.Charge * 3600.0f, battery.MaxOutPut);
float takePower = Math.Min(powerConsumption - availablePower, batteryPower);
battery.Charge -= takePower / 3600.0f;
#if SERVER
if (GameMain.Server != null)
batteries.RemoveAll(b => b.Charge <= 0.0001f || b.MaxOutPut <= 0.0001f);
float takePower = neededPower / batteries.Count;
takePower = Math.Min(takePower, batteries.Min(b => Math.Min(b.Charge * 3600.0f, b.MaxOutPut)));
foreach (PowerContainer battery in batteries)
{
battery.Item.CreateServerEvent(battery);
}
neededPower -= takePower;
battery.Charge -= takePower / 3600.0f;
#if SERVER
if (GameMain.Server != null)
{
battery.Item.CreateServerEvent(battery);
}
#endif
}
}
Launch(projectiles[0].Item, character);
@@ -477,12 +482,12 @@ namespace Barotrauma.Items.Components
//enough shells and power
Character closestEnemy = null;
float closestDist = 10000.0f * 10000.0f;
float closestDist = 3000 * 3000;
foreach (Character enemy in Character.CharacterList)
{
//ignore humans and characters that are inside the sub
if (enemy.IsDead|| enemy.AnimController.CurrentHull != null || !enemy.Enabled) { continue; }
if (enemy.SpeciesName == character.SpeciesName && enemy.TeamID == character.TeamID) { continue; }
// Ignore friendly and those that are inside the sub
if (enemy.IsDead || enemy.AnimController.CurrentHull != null || !enemy.Enabled) { continue; }
if (HumanAIController.IsFriendly(character, enemy)) { continue; }
float dist = Vector2.DistanceSquared(enemy.WorldPosition, item.WorldPosition);
if (dist > closestDist) { continue; }
@@ -510,8 +515,21 @@ namespace Barotrauma.Items.Components
if (Math.Abs(MathUtils.GetShortestAngle(enemyAngle, turretAngle)) > 0.15f) { return false; }
var pickedBody = Submarine.PickBody(ConvertUnits.ToSimUnits(item.WorldPosition), closestEnemy.SimPosition, null);
if (pickedBody != null && !(pickedBody.UserData is Limb)) { return false; }
var pickedBody = Submarine.PickBody(ConvertUnits.ToSimUnits(item.WorldPosition), closestEnemy.SimPosition);
if (pickedBody == null) { return false; }
Character target = null;
if (pickedBody.UserData is Character c)
{
target = c;
}
else if (pickedBody.UserData is Limb limb)
{
target = limb.character;
}
if (target == null || HumanAIController.IsFriendly(character, target))
{
return false;
}
if (objective.Option.ToLowerInvariant() == "fireatwill")
{
@@ -554,8 +572,8 @@ namespace Barotrauma.Items.Components
{
base.RemoveComponentSpecific();
if (barrelSprite != null) barrelSprite.Remove();
if (railSprite != null) railSprite.Remove();
barrelSprite?.Remove(); barrelSprite = null;
railSprite?.Remove(); railSprite = null;
#if CLIENT
moveSoundChannel?.Dispose(); moveSoundChannel = null;
@@ -6,6 +6,7 @@ using System.Linq;
using System.Xml.Linq;
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using Barotrauma.Networking;
namespace Barotrauma
{
@@ -23,6 +24,7 @@ namespace Barotrauma
class WearableSprite
{
public string UnassignedSpritePath { get; private set; }
public string SpritePath { get; private set; }
public XElement SourceElement { get; private set; }
@@ -83,7 +85,7 @@ namespace Barotrauma
if (value == _gender) { return; }
_gender = value;
IsInitialized = false;
SpritePath = ParseSpritePath(SourceElement.GetAttributeString("texture", string.Empty));
UnassignedSpritePath = ParseSpritePath(SourceElement.GetAttributeString("texture", string.Empty));
Init(_gender);
}
}
@@ -92,7 +94,7 @@ namespace Barotrauma
{
Type = type;
SourceElement = subElement;
SpritePath = subElement.GetAttributeString("texture", string.Empty);
UnassignedSpritePath = subElement.GetAttributeString("texture", string.Empty);
Init();
switch (type)
{
@@ -122,42 +124,24 @@ namespace Barotrauma
Type = WearableType.Item;
WearableComponent = wearable;
Variant = Math.Max(variant, 0);
SpritePath = ParseSpritePath(subElement.GetAttributeString("texture", string.Empty));
UnassignedSpritePath = ParseSpritePath(subElement.GetAttributeString("texture", string.Empty));
SourceElement = subElement;
}
private string ParseSpritePath(string texturePath) => texturePath.Contains("/") ? texturePath : $"{Path.GetDirectoryName(WearableComponent.Item.Prefab.ConfigFile)}/{texturePath}";
public void RefreshPath()
{
if (Variant > 0)
{
// Restore the tag so that we can parse it again.
ReplaceNumbersWith("[VARIANT]");
}
ParsePath(true);
}
private void ReplaceNumbersWith(string replacement)
{
var fileName = Path.GetFileName(SpritePath);
var path = Path.GetDirectoryName(SpritePath);
fileName = fileName.Replace(replacement, c => char.IsNumber(c));
SpritePath = Path.Combine(path, fileName);
}
private void ParsePath(bool parseSpritePath)
public void ParsePath(bool parseSpritePath)
{
string tempPath = UnassignedSpritePath;
if (_gender != Gender.None)
{
SpritePath = SpritePath.Replace("[GENDER]", (_gender == Gender.Female) ? "female" : "male");
tempPath = tempPath.Replace("[GENDER]", (_gender == Gender.Female) ? "female" : "male");
}
SpritePath = SpritePath.Replace("[VARIANT]", Variant.ToString());
SpritePath = tempPath.Replace("[VARIANT]", Variant.ToString());
if (!File.Exists(SpritePath))
{
// If the variant does not exist, parse the path so that it uses first variant.
Variant = 1;
ReplaceNumbersWith(Variant.ToString());
SpritePath = tempPath.Replace("[VARIANT]", "1");
}
if (parseSpritePath)
{
@@ -169,13 +153,13 @@ namespace Barotrauma
public void Init(Gender gender = Gender.None)
{
if (IsInitialized) { return; }
_gender = SpritePath.Contains("[GENDER]") ? gender : Gender.None;
_gender = UnassignedSpritePath.Contains("[GENDER]") ? gender : Gender.None;
ParsePath(false);
if (Sprite != null)
{
Sprite.Remove();
}
Sprite = new Sprite(SourceElement, file: SpritePath);
Sprite = new Sprite(SourceElement, file: SpritePath, preMultiplyAlpha: true);
Limb = (LimbType)Enum.Parse(typeof(LimbType), SourceElement.GetAttributeString("limb", "Head"), true);
HideLimb = SourceElement.GetAttributeBool("hidelimb", false);
HideOtherWearables = SourceElement.GetAttributeBool("hideotherwearables", false);
@@ -197,25 +181,60 @@ namespace Barotrauma
namespace Barotrauma.Items.Components
{
class Wearable : Pickable
class Wearable : Pickable, IServerSerializable
{
private WearableSprite[] wearableSprites;
private LimbType[] limbType;
private Limb[] limb;
private readonly XElement[] wearableElements;
private readonly WearableSprite[] wearableSprites;
private readonly LimbType[] limbType;
private readonly Limb[] limb;
private List<DamageModifier> damageModifiers;
private readonly List<DamageModifier> damageModifiers;
public List<DamageModifier> DamageModifiers
public IEnumerable<DamageModifier> DamageModifiers
{
get { return damageModifiers; }
}
private bool autoEquipWhenFull;
public bool AutoEquipWhenFull
public bool AutoEquipWhenFull { get; private set; }
public readonly int Variants;
private int variant;
public int Variant
{
get { return autoEquipWhenFull; }
}
get { return variant; }
set
{
#if SERVER
variant = value;
item.CreateServerEvent(this);
#elif CLIENT
if (variant == value) { return; }
Character character = picker;
if (character != null)
{
Unequip(character);
}
for (int i = 0; i < wearableSprites.Length; i++)
{
var subElement = wearableElements[i];
wearableSprites[i]?.Sprite?.Remove();
wearableSprites[i] = new WearableSprite(subElement, this, value);
}
if (character != null)
{
Equip(character);
}
variant = value;
#endif
}
}
public Wearable(Item item, XElement element) : base(item, element)
{
this.item = item;
@@ -223,12 +242,13 @@ namespace Barotrauma.Items.Components
damageModifiers = new List<DamageModifier>();
int spriteCount = element.Elements().Count(x => x.Name.ToString() == "sprite");
int variants = element.GetAttributeInt("variants", 0);
int variant = variants > 0 ? Rand.Range(1, variants + 1, Rand.RandSync.Server) : 1;
Variants = element.GetAttributeInt("variants", 0);
variant = Rand.Range(1, Variants + 1, Rand.RandSync.Server);
wearableSprites = new WearableSprite[spriteCount];
wearableElements = new XElement[spriteCount];
limbType = new LimbType[spriteCount];
limb = new Limb[spriteCount];
autoEquipWhenFull = element.GetAttributeBool("autoequipwhenfull", true);
AutoEquipWhenFull = element.GetAttributeBool("autoequipwhenfull", true);
int i = 0;
foreach (XElement subElement in element.Elements())
{
@@ -245,6 +265,7 @@ namespace Barotrauma.Items.Components
subElement.GetAttributeString("limb", "Head"), true);
wearableSprites[i] = new WearableSprite(subElement, this, variant);
wearableElements[i] = subElement;
foreach (XElement lightElement in subElement.Elements())
{
@@ -380,5 +401,39 @@ namespace Barotrauma.Items.Components
}
}
public override XElement Save(XElement parentElement)
{
XElement componentElement = base.Save(parentElement);
componentElement.Add(new XAttribute("variant", variant));
return componentElement;
}
private int loadedVariant = -1;
public override void Load(XElement componentElement, bool usePrefabValues)
{
base.Load(componentElement, usePrefabValues);
loadedVariant = componentElement.GetAttributeInt("variant", -1);
}
public override void OnItemLoaded()
{
base.OnItemLoaded();
//do this here to prevent creating a network event before the item has been fully initialized
if (loadedVariant > 0 && loadedVariant < Variants + 1)
{
Variant = loadedVariant;
}
}
public override void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
{
msg.Write((byte)Variant);
base.ServerWrite(msg, c, extraData);
}
public override void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime)
{
Variant = (int)msg.ReadByte();
base.ClientRead(type, msg, sendingTime);
}
}
}
@@ -338,7 +338,7 @@ namespace Barotrauma
public Item FindItem(Func<Item, bool> predicate, bool recursive)
{
Item match = Items.FirstOrDefault(predicate);
Item match = Items.FirstOrDefault(i => i != null && predicate(i));
if (match == null && recursive)
{
foreach (var item in Items)
@@ -360,13 +360,13 @@ namespace Barotrauma
public Item FindItemByTag(string tag, bool recursive = false)
{
if (tag == null) { return null; }
return FindItem(i => i != null && i.HasTag(tag), recursive);
return FindItem(i => i.HasTag(tag), recursive);
}
public Item FindItemByIdentifier(string identifier, bool recursive = false)
{
if (identifier == null) return null;
return FindItem(i => i != null && i.Prefab.Identifier == identifier, recursive);
return FindItem(i => i.Prefab.Identifier == identifier, recursive);
}
public virtual void RemoveItem(Item item)
@@ -59,6 +59,8 @@ namespace Barotrauma
public readonly XElement StaticBodyConfig;
private bool transformDirty = true;
private float lastSentCondition;
private float sendConditionUpdateTimer;
private bool conditionUpdatePending;
@@ -465,7 +467,7 @@ namespace Barotrauma
{
get;
private set;
} = new List<Item>();
} = new List<Item>(20);
public string ConfigFile
{
@@ -578,7 +580,7 @@ namespace Barotrauma
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
if (submarine == null || !submarine.Loading) FindHull();
if (submarine == null || !submarine.Loading) { FindHull(); }
SetActiveSprite();
@@ -588,8 +590,35 @@ namespace Barotrauma
{
case "body":
body = new PhysicsBody(subElement, ConvertUnits.ToSimUnits(Position), Scale);
body.FarseerBody.AngularDamping = 0.2f;
body.FarseerBody.LinearDamping = 0.1f;
string collisionCategory = subElement.GetAttributeString("collisioncategory", null);
if (Prefab.DamagedByProjectiles || Prefab.DamagedByMeleeWeapons)
{
//force collision category to Character to allow projectiles and weapons to hit
//(we could also do this by making the projectiles and weapons hit CollisionItem
//and check if the collision should be ignored in the OnCollision callback, but
//that'd make the hit detection more expensive because every item would be included)
body.CollisionCategories = Physics.CollisionCharacter;
body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform | Physics.CollisionProjectile;
}
if (collisionCategory != null)
{
if (!Physics.TryParseCollisionCategory(collisionCategory, out Category cat))
{
DebugConsole.ThrowError("Invalid collision category in item \"" + Name+"\" (" + collisionCategory + ")");
}
else
{
body.CollisionCategories = cat;
if (cat.HasFlag(Physics.CollisionCharacter))
{
body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform | Physics.CollisionProjectile;
}
}
}
body.FarseerBody.AngularDamping = element.GetAttributeFloat("angulardamping", 0.2f);
body.FarseerBody.LinearDamping = element.GetAttributeFloat("lineardamping", 0.1f);
body.UserData = this;
break;
case "trigger":
case "inventoryicon":
@@ -874,7 +903,7 @@ namespace Barotrauma
rect.X = (int)(displayPos.X - rect.Width / 2.0f);
rect.Y = (int)(displayPos.Y + rect.Height / 2.0f);
if (findNewHull) FindHull();
if (findNewHull) { FindHull(); }
}
public void SetActiveSprite()
@@ -966,6 +995,8 @@ namespace Barotrauma
return rootContainer;
}
public bool IsOwnedBy(Character character) => FindParentInventory(i => i.Owner == character) != null;
public Inventory FindParentInventory(Func<Inventory, bool> predicate)
{
if (parentInventory != null)
@@ -1214,11 +1245,13 @@ namespace Barotrauma
}
}
if (Removed) { return; }
if (body != null && body.Enabled)
{
System.Diagnostics.Debug.Assert(body.FarseerBody.FixtureList != null);
if (Math.Abs(body.LinearVelocity.X) > 0.01f || Math.Abs(body.LinearVelocity.Y) > 0.01f)
if (Math.Abs(body.LinearVelocity.X) > 0.01f || Math.Abs(body.LinearVelocity.Y) > 0.01f || transformDirty)
{
UpdateTransform();
if (CurrentHull == null && body.SimPosition.Y < ConvertUnits.ToSimUnits(Level.MaxEntityDepth))
@@ -1255,6 +1288,8 @@ namespace Barotrauma
public void UpdateTransform()
{
if (body == null) { return; }
Submarine prevSub = Submarine;
FindHull();
@@ -1283,6 +1318,8 @@ namespace Barotrauma
MathHelper.Clamp(body.LinearVelocity.X, -NetConfig.MaxPhysicsBodyVelocity, NetConfig.MaxPhysicsBodyVelocity),
MathHelper.Clamp(body.LinearVelocity.Y, -NetConfig.MaxPhysicsBodyVelocity, NetConfig.MaxPhysicsBodyVelocity));
}
transformDirty = false;
}
/// <summary>
@@ -1321,6 +1358,8 @@ namespace Barotrauma
private bool OnCollision(Fixture f1, Fixture f2, Contact contact)
{
if (transformDirty) { return false; }
Vector2 normal = contact.Manifold.LocalNormal;
float impact = Vector2.Dot(f1.Body.LinearVelocity, -normal);
@@ -1503,34 +1542,37 @@ namespace Barotrauma
}
}
}
public void SendSignal(int stepsTaken, string signal, string connectionName, Character sender, float power = 0.0f, Item source = null, float signalStrength = 1.0f)
{
LastSentSignalRecipients.Clear();
if (connections == null) { return; }
if (!connections.TryGetValue(connectionName, out Connection c)) { return; }
SendSignal(stepsTaken, signal, c, sender, power, source, signalStrength);
}
public void SendSignal(int stepsTaken, string signal, Connection connection, Character sender, float power = 0.0f, Item source = null, float signalStrength = 1.0f)
{
LastSentSignalRecipients.Clear();
if (connections == null || connection == null) { return; }
stepsTaken++;
if (!connections.TryGetValue(connectionName, out Connection c)) { return; }
if (stepsTaken > 10)
{
//use a coroutine to prevent infinite loops by creating a one
//frame delay if the "signal chain" gets too long
CoroutineManager.StartCoroutine(SendSignal(signal, c, sender, power, signalStrength));
CoroutineManager.StartCoroutine(SendSignal(signal, connection, sender, power, signalStrength));
}
else
{
foreach (StatusEffect effect in c.Effects)
foreach (StatusEffect effect in connection.Effects)
{
if (condition <= 0.0f && effect.type != ActionType.OnBroken) { continue; }
if (signal != "0" && !string.IsNullOrEmpty(signal)) { ApplyStatusEffect(effect, ActionType.OnUse, (float)Timing.Step, null, null, false, false); }
}
c.SendSignal(stepsTaken, signal, source ?? this, sender, power, signalStrength);
}
connection.SendSignal(stepsTaken, signal, source ?? this, sender, power, signalStrength);
}
}
private IEnumerable<object> SendSignal(string signal, Connection connection, Character sender, float power = 0.0f, float signalStrength = 1.0f)
{
//wait one frame
@@ -1854,7 +1896,6 @@ namespace Barotrauma
foreach (ItemComponent ic in components) ic.Unequip(character);
}
public List<Pair<object, SerializableProperty>> GetProperties<T>()
{
List<Pair<object, SerializableProperty>> allProperties = new List<Pair<object, SerializableProperty>>();
@@ -5,6 +5,8 @@ using System.Collections.Generic;
using System.IO;
using System.Xml.Linq;
using System.Linq;
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
namespace Barotrauma
{
@@ -247,6 +249,27 @@ namespace Barotrauma
private set;
}
[Serialize(false, false)]
public bool DamagedByExplosions
{
get;
private set;
}
[Serialize(false, false)]
public bool DamagedByProjectiles
{
get;
private set;
}
[Serialize(false, false)]
public bool DamagedByMeleeWeapons
{
get;
private set;
}
[Serialize(false, false)]
public bool FireProof
{
@@ -310,6 +333,17 @@ namespace Barotrauma
private set;
}
private HashSet<string> preferredContainers = new HashSet<string>();
[Serialize("", true, description: "Define containers (by identifiers or tags) that this item should be placed in. These are preferences, which are not enforced.")]
public string PreferredContainers
{
get { return string.Join(",", preferredContainers); }
set
{
StringFormatter.ParseCommaSeparatedStringToCollection(value, preferredContainers);
}
}
/// <summary>
/// How likely it is for the item to spawn in a level of a given type.
/// Key = name of the LevelGenerationParameters (empty string = default value)
@@ -794,9 +828,24 @@ namespace Barotrauma
}
return prefab;
}
public IEnumerable<PriceInfo> GetPrices()
{
return prices?.Values;
}
public bool IsContainerPreferred(ItemContainer itemContainer, out bool isPreferencesDefined)
{
isPreferencesDefined = preferredContainers.Any();
if (!isPreferencesDefined) { return true; }
return preferredContainers.Any(id => itemContainer.Item.Prefab.Identifier == id || itemContainer.Item.HasTag(id));
}
public bool IsContainerPreferred(string[] identifiersOrTags, out bool isPreferencesDefined)
{
isPreferencesDefined = preferredContainers.Any();
if (!isPreferencesDefined) { return true; }
return preferredContainers.Any(id => preferredContainers.Any(p => p == id));
}
}
}
@@ -98,10 +98,11 @@ namespace Barotrauma
if (parentItem == null) { return false; }
return CheckContained(parentItem);
case RelationType.Container:
if (parentItem == null || parentItem.Container == null) { return false; }
if (parentItem == null || parentItem.Container == null) { return MatchOnEmpty; }
return parentItem.Container.Condition > 0.0f && MatchesItem(parentItem.Container);
case RelationType.Equipped:
if (character == null) { return false; }
if (MatchOnEmpty && character.SelectedItems.All(it => it == null)) { return true; }
foreach (Item equippedItem in character.SelectedItems)
{
if (equippedItem == null) { continue; }
@@ -158,7 +159,7 @@ namespace Barotrauma
if (!string.IsNullOrWhiteSpace(Msg)) element.Add(new XAttribute("msg", Msg));
}
public static RelatedItem Load(XElement element, string parentDebugName)
public static RelatedItem Load(XElement element, bool returnEmpty, string parentDebugName)
{
string[] identifiers;
if (element.Attribute("name") != null)
@@ -205,10 +206,9 @@ namespace Barotrauma
}
}
if (identifiers.Length == 0 && excludedIdentifiers.Length == 0) { return null; }
if (identifiers.Length == 0 && excludedIdentifiers.Length == 0 && !returnEmpty) { return null; }
RelatedItem ri = new RelatedItem(identifiers, excludedIdentifiers);
string typeStr = element.GetAttributeString("type", "");
if (string.IsNullOrEmpty(typeStr))
{
@@ -110,9 +110,17 @@ namespace Barotrauma
get { return aiTarget; }
}
public double SpawnTime
{
get { return spawnTime; }
}
private readonly double spawnTime;
public Entity(Submarine submarine)
{
this.Submarine = submarine;
spawnTime = Timing.TotalTime;
//give a unique ID
id = this is EntitySpawner ?
@@ -124,39 +124,55 @@ namespace Barotrauma
if (powerContainer != null)
{
powerContainer.Charge -= powerContainer.Capacity * empStrength * distFactor;
}
}
}
}
if (force == 0.0f && attack.Stun == 0.0f && attack.GetTotalDamage(false) == 0.0f) return;
if (MathUtils.NearlyEqual(force, 0.0f) && MathUtils.NearlyEqual(attack.Stun, 0.0f) && MathUtils.NearlyEqual(attack.GetTotalDamage(false), 0.0f))
{
return;
}
DamageCharacters(worldPosition, attack, force, damageSource, attacker);
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
{
if (flames)
{
foreach (Item item in Item.ItemList)
{
if (item.CurrentHull != hull || item.FireProof || item.Condition <= 0.0f) continue;
if (item.CurrentHull != hull || item.FireProof || item.Condition <= 0.0f) { continue; }
//don't apply OnFire effects if the item is inside a fireproof container
//(or if it's inside a container that's inside a fireproof container, etc)
Item container = item.Container;
bool fireProof = false;
while (container != null)
{
if (container.FireProof) return;
if (container.FireProof) { fireProof = true; break; }
container = container.Container;
}
if (Vector2.Distance(item.WorldPosition, worldPosition) > attack.Range * 0.1f) continue;
if (fireProof || Vector2.Distance(item.WorldPosition, worldPosition) > attack.Range * 0.5f) { continue; }
item.ApplyStatusEffects(ActionType.OnFire, 1.0f);
if (item.Condition <= 0.0f && GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnFire });
}
if (item.Prefab.DamagedByExplosions && !item.Indestructible)
{
float limbRadius = item.body == null ? 0.0f : item.body.GetMaxExtent();
float dist = Vector2.Distance(item.WorldPosition, worldPosition);
dist = Math.Max(0.0f, dist - ConvertUnits.ToDisplayUnits(limbRadius));
if (dist > attack.Range) { continue; }
float distFactor = 1.0f - dist / attack.Range;
float damageAmount = attack.GetItemDamage(1.0f);
item.Condition -= damageAmount * distFactor;
}
}
}
}
@@ -197,7 +213,7 @@ namespace Barotrauma
//calculate distance from the "outer surface" of the physics body
//doesn't take the rotation of the limb into account, but should be accurate enough for this purpose
float limbRadius = Math.Max(Math.Max(limb.body.width * 0.5f, limb.body.height * 0.5f), limb.body.radius);
dist = Math.Max(0.0f, dist - FarseerPhysics.ConvertUnits.ToDisplayUnits(limbRadius));
dist = Math.Max(0.0f, dist - ConvertUnits.ToDisplayUnits(limbRadius));
if (dist > attack.Range) { continue; }
@@ -240,7 +256,7 @@ namespace Barotrauma
}
}
if (limb.WorldPosition != worldPosition && force > 0.0f)
if (limb.WorldPosition != worldPosition && !MathUtils.NearlyEqual(force, 0.0f))
{
Vector2 limbDiff = Vector2.Normalize(limb.WorldPosition - worldPosition);
if (!MathUtils.IsValid(limbDiff)) limbDiff = Rand.Vector(1.0f);
@@ -55,6 +55,8 @@ namespace Barotrauma
set { open = MathHelper.Clamp(value, 0.0f, 1.0f); }
}
public float Size => IsHorizontal ? Rect.Height : Rect.Width;
public Door ConnectedDoor;
public Structure ConnectedWall;
+20 -13
View File
@@ -228,13 +228,16 @@ namespace Barotrauma
surface = rect.Y - rect.Height;
aiTarget = new AITarget(this)
if (submarine != null)
{
MinSightRange = 2000,
MaxSightRange = 5000,
MaxSoundRange = 5000,
SoundRange = 0
};
aiTarget = new AITarget(this)
{
MinSightRange = 2000,
MaxSightRange = 5000,
MaxSoundRange = 5000,
SoundRange = 0
};
}
hullList.Add(this);
@@ -430,8 +433,11 @@ namespace Barotrauma
FireSource.UpdateAll(FireSources, deltaTime);
aiTarget.SightRange = Submarine == null ? aiTarget.MinSightRange : Submarine.Velocity.Length() / 2 * aiTarget.MaxSightRange;
aiTarget.SoundRange -= deltaTime * 1000.0f;
if (aiTarget != null)
{
aiTarget.SightRange = Submarine == null ? aiTarget.MinSightRange : Submarine.Velocity.Length() / 2 * aiTarget.MaxSightRange;
aiTarget.SoundRange -= deltaTime * 1000.0f;
}
if (!update)
{
@@ -594,16 +600,17 @@ namespace Barotrauma
{
adjacentHulls.Clear();
int startStep = 0;
return GetAdjacentHulls(includingThis, adjacentHulls, ref startStep, searchDepth);
searchDepth = searchDepth ?? 100;
return GetAdjacentHulls(includingThis, adjacentHulls, ref startStep, searchDepth.Value);
}
private HashSet<Hull> GetAdjacentHulls(bool includingThis, HashSet<Hull> connectedHulls, ref int step, int? searchDepth)
private HashSet<Hull> GetAdjacentHulls(bool includingThis, HashSet<Hull> connectedHulls, ref int step, int searchDepth)
{
if (includingThis)
{
connectedHulls.Add(this);
}
if (step > searchDepth.Value)
if (step > searchDepth)
{
return connectedHulls;
}
@@ -642,7 +649,7 @@ namespace Barotrauma
foreach (Gap g in ConnectedGaps)
{
if (g.ConnectedDoor != null)
if (g.ConnectedDoor != null && !g.ConnectedDoor.IsBroken)
{
//gap blocked if the door is not open or the predicted state is not open
if (!g.ConnectedDoor.IsOpen || (g.ConnectedDoor.PredictedState.HasValue && !g.ConnectedDoor.PredictedState.Value))
@@ -660,7 +667,7 @@ namespace Barotrauma
if (g.linkedTo[i] is Hull hull && !connectedHulls.Contains(hull))
{
float dist = hull.GetApproximateHullDistance(g.Position, endPos, connectedHulls, target, distance + Vector2.Distance(startPos, g.Position), maxDistance);
if (dist < float.MaxValue) return dist;
if (dist < float.MaxValue) { return dist; }
}
}
}
@@ -40,12 +40,24 @@ namespace Barotrauma.RuinGeneration
this.rect = rect;
}
public void Split(float minDivRatio, float verticalProbability = 0.5f, int minWidth = 200)
public void Split(float minDivRatio, float verticalProbability = 0.5f, int minWidth = 200, int minHeight = 200)
{
subRooms = new BTRoom[2];
bool verticalSplit = Rand.Range(0.0f, rect.Height / (float)rect.Width, Rand.RandSync.Server) < verticalProbability;
if (rect.Width * minDivRatio < minWidth && rect.Height * minDivRatio < minHeight)
{
minDivRatio = 0.5f;
}
else if (rect.Width * minDivRatio < minWidth)
{
verticalSplit = false;
}
else if (rect.Height * minDivRatio < minHeight)
{
verticalSplit = true;
}
if (Rand.Range(0.0f, rect.Height / (float)rect.Width, Rand.RandSync.Server) < verticalProbability &&
rect.Width * minDivRatio >= minWidth)
subRooms = new BTRoom[2];
if (verticalSplit)
{
SplitVertical(minDivRatio);
}
@@ -69,12 +69,18 @@ namespace Barotrauma.RuinGeneration
set;
}
[Serialize(400, false, description: "The splitting algorithm attempts to keep the dimensions the split areas larger than this. For example, if the width of the split areas would be smaller than this after a vertical split, the algorithm will do a horizontal split."), Editable]
[Serialize(400, false, description: "The splitting algorithm attempts to keep the width of the split areas larger than this. If the width of the split areas would be smaller than this after a vertical split, the algorithm would do a horizontal split."), Editable]
public int MinSplitWidth
{
get;
set;
}
[Serialize(400, false, description: "The splitting algorithm attempts to keep the height of the split areas larger than this. If the height of the split areas would be smaller than this after a vertical split, the algorithm would do a horizontal split."), Editable]
public int MinSplitHeight
{
get;
set;
}
[Serialize("0.5,0.9", false, description: "The minimum and maximum width of a room relative to the areas created by the split algorithm."), Editable]
public Vector2 RoomWidthRange
@@ -239,7 +239,7 @@ namespace Barotrauma.RuinGeneration
for (int i = 0; i < iterations; i++)
{
rooms.ForEach(l => l.Split(0.3f, verticalProbability, generationParams.MinSplitWidth));
rooms.ForEach(l => l.Split(0.3f, verticalProbability, generationParams.MinSplitWidth, generationParams.MinSplitHeight));
rooms = baseRoom.GetLeaves();
}
@@ -559,6 +559,11 @@ namespace Barotrauma.RuinGeneration
foreach (MapEntity e in entities)
{
e.Move(doorOffset);
Door doorComponent = (e as Item)?.GetComponent<Door>();
if (doorComponent != null && !entities.Contains(doorComponent.LinkedGap))
{
doorComponent.LinkedGap.Move(doorOffset);
}
}
}
}
@@ -43,7 +43,7 @@ namespace Barotrauma
LocationConnection connection = Connections[(MissionsCompleted + i) % Connections.Count];
Location destination = connection.OtherLocation(this);
var mission = Mission.LoadRandom(new Location[] { this, destination }, rand, true, MissionType.Random, true);
var mission = Mission.LoadRandom(new Location[] { this, destination }, rand, true, MissionType.All, true);
if (mission == null) { continue; }
if (availableMissions.Any(m => m.Prefab == mission.Prefab)) { continue; }
if (GameSettings.VerboseLogging && mission != null)
@@ -65,7 +65,11 @@ namespace Barotrauma
public int SelectedMissionIndex
{
get { return availableMissions.IndexOf(SelectedMission); }
get
{
if (SelectedMission == null) { return -1; }
return availableMissions.IndexOf(SelectedMission);
}
set
{
if (value < 0 || value >= AvailableMissions.Count())

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