(3dc4135ce) v0.9.5.1
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+22
-16
@@ -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);
|
||||
|
||||
+229
-106
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+86
-66
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+41
-46
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+17
-10
@@ -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;
|
||||
}
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
+1
-2
@@ -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;
|
||||
|
||||
+74
-30
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+104
-85
@@ -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)
|
||||
|
||||
+104
-141
@@ -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;
|
||||
|
||||
+25
-18
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+22
-13
@@ -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);
|
||||
|
||||
+42
-33
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+22
-5
@@ -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)
|
||||
|
||||
+102
-106
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+11
-9
@@ -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?
|
||||
|
||||
+28
-5
@@ -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)]
|
||||
|
||||
Reference in New Issue
Block a user