(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)]
|
||||
|
||||
@@ -216,6 +216,8 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public bool NeedsRestart;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Name;
|
||||
|
||||
@@ -79,7 +79,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private static Queue<ColoredText> queuedMessages = new Queue<ColoredText>();
|
||||
private static readonly Queue<ColoredText> queuedMessages = new Queue<ColoredText>();
|
||||
|
||||
static partial void ShowHelpMessage(Command command);
|
||||
|
||||
@@ -90,7 +90,7 @@ namespace Barotrauma
|
||||
public delegate void QuestionCallback(string answer);
|
||||
private static QuestionCallback activeQuestionCallback;
|
||||
|
||||
private static List<Command> commands = new List<Command>();
|
||||
private static readonly List<Command> commands = new List<Command>();
|
||||
public static List<Command> Commands
|
||||
{
|
||||
get { return commands; }
|
||||
@@ -100,12 +100,12 @@ namespace Barotrauma
|
||||
private static int currentAutoCompletedIndex;
|
||||
|
||||
//used for keeping track of the message entered when pressing up/down
|
||||
static int selectedIndex;
|
||||
private static int selectedIndex;
|
||||
|
||||
public static bool CheatsEnabled;
|
||||
|
||||
private static List<ColoredText> unsavedMessages = new List<ColoredText>();
|
||||
private static int messagesPerFile = 5000;
|
||||
private static readonly List<ColoredText> unsavedMessages = new List<ColoredText>();
|
||||
private static readonly int messagesPerFile = 5000;
|
||||
public const string SavePath = "ConsoleLogs";
|
||||
|
||||
private static void AssignOnExecute(string names, Action<string[]> onExecute)
|
||||
@@ -113,7 +113,7 @@ namespace Barotrauma
|
||||
var matchingCommand = commands.Find(c => c.names.Intersect(names.Split('|')).Count() > 0);
|
||||
if (matchingCommand == null)
|
||||
{
|
||||
throw new Exception("AssignOnExecute failed. Command matching the name(s) \""+names+"\" not found.");
|
||||
throw new Exception("AssignOnExecute failed. Command matching the name(s) \"" + names + "\" not found.");
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -165,8 +165,7 @@ namespace Barotrauma
|
||||
NewMessage("***************", Color.Cyan);
|
||||
foreach (MapEntityPrefab ep in MapEntityPrefab.List)
|
||||
{
|
||||
var itemPrefab = ep as ItemPrefab;
|
||||
if (itemPrefab == null || itemPrefab.Name == null) continue;
|
||||
if (!(ep is ItemPrefab itemPrefab) || itemPrefab.Name == null) continue;
|
||||
string text = $"- {itemPrefab.Name}";
|
||||
if (itemPrefab.Tags.Any())
|
||||
{
|
||||
@@ -284,6 +283,8 @@ namespace Barotrauma
|
||||
NewMessage("Enemy AI enabled", Color.Green);
|
||||
}, isCheat: true));
|
||||
|
||||
commands.Add(new Command("starttraitormissionimmediately", "starttraitormissionimmediately: Skip the initial delay of the traitor mission and start one immediately.", null));
|
||||
|
||||
commands.Add(new Command("botcount", "botcount [x]: Set the number of bots in the crew in multiplayer.", null));
|
||||
|
||||
commands.Add(new Command("botspawnmode", "botspawnmode [fill/normal]: Set how bots are spawned in the multiplayer.", null));
|
||||
@@ -335,18 +336,19 @@ namespace Barotrauma
|
||||
|
||||
commands.Add(new Command("kick", "kick [name]: Kick a player out of the server.", (string[] args) =>
|
||||
{
|
||||
if (GameMain.NetworkMember == null || args.Length == 0) return;
|
||||
if (GameMain.NetworkMember == null || args.Length == 0) { return; }
|
||||
|
||||
string playerName = string.Join(" ", args);
|
||||
|
||||
ShowQuestionPrompt("Reason for kicking \"" + playerName + "\"?", (reason) =>
|
||||
ShowQuestionPrompt("Reason for kicking \"" + playerName + "\"? (Enter c to cancel)", (reason) =>
|
||||
{
|
||||
if (reason == "c" || reason == "C") { return; }
|
||||
GameMain.NetworkMember.KickPlayer(playerName, reason);
|
||||
});
|
||||
},
|
||||
() =>
|
||||
{
|
||||
if (GameMain.NetworkMember == null) return null;
|
||||
if (GameMain.NetworkMember == null) { return null; }
|
||||
|
||||
return new string[][]
|
||||
{
|
||||
@@ -366,8 +368,9 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
|
||||
ShowQuestionPrompt("Reason for kicking \"" + client.Name + "\"?", (reason) =>
|
||||
ShowQuestionPrompt("Reason for kicking \"" + client.Name + "\"? (Enter c to cancel)", (reason) =>
|
||||
{
|
||||
if (reason == "c" || reason == "C") { return; }
|
||||
GameMain.NetworkMember.KickPlayer(client.Name, reason);
|
||||
});
|
||||
}));
|
||||
@@ -377,10 +380,12 @@ namespace Barotrauma
|
||||
if (GameMain.NetworkMember == null || args.Length == 0) return;
|
||||
|
||||
string clientName = string.Join(" ", args);
|
||||
ShowQuestionPrompt("Reason for banning \"" + clientName + "\"?", (reason) =>
|
||||
ShowQuestionPrompt("Reason for banning \"" + clientName + "\"? (Enter c to cancel)", (reason) =>
|
||||
{
|
||||
ShowQuestionPrompt("Enter the duration of the ban (leave empty to ban permanently, or use the format \"[days] d [hours] h\")", (duration) =>
|
||||
if (reason == "c" || reason == "C") { return; }
|
||||
ShowQuestionPrompt("Enter the duration of the ban (leave empty to ban permanently, or use the format \"[days] d [hours] h\") (Enter c to cancel)", (duration) =>
|
||||
{
|
||||
if (duration == "c" || duration == "C") { return; }
|
||||
TimeSpan? banDuration = null;
|
||||
if (!string.IsNullOrWhiteSpace(duration))
|
||||
{
|
||||
@@ -418,10 +423,12 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
|
||||
ShowQuestionPrompt("Reason for banning \"" + client.Name + "\"?", (reason) =>
|
||||
ShowQuestionPrompt("Reason for banning \"" + client.Name + "\"? (Enter c to cancel)", (reason) =>
|
||||
{
|
||||
ShowQuestionPrompt("Enter the duration of the ban (leave empty to ban permanently, or use the format \"[days] d [hours] h\")", (duration) =>
|
||||
if (reason == "c" || reason == "C") { return; }
|
||||
ShowQuestionPrompt("Enter the duration of the ban (leave empty to ban permanently, or use the format \"[days] d [hours] h\") (c to cancel)", (duration) =>
|
||||
{
|
||||
if (duration == "c" || duration == "C") { return; }
|
||||
TimeSpan? banDuration = null;
|
||||
if (!string.IsNullOrWhiteSpace(duration))
|
||||
{
|
||||
@@ -874,8 +881,7 @@ namespace Barotrauma
|
||||
|
||||
commands.Add(new Command("campaigninfo|campaignstatus", "campaigninfo: Display information about the state of the currently active campaign.", (string[] args) =>
|
||||
{
|
||||
var campaign = GameMain.GameSession?.GameMode as CampaignMode;
|
||||
if (campaign == null)
|
||||
if (!(GameMain.GameSession?.GameMode is CampaignMode campaign))
|
||||
{
|
||||
ThrowError("No campaign active!");
|
||||
return;
|
||||
@@ -886,8 +892,7 @@ namespace Barotrauma
|
||||
|
||||
commands.Add(new Command("campaigndestination|setcampaigndestination", "campaigndestination [index]: Set the location to head towards in the currently active campaign.", (string[] args) =>
|
||||
{
|
||||
var campaign = GameMain.GameSession?.GameMode as CampaignMode;
|
||||
if (campaign == null)
|
||||
if (!(GameMain.GameSession?.GameMode is CampaignMode campaign))
|
||||
{
|
||||
ThrowError("No campaign active!");
|
||||
return;
|
||||
@@ -938,7 +943,6 @@ namespace Barotrauma
|
||||
NewMessage((GameSettings.VerboseLogging ? "Enabled" : "Disabled") + " verbose logging.", Color.White);
|
||||
}, isCheat: false));
|
||||
|
||||
|
||||
commands.Add(new Command("calculatehashes", "calculatehashes [content package name]: Show the MD5 hashes of the files in the selected content package. If the name parameter is omitted, the first content package is selected.", (string[] args) =>
|
||||
{
|
||||
if (args.Length > 0)
|
||||
@@ -967,7 +971,21 @@ namespace Barotrauma
|
||||
};
|
||||
}));
|
||||
|
||||
#if DEBUG
|
||||
commands.Add(new Command("debugai", "", onExecute: (string[] args) =>
|
||||
{
|
||||
var commands = new List<KeyValuePair<string, string[]>>()
|
||||
{
|
||||
new KeyValuePair<string, string[]>("debugdraw", new string[]{ "true" }),
|
||||
new KeyValuePair<string, string[]>("los", new string[]{ "false" }),
|
||||
new KeyValuePair<string, string[]>("lights", new string[]{ "false" }),
|
||||
new KeyValuePair<string, string[]>("freecam", new string[0]),
|
||||
};
|
||||
foreach (var command in commands)
|
||||
{
|
||||
Commands.Find(c => c.names.Any(n => n.Equals(command.Key, StringComparison.OrdinalIgnoreCase)))?.Execute(command.Value);
|
||||
}
|
||||
}));
|
||||
|
||||
commands.Add(new Command("simulatedlatency", "simulatedlatency [minimumlatencyseconds] [randomlatencyseconds]: applies a simulated latency to network messages. Useful for simulating real network conditions when testing the multiplayer locally.", (string[] args) =>
|
||||
{
|
||||
if (args.Count() < 2 || (GameMain.NetworkMember == null)) return;
|
||||
@@ -1039,7 +1057,6 @@ namespace Barotrauma
|
||||
#endif
|
||||
NewMessage("Set packet duplication to " + (int)(duplicates * 100) + "%.", Color.White);
|
||||
}));
|
||||
#endif
|
||||
|
||||
//"dummy commands" that only exist so that the server can give clients permissions to use them
|
||||
//TODO: alphabetical order?
|
||||
@@ -1170,16 +1187,6 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private static string AutoCompleteStr(string str, IEnumerable<string> validStrings)
|
||||
{
|
||||
if (string.IsNullOrEmpty(str)) return str;
|
||||
foreach (string validStr in validStrings)
|
||||
{
|
||||
if (validStr.Length > str.Length && validStr.Substring(0, str.Length) == str) return validStr;
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
public static void ResetAutoComplete()
|
||||
{
|
||||
currentAutoCompletedCommand = "";
|
||||
@@ -1197,7 +1204,7 @@ namespace Barotrauma
|
||||
{
|
||||
selectedIndex += direction;
|
||||
if (selectedIndex < 0) selectedIndex = Messages.Count - 1;
|
||||
selectedIndex = selectedIndex % Messages.Count;
|
||||
selectedIndex %= Messages.Count;
|
||||
if (++i >= Messages.Count) break;
|
||||
} while (!Messages[selectedIndex].IsCommand || Messages[selectedIndex].Text == currentText);
|
||||
|
||||
@@ -1257,13 +1264,6 @@ namespace Barotrauma
|
||||
|
||||
return;
|
||||
}
|
||||
#if !DEBUG
|
||||
if (!IsCommandPermitted(splitCommand[0].ToLowerInvariant(), GameMain.Client))
|
||||
{
|
||||
ThrowError("You're not permitted to use the command \"" + splitCommand[0].ToLowerInvariant() + "\"!");
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1509,9 +1509,7 @@ namespace Barotrauma
|
||||
|
||||
public static void NewMessage(string msg, Color color, bool isCommand = false)
|
||||
{
|
||||
if (string.IsNullOrEmpty((msg))) return;
|
||||
|
||||
var newMsg = new ColoredText(msg, color, isCommand);
|
||||
if (string.IsNullOrEmpty(msg)) { return; }
|
||||
|
||||
lock (queuedMessages)
|
||||
{
|
||||
|
||||
@@ -10,6 +10,7 @@ namespace Barotrauma
|
||||
{
|
||||
public static readonly List<EventManagerSettings> List = new List<EventManagerSettings>();
|
||||
|
||||
public readonly string Identifier;
|
||||
public readonly string Name;
|
||||
|
||||
//How much the event threshold increases per second. 0.0005f = 0.03f per minute
|
||||
@@ -48,28 +49,30 @@ namespace Barotrauma
|
||||
foreach (XElement subElement in mainElement.Elements())
|
||||
{
|
||||
var element = subElement.IsOverride() ? subElement.FirstElement() : subElement;
|
||||
string name = element.Name.ToString();
|
||||
var duplicate = List.FirstOrDefault(e => e.Name.ToString().Equals(name, StringComparison.OrdinalIgnoreCase));
|
||||
string identifier = element.Name.ToString();
|
||||
var duplicate = List.FirstOrDefault(e => e.Identifier.ToString().Equals(identifier, StringComparison.OrdinalIgnoreCase));
|
||||
if (duplicate != null)
|
||||
{
|
||||
if (allowOverriding || subElement.IsOverride())
|
||||
{
|
||||
DebugConsole.NewMessage($"Overriding the existing preset '{name}' in the event manager settings using the file '{file}'", Color.Yellow);
|
||||
DebugConsole.NewMessage($"Overriding the existing preset '{identifier}' in the event manager settings using the file '{file}'", Color.Yellow);
|
||||
List.Remove(duplicate);
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in '{file}': Another element with the name '{name}' found! Each element must have a unique name. Use <override></override> tags if you want to override an existing preset.");
|
||||
DebugConsole.ThrowError($"Error in '{file}': Another element with the name '{identifier}' found! Each element must have a unique name. Use <override></override> tags if you want to override an existing preset.");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
List.Add(new EventManagerSettings(element));
|
||||
}
|
||||
List.Sort((x, y) => { return Math.Sign((x.MinLevelDifficulty + x.MaxLevelDifficulty) / 2.0f - (y.MinLevelDifficulty + y.MaxLevelDifficulty) / 2.0f); });
|
||||
}
|
||||
|
||||
public EventManagerSettings(XElement element)
|
||||
{
|
||||
Name = element.Name.ToString();
|
||||
Identifier = element.Name.ToString();
|
||||
Name = TextManager.Get("difficulty." + Identifier, returnNull: true) ?? Identifier;
|
||||
EventThresholdIncrease = element.GetAttributeFloat("EventThresholdIncrease", 0.0005f);
|
||||
DefaultEventThreshold = element.GetAttributeFloat("DefaultEventThreshold", 0.2f);
|
||||
EventCooldown = element.GetAttributeFloat("EventCooldown", 360.0f);
|
||||
|
||||
@@ -9,7 +9,25 @@ namespace Barotrauma
|
||||
{
|
||||
public readonly MissionPrefab Prefab;
|
||||
protected bool completed;
|
||||
|
||||
protected int state;
|
||||
public int State
|
||||
{
|
||||
get { return state; }
|
||||
protected set
|
||||
{
|
||||
if (state != value)
|
||||
{
|
||||
state = value;
|
||||
#if SERVER
|
||||
GameMain.Server?.UpdateMissionState(state);
|
||||
#endif
|
||||
ShowMessage(State);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected bool IsClient => GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient;
|
||||
|
||||
public readonly List<string> Headers;
|
||||
public readonly List<string> Messages;
|
||||
|
||||
@@ -107,17 +125,13 @@ namespace Barotrauma
|
||||
public static Mission LoadRandom(Location[] locations, MTRandom rand, bool requireCorrectLocationType, MissionType missionType, bool isSinglePlayer = false)
|
||||
{
|
||||
List<MissionPrefab> allowedMissions = new List<MissionPrefab>();
|
||||
if (missionType == MissionType.Random)
|
||||
{
|
||||
allowedMissions.AddRange(MissionPrefab.List);
|
||||
}
|
||||
else if (missionType == MissionType.None)
|
||||
if (missionType == MissionType.None)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
allowedMissions = MissionPrefab.List.FindAll(m => m.type == missionType);
|
||||
allowedMissions.AddRange(MissionPrefab.List.Where(m => ((int)(missionType & m.type)) != 0));
|
||||
}
|
||||
|
||||
allowedMissions.RemoveAll(m => isSinglePlayer ? m.MultiplayerOnly : m.SingleplayerOnly);
|
||||
|
||||
@@ -6,14 +6,15 @@ using Microsoft.Xna.Framework;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
[Flags]
|
||||
public enum MissionType
|
||||
{
|
||||
Random,
|
||||
None,
|
||||
Salvage,
|
||||
Monster,
|
||||
Cargo,
|
||||
Combat
|
||||
None = 0x0,
|
||||
Salvage = 0x1,
|
||||
Monster = 0x2,
|
||||
Cargo = 0x4,
|
||||
Combat = 0x8,
|
||||
All = 0xf
|
||||
}
|
||||
|
||||
partial class MissionPrefab
|
||||
@@ -155,11 +156,6 @@ namespace Barotrauma
|
||||
DebugConsole.ThrowError("Error in mission prefab \"" + Name + "\" - \"" + missionTypeName + "\" is not a valid mission type.");
|
||||
return;
|
||||
}
|
||||
if (type == MissionType.Random)
|
||||
{
|
||||
DebugConsole.ThrowError("Error in mission prefab \"" + Name + "\" - mission type cannot be random.");
|
||||
return;
|
||||
}
|
||||
if (type == MissionType.None)
|
||||
{
|
||||
DebugConsole.ThrowError("Error in mission prefab \"" + Name + "\" - mission type cannot be none.");
|
||||
|
||||
@@ -1,34 +1,41 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class MonsterMission : Mission
|
||||
{
|
||||
private string monsterFile;
|
||||
|
||||
private int state;
|
||||
|
||||
private int monsterCount;
|
||||
|
||||
private readonly string monsterFile;
|
||||
private readonly int monsterCount;
|
||||
private readonly HashSet<Tuple<string, int>> monsterFiles = new HashSet<Tuple<string, int>>();
|
||||
private readonly List<Character> monsters = new List<Character>();
|
||||
private readonly List<Vector2> sonarPositions = new List<Vector2>();
|
||||
|
||||
public override IEnumerable<Vector2> SonarPositions
|
||||
{
|
||||
get
|
||||
{
|
||||
return sonarPositions;
|
||||
}
|
||||
}
|
||||
public override IEnumerable<Vector2> SonarPositions => sonarPositions;
|
||||
|
||||
public MonsterMission(MissionPrefab prefab, Location[] locations)
|
||||
: base(prefab, locations)
|
||||
{
|
||||
monsterFile = prefab.ConfigElement.GetAttributeString("monsterfile", "");
|
||||
monsterFile = prefab.ConfigElement.GetAttributeString("monsterfile", null);
|
||||
monsterCount = prefab.ConfigElement.GetAttributeInt("monstercount", 1);
|
||||
|
||||
foreach (var monsterElement in prefab.ConfigElement.GetChildElements("monster"))
|
||||
{
|
||||
string monster = monsterElement.GetAttributeString("character", string.Empty);
|
||||
if (monsterFile == null)
|
||||
{
|
||||
monsterFile = monster;
|
||||
}
|
||||
int defaultCount = monsterElement.GetAttributeInt("count", -1);
|
||||
if (defaultCount < 0)
|
||||
{
|
||||
defaultCount = monsterElement.GetAttributeInt("amount", 1);
|
||||
}
|
||||
int min = monsterElement.GetAttributeInt("min", defaultCount);
|
||||
int max = Math.Max(min, monsterElement.GetAttributeInt("max", defaultCount));
|
||||
monsterFiles.Add(new Tuple<string, int>(monster, Rand.Range(min, max + 1, Rand.RandSync.Server)));
|
||||
}
|
||||
description = description.Replace("[monster]",
|
||||
TextManager.Get("character." + System.IO.Path.GetFileNameWithoutExtension(monsterFile)));
|
||||
}
|
||||
@@ -37,11 +44,22 @@ namespace Barotrauma
|
||||
{
|
||||
Level.Loaded.TryGetInterestingPosition(true, Level.PositionType.MainPath, Level.Loaded.Size.X * 0.3f, out Vector2 spawnPos);
|
||||
|
||||
bool isClient = GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient;
|
||||
for (int i = 0; i < monsterCount; i++)
|
||||
bool isClient = IsClient;
|
||||
if (!string.IsNullOrEmpty(monsterFile))
|
||||
{
|
||||
monsters.Add(Character.Create(monsterFile, spawnPos, ToolBox.RandomSeed(8), null, isClient, true, false));
|
||||
for (int i = 0; i < monsterCount; i++)
|
||||
{
|
||||
monsters.Add(Character.Create(monsterFile, spawnPos, ToolBox.RandomSeed(8), null, isClient, true, false));
|
||||
}
|
||||
}
|
||||
foreach (var monster in monsterFiles)
|
||||
{
|
||||
for (int i = 0; i < monster.Item2; i++)
|
||||
{
|
||||
monsters.Add(Character.Create(monster.Item1, spawnPos, ToolBox.RandomSeed(8), null, isClient, true, false));
|
||||
}
|
||||
}
|
||||
|
||||
monsters.ForEach(m => m.Enabled = false);
|
||||
SwarmBehavior.CreateSwarm(monsters.Cast<AICharacter>());
|
||||
sonarPositions.Add(spawnPos);
|
||||
@@ -49,40 +67,35 @@ namespace Barotrauma
|
||||
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
switch (state)
|
||||
switch (State)
|
||||
{
|
||||
case 0:
|
||||
sonarPositions.Clear();
|
||||
var activeMonsters = monsters.Where(m => m != null && !m.Removed && !m.IsDead);
|
||||
if (activeMonsters.Any())
|
||||
foreach (var monster in monsters)
|
||||
{
|
||||
Vector2 centerOfMass = Vector2.Zero;
|
||||
foreach (var monster in activeMonsters)
|
||||
if (monster.Removed || monster.IsDead) { continue; }
|
||||
//don't add another label if there's another monster roughly at the same spot
|
||||
if (sonarPositions.All(p => Vector2.DistanceSquared(p, monster.Position) > 1000.0f * 1000.0f))
|
||||
{
|
||||
//don't add another label if there's another monster roughly at the same spot
|
||||
if (sonarPositions.All(p => Vector2.DistanceSquared(p, monster.Position) > 1000.0f * 1000.0f))
|
||||
{
|
||||
sonarPositions.Add(monster.Position);
|
||||
}
|
||||
sonarPositions.Add(monster.Position);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (activeMonsters.Any()) { return; }
|
||||
|
||||
ShowMessage(state);
|
||||
|
||||
state = 1;
|
||||
if (!IsClient && monsters.All(m => IsEliminated(m)))
|
||||
{
|
||||
State = 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public override void End()
|
||||
{
|
||||
if (!monsters.All(m => m.Removed || m.IsDead)) { return; }
|
||||
if (State < 1) { return; }
|
||||
|
||||
GiveReward();
|
||||
completed = true;
|
||||
}
|
||||
|
||||
public bool IsEliminated(Character enemy) => enemy.Removed || enemy.IsDead || enemy.AIController is EnemyAIController ai && ai.State == AIState.Flee;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,13 +14,11 @@ namespace Barotrauma
|
||||
|
||||
private Level.PositionType spawnPositionType;
|
||||
|
||||
private int state;
|
||||
|
||||
public override IEnumerable<Vector2> SonarPositions
|
||||
{
|
||||
get
|
||||
{
|
||||
if (state > 0 )
|
||||
if (State > 0 )
|
||||
{
|
||||
Enumerable.Empty<Vector2>();
|
||||
}
|
||||
@@ -87,34 +85,27 @@ namespace Barotrauma
|
||||
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
switch (state)
|
||||
if (IsClient) { return; }
|
||||
switch (State)
|
||||
{
|
||||
case 0:
|
||||
//item.body.LinearVelocity = Vector2.Zero;
|
||||
if (item.ParentInventory != null) item.body.FarseerBody.IsKinematic = false;
|
||||
if (item.CurrentHull?.Submarine == null) return;
|
||||
|
||||
ShowMessage(state);
|
||||
|
||||
state = 1;
|
||||
if (item.ParentInventory != null) { item.body.FarseerBody.IsKinematic = false; }
|
||||
if (item.CurrentHull?.Submarine == null) { return; }
|
||||
State = 1;
|
||||
break;
|
||||
case 1:
|
||||
if (!Submarine.MainSub.AtEndPosition && !Submarine.MainSub.AtStartPosition) return;
|
||||
|
||||
ShowMessage(state);
|
||||
|
||||
state = 2;
|
||||
if (!Submarine.MainSub.AtEndPosition && !Submarine.MainSub.AtStartPosition) { return; }
|
||||
State = 2;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public override void End()
|
||||
{
|
||||
if (item.CurrentHull?.Submarine == null || !item.CurrentHull.Submarine.AtEndPosition || item.Removed) return;
|
||||
if (item.CurrentHull?.Submarine == null || !item.CurrentHull.Submarine.AtEndPosition || item.Removed) { return; }
|
||||
|
||||
item.Remove();
|
||||
|
||||
GiveReward();
|
||||
|
||||
completed = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,5 +110,60 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static ICollection<string> ParseCommaSeparatedStringToCollection(string input, ICollection<string> texts = null, bool convertToLowerInvariant = true)
|
||||
{
|
||||
if (texts == null)
|
||||
{
|
||||
texts = new HashSet<string>();
|
||||
}
|
||||
else
|
||||
{
|
||||
texts.Clear();
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(input))
|
||||
{
|
||||
foreach (string value in input.Split(','))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value)) { continue; }
|
||||
if (convertToLowerInvariant)
|
||||
{
|
||||
texts.Add(value.ToLowerInvariant());
|
||||
}
|
||||
else
|
||||
{
|
||||
texts.Add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return texts;
|
||||
}
|
||||
|
||||
public static ICollection<string> ParseSeparatedStringToCollection(string input, string[] separators, ICollection<string> texts = null, bool convertToLowerInvariant = true)
|
||||
{
|
||||
if (texts == null)
|
||||
{
|
||||
texts = new HashSet<string>();
|
||||
}
|
||||
else
|
||||
{
|
||||
texts.Clear();
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(input))
|
||||
{
|
||||
foreach (string value in input.Split(separators, StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
if (convertToLowerInvariant)
|
||||
{
|
||||
texts.Add(value.ToLowerInvariant());
|
||||
}
|
||||
else
|
||||
{
|
||||
texts.Add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return texts;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,8 +83,16 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public static void AddErrorEventOnce(string identifier, EGAErrorSeverity errorSeverity, string message)
|
||||
{
|
||||
if (!GameSettings.SendUserStatistics) return;
|
||||
if (sentEventIdentifiers.Contains(identifier)) return;
|
||||
if (!GameSettings.SendUserStatistics) { return; }
|
||||
if (sentEventIdentifiers.Contains(identifier)) { return; }
|
||||
|
||||
if (GameMain.SelectedPackages != null)
|
||||
{
|
||||
if (GameMain.VanillaContent == null || GameMain.SelectedPackages.Any(p => p.HasMultiplayerIncompatibleContent && p != GameMain.VanillaContent))
|
||||
{
|
||||
message = "[MODDED] " + message;
|
||||
}
|
||||
}
|
||||
|
||||
GameAnalytics.AddErrorEvent(errorSeverity, message);
|
||||
sentEventIdentifiers.Add(identifier);
|
||||
|
||||
@@ -104,6 +104,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (c.Character?.Info != null && !c.Character.IsDead)
|
||||
{
|
||||
c.CharacterInfo = c.Character.Info;
|
||||
characterData.Add(new CharacterCampaignData(c));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,8 @@ namespace Barotrauma
|
||||
public bool VoipAttenuationEnabled { get; set; }
|
||||
public bool UseDirectionalVoiceChat { get; set; }
|
||||
|
||||
public IList<string> CaptureDeviceNames;
|
||||
|
||||
public enum VoiceMode
|
||||
{
|
||||
Disabled,
|
||||
@@ -69,7 +71,7 @@ namespace Barotrauma
|
||||
|
||||
private LosMode losMode;
|
||||
|
||||
public List<string> jobPreferences;
|
||||
public List<Pair<string, int>> jobPreferences;
|
||||
|
||||
private bool useSteamMatchmaking;
|
||||
private bool requireSteamAuthentication;
|
||||
@@ -119,7 +121,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public List<string> JobPreferences
|
||||
public List<Pair<string, int>> JobPreferences
|
||||
{
|
||||
get { return jobPreferences; }
|
||||
set { jobPreferences = value; }
|
||||
@@ -211,12 +213,13 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public const float MaxMicrophoneVolume = 10.0f;
|
||||
public float MicrophoneVolume
|
||||
{
|
||||
get { return microphoneVolume; }
|
||||
set
|
||||
{
|
||||
microphoneVolume = MathHelper.Clamp(value, 0.2f, 10.0f);
|
||||
microphoneVolume = MathHelper.Clamp(value, 0.2f, MaxMicrophoneVolume);
|
||||
}
|
||||
}
|
||||
public string Language
|
||||
@@ -232,6 +235,7 @@ namespace Barotrauma
|
||||
if (!SelectedContentPackages.Contains(contentPackage))
|
||||
{
|
||||
SelectedContentPackages.Add(contentPackage);
|
||||
contentPackage.NeedsRestart |= contentPackage.HasMultiplayerIncompatibleContent;
|
||||
ContentPackage.SortContentPackages();
|
||||
}
|
||||
}
|
||||
@@ -241,6 +245,7 @@ namespace Barotrauma
|
||||
if (SelectedContentPackages.Contains(contentPackage))
|
||||
{
|
||||
SelectedContentPackages.Remove(contentPackage);
|
||||
contentPackage.NeedsRestart |= contentPackage.HasMultiplayerIncompatibleContent;
|
||||
ContentPackage.SortContentPackages();
|
||||
}
|
||||
}
|
||||
@@ -442,11 +447,7 @@ namespace Barotrauma
|
||||
GraphicsHeight = 768;
|
||||
MasterServerUrl = "";
|
||||
SelectContentPackage(ContentPackage.List.Any() ? ContentPackage.List[0] : new ContentPackage(""));
|
||||
jobPreferences = new List<string>();
|
||||
foreach (string job in JobPrefab.List.Keys)
|
||||
{
|
||||
jobPreferences.Add(job);
|
||||
}
|
||||
jobPreferences = new List<Pair<string, int>>();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -570,9 +571,12 @@ namespace Barotrauma
|
||||
|
||||
var gameplay = new XElement("gameplay");
|
||||
var jobPreferences = new XElement("jobpreferences");
|
||||
foreach (string jobName in JobPreferences)
|
||||
foreach (Pair<string, int> job in JobPreferences)
|
||||
{
|
||||
jobPreferences.Add(new XElement("job", new XAttribute("identifier", jobName)));
|
||||
XElement jobElement = new XElement("job");
|
||||
jobElement.Add(new XAttribute("identifier", job.First));
|
||||
jobElement.Add(new XAttribute("variant", job.Second));
|
||||
jobPreferences.Add(jobElement);
|
||||
}
|
||||
gameplay.Add(jobPreferences);
|
||||
doc.Root.Add(gameplay);
|
||||
@@ -890,9 +894,12 @@ namespace Barotrauma
|
||||
|
||||
var gameplay = new XElement("gameplay");
|
||||
var jobPreferences = new XElement("jobpreferences");
|
||||
foreach (string jobName in JobPreferences)
|
||||
foreach (Pair<string, int> job in JobPreferences)
|
||||
{
|
||||
jobPreferences.Add(new XElement("job", new XAttribute("identifier", jobName)));
|
||||
XElement jobElement = new XElement("job");
|
||||
jobElement.Add(new XAttribute("identifier", job.First));
|
||||
jobElement.Add(new XAttribute("variant", job.Second));
|
||||
jobPreferences.Add(jobElement);
|
||||
}
|
||||
gameplay.Add(jobPreferences);
|
||||
doc.Root.Add(gameplay);
|
||||
@@ -972,14 +979,19 @@ namespace Barotrauma
|
||||
CampaignDisclaimerShown = doc.Root.GetAttributeBool("campaigndisclaimershown", CampaignDisclaimerShown);
|
||||
EditorDisclaimerShown = doc.Root.GetAttributeBool("editordisclaimershown", EditorDisclaimerShown);
|
||||
XElement gameplayElement = doc.Root.Element("gameplay");
|
||||
jobPreferences = new List<Pair<string, int>>();
|
||||
if (gameplayElement != null)
|
||||
{
|
||||
jobPreferences = new List<string>();
|
||||
foreach (XElement ele in gameplayElement.Element("jobpreferences").Elements("job"))
|
||||
var preferencesElement = gameplayElement.Element("jobpreferences");
|
||||
if (preferencesElement != null)
|
||||
{
|
||||
string jobIdentifier = ele.GetAttributeString("identifier", "");
|
||||
if (string.IsNullOrEmpty(jobIdentifier)) continue;
|
||||
jobPreferences.Add(jobIdentifier);
|
||||
foreach (XElement ele in preferencesElement.Elements("job"))
|
||||
{
|
||||
string jobIdentifier = ele.GetAttributeString("identifier", "");
|
||||
int outfitVariant = ele.GetAttributeInt("variant", 1);
|
||||
if (string.IsNullOrEmpty(jobIdentifier)) continue;
|
||||
jobPreferences.Add(new Pair<string, int>(jobIdentifier, outfitVariant));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,9 +20,9 @@ namespace Barotrauma.Items.Components
|
||||
private bool isOpen;
|
||||
|
||||
private float openState;
|
||||
private Sprite doorSprite, weldedSprite, brokenSprite;
|
||||
private bool scaleBrokenSprite, fadeBrokenSprite;
|
||||
private bool autoOrientGap;
|
||||
private readonly Sprite doorSprite, weldedSprite, brokenSprite;
|
||||
private readonly bool scaleBrokenSprite, fadeBrokenSprite;
|
||||
private readonly bool autoOrientGap;
|
||||
|
||||
private bool isStuck;
|
||||
public bool IsStuck => isStuck;
|
||||
@@ -221,8 +221,8 @@ namespace Barotrauma.Items.Components
|
||||
#endif
|
||||
}
|
||||
|
||||
private string accessDeniedTxt = TextManager.Get("AccessDenied");
|
||||
private string cannotOpenText = TextManager.Get("DoorMsgCannotOpen");
|
||||
private readonly string accessDeniedTxt = TextManager.Get("AccessDenied");
|
||||
private readonly string cannotOpenText = TextManager.Get("DoorMsgCannotOpen");
|
||||
private bool hasValidIdCard;
|
||||
public override bool HasRequiredItems(Character character, bool addMessage, string msg = null)
|
||||
{
|
||||
@@ -272,14 +272,15 @@ namespace Barotrauma.Items.Components
|
||||
ToggleState(ActionType.OnUse);
|
||||
PickingTime = originalPickingTime;
|
||||
}
|
||||
else if (hasRequiredItems)
|
||||
{
|
||||
#if CLIENT
|
||||
else if (hasRequiredItems && character != null && character == Character.Controlled)
|
||||
{
|
||||
GUI.AddMessage(accessDeniedTxt, Color.Red);
|
||||
#endif
|
||||
|
||||
}
|
||||
#endif
|
||||
}
|
||||
return item.Condition <= RepairThreshold;
|
||||
return false;
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
@@ -340,6 +341,13 @@ namespace Barotrauma.Items.Components
|
||||
if (!Impassable)
|
||||
{
|
||||
Body.FarseerBody.IsSensor = false;
|
||||
var ce = Body.FarseerBody.ContactList;
|
||||
while (ce != null && ce.Contact != null)
|
||||
{
|
||||
ce.Contact.Enabled = false;
|
||||
ce = ce.Next;
|
||||
}
|
||||
PushCharactersAway();
|
||||
}
|
||||
#if CLIENT
|
||||
UpdateConvexHulls();
|
||||
@@ -354,6 +362,12 @@ namespace Barotrauma.Items.Components
|
||||
if (!Impassable)
|
||||
{
|
||||
Body.FarseerBody.IsSensor = true;
|
||||
var ce = Body.FarseerBody.ContactList;
|
||||
while (ce != null && ce.Contact != null)
|
||||
{
|
||||
ce.Contact.Enabled = false;
|
||||
ce = ce.Next;
|
||||
}
|
||||
}
|
||||
linkedGap.Open = 1.0f;
|
||||
IsOpen = false;
|
||||
@@ -413,15 +427,14 @@ namespace Barotrauma.Items.Components
|
||||
//otherwise the gap will be removed twice and cause console warnings
|
||||
if (!Submarine.Unloading)
|
||||
{
|
||||
if (linkedGap != null) linkedGap.Remove();
|
||||
linkedGap?.Remove();
|
||||
}
|
||||
|
||||
doorSprite.Remove();
|
||||
if (weldedSprite != null) weldedSprite.Remove();
|
||||
doorSprite?.Remove();
|
||||
weldedSprite?.Remove();
|
||||
|
||||
#if CLIENT
|
||||
if (convexHull != null) convexHull.Remove();
|
||||
if (convexHull2 != null) convexHull2.Remove();
|
||||
convexHull?.Remove();
|
||||
convexHull2?.Remove();
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -474,7 +487,6 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
private bool PushBodyOutOfDoorway(Character c, PhysicsBody body, int dir, Vector2 doorRectSimPos, Vector2 doorRectSimSize)
|
||||
{
|
||||
float diff = 0.0f;
|
||||
if (!MathUtils.IsValid(body.SimPosition))
|
||||
{
|
||||
DebugConsole.ThrowError("Failed to push a limb out of a doorway - position of the body (character \"" + c.Name + "\") is not valid (" + body.SimPosition + ")");
|
||||
@@ -484,7 +496,8 @@ namespace Barotrauma.Items.Components
|
||||
" Remoteplayer: " + c.IsRemotePlayer);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
float diff;
|
||||
if (IsHorizontal)
|
||||
{
|
||||
if (body.SimPosition.X < doorRectSimPos.X || body.SimPosition.X > doorRectSimPos.X + doorRectSimSize.X) { return false; }
|
||||
|
||||
@@ -131,7 +131,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (charging)
|
||||
{
|
||||
if (voltage > minVoltage || powerConsumption <= 0.0f)
|
||||
if (Voltage > MinVoltage)
|
||||
{
|
||||
Discharge();
|
||||
}
|
||||
@@ -142,8 +142,6 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
IsActive = false;
|
||||
}
|
||||
|
||||
voltage = 0.0f;
|
||||
}
|
||||
|
||||
public override void UpdateBroken(float deltaTime, Camera cam)
|
||||
@@ -455,6 +453,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
protected override void RemoveComponentSpecific()
|
||||
{
|
||||
base.RemoveComponentSpecific();
|
||||
list.Remove(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,9 +242,19 @@ namespace Barotrauma.Items.Components
|
||||
User = null;
|
||||
}
|
||||
|
||||
//ignore collision if there's a wall between the user and the weapon to prevent hitting through walls
|
||||
if (Submarine.PickBody(User.AnimController.AimSourceSimPos,
|
||||
item.SimPosition,
|
||||
collisionCategory: Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking,
|
||||
allowInsideFixture: true) != null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Character targetCharacter = null;
|
||||
Limb targetLimb = null;
|
||||
Structure targetStructure = null;
|
||||
Item targetItem = null;
|
||||
|
||||
attack?.SetUser(User);
|
||||
|
||||
@@ -292,6 +302,19 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
hitTargets.Add(targetStructure);
|
||||
}
|
||||
else if (f2.Body.UserData is Item)
|
||||
{
|
||||
targetItem = (Item)f2.Body.UserData;
|
||||
if (AllowHitMultiple)
|
||||
{
|
||||
if (hitTargets.Contains(targetItem)) { return true; }
|
||||
}
|
||||
else
|
||||
{
|
||||
if (hitTargets.Any(t => t is Item)) { return true; }
|
||||
}
|
||||
hitTargets.Add(targetItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
@@ -313,6 +336,10 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
attack.DoDamage(User, targetStructure, item.WorldPosition, 1.0f);
|
||||
}
|
||||
else if (targetItem != null && targetItem.Prefab.DamagedByMeleeWeapons)
|
||||
{
|
||||
attack.DoDamage(User, targetItem, item.WorldPosition, 1.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
|
||||
@@ -127,6 +127,7 @@ namespace Barotrauma.Items.Components
|
||||
if (activeTimer <= 0.0f) IsActive = false;
|
||||
}
|
||||
|
||||
private List<Body> ignoredBodies = new List<Body>();
|
||||
public override bool Use(float deltaTime, Character character = null)
|
||||
{
|
||||
if (character == null || character.Removed) return false;
|
||||
@@ -185,7 +186,7 @@ namespace Barotrauma.Items.Components
|
||||
(float)Math.Cos(angle),
|
||||
(float)Math.Sin(angle)) * Range * item.body.Dir);
|
||||
|
||||
List<Body> ignoredBodies = new List<Body>();
|
||||
ignoredBodies.Clear();
|
||||
foreach (Limb limb in character.AnimController.Limbs)
|
||||
{
|
||||
if (Rand.Range(0.0f, 0.5f) > degreeOfSuccess) continue;
|
||||
@@ -438,27 +439,48 @@ namespace Barotrauma.Items.Components
|
||||
private float sinTime;
|
||||
public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
|
||||
{
|
||||
if (!(objective.OperateTarget is Gap leak)) return true;
|
||||
|
||||
Vector2 fromItemToLeak = leak.WorldPosition - item.WorldPosition;
|
||||
float dist = fromItemToLeak.Length();
|
||||
if (!(objective.OperateTarget is Gap leak)) { return true; }
|
||||
if (leak.Submarine == null) { return true; }
|
||||
Vector2 fromCharacterToLeak = leak.WorldPosition - character.WorldPosition;
|
||||
float dist = fromCharacterToLeak.Length();
|
||||
float reach = Range + ConvertUnits.ToDisplayUnits(((HumanoidAnimController)character.AnimController).ArmLength);
|
||||
|
||||
//too far away -> consider this done and hope the AI is smart enough to move closer
|
||||
if (dist > Range * 3.0f) { return true; }
|
||||
|
||||
// TODO: use the collider size?
|
||||
if (!character.AnimController.InWater && character.AnimController is HumanoidAnimController &&
|
||||
Math.Abs(fromItemToLeak.X) < 100.0f && fromItemToLeak.Y < 0.0f && fromItemToLeak.Y > -150.0f)
|
||||
{
|
||||
((HumanoidAnimController)character.AnimController).Crouching = true;
|
||||
}
|
||||
|
||||
if (dist > reach * 2) { return true; }
|
||||
character.AIController.SteeringManager.Reset();
|
||||
//steer closer if almost in range
|
||||
if (dist > Range)
|
||||
if (dist > reach)
|
||||
{
|
||||
Vector2 standPos = new Vector2(Math.Sign(-fromItemToLeak.X), Math.Sign(-fromItemToLeak.Y)) / 2;
|
||||
if (!character.AnimController.InWater)
|
||||
if (character.AnimController.InWater)
|
||||
{
|
||||
if (character.AIController.SteeringManager is IndoorsSteeringManager indoorSteering)
|
||||
{
|
||||
// Swimming inside the sub
|
||||
if (indoorSteering.CurrentPath != null && !indoorSteering.IsPathDirty && indoorSteering.CurrentPath.Unreachable)
|
||||
{
|
||||
Vector2 dir = Vector2.Normalize(fromCharacterToLeak);
|
||||
character.AIController.SteeringManager.SteeringManual(deltaTime, dir);
|
||||
}
|
||||
else
|
||||
{
|
||||
character.AIController.SteeringManager.SteeringSeek(character.GetRelativeSimPosition(leak));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Swimming outside the sub
|
||||
character.AIController.SteeringManager.SteeringSeek(character.GetRelativeSimPosition(leak));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: use the collider size?
|
||||
if (!character.AnimController.InWater && character.AnimController is HumanoidAnimController &&
|
||||
Math.Abs(fromCharacterToLeak.X) < 100.0f && fromCharacterToLeak.Y < 0.0f && fromCharacterToLeak.Y > -150.0f)
|
||||
{
|
||||
((HumanoidAnimController)character.AnimController).Crouching = true;
|
||||
}
|
||||
Vector2 standPos = new Vector2(Math.Sign(-fromCharacterToLeak.X), Math.Sign(-fromCharacterToLeak.Y)) / 2;
|
||||
if (leak.IsHorizontal)
|
||||
{
|
||||
standPos.X *= 2;
|
||||
@@ -468,43 +490,40 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
standPos.X = 0;
|
||||
}
|
||||
}
|
||||
if (character.AIController.SteeringManager is IndoorsSteeringManager indoorSteering)
|
||||
{
|
||||
if (indoorSteering.CurrentPath != null && !indoorSteering.IsPathDirty && indoorSteering.CurrentPath.Unreachable)
|
||||
{
|
||||
Vector2 dir = Vector2.Normalize(standPos - character.WorldPosition);
|
||||
character.AIController.SteeringManager.SteeringManual(deltaTime, dir / 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
character.AIController.SteeringManager.SteeringSeek(standPos);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
character.AIController.SteeringManager.SteeringSeek(standPos);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (dist < Range / 2)
|
||||
if (dist < reach / 2)
|
||||
{
|
||||
// Too close -> steer away
|
||||
character.AIController.SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(character.SimPosition - leak.SimPosition) / 2);
|
||||
character.AIController.SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(character.SimPosition - leak.SimPosition));
|
||||
}
|
||||
else if (dist <= Range)
|
||||
else if (dist <= reach)
|
||||
{
|
||||
// In range
|
||||
character.AIController.SteeringManager.Reset();
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
character.CursorPosition = leak.Position;
|
||||
character.CursorPosition += VectorExtensions.Forward(Item.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, dist / 2);
|
||||
if (character.AnimController.InWater)
|
||||
{
|
||||
var torso = character.AnimController.GetLimb(LimbType.Torso);
|
||||
// Turn facing the target when not moving (handled in the animcontroller if not moving)
|
||||
Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition);
|
||||
Vector2 diff = (mousePos - torso.SimPosition) * character.AnimController.Dir;
|
||||
float newRotation = MathUtils.VectorToAngle(diff);
|
||||
character.AnimController.Collider.SmoothRotate(newRotation, 5.0f);
|
||||
|
||||
if (VectorExtensions.Angle(VectorExtensions.Forward(torso.body.TransformedRotation), fromCharacterToLeak) < MathHelper.PiOver4)
|
||||
{
|
||||
// Swim past
|
||||
Vector2 moveDir = leak.IsHorizontal ? Vector2.UnitY : Vector2.UnitX;
|
||||
moveDir *= character.AnimController.Dir;
|
||||
character.AIController.SteeringManager.SteeringManual(deltaTime, moveDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
sinTime += deltaTime;
|
||||
character.CursorPosition = leak.Position + VectorExtensions.Forward(Item.body.TransformedRotation + (float)Math.Sin(sinTime), dist);
|
||||
if (item.RequireAimToUse)
|
||||
{
|
||||
bool isOperatingButtons = false;
|
||||
@@ -520,13 +539,33 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
character.SetInput(InputType.Aim, false, true);
|
||||
}
|
||||
bool isAiming = false;
|
||||
var holdable = item.GetComponent<Holdable>();
|
||||
if (holdable != null)
|
||||
{
|
||||
isAiming = holdable.ControlPose;
|
||||
}
|
||||
sinTime = isAiming ? sinTime + deltaTime * 5 : 0;
|
||||
}
|
||||
// Press the trigger only when the tool is approximately facing the target.
|
||||
Vector2 fromItemToLeak = leak.WorldPosition - item.WorldPosition;
|
||||
var angle = VectorExtensions.Angle(VectorExtensions.Forward(item.body.TransformedRotation), fromItemToLeak);
|
||||
if (angle < MathHelper.PiOver4)
|
||||
{
|
||||
character.SetInput(InputType.Shoot, false, true);
|
||||
Use(deltaTime, character);
|
||||
// Check that we don't hit any friendlies
|
||||
if (Submarine.PickBodies(item.SimPosition, leak.SimPosition, collisionCategory: Physics.CollisionCharacter).None(hit =>
|
||||
{
|
||||
if (hit.UserData is Character c)
|
||||
{
|
||||
if (c == character) { return false; }
|
||||
return HumanAIController.IsFriendly(character, c);
|
||||
}
|
||||
return false;
|
||||
}))
|
||||
{
|
||||
character.SetInput(InputType.Shoot, false, true);
|
||||
Use(deltaTime, character);
|
||||
}
|
||||
}
|
||||
|
||||
bool leakFixed = (leak.Open <= 0.0f || leak.Removed) &&
|
||||
@@ -534,7 +573,6 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (leakFixed && leak.FlowTargetHull != null)
|
||||
{
|
||||
sinTime = 0;
|
||||
if (!leak.FlowTargetHull.ConnectedGaps.Any(g => !g.IsRoomToRoom && g.Open > 0.0f))
|
||||
{
|
||||
|
||||
|
||||
@@ -44,8 +44,9 @@ namespace Barotrauma.Items.Components
|
||||
public bool WasUsed;
|
||||
|
||||
public readonly Dictionary<ActionType, List<StatusEffect>> statusEffectLists;
|
||||
|
||||
|
||||
public Dictionary<RelatedItem.RelationType, List<RelatedItem>> requiredItems;
|
||||
public readonly List<RelatedItem> DisabledRequiredItems = new List<RelatedItem>();
|
||||
|
||||
public List<Skill> requiredSkills;
|
||||
|
||||
@@ -271,19 +272,7 @@ namespace Barotrauma.Items.Components
|
||||
break;
|
||||
case "requireditem":
|
||||
case "requireditems":
|
||||
RelatedItem ri = RelatedItem.Load(subElement, item.Name);
|
||||
if (ri != null)
|
||||
{
|
||||
if (!requiredItems.ContainsKey(ri.Type))
|
||||
{
|
||||
requiredItems.Add(ri.Type, new List<RelatedItem>());
|
||||
}
|
||||
requiredItems[ri.Type].Add(ri);
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.ThrowError("Error in item config \"" + item.ConfigFile + "\" - component " + GetType().ToString() + " requires an item with no identifiers.");
|
||||
}
|
||||
SetRequiredItems(subElement);
|
||||
break;
|
||||
case "requiredskill":
|
||||
case "requiredskills":
|
||||
@@ -323,6 +312,34 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
public void SetRequiredItems(XElement element)
|
||||
{
|
||||
bool returnEmpty = false;
|
||||
#if CLIENT
|
||||
returnEmpty = Screen.Selected == GameMain.SubEditorScreen;
|
||||
#endif
|
||||
RelatedItem ri = RelatedItem.Load(element, returnEmpty, item.Name);
|
||||
if (ri != null)
|
||||
{
|
||||
if (ri.Identifiers.Length == 0)
|
||||
{
|
||||
DisabledRequiredItems.Add(ri);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!requiredItems.ContainsKey(ri.Type))
|
||||
{
|
||||
requiredItems.Add(ri.Type, new List<RelatedItem>());
|
||||
}
|
||||
requiredItems[ri.Type].Add(ri);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.ThrowError("Error in item config \"" + item.ConfigFile + "\" - component " + GetType().ToString() + " requires an item with no identifiers.");
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void Move(Vector2 amount) { }
|
||||
|
||||
/// <summary>a Character has picked the item</summary>
|
||||
@@ -762,6 +779,12 @@ namespace Barotrauma.Items.Components
|
||||
componentElement.Add(newElement);
|
||||
}
|
||||
}
|
||||
foreach (RelatedItem ri in DisabledRequiredItems)
|
||||
{
|
||||
XElement newElement = new XElement("requireditem");
|
||||
ri.Save(newElement);
|
||||
componentElement.Add(newElement);
|
||||
}
|
||||
|
||||
|
||||
SerializableProperty.SerializeProperties(this, componentElement);
|
||||
@@ -783,12 +806,16 @@ namespace Barotrauma.Items.Components
|
||||
var prevRequiredItems = new Dictionary<RelatedItem.RelationType, List<RelatedItem>>(requiredItems);
|
||||
requiredItems.Clear();
|
||||
|
||||
bool returnEmptyRequirements = false;
|
||||
#if CLIENT
|
||||
returnEmptyRequirements = Screen.Selected == GameMain.SubEditorScreen;
|
||||
#endif
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
{
|
||||
case "requireditem":
|
||||
RelatedItem newRequiredItem = RelatedItem.Load(subElement, item.Name);
|
||||
RelatedItem newRequiredItem = RelatedItem.Load(subElement, returnEmptyRequirements, item.Name);
|
||||
if (newRequiredItem == null) continue;
|
||||
|
||||
var prevRequiredItem = prevRequiredItems.ContainsKey(newRequiredItem.Type) ?
|
||||
|
||||
@@ -3,6 +3,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
@@ -54,20 +55,44 @@ namespace Barotrauma.Items.Components
|
||||
[Serialize(5, false, description: "How many inventory slots the inventory has per row.")]
|
||||
public int SlotsPerRow { get; set; }
|
||||
|
||||
public List<RelatedItem> ContainableItems { get; private set; }
|
||||
private HashSet<string> containableRestrictions = new HashSet<string>();
|
||||
[Editable, Serialize("", true, description: "Define items (by identifiers or tags) that bots should place inside this container. If empty, no restrictions are applied.")]
|
||||
public string ContainableRestrictions
|
||||
{
|
||||
get { return string.Join(",", containableRestrictions); }
|
||||
set
|
||||
{
|
||||
StringFormatter.ParseCommaSeparatedStringToCollection(value, containableRestrictions);
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShouldBeContained(string[] identifiersOrTags, out bool isRestrictionsDefined)
|
||||
{
|
||||
isRestrictionsDefined = containableRestrictions.Any();
|
||||
if (!isRestrictionsDefined) { return true; }
|
||||
return identifiersOrTags.Any(id => containableRestrictions.Any(r => r == id));
|
||||
}
|
||||
|
||||
public bool ShouldBeContained(Item item, out bool isRestrictionsDefined)
|
||||
{
|
||||
isRestrictionsDefined = containableRestrictions.Any();
|
||||
if (!isRestrictionsDefined) { return true; }
|
||||
return containableRestrictions.Any(id => item.Prefab.Identifier == id || item.HasTag(id));
|
||||
}
|
||||
|
||||
public List<RelatedItem> ContainableItems { get; private set; } = new List<RelatedItem>();
|
||||
|
||||
public ItemContainer(Item item, XElement element)
|
||||
: base (item, element)
|
||||
{
|
||||
Inventory = new ItemInventory(item, this, capacity, SlotsPerRow);
|
||||
ContainableItems = new List<RelatedItem>();
|
||||
Inventory = new ItemInventory(item, this, capacity, SlotsPerRow);
|
||||
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
{
|
||||
case "containable":
|
||||
RelatedItem containable = RelatedItem.Load(subElement, item.Name);
|
||||
RelatedItem containable = RelatedItem.Load(subElement, returnEmpty: false, parentDebugName: item.Name);
|
||||
if (containable == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Error in item config \"" + item.ConfigFile + "\" - containable with no identifiers.");
|
||||
|
||||
@@ -34,6 +34,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public override void OnItemLoaded()
|
||||
{
|
||||
base.OnItemLoaded();
|
||||
var containers = item.GetComponents<ItemContainer>().ToList();
|
||||
if (containers.Count < 2)
|
||||
{
|
||||
@@ -59,7 +60,7 @@ namespace Barotrauma.Items.Components
|
||||
return;
|
||||
}
|
||||
|
||||
hasPower = voltage >= minVoltage;
|
||||
hasPower = Voltage >= MinVoltage;
|
||||
if (!hasPower) { return; }
|
||||
|
||||
var repairable = item.GetComponent<Repairable>();
|
||||
@@ -70,10 +71,8 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
|
||||
|
||||
if (powerConsumption == 0.0f) { voltage = 1.0f; }
|
||||
|
||||
progressTimer += deltaTime * voltage;
|
||||
Voltage -= deltaTime * 10.0f;
|
||||
if (powerConsumption <= 0.0f) { Voltage = 1.0f; }
|
||||
progressTimer += deltaTime * Voltage;
|
||||
|
||||
var targetItem = inputContainer.Inventory.Items.LastOrDefault(i => i != null);
|
||||
if (targetItem == null) { return; }
|
||||
@@ -99,7 +98,7 @@ namespace Barotrauma.Items.Components
|
||||
float condition = deconstructProduct.CopyCondition ?
|
||||
percentageHealth * itemPrefab.Health :
|
||||
itemPrefab.Health * deconstructProduct.OutCondition;
|
||||
|
||||
|
||||
//container full, drop the items outside the deconstructor
|
||||
if (emptySlots <= 0)
|
||||
{
|
||||
@@ -111,7 +110,7 @@ namespace Barotrauma.Items.Components
|
||||
emptySlots--;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
|
||||
{
|
||||
if (targetItem.Prefab.DeconstructItems.Any())
|
||||
@@ -149,8 +148,6 @@ namespace Barotrauma.Items.Components
|
||||
progressState = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
voltage -= deltaTime * 10.0f;
|
||||
}
|
||||
|
||||
private void PutItemsToLinkedContainer()
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public float CurrentVolume
|
||||
{
|
||||
get { return Math.Abs((force / 100.0f) * (minVoltage <= 0.0f ? 1.0f : Math.Min(prevVoltage / minVoltage, 1.0f))); }
|
||||
get { return Math.Abs((force / 100.0f) * (MinVoltage <= 0.0f ? 1.0f : Math.Min(prevVoltage / MinVoltage, 1.0f))); }
|
||||
}
|
||||
|
||||
public Engine(Item item, XElement element)
|
||||
@@ -83,15 +83,15 @@ namespace Barotrauma.Items.Components
|
||||
//pumps consume more power when in a bad condition
|
||||
currPowerConsumption *= MathHelper.Lerp(2.0f, 1.0f, item.Condition / item.MaxCondition);
|
||||
|
||||
if (powerConsumption == 0.0f) voltage = 1.0f;
|
||||
if (powerConsumption == 0.0f) { Voltage = 1.0f; }
|
||||
|
||||
prevVoltage = voltage;
|
||||
hasPower = voltage > minVoltage;
|
||||
prevVoltage = Voltage;
|
||||
hasPower = Voltage > MinVoltage;
|
||||
|
||||
Force = MathHelper.Lerp(force, (voltage < minVoltage) ? 0.0f : targetForce, 0.1f);
|
||||
Force = MathHelper.Lerp(force, (Voltage < MinVoltage) ? 0.0f : targetForce, 0.1f);
|
||||
if (Math.Abs(Force) > 1.0f)
|
||||
{
|
||||
Vector2 currForce = new Vector2((force / 10.0f) * maxForce * Math.Min(voltage / minVoltage, 1.0f), 0.0f);
|
||||
Vector2 currForce = new Vector2((force / 10.0f) * maxForce * Math.Min(Voltage / MinVoltage, 1.0f), 0.0f);
|
||||
//less effective when in a bad condition
|
||||
currForce *= MathHelper.Lerp(0.5f, 2.0f, item.Condition / item.MaxCondition);
|
||||
|
||||
@@ -119,8 +119,6 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
voltage -= deltaTime;
|
||||
}
|
||||
|
||||
private void UpdatePropellerDamage(float deltaTime)
|
||||
@@ -172,5 +170,16 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override XElement Save(XElement parentElement)
|
||||
{
|
||||
Vector2 prevPropellerPos = PropellerPos;
|
||||
//undo flipping before saving
|
||||
if (item.FlippedX) { PropellerPos = new Vector2(-PropellerPos.X, PropellerPos.Y); }
|
||||
if (item.FlippedY) { PropellerPos = new Vector2(PropellerPos.X, -PropellerPos.Y); }
|
||||
XElement element = base.Save(parentElement);
|
||||
PropellerPos = prevPropellerPos;
|
||||
return element;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public override void OnItemLoaded()
|
||||
{
|
||||
base.OnItemLoaded();
|
||||
var containers = item.GetComponents<ItemContainer>().ToList();
|
||||
if (containers.Count < 2)
|
||||
{
|
||||
@@ -199,7 +200,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
progressState = fabricatedItem == null ? 0.0f : (requiredTime - timeUntilReady) / requiredTime;
|
||||
|
||||
hasPower = voltage >= minVoltage;
|
||||
hasPower = Voltage >= MinVoltage;
|
||||
if (!hasPower) { return; }
|
||||
|
||||
var repairable = item.GetComponent<Repairable>();
|
||||
@@ -210,10 +211,9 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
|
||||
|
||||
if (powerConsumption <= 0) { voltage = 1.0f; }
|
||||
if (powerConsumption <= 0) { Voltage = 1.0f; }
|
||||
|
||||
timeUntilReady -= deltaTime * voltage;
|
||||
voltage -= deltaTime * 10.0f;
|
||||
timeUntilReady -= deltaTime * Voltage;
|
||||
|
||||
if (timeUntilReady > 0.0f) { return; }
|
||||
|
||||
|
||||
@@ -75,13 +75,11 @@ namespace Barotrauma.Items.Components
|
||||
currPowerConsumption = powerConsumption;
|
||||
currPowerConsumption *= MathHelper.Lerp(2.0f, 1.0f, item.Condition / item.MaxCondition);
|
||||
|
||||
hasPower = voltage > minVoltage;
|
||||
hasPower = Voltage > MinVoltage;
|
||||
if (hasPower)
|
||||
{
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
|
||||
}
|
||||
|
||||
voltage -= deltaTime;
|
||||
}
|
||||
|
||||
public override bool Pick(Character picker)
|
||||
|
||||
@@ -46,12 +46,12 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (powerConsumption <= 0.0f)
|
||||
{
|
||||
voltage = 1.0f;
|
||||
Voltage = 1.0f;
|
||||
}
|
||||
|
||||
if (item.CurrentHull == null) return;
|
||||
|
||||
if (voltage < minVoltage)
|
||||
if (Voltage < MinVoltage)
|
||||
{
|
||||
powerDownTimer += deltaTime;
|
||||
return;
|
||||
@@ -61,7 +61,7 @@ namespace Barotrauma.Items.Components
|
||||
powerDownTimer = 0.0f;
|
||||
}
|
||||
|
||||
CurrFlow = Math.Min(voltage, 1.0f) * generatedAmount * 100.0f;
|
||||
CurrFlow = Math.Min(Voltage, 1.0f) * generatedAmount * 100.0f;
|
||||
|
||||
//less effective when in bad condition
|
||||
float conditionMult = item.Condition / item.MaxCondition;
|
||||
@@ -71,8 +71,6 @@ namespace Barotrauma.Items.Components
|
||||
CurrFlow *= conditionMult * conditionMult;
|
||||
|
||||
UpdateVents(CurrFlow);
|
||||
|
||||
voltage -= deltaTime;
|
||||
}
|
||||
|
||||
public override void UpdateBroken(float deltaTime, Camera cam)
|
||||
|
||||
@@ -76,7 +76,7 @@ namespace Barotrauma.Items.Components
|
||||
//pumps consume more power when in a bad condition
|
||||
currPowerConsumption *= MathHelper.Lerp(2.0f, 1.0f, item.Condition / item.MaxCondition);
|
||||
|
||||
if (voltage < minVoltage) { return; }
|
||||
if (Voltage < MinVoltage) { return; }
|
||||
|
||||
UpdateProjSpecific(deltaTime);
|
||||
|
||||
@@ -86,7 +86,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (item.CurrentHull == null) { return; }
|
||||
|
||||
float powerFactor = currPowerConsumption <= 0.0f ? 1.0f : voltage;
|
||||
float powerFactor = currPowerConsumption <= 0.0f ? 1.0f : Voltage;
|
||||
|
||||
currFlow = flowPercentage / 100.0f * maxFlow * powerFactor;
|
||||
//less effective when in a bad condition
|
||||
@@ -94,8 +94,6 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
item.CurrentHull.WaterVolume += currFlow;
|
||||
if (item.CurrentHull.WaterVolume > item.CurrentHull.Volume) { item.CurrentHull.Pressure += 0.5f; }
|
||||
|
||||
voltage -= deltaTime;
|
||||
}
|
||||
|
||||
partial void UpdateProjSpecific(float deltaTime);
|
||||
@@ -124,7 +122,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (float.TryParse(signal, NumberStyles.Any, CultureInfo.InvariantCulture, out float tempTarget))
|
||||
{
|
||||
targetLevel = MathHelper.Clamp((tempTarget + 100.0f) / 2.0f, 0.0f, 100.0f);
|
||||
targetLevel = MathHelper.Clamp(tempTarget + 50.0f, 0.0f, 100.0f);
|
||||
controlLockTimer = 0.1f;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
@@ -35,6 +36,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
private float maxPowerOutput;
|
||||
|
||||
private Queue<float> loadQueue = new Queue<float>();
|
||||
private float load;
|
||||
|
||||
private bool unsentChanges;
|
||||
@@ -165,7 +167,10 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
private float prevAvailableFuel;
|
||||
public float AvailableFuel { get; set; }
|
||||
|
||||
|
||||
private readonly string[] fuelTags = new string[1] { "reactorfuel" };
|
||||
|
||||
|
||||
public Reactor(Item item, XElement element)
|
||||
: base(item, element)
|
||||
{
|
||||
@@ -267,8 +272,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
UpdateAutoTemp(2.0f, deltaTime);
|
||||
}
|
||||
|
||||
load = 0.0f;
|
||||
float currentLoad = 0.0f;
|
||||
List<Connection> connections = item.Connections;
|
||||
if (connections != null && connections.Count > 0)
|
||||
{
|
||||
@@ -284,13 +288,20 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
//calculate how much external power there is in the grid
|
||||
//(power coming from somewhere else than this reactor, e.g. batteries)
|
||||
float externalPower = Math.Max(CurrPowerConsumption - pt.CurrPowerConsumption, 0);
|
||||
float externalPower = Math.Max(CurrPowerConsumption - pt.CurrPowerConsumption, 0) * 0.95f;
|
||||
//reduce the external power from the load to prevent overloading the grid
|
||||
load = Math.Max(load, pt.PowerLoad - externalPower);
|
||||
currentLoad = Math.Max(currentLoad, pt.PowerLoad - externalPower);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadQueue.Enqueue(currentLoad);
|
||||
while (loadQueue.Count() > 60.0f)
|
||||
{
|
||||
load = loadQueue.Average();
|
||||
loadQueue.Dequeue();
|
||||
}
|
||||
|
||||
if (fissionRate > 0.0f)
|
||||
{
|
||||
foreach (Item item in item.ContainedItems)
|
||||
@@ -505,6 +516,19 @@ namespace Barotrauma.Items.Components
|
||||
return picker != null;
|
||||
}
|
||||
|
||||
private int itemIndex;
|
||||
private List<Item> ignoredContainers = new List<Item>();
|
||||
private bool FindSuitableContainer(Character character, Func<Item, float> priority, out Item suitableContainer)
|
||||
{
|
||||
suitableContainer = null;
|
||||
if (character.FindItem(ref itemIndex, out Item targetContainer, ignoredItems: ignoredContainers, customPriorityFunction: priority))
|
||||
{
|
||||
suitableContainer = targetContainer;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
|
||||
{
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return false; }
|
||||
@@ -516,13 +540,56 @@ namespace Barotrauma.Items.Components
|
||||
//characters with insufficient skill levels don't refuel the reactor
|
||||
if (degreeOfSuccess > 0.2f)
|
||||
{
|
||||
//remove used-up fuel from the reactor
|
||||
var containedItems = item.ContainedItems;
|
||||
foreach (Item item in containedItems)
|
||||
if (objective.SubObjectives.None())
|
||||
{
|
||||
if (item != null && item.Condition <= 0.0f)
|
||||
var containedItems = item.ContainedItems;
|
||||
foreach (Item fuelRod in containedItems)
|
||||
{
|
||||
item.Drop(character);
|
||||
if (fuelRod != null && fuelRod.Condition <= 0.0f)
|
||||
{
|
||||
if (!FindSuitableContainer(character,
|
||||
i =>
|
||||
{
|
||||
var container = i.GetComponent<ItemContainer>();
|
||||
if (container == null) { return 0; }
|
||||
if (container.Inventory.IsFull()) { return 0; }
|
||||
if (container.ShouldBeContained(fuelRod, out bool isRestrictionsDefined))
|
||||
{
|
||||
if (isRestrictionsDefined)
|
||||
{
|
||||
return 3;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (fuelRod.Prefab.IsContainerPreferred(container, out bool isPreferencesDefined))
|
||||
{
|
||||
return isPreferencesDefined ? 2 : 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
return isPreferencesDefined ? 0 : 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}, out Item targetContainer))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var decontainObjective = new AIObjectiveDecontainItem(character, fuelRod, item.GetComponent<ItemContainer>(), objective.objectiveManager, targetContainer?.GetComponent<ItemContainer>());
|
||||
decontainObjective.Abandoned += () =>
|
||||
{
|
||||
itemIndex = 0;
|
||||
if (targetContainer != null)
|
||||
{
|
||||
ignoredContainers.Add(targetContainer);
|
||||
}
|
||||
};
|
||||
objective.AddSubObjectiveInQueue(decontainObjective);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -535,31 +602,33 @@ namespace Barotrauma.Items.Components
|
||||
//load more fuel if the current maximum output is only 50% of the current load
|
||||
if (NeedMoreFuel(minimumOutputRatio: 0.5f))
|
||||
{
|
||||
var containFuelObjective = new AIObjectiveContainItem(character, new string[] { "fuelrod", "reactorfuel" }, item.GetComponent<ItemContainer>(), objective.objectiveManager)
|
||||
{
|
||||
targetItemCount = item.ContainedItems.Count(i => i != null && i.Prefab.Identifier == "fuelrod" || i.HasTag("reactorfuel")) + 1,
|
||||
GetItemPriority = (Item fuelItem) =>
|
||||
{
|
||||
if (fuelItem.ParentInventory?.Owner is Item)
|
||||
{
|
||||
//don't take fuel from other reactors
|
||||
if (((Item)fuelItem.ParentInventory.Owner).GetComponent<Reactor>() != null) return 0.0f;
|
||||
}
|
||||
return 1.0f;
|
||||
}
|
||||
};
|
||||
objective.AddSubObjective(containFuelObjective);
|
||||
|
||||
character?.Speak(TextManager.Get("DialogReactorFuel"), null, 0.0f, "reactorfuel", 30.0f);
|
||||
|
||||
aiUpdateTimer = AIUpdateInterval;
|
||||
if (objective.SubObjectives.None())
|
||||
{
|
||||
var containFuelObjective = new AIObjectiveContainItem(character, fuelTags, item.GetComponent<ItemContainer>(), objective.objectiveManager)
|
||||
{
|
||||
targetItemCount = item.ContainedItems.Count(i => i != null && fuelTags.Any(t => i.Prefab.Identifier == t || i.HasTag(t))) + 1,
|
||||
GetItemPriority = (Item fuelItem) =>
|
||||
{
|
||||
if (fuelItem.ParentInventory?.Owner is Item)
|
||||
{
|
||||
//don't take fuel from other reactors
|
||||
if (((Item)fuelItem.ParentInventory.Owner).GetComponent<Reactor>() != null) return 0.0f;
|
||||
}
|
||||
return 1.0f;
|
||||
}
|
||||
};
|
||||
containFuelObjective.Abandoned += () => objective.Abandon = true;
|
||||
objective.AddSubObjective(containFuelObjective);
|
||||
character?.Speak(TextManager.Get("DialogReactorFuel"), null, 0.0f, "reactorfuel", 30.0f);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
else if (TooMuchFuel())
|
||||
{
|
||||
foreach (Item item in item.ContainedItems)
|
||||
{
|
||||
if (item != null && item.HasTag("reactorfuel"))
|
||||
if (item != null && fuelTags.Any(t => item.Prefab.Identifier == t || item.HasTag(t)))
|
||||
{
|
||||
if (!character.Inventory.TryPutItem(item, character, allowedSlots: item.AllowedSlots))
|
||||
{
|
||||
@@ -577,22 +646,28 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
|
||||
LastUser = lastAIUser = character;
|
||||
|
||||
|
||||
bool prevAutoTemp = autoTemp;
|
||||
bool prevShutDown = shutDown;
|
||||
float prevFissionRate = targetFissionRate;
|
||||
float prevTurbineOutput = targetTurbineOutput;
|
||||
|
||||
switch (objective.Option.ToLowerInvariant())
|
||||
{
|
||||
case "powerup":
|
||||
shutDown = false;
|
||||
//characters with insufficient skill levels simply set the autotemp on instead of trying to adjust the temperature manually
|
||||
if (degreeOfSuccess < 0.5f)
|
||||
if (objective.Override || !autoTemp)
|
||||
{
|
||||
if (!autoTemp) unsentChanges = true;
|
||||
AutoTemp = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
AutoTemp = false;
|
||||
unsentChanges = true;
|
||||
UpdateAutoTemp(MathHelper.Lerp(0.5f, 2.0f, degreeOfSuccess), 1.0f);
|
||||
//characters with insufficient skill levels simply set the autotemp on instead of trying to adjust the temperature manually
|
||||
if (degreeOfSuccess < 0.5f)
|
||||
{
|
||||
AutoTemp = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
AutoTemp = false;
|
||||
UpdateAutoTemp(MathHelper.Lerp(0.5f, 2.0f, degreeOfSuccess), 1.0f);
|
||||
}
|
||||
}
|
||||
#if CLIENT
|
||||
onOffSwitch.BarScroll = 0.0f;
|
||||
@@ -604,11 +679,6 @@ namespace Barotrauma.Items.Components
|
||||
#if CLIENT
|
||||
onOffSwitch.BarScroll = 1.0f;
|
||||
#endif
|
||||
if (AutoTemp || !shutDown || targetFissionRate > 0.0f || targetTurbineOutput > 0.0f)
|
||||
{
|
||||
unsentChanges = true;
|
||||
}
|
||||
|
||||
AutoTemp = false;
|
||||
shutDown = true;
|
||||
targetFissionRate = 0.0f;
|
||||
@@ -616,6 +686,14 @@ namespace Barotrauma.Items.Components
|
||||
break;
|
||||
}
|
||||
|
||||
if (autoTemp != prevAutoTemp ||
|
||||
prevShutDown != shutDown ||
|
||||
Math.Abs(prevFissionRate - targetFissionRate) > 1.0f ||
|
||||
Math.Abs(prevTurbineOutput - targetTurbineOutput) > 1.0f)
|
||||
{
|
||||
unsentChanges = true;
|
||||
}
|
||||
|
||||
aiUpdateTimer = AIUpdateInterval;
|
||||
|
||||
return false;
|
||||
|
||||
@@ -162,7 +162,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (currentMode == Mode.Active)
|
||||
{
|
||||
if ((voltage >= minVoltage || powerConsumption <= 0.0f) &&
|
||||
if ((Voltage >= MinVoltage) &&
|
||||
(!UseTransducers || connectedTransducers.Count > 0))
|
||||
{
|
||||
if (currentPingIndex != -1)
|
||||
@@ -201,7 +201,6 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
item.AiTarget.SectorDegrees = 360.0f;
|
||||
}
|
||||
currentPingIndex = -1;
|
||||
aiPingCheckPending = false;
|
||||
}
|
||||
}
|
||||
@@ -235,6 +234,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
protected override void RemoveComponentSpecific()
|
||||
{
|
||||
base.RemoveComponentSpecific();
|
||||
sonarBlip?.Remove();
|
||||
pingCircle?.Remove();
|
||||
directionalPingCircle?.Remove();
|
||||
@@ -247,6 +247,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (currentMode == Mode.Passive || !aiPingCheckPending) return false;
|
||||
|
||||
// TODO: Don't create new collections here
|
||||
Dictionary<string, List<Character>> targetGroups = new Dictionary<string, List<Character>>();
|
||||
|
||||
foreach (Character c in Character.CharacterList)
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
UpdateOnActiveEffects(deltaTime);
|
||||
|
||||
if (voltage >= minVoltage || PowerConsumption <= 0.0f)
|
||||
if (Voltage >= MinVoltage)
|
||||
{
|
||||
sendSignalTimer += deltaTime;
|
||||
if (sendSignalTimer > SendSignalInterval)
|
||||
@@ -29,8 +29,6 @@ namespace Barotrauma.Items.Components
|
||||
sendSignalTimer = SendSignalInterval;
|
||||
}
|
||||
}
|
||||
|
||||
voltage = 0.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,13 @@ namespace Barotrauma.Items.Components
|
||||
private const float AutopilotRayCastInterval = 0.5f;
|
||||
private const float RecalculatePathInterval = 5.0f;
|
||||
|
||||
private const float AutopilotMinDistToPathNode = 30.0f;
|
||||
|
||||
private const float AutoPilotSteeringLerp = 0.1f;
|
||||
|
||||
private const float AutoPilotMaxSpeed = 0.5f;
|
||||
private const float AIPilotMaxSpeed = 1.0f;
|
||||
|
||||
private Vector2 currVelocity;
|
||||
private Vector2 targetVelocity;
|
||||
|
||||
@@ -162,6 +169,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public override void OnItemLoaded()
|
||||
{
|
||||
base.OnItemLoaded();
|
||||
sonar = item.GetComponent<Sonar>();
|
||||
}
|
||||
|
||||
@@ -209,7 +217,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
currPowerConsumption = powerConsumption;
|
||||
|
||||
if (voltage < minVoltage && currPowerConsumption > 0.0f) { return; }
|
||||
if (Voltage < MinVoltage) { return; }
|
||||
|
||||
if (user != null && user.Removed)
|
||||
{
|
||||
@@ -221,6 +229,12 @@ namespace Barotrauma.Items.Components
|
||||
if (autoPilot)
|
||||
{
|
||||
UpdateAutoPilot(deltaTime);
|
||||
float userSkill = 0.0f;
|
||||
if (user != null && (user.SelectedConstruction == item || item.linkedTo.Contains(user.SelectedConstruction)))
|
||||
{
|
||||
userSkill = user.GetSkillLevel("helm") / 100.0f;
|
||||
}
|
||||
targetVelocity = targetVelocity.ClampLength(MathHelper.Lerp(AutoPilotMaxSpeed, AIPilotMaxSpeed, userSkill) * 100.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -253,8 +267,6 @@ namespace Barotrauma.Items.Components
|
||||
targetLevel += (neutralBallastLevel - 0.5f) * 100.0f;
|
||||
|
||||
item.SendSignal(0, targetLevel.ToString(CultureInfo.InvariantCulture), "velocity_y_out", null);
|
||||
|
||||
voltage -= deltaTime;
|
||||
}
|
||||
|
||||
private void UpdateAutoPilot(float deltaTime)
|
||||
@@ -262,7 +274,8 @@ namespace Barotrauma.Items.Components
|
||||
if (controlledSub == null) return;
|
||||
if (posToMaintain != null)
|
||||
{
|
||||
SteerTowardsPosition((Vector2)posToMaintain);
|
||||
Vector2 steeringVel = GetSteeringVelocity((Vector2)posToMaintain);
|
||||
TargetVelocity = Vector2.Lerp(TargetVelocity, steeringVel, AutoPilotSteeringLerp);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -284,7 +297,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
//if the node is close enough, check if it's visible
|
||||
float lengthSqr = diff.LengthSquared();
|
||||
if (lengthSqr > 0.001f && lengthSqr < 500.0f)
|
||||
if (lengthSqr > 0.001f && lengthSqr < AutopilotMinDistToPathNode * AutopilotMinDistToPathNode)
|
||||
{
|
||||
diff = Vector2.Normalize(diff);
|
||||
|
||||
@@ -298,11 +311,11 @@ namespace Barotrauma.Items.Components
|
||||
Vector2 cornerPos =
|
||||
new Vector2(controlledSub.Borders.Width * x, controlledSub.Borders.Height * y) / 2.0f;
|
||||
|
||||
cornerPos = ConvertUnits.ToSimUnits(cornerPos * 1.2f + controlledSub.WorldPosition);
|
||||
cornerPos = ConvertUnits.ToSimUnits(cornerPos * 1.1f + controlledSub.WorldPosition);
|
||||
|
||||
float dist = Vector2.Distance(cornerPos, steeringPath.NextNode.SimPosition);
|
||||
|
||||
if (Submarine.PickBody(cornerPos, cornerPos + diff * dist, null, Physics.CollisionLevel) == null) continue;
|
||||
if (Submarine.PickBody(cornerPos, cornerPos + diff * dist, null, Physics.CollisionLevel) == null) { continue; }
|
||||
|
||||
nextVisible = false;
|
||||
x = 2;
|
||||
@@ -313,19 +326,18 @@ namespace Barotrauma.Items.Components
|
||||
if (nextVisible) steeringPath.SkipToNextNode();
|
||||
}
|
||||
|
||||
|
||||
|
||||
autopilotRayCastTimer = AutopilotRayCastInterval;
|
||||
}
|
||||
|
||||
Vector2 newVelocity = Vector2.Zero;
|
||||
if (steeringPath.CurrentNode != null)
|
||||
{
|
||||
SteerTowardsPosition(steeringPath.CurrentNode.WorldPosition);
|
||||
newVelocity = GetSteeringVelocity(steeringPath.CurrentNode.WorldPosition);
|
||||
}
|
||||
|
||||
Vector2 avoidDist = new Vector2(
|
||||
Math.Max(1000.0f * Math.Abs(controlledSub.Velocity.X), controlledSub.Borders.Width * 1.5f),
|
||||
Math.Max(1000.0f * Math.Abs(controlledSub.Velocity.Y), controlledSub.Borders.Height * 1.5f));
|
||||
Math.Max(1000.0f * Math.Abs(controlledSub.Velocity.X), controlledSub.Borders.Width * 0.75f),
|
||||
Math.Max(1000.0f * Math.Abs(controlledSub.Velocity.Y), controlledSub.Borders.Height * 0.75f));
|
||||
|
||||
float avoidRadius = avoidDist.Length();
|
||||
|
||||
@@ -356,22 +368,22 @@ namespace Barotrauma.Items.Components
|
||||
0.0f : Vector2.Dot(controlledSub.Velocity, -normalizedDiff);
|
||||
|
||||
//not heading towards the wall -> ignore
|
||||
if (dot < 0.5)
|
||||
if (dot < 1.0)
|
||||
{
|
||||
debugDrawObstacles.Add(new ObstacleDebugInfo(edge, intersection, dot, Vector2.Zero, cell.Translation));
|
||||
continue;
|
||||
}
|
||||
|
||||
Vector2 change = (normalizedDiff * Math.Max((avoidRadius - diff.Length()), 0.0f)) / avoidRadius;
|
||||
newAvoidStrength += change * dot;
|
||||
debugDrawObstacles.Add(new ObstacleDebugInfo(edge, intersection, dot, change * dot, cell.Translation));
|
||||
Vector2 change = (normalizedDiff * Math.Max((avoidRadius - diff.Length()), 0.0f)) / avoidRadius;
|
||||
if (change.LengthSquared() < 0.001f) { continue; }
|
||||
newAvoidStrength += change * (dot - 1.0f);
|
||||
debugDrawObstacles.Add(new ObstacleDebugInfo(edge, intersection, dot - 1.0f, change * (dot - 1.0f), cell.Translation));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
avoidStrength = Vector2.Lerp(avoidStrength, newAvoidStrength, deltaTime * 10.0f);
|
||||
|
||||
targetVelocity += avoidStrength * 100.0f;
|
||||
TargetVelocity = Vector2.Lerp(TargetVelocity, newVelocity + avoidStrength * 100.0f, AutoPilotSteeringLerp);
|
||||
|
||||
//steer away from other subs
|
||||
foreach (Submarine sub in Submarine.Loaded)
|
||||
@@ -447,21 +459,21 @@ namespace Barotrauma.Items.Components
|
||||
UpdatePath();
|
||||
}
|
||||
}
|
||||
private void SteerTowardsPosition(Vector2 worldPosition)
|
||||
private Vector2 GetSteeringVelocity(Vector2 worldPosition)
|
||||
{
|
||||
float prediction = 10.0f;
|
||||
float prediction = 2.0f;
|
||||
|
||||
Vector2 futurePosition = ConvertUnits.ToDisplayUnits(controlledSub.Velocity) * prediction;
|
||||
Vector2 targetSpeed = ((worldPosition - controlledSub.WorldPosition) - futurePosition);
|
||||
|
||||
if (targetSpeed.Length() > 500.0f)
|
||||
if (targetSpeed.LengthSquared() > 500.0f * 500.0f)
|
||||
{
|
||||
targetSpeed = Vector2.Normalize(targetSpeed);
|
||||
TargetVelocity = targetSpeed * 100.0f;
|
||||
|
||||
return Vector2.Normalize(targetSpeed) * 100.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
TargetVelocity = targetSpeed / 5.0f;
|
||||
return targetSpeed / 5.0f;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -471,43 +483,53 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
character.Speak(TextManager.Get("DialogSteeringTaken"), null, 0.0f, "steeringtaken", 10.0f);
|
||||
}
|
||||
|
||||
user = character;
|
||||
|
||||
if (!AutoPilot)
|
||||
{
|
||||
unsentChanges = true;
|
||||
AutoPilot = true;
|
||||
}
|
||||
switch (objective.Option.ToLowerInvariant())
|
||||
{
|
||||
case "maintainposition":
|
||||
if (!posToMaintain.HasValue)
|
||||
if (objective.Override)
|
||||
{
|
||||
unsentChanges = true;
|
||||
posToMaintain = controlledSub != null ?
|
||||
controlledSub.WorldPosition :
|
||||
item.Submarine == null ? item.WorldPosition : item.Submarine.WorldPosition;
|
||||
if (!MaintainPos)
|
||||
{
|
||||
unsentChanges = true;
|
||||
MaintainPos = true;
|
||||
}
|
||||
if (!posToMaintain.HasValue)
|
||||
{
|
||||
unsentChanges = true;
|
||||
posToMaintain = controlledSub != null ?
|
||||
controlledSub.WorldPosition :
|
||||
item.Submarine == null ? item.WorldPosition : item.Submarine.WorldPosition;
|
||||
}
|
||||
}
|
||||
|
||||
if (!AutoPilot || !MaintainPos) unsentChanges = true;
|
||||
|
||||
AutoPilot = true;
|
||||
MaintainPos = true;
|
||||
break;
|
||||
case "navigateback":
|
||||
if (!AutoPilot || MaintainPos || LevelEndSelected || !LevelStartSelected)
|
||||
if (objective.Override)
|
||||
{
|
||||
unsentChanges = true;
|
||||
if (MaintainPos || LevelEndSelected || !LevelStartSelected)
|
||||
{
|
||||
unsentChanges = true;
|
||||
}
|
||||
SetDestinationLevelStart();
|
||||
}
|
||||
SetDestinationLevelStart();
|
||||
break;
|
||||
case "navigatetodestination":
|
||||
if (!AutoPilot || MaintainPos || !LevelEndSelected || LevelStartSelected)
|
||||
if (objective.Override)
|
||||
{
|
||||
unsentChanges = true;
|
||||
if (MaintainPos || !LevelEndSelected || LevelStartSelected)
|
||||
{
|
||||
unsentChanges = true;
|
||||
}
|
||||
SetDestinationLevelEnd();
|
||||
}
|
||||
SetDestinationLevelEnd();
|
||||
break;
|
||||
}
|
||||
|
||||
sonar?.AIOperate(deltaTime, character, objective);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
private float charge;
|
||||
|
||||
private float rechargeVoltage;
|
||||
//private float rechargeVoltage;
|
||||
|
||||
//how fast the battery can be recharged
|
||||
private float maxRechargeSpeed;
|
||||
@@ -28,10 +28,7 @@ namespace Barotrauma.Items.Components
|
||||
protected Vector2 indicatorPosition, indicatorSize;
|
||||
|
||||
protected bool isHorizontal;
|
||||
|
||||
//a list of powered devices connected directly to this item
|
||||
private readonly List<Pair<Powered, Connection>> directlyConnected = new List<Pair<Powered, Connection>>(10);
|
||||
|
||||
|
||||
public float CurrPowerOutput
|
||||
{
|
||||
get;
|
||||
@@ -107,12 +104,18 @@ namespace Barotrauma.Items.Components
|
||||
if (!MathUtils.IsValid(value)) return;
|
||||
rechargeSpeed = MathHelper.Clamp(value, 0.0f, maxRechargeSpeed);
|
||||
rechargeSpeed = MathUtils.RoundTowardsClosest(rechargeSpeed, Math.Max(maxRechargeSpeed * 0.1f, 1.0f));
|
||||
if (isRunning)
|
||||
{
|
||||
HasBeenTuned = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public float RechargeRatio => RechargeSpeed / MaxRechargeSpeed;
|
||||
|
||||
public const float aiRechargeTargetRatio = 0.5f;
|
||||
private bool isRunning;
|
||||
public bool HasBeenTuned { get; private set; }
|
||||
|
||||
public PowerContainer(Item item, XElement element)
|
||||
: base(item, element)
|
||||
@@ -131,14 +134,13 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
isRunning = true;
|
||||
float chargeRatio = charge / capacity;
|
||||
float gridPower = 0.0f;
|
||||
float gridLoad = 0.0f;
|
||||
directlyConnected.Clear();
|
||||
|
||||
foreach (Connection c in item.Connections)
|
||||
{
|
||||
if (c.Name == "power_in") continue;
|
||||
if (!c.IsPower || !c.IsOutput) { continue; }
|
||||
foreach (Connection c2 in c.Recipients)
|
||||
{
|
||||
if (c2.Item.Condition <= 0.0f) { continue; }
|
||||
@@ -149,15 +151,13 @@ namespace Barotrauma.Items.Components
|
||||
foreach (Powered powered in c2.Item.GetComponents<Powered>())
|
||||
{
|
||||
if (!powered.IsActive) continue;
|
||||
directlyConnected.Add(new Pair<Powered, Connection>(powered, c2));
|
||||
gridLoad += powered.CurrPowerConsumption;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!pt.IsActive || !pt.CanTransfer) { continue; }
|
||||
|
||||
gridLoad += pt.PowerLoad;
|
||||
gridPower -= pt.CurrPowerConsumption;
|
||||
gridLoad += pt.PowerLoad;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,66 +168,51 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (charge >= capacity)
|
||||
{
|
||||
rechargeVoltage = 0.0f;
|
||||
//rechargeVoltage = 0.0f;
|
||||
charge = capacity;
|
||||
|
||||
CurrPowerConsumption = 0.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
currPowerConsumption = MathHelper.Lerp(currPowerConsumption, rechargeSpeed, 0.05f);
|
||||
Charge += currPowerConsumption * rechargeVoltage / 3600.0f;
|
||||
Charge += currPowerConsumption * Voltage / 3600.0f;
|
||||
}
|
||||
|
||||
//provide power to the grid
|
||||
if (gridLoad > 0.0f)
|
||||
|
||||
|
||||
if (charge <= 0.0f)
|
||||
{
|
||||
if (charge <= 0.0f)
|
||||
{
|
||||
CurrPowerOutput = 0.0f;
|
||||
charge = 0.0f;
|
||||
return;
|
||||
}
|
||||
|
||||
if (gridPower < gridLoad)
|
||||
{
|
||||
//output starts dropping when the charge is less than 10%
|
||||
float maxOutputRatio = 1.0f;
|
||||
if (chargeRatio < 0.1f)
|
||||
{
|
||||
maxOutputRatio = Math.Max(chargeRatio * 10.0f, 0.0f);
|
||||
}
|
||||
|
||||
CurrPowerOutput = MathHelper.Lerp(
|
||||
CurrPowerOutput,
|
||||
Math.Min(MaxOutPut * maxOutputRatio, gridLoad),
|
||||
deltaTime * 10.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
CurrPowerOutput = MathHelper.Lerp(CurrPowerOutput, 0.0f, deltaTime * 10.0f);
|
||||
}
|
||||
|
||||
Charge -= CurrPowerOutput / 3600.0f;
|
||||
CurrPowerOutput = 0.0f;
|
||||
charge = 0.0f;
|
||||
return;
|
||||
}
|
||||
item.SendSignal(0, ((int)Charge).ToString(), "charge", null);
|
||||
item.SendSignal(0, ((int)((Charge / capacity) * 100)).ToString(), "charge_%", null);
|
||||
item.SendSignal(0, ((int)((RechargeSpeed / maxRechargeSpeed) * 100)).ToString(), "charge_rate", null);
|
||||
|
||||
foreach (Pair<Powered, Connection> connected in directlyConnected)
|
||||
//output starts dropping when the charge is less than 10%
|
||||
float maxOutputRatio = 1.0f;
|
||||
if (chargeRatio < 0.1f)
|
||||
{
|
||||
connected.First.ReceiveSignal(0, "", connected.Second, source: item, sender: null,
|
||||
power: gridLoad <= 0.0f ? 1.0f : CurrPowerOutput / gridLoad);
|
||||
maxOutputRatio = Math.Max(chargeRatio * 10.0f, 0.0f);
|
||||
}
|
||||
|
||||
rechargeVoltage = 0.0f;
|
||||
CurrPowerOutput += (gridLoad - gridPower) * deltaTime;
|
||||
|
||||
float maxOutput = Math.Min(MaxOutPut * maxOutputRatio, gridLoad);
|
||||
CurrPowerOutput = MathHelper.Clamp(CurrPowerOutput, 0.0f, maxOutput);
|
||||
Charge -= CurrPowerOutput / 3600.0f;
|
||||
|
||||
item.SendSignal(0, ((int)Math.Round(Charge)).ToString(), "charge", null);
|
||||
item.SendSignal(0, ((int)Math.Round((Charge / capacity) * 100)).ToString(), "charge_%", null);
|
||||
item.SendSignal(0, ((int)Math.Round((RechargeSpeed / maxRechargeSpeed) * 100)).ToString(), "charge_rate", null);
|
||||
}
|
||||
|
||||
public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
|
||||
{
|
||||
#if CLIENT
|
||||
if (GameMain.Client != null) return false;
|
||||
#endif
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return false; }
|
||||
|
||||
if (objective.Override)
|
||||
{
|
||||
HasBeenTuned = false;
|
||||
}
|
||||
if (HasBeenTuned) { return true; }
|
||||
|
||||
if (string.IsNullOrEmpty(objective.Option) || objective.Option.ToLowerInvariant() == "charge")
|
||||
{
|
||||
@@ -274,6 +259,8 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power, float signalStrength = 1.0f)
|
||||
{
|
||||
if (connection.IsPower) { return; }
|
||||
|
||||
if (connection.Name == "set_rate")
|
||||
{
|
||||
if (float.TryParse(signal, NumberStyles.Any, CultureInfo.InvariantCulture, out float tempSpeed))
|
||||
@@ -290,12 +277,6 @@ namespace Barotrauma.Items.Components
|
||||
#endif
|
||||
}
|
||||
}
|
||||
if (!connection.IsPower) { return; }
|
||||
|
||||
if (connection.Name == "power_in")
|
||||
{
|
||||
rechargeVoltage = Math.Min(power, 1.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,37 +8,21 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
partial class PowerTransfer : Powered
|
||||
{
|
||||
private static float fullPower;
|
||||
private static float fullLoad;
|
||||
public List<Connection> PowerConnections { get; private set; }
|
||||
|
||||
private int updateCount;
|
||||
|
||||
//affects how fast changes in power/load are carried over the grid
|
||||
static float inertia = 5.0f;
|
||||
|
||||
private static HashSet<Powered> connectedList = new HashSet<Powered>();
|
||||
private List<Connection> powerConnections;
|
||||
public List<Connection> PowerConnections
|
||||
{
|
||||
get
|
||||
{
|
||||
return powerConnections;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private Dictionary<Connection, bool> connectionDirty = new Dictionary<Connection, bool>();
|
||||
private readonly Dictionary<Connection, bool> connectionDirty = new Dictionary<Connection, bool>();
|
||||
|
||||
//a list of connections a given connection is connected to, either directly or via other power transfer components
|
||||
private Dictionary<Connection, HashSet<Connection>> connectedRecipients = new Dictionary<Connection, HashSet<Connection>>();
|
||||
private readonly Dictionary<Connection, HashSet<Connection>> connectedRecipients = new Dictionary<Connection, HashSet<Connection>>();
|
||||
|
||||
private float powerLoad;
|
||||
protected float powerLoad;
|
||||
|
||||
private bool isBroken;
|
||||
protected bool isBroken;
|
||||
|
||||
public float PowerLoad
|
||||
{
|
||||
get { return powerLoad; }
|
||||
set { powerLoad = value; }
|
||||
}
|
||||
|
||||
[Editable, Serialize(true, true, description: "Can the item be damaged if too much power is supplied to the power grid.")]
|
||||
@@ -145,97 +129,43 @@ namespace Barotrauma.Items.Components
|
||||
SetAllConnectionsDirty();
|
||||
isBroken = false;
|
||||
}
|
||||
|
||||
if (updateCount > 0)
|
||||
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
|
||||
|
||||
//if the item can't be fixed, don't allow it to break
|
||||
if (!item.Repairables.Any() || !CanBeOverloaded) { return; }
|
||||
|
||||
float maxOverVoltage = Math.Max(OverloadVoltage, 1.0f);
|
||||
Overload = -currPowerConsumption > Math.Max(powerLoad, 200.0f) * maxOverVoltage;
|
||||
if (Overload && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer))
|
||||
{
|
||||
//this junction box has already been updated this frame
|
||||
updateCount--;
|
||||
return;
|
||||
}
|
||||
//damage the item if voltage is too high (except if running as a client)
|
||||
float prevCondition = item.Condition;
|
||||
item.Condition -= deltaTime * 10.0f;
|
||||
|
||||
Overload = false;
|
||||
|
||||
//reset and recalculate the power generated/consumed
|
||||
//by the constructions connected to the grid
|
||||
fullPower = 0.0f;
|
||||
fullLoad = 0.0f;
|
||||
|
||||
connectedList.Clear();
|
||||
|
||||
updateCount = 0;
|
||||
CheckJunctions(deltaTime);
|
||||
|
||||
foreach (Powered p in connectedList)
|
||||
{
|
||||
PowerTransfer pt = p as PowerTransfer;
|
||||
if (pt == null || pt.updateCount == 0) { continue; }
|
||||
|
||||
if (pt is RelayComponent != this is RelayComponent) { continue; }
|
||||
|
||||
pt.Overload = false;
|
||||
pt.powerLoad += (fullLoad - pt.powerLoad) / inertia;
|
||||
pt.currPowerConsumption += (-fullPower - pt.currPowerConsumption) / inertia;
|
||||
|
||||
float voltage = fullPower / Math.Max(fullLoad, 1.0f);
|
||||
if (this is RelayComponent)
|
||||
{
|
||||
pt.currPowerConsumption = Math.Max(-fullLoad, pt.currPowerConsumption);
|
||||
voltage = Math.Min(voltage, 1.0f);
|
||||
}
|
||||
|
||||
pt.Item.SendSignal(0, "", "power", null, voltage);
|
||||
pt.Item.SendSignal(0, "", "power_out", null, voltage);
|
||||
|
||||
//items in a bad condition are more sensitive to overvoltage
|
||||
float maxOverVoltage = MathHelper.Lerp(OverloadVoltage * 0.75f, OverloadVoltage, pt.item.Condition / pt.item.MaxCondition);
|
||||
maxOverVoltage = Math.Max(OverloadVoltage, 1.0f);
|
||||
|
||||
//if the item can't be fixed, don't allow it to break
|
||||
if (!pt.item.Repairables.Any() || !pt.CanBeOverloaded) { continue; }
|
||||
|
||||
//relays don't blow up if the power is higher than load, only if the output is high enough
|
||||
//(i.e. enough power passing through the relay)
|
||||
if (pt is RelayComponent) { continue; }
|
||||
|
||||
if (-pt.currPowerConsumption < Math.Max(pt.powerLoad, 200.0f) * maxOverVoltage) { continue; }
|
||||
|
||||
pt.Overload = true;
|
||||
#if CLIENT
|
||||
//damage the item if voltage is too high
|
||||
//(except if running as a client)
|
||||
if (GameMain.Client != null) { continue; }
|
||||
#endif
|
||||
float prevCondition = pt.item.Condition;
|
||||
pt.item.Condition -= deltaTime * 10.0f;
|
||||
|
||||
if (pt.item.Condition <= 0.0f && prevCondition > 0.0f)
|
||||
if (item.Condition <= 0.0f && prevCondition > 0.0f)
|
||||
{
|
||||
#if CLIENT
|
||||
SoundPlayer.PlaySound("zap", item.WorldPosition, hullGuess: pt.item.CurrentHull);
|
||||
|
||||
SoundPlayer.PlaySound("zap", item.WorldPosition, hullGuess: item.CurrentHull);
|
||||
Vector2 baseVel = Rand.Vector(300.0f);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var particle = GameMain.ParticleManager.CreateParticle("spark", pt.item.WorldPosition,
|
||||
baseVel + Rand.Vector(100.0f), 0.0f, pt.item.CurrentHull);
|
||||
|
||||
var particle = GameMain.ParticleManager.CreateParticle("spark", item.WorldPosition,
|
||||
baseVel + Rand.Vector(100.0f), 0.0f, item.CurrentHull);
|
||||
if (particle != null) particle.Size *= Rand.Range(0.5f, 1.0f);
|
||||
}
|
||||
#endif
|
||||
|
||||
float currentIntensity = GameMain.GameSession?.EventManager != null ?
|
||||
float currentIntensity = GameMain.GameSession?.EventManager != null ?
|
||||
GameMain.GameSession.EventManager.CurrentIntensity : 0.5f;
|
||||
|
||||
|
||||
//higher probability for fires if the current intensity is low
|
||||
if (pt.FireProbability > 0.0f &&
|
||||
Rand.Range(0.0f, 1.0f) < MathHelper.Lerp(pt.FireProbability, pt.FireProbability * 0.1f, currentIntensity))
|
||||
if (FireProbability > 0.0f &&
|
||||
Rand.Range(0.0f, 1.0f) < MathHelper.Lerp(FireProbability, FireProbability * 0.1f, currentIntensity))
|
||||
{
|
||||
new FireSource(pt.item.WorldPosition);
|
||||
new FireSource(item.WorldPosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateCount = 0;
|
||||
}
|
||||
|
||||
public override bool Pick(Character picker)
|
||||
@@ -243,7 +173,7 @@ namespace Barotrauma.Items.Components
|
||||
return picker != null;
|
||||
}
|
||||
|
||||
private void RefreshConnections()
|
||||
protected void RefreshConnections()
|
||||
{
|
||||
var connections = item.Connections;
|
||||
foreach (Connection c in connections)
|
||||
@@ -317,102 +247,6 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
//a recursive function that goes through all the junctions and adds up
|
||||
//all the generated/consumed power of the constructions connected to the grid
|
||||
private void CheckJunctions(float deltaTime, bool increaseUpdateCount = true, float clampPower = float.MaxValue, float clampLoad = float.MaxValue)
|
||||
{
|
||||
if (increaseUpdateCount)
|
||||
{
|
||||
updateCount = 1;
|
||||
}
|
||||
connectedList.Add(this);
|
||||
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
|
||||
|
||||
//float maxPower = this is RelayComponent relayComponent ? relayComponent.MaxPower : float.PositiveInfinity;
|
||||
RelayComponent thisRelayComponent = this as RelayComponent;
|
||||
if (thisRelayComponent != null)
|
||||
{
|
||||
clampPower = Math.Min(Math.Min(clampPower, thisRelayComponent.MaxPower), powerLoad);
|
||||
clampLoad = Math.Min(clampLoad, thisRelayComponent.MaxPower);
|
||||
}
|
||||
|
||||
foreach (Connection c in PowerConnections)
|
||||
{
|
||||
var recipients = c.Recipients;
|
||||
foreach (Connection recipient in recipients)
|
||||
{
|
||||
if (recipient?.Item == null || !recipient.IsPower) { continue; }
|
||||
|
||||
Item it = recipient.Item;
|
||||
if (it.Condition <= 0.0f) { continue; }
|
||||
|
||||
foreach (ItemComponent ic in it.Components)
|
||||
{
|
||||
if (!(ic is Powered powered) || !powered.IsActive) { continue; }
|
||||
if (connectedList.Contains(powered)) { continue; }
|
||||
|
||||
if (powered is PowerTransfer powerTransfer)
|
||||
{
|
||||
RelayComponent otherRelayComponent = powerTransfer as RelayComponent;
|
||||
if ((thisRelayComponent == null) == (otherRelayComponent == null))
|
||||
{
|
||||
if (!powerTransfer.CanTransfer) { continue; }
|
||||
powerTransfer.CheckJunctions(deltaTime, increaseUpdateCount, clampPower, clampLoad);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!powerTransfer.CanTransfer) continue;
|
||||
float maxPowerIn = (thisRelayComponent != null && c.IsOutput) ? 0.0f : clampPower;
|
||||
float maxPowerOut = (thisRelayComponent != null && !c.IsOutput) ? 0.0f : clampLoad;
|
||||
if (maxPowerIn > 0.0f || maxPowerOut > 0.0f)
|
||||
{
|
||||
powerTransfer.CheckJunctions(deltaTime, false, maxPowerIn, maxPowerOut);
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
float addLoad = 0.0f;
|
||||
float addPower = 0.0f;
|
||||
if (powered is PowerContainer powerContainer)
|
||||
{
|
||||
if (recipient.Name == "power_in")
|
||||
{
|
||||
addLoad = powerContainer.CurrPowerConsumption;
|
||||
}
|
||||
else
|
||||
{
|
||||
addPower = powerContainer.CurrPowerOutput;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
connectedList.Add(powered);
|
||||
//positive power consumption = the construction requires power -> increase load
|
||||
if (powered.CurrPowerConsumption > 0.0f)
|
||||
{
|
||||
addLoad = powered.CurrPowerConsumption;
|
||||
}
|
||||
else if (powered.CurrPowerConsumption < 0.0f)
|
||||
//negative power consumption = the construction is a
|
||||
//generator/battery or another junction box
|
||||
{
|
||||
addPower -= powered.CurrPowerConsumption;
|
||||
}
|
||||
}
|
||||
|
||||
if (addPower + fullPower > clampPower) { addPower -= (addPower + fullPower) - clampPower; };
|
||||
if (addPower > 0) { fullPower += addPower; }
|
||||
|
||||
if (addLoad + fullLoad > clampLoad) { addLoad -= (addLoad + fullLoad) - clampLoad; };
|
||||
if (addLoad > 0) { fullLoad += addLoad; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SetAllConnectionsDirty()
|
||||
{
|
||||
if (item.Connections == null) return;
|
||||
@@ -431,8 +265,9 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public override void OnItemLoaded()
|
||||
{
|
||||
base.OnItemLoaded();
|
||||
var connections = Item.Connections;
|
||||
powerConnections = connections == null ? new List<Connection>() : connections.FindAll(c => c.IsPower);
|
||||
PowerConnections = connections == null ? new List<Connection>() : connections.FindAll(c => c.IsPower);
|
||||
if (connections == null)
|
||||
{
|
||||
IsActive = false;
|
||||
@@ -440,33 +275,45 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
SetAllConnectionsDirty();
|
||||
}
|
||||
|
||||
|
||||
public override void ReceivePowerProbeSignal(Connection connection, Item source, float power)
|
||||
{
|
||||
//we've already received this signal
|
||||
if (lastPowerProbeRecipients.Contains(this)) { return; }
|
||||
lastPowerProbeRecipients.Add(this);
|
||||
|
||||
if (power < 0.0f)
|
||||
{
|
||||
powerLoad -= power;
|
||||
}
|
||||
else
|
||||
{
|
||||
currPowerConsumption -= power;
|
||||
}
|
||||
powerOut?.SendPowerProbeSignal(source, power);
|
||||
}
|
||||
|
||||
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power, float signalStrength = 1.0f)
|
||||
{
|
||||
if (connection.IsPower) return;
|
||||
|
||||
base.ReceiveSignal(stepsTaken, signal, connection, source, sender, power);
|
||||
|
||||
if (!connectedRecipients.ContainsKey(connection)) return;
|
||||
if (item.Condition <= 0.0f || connection.IsPower) { return; }
|
||||
if (!connectedRecipients.ContainsKey(connection)) { return; }
|
||||
|
||||
if (connection.Name.Length > 5 && connection.Name.Substring(0, 6) == "signal")
|
||||
{
|
||||
foreach (Connection recipient in connectedRecipients[connection])
|
||||
{
|
||||
if (recipient.Item == item || recipient.Item == source) continue;
|
||||
if (recipient.Item == item || recipient.Item == source) { continue; }
|
||||
|
||||
foreach (ItemComponent ic in recipient.Item.Components)
|
||||
{
|
||||
//powertransfer components don't need to receive the signal in the pass-through signal connections
|
||||
//because we relay it straight to the connected items without going through the whole chain of junction boxes
|
||||
if (ic is PowerTransfer && connection.Name.Contains("signal")) continue;
|
||||
if (ic is PowerTransfer && connection.Name.Contains("signal")) { continue; }
|
||||
ic.ReceiveSignal(stepsTaken, signal, recipient, source, sender, 0.0f, signalStrength);
|
||||
}
|
||||
|
||||
bool broken = recipient.Item.Condition <= 0.0f;
|
||||
foreach (StatusEffect effect in recipient.Effects)
|
||||
{
|
||||
if (broken && effect.type != ActionType.OnBroken) continue;
|
||||
recipient.Item.ApplyStatusEffect(effect, ActionType.OnUse, 1.0f, null, null, false, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Collections.Generic;
|
||||
#if CLIENT
|
||||
using Barotrauma.Sounds;
|
||||
#endif
|
||||
@@ -9,25 +10,47 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
partial class Powered : ItemComponent
|
||||
{
|
||||
//the amount of power CURRENTLY consumed by the item
|
||||
//negative values mean that the item is providing power to connected items
|
||||
private static float updateTimer;
|
||||
protected static float UpdateInterval = 0.2f;
|
||||
|
||||
/// <summary>
|
||||
/// List of all powered ItemComponents
|
||||
/// </summary>
|
||||
private static readonly List<Powered> poweredList = new List<Powered>();
|
||||
|
||||
/// <summary>
|
||||
/// Items that have already received the "probe signal" that's used to distribute power and load across the grid
|
||||
/// </summary>
|
||||
protected static HashSet<PowerTransfer> lastPowerProbeRecipients = new HashSet<PowerTransfer>();
|
||||
|
||||
/// <summary>
|
||||
/// The amount of power currently consumed by the item. Negative values mean that the item is providing power to connected items
|
||||
/// </summary>
|
||||
protected float currPowerConsumption;
|
||||
|
||||
//current voltage of the item (load / power)
|
||||
protected float voltage;
|
||||
/// <summary>
|
||||
/// Current voltage of the item (load / power)
|
||||
/// </summary>
|
||||
private float voltage;
|
||||
|
||||
//the minimum voltage required for the item to work
|
||||
protected float minVoltage;
|
||||
/// <summary>
|
||||
/// The minimum voltage required for the item to work
|
||||
/// </summary>
|
||||
private float minVoltage;
|
||||
|
||||
//the maximum amount of power the item can draw from connected items
|
||||
/// <summary>
|
||||
/// The maximum amount of power the item can draw from connected items
|
||||
/// </summary>
|
||||
protected float powerConsumption;
|
||||
|
||||
protected Connection powerIn, powerOut;
|
||||
|
||||
[Editable, Serialize(0.5f, true, description: "The minimum voltage required for the device to function. " +
|
||||
"The voltage is calculated as power / powerconsumption, meaning that a device " +
|
||||
"with a power consumption of 1000 kW would need at least 500 kW of power to work if the minimum voltage is set to 0.5.")]
|
||||
public float MinVoltage
|
||||
{
|
||||
get { return minVoltage; }
|
||||
get { return powerConsumption <= 0.0f ? 0.0f : minVoltage; }
|
||||
set { minVoltage = value; }
|
||||
}
|
||||
|
||||
@@ -76,34 +99,32 @@ namespace Barotrauma.Items.Components
|
||||
public Powered(Item item, XElement element)
|
||||
: base(item, element)
|
||||
{
|
||||
poweredList.Add(this);
|
||||
InitProjectSpecific(element);
|
||||
}
|
||||
|
||||
partial void InitProjectSpecific(XElement element);
|
||||
|
||||
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0, float signalStrength = 1.0f)
|
||||
{
|
||||
if (currPowerConsumption == 0.0f) voltage = 0.0f;
|
||||
if (connection.IsPower) voltage = Math.Max(0.0f, power);
|
||||
}
|
||||
|
||||
protected void UpdateOnActiveEffects(float deltaTime)
|
||||
{
|
||||
if (currPowerConsumption == 0.0f)
|
||||
if (currPowerConsumption <= 0.0f)
|
||||
{
|
||||
//if the item consumes no power, ignore the voltage requirement and
|
||||
//apply OnActive statuseffects as long as this component is active
|
||||
if (powerConsumption == 0.0f)
|
||||
if (powerConsumption <= 0.0f)
|
||||
{
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
if (voltage > minVoltage)
|
||||
{
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
|
||||
}
|
||||
#if CLIENT
|
||||
if (voltage > minVoltage)
|
||||
{
|
||||
if (!powerOnSoundPlayed && powerOnSound != null)
|
||||
{
|
||||
SoundPlayer.PlaySound(powerOnSound.Sound, item.WorldPosition, powerOnSound.Volume, powerOnSound.Range, item.CurrentHull);
|
||||
@@ -114,21 +135,160 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
powerOnSoundPlayed = false;
|
||||
}
|
||||
#else
|
||||
if (voltage > minVoltage)
|
||||
{
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
UpdateOnActiveEffects(deltaTime);
|
||||
|
||||
voltage = 0.0f;
|
||||
}
|
||||
|
||||
public override void OnItemLoaded()
|
||||
{
|
||||
if (item.Connections == null) { return; }
|
||||
foreach (Connection c in item.Connections)
|
||||
{
|
||||
if (!c.IsPower) { continue; }
|
||||
if (this is PowerTransfer pt)
|
||||
{
|
||||
if (c.Name == "power_in")
|
||||
{
|
||||
powerIn = c;
|
||||
}
|
||||
else if (c.Name == "power_out")
|
||||
{
|
||||
powerOut = c;
|
||||
}
|
||||
else if (c.Name == "power")
|
||||
{
|
||||
powerIn = powerOut = c;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (c.IsOutput)
|
||||
{
|
||||
if (c.Name == "power_in")
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.ThrowError($"Item \"{item.Name}\" has a power output connection called power_in. If the item is supposed to receive power through the connection, change it to an input connection.");
|
||||
#else
|
||||
DebugConsole.NewMessage($"Item \"{item.Name}\" has a power output connection called power_in. If the item is supposed to receive power through the connection, change it to an input connection.", Color.Orange);
|
||||
#endif
|
||||
}
|
||||
powerOut = c;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (c.Name == "power_out")
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.ThrowError($"Item \"{item.Name}\" has a power input connection called power_out. If the item is supposed to output power through the connection, change it to an output connection.");
|
||||
#else
|
||||
DebugConsole.NewMessage($"Item \"{item.Name}\" has a power input connection called power_out. If the item is supposed to output power through the connection, change it to an output connection.", Color.Orange);
|
||||
#endif
|
||||
}
|
||||
powerIn = c;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void ReceivePowerProbeSignal(Connection connection, Item source, float power) { }
|
||||
|
||||
public static void UpdatePower(float deltaTime)
|
||||
{
|
||||
if (updateTimer > 0.0f)
|
||||
{
|
||||
updateTimer -= deltaTime;
|
||||
return;
|
||||
}
|
||||
updateTimer = UpdateInterval;
|
||||
|
||||
//reset power first
|
||||
foreach (Powered powered in poweredList)
|
||||
{
|
||||
if (powered is PowerTransfer pt)
|
||||
{
|
||||
powered.CurrPowerConsumption = 0.0f;
|
||||
pt.PowerLoad = 0.0f;
|
||||
if (pt is RelayComponent relay)
|
||||
{
|
||||
relay.DisplayLoad = 0.0f;
|
||||
}
|
||||
}
|
||||
//only reset voltage if the item has a power connector
|
||||
//(other items, such as handheld devices, get power through other means and shouldn't be updated here)
|
||||
if (powered.powerIn != null || powered.powerOut != null) { powered.voltage = 0.0f; }
|
||||
}
|
||||
|
||||
//go through all the devices that are consuming/providing power
|
||||
//and send out a "probe signal" which the PowerTransfer components use to add up the grid power/load
|
||||
foreach (Powered powered in poweredList)
|
||||
{
|
||||
if (powered is PowerTransfer) { continue; }
|
||||
if (powered.currPowerConsumption > 0.0f)
|
||||
{
|
||||
//consuming power
|
||||
lastPowerProbeRecipients.Clear();
|
||||
powered.powerIn?.SendPowerProbeSignal(powered.item, -powered.currPowerConsumption);
|
||||
}
|
||||
}
|
||||
foreach (Powered powered in poweredList)
|
||||
{
|
||||
if (powered is PowerTransfer) { continue; }
|
||||
else if (powered.currPowerConsumption < 0.0f)
|
||||
{
|
||||
//providing power
|
||||
lastPowerProbeRecipients.Clear();
|
||||
powered.powerOut?.SendPowerProbeSignal(powered.item, -powered.currPowerConsumption);
|
||||
}
|
||||
if (powered is PowerContainer pc)
|
||||
{
|
||||
if (pc.CurrPowerOutput <= 0.0f) { continue; }
|
||||
//providing power
|
||||
lastPowerProbeRecipients.Clear();
|
||||
powered.powerOut?.SendPowerProbeSignal(powered.item, pc.CurrPowerOutput);
|
||||
}
|
||||
}
|
||||
//go through powered items and calculate their current voltage
|
||||
foreach (Powered powered in poweredList)
|
||||
{
|
||||
if (powered is PowerTransfer pt1 || (pt1 = powered.Item.GetComponent<PowerTransfer>()) != null)
|
||||
{
|
||||
powered.voltage = -pt1.CurrPowerConsumption / Math.Max(pt1.PowerLoad, 1.0f);
|
||||
continue;
|
||||
}
|
||||
if (powered.powerConsumption <= 0.0f && !(powered is PowerContainer))
|
||||
{
|
||||
powered.voltage = 1.0f;
|
||||
continue;
|
||||
}
|
||||
if (powered.powerIn == null) { continue; }
|
||||
|
||||
foreach (Connection powerSource in powered.powerIn.Recipients)
|
||||
{
|
||||
if (!powerSource.IsPower || !powerSource.IsOutput) { continue; }
|
||||
var pt = powerSource.Item.GetComponent<PowerTransfer>();
|
||||
if (pt != null)
|
||||
{
|
||||
float voltage = -pt.CurrPowerConsumption / Math.Max(pt.PowerLoad, 1.0f);
|
||||
powered.voltage = Math.Max(powered.voltage, voltage);
|
||||
continue;
|
||||
}
|
||||
var pc = powerSource.Item.GetComponent<PowerContainer>();
|
||||
if (pc != null)
|
||||
{
|
||||
float voltage = -pc.CurrPowerOutput / Math.Max(powered.CurrPowerConsumption, 1.0f);
|
||||
powered.voltage += voltage;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void RemoveComponentSpecific()
|
||||
{
|
||||
poweredList.Remove(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -416,9 +416,8 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (IgnoredBodies.Contains(target.Body)) { return false; }
|
||||
|
||||
if (target.UserData is Item) { return false; }
|
||||
|
||||
if (target.CollisionCategories == Physics.CollisionCharacter && !(target.Body.UserData is Limb))
|
||||
//ignore character colliders (the projectile only hits limbs)
|
||||
if (target.CollisionCategories == Physics.CollisionCharacter && target.Body.UserData is Character)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -445,17 +444,52 @@ namespace Barotrauma.Items.Components
|
||||
if (attack != null) { attackResult = attack.DoDamageToLimb(User, limb, item.WorldPosition, 1.0f); }
|
||||
if (limb.character != null) { character = limb.character; }
|
||||
}
|
||||
else if (target.Body.UserData is Structure structure)
|
||||
else if (target.Body.UserData is Item targetItem)
|
||||
{
|
||||
if (attack != null) { attackResult = attack.DoDamage(User, structure, item.WorldPosition, 1.0f); }
|
||||
if (attack != null && targetItem.Prefab.DamagedByProjectiles)
|
||||
{
|
||||
attackResult = attack.DoDamage(User, targetItem, item.WorldPosition, 1.0f);
|
||||
}
|
||||
}
|
||||
else if (target.Body.UserData is IDamageable damageable)
|
||||
{
|
||||
if (attack != null) { attackResult = attack.DoDamage(User, damageable, item.WorldPosition, 1.0f); }
|
||||
}
|
||||
|
||||
if (character != null) { character.LastDamageSource = item; }
|
||||
|
||||
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
|
||||
{
|
||||
ApplyStatusEffects(ActionType.OnUse, 1.0f, character, target.Body.UserData as Limb, user: user);
|
||||
ApplyStatusEffects(ActionType.OnImpact, 1.0f, character, target.Body.UserData as Limb, user: user);
|
||||
if (target.Body.UserData is Limb targetLimb)
|
||||
{
|
||||
ApplyStatusEffects(ActionType.OnUse, 1.0f, character, targetLimb, user: user);
|
||||
ApplyStatusEffects(ActionType.OnImpact, 1.0f, character, targetLimb, user: user);
|
||||
var attack = targetLimb.attack;
|
||||
if (attack != null)
|
||||
{
|
||||
// Apply the status effects defined in the limb's attack that was hit
|
||||
foreach (var effect in attack.StatusEffects)
|
||||
{
|
||||
if (effect.type == ActionType.OnImpact)
|
||||
{
|
||||
//effect.Apply(effect.type, 1.0f, targetLimb.character, targetLimb.character, targetLimb.WorldPosition);
|
||||
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.This))
|
||||
{
|
||||
effect.Apply(effect.type, 1.0f, targetLimb.character, targetLimb.character, targetLimb.WorldPosition);
|
||||
}
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
|
||||
effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
|
||||
{
|
||||
var targets = new List<ISerializableEntity>();
|
||||
effect.GetNearbyTargets(targetLimb.WorldPosition, targets);
|
||||
effect.Apply(ActionType.OnActive, 1.0f, targetLimb.character, targets);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#if SERVER
|
||||
if (GameMain.NetworkMember.IsServer)
|
||||
{
|
||||
|
||||
@@ -92,9 +92,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
foreach (XElement connectionElement in subElement.Elements())
|
||||
{
|
||||
if (connectionElement.Name.ToString() != element.Name.ToString()) { continue; }
|
||||
|
||||
string prefabConnectionName = element.GetAttributeString("name", IsOutput ? "output" : "input");
|
||||
string prefabConnectionName = element.GetAttributeString("name", null);
|
||||
if (prefabConnectionName == Name)
|
||||
{
|
||||
displayNameTag = connectionElement.GetAttributeString("displayname", "");
|
||||
@@ -245,31 +243,38 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
for (int i = 0; i < MaxLinked; i++)
|
||||
{
|
||||
if (wires[i] == null) continue;
|
||||
if (wires[i] == null) { continue; }
|
||||
|
||||
Connection recipient = wires[i].OtherConnection(this);
|
||||
if (recipient == null) continue;
|
||||
if (recipient.item == this.item || recipient.item == source) continue;
|
||||
if (recipient == null) { continue; }
|
||||
if (recipient.item == this.item || recipient.item == source) { continue; }
|
||||
|
||||
if (source != null && !source.LastSentSignalRecipients.Contains(recipient.item))
|
||||
{
|
||||
source.LastSentSignalRecipients.Add(recipient.item);
|
||||
}
|
||||
source?.LastSentSignalRecipients.Add(recipient.item);
|
||||
|
||||
foreach (ItemComponent ic in recipient.item.Components)
|
||||
{
|
||||
ic.ReceiveSignal(stepsTaken, signal, recipient, source, sender, power, signalStrength);
|
||||
}
|
||||
|
||||
bool broken = recipient.Item.Condition <= 0.0f;
|
||||
foreach (StatusEffect effect in recipient.Effects)
|
||||
{
|
||||
if (broken && effect.type != ActionType.OnBroken) continue;
|
||||
recipient.Item.ApplyStatusEffect(effect, ActionType.OnUse, (float)Timing.Step, null, null, false, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SendPowerProbeSignal(Item source, float power)
|
||||
{
|
||||
for (int i = 0; i < MaxLinked; i++)
|
||||
{
|
||||
if (wires[i] == null) { continue; }
|
||||
|
||||
Connection recipient = wires[i].OtherConnection(this);
|
||||
if (recipient == null) { continue; }
|
||||
|
||||
recipient.item.GetComponent<Powered>()?.ReceivePowerProbeSignal(recipient, source, power);
|
||||
}
|
||||
}
|
||||
public void ClearConnections()
|
||||
{
|
||||
for (int i = 0; i < MaxLinked; i++)
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
class FunctionComponent : ItemComponent
|
||||
{
|
||||
public enum FunctionType
|
||||
{
|
||||
Round,
|
||||
Ceil,
|
||||
Floor,
|
||||
Factorial
|
||||
}
|
||||
|
||||
[Serialize(FunctionType.Round, false, description: "Which kind of function to run the input through.")]
|
||||
public FunctionType Function
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public FunctionComponent(Item item, XElement element)
|
||||
: base(item, element)
|
||||
{
|
||||
IsActive = true;
|
||||
}
|
||||
|
||||
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0, float signalStrength = 1)
|
||||
{
|
||||
float.TryParse(signal, out float value);
|
||||
switch (Function)
|
||||
{
|
||||
case FunctionType.Round:
|
||||
item.SendSignal(0, Math.Round(value).ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
|
||||
break;
|
||||
case FunctionType.Ceil:
|
||||
item.SendSignal(0, Math.Ceiling(value).ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
|
||||
break;
|
||||
case FunctionType.Floor:
|
||||
item.SendSignal(0, Math.Floor(value).ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
|
||||
break;
|
||||
case FunctionType.Factorial:
|
||||
int intVal = (int)Math.Min(value, 20);
|
||||
ulong factorial = 1;
|
||||
for (int i = intVal; i > 0; i--)
|
||||
{
|
||||
factorial *= (ulong)i;
|
||||
}
|
||||
item.SendSignal(0, factorial.ToString(), "signal_out", null);
|
||||
break;
|
||||
default:
|
||||
throw new NotImplementedException($"Function {Function} has not been implemented.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -216,19 +216,12 @@ namespace Barotrauma.Items.Components
|
||||
#endif
|
||||
}
|
||||
|
||||
if (powerConsumption == 0.0f)
|
||||
{
|
||||
voltage = 1.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
currPowerConsumption = powerConsumption;
|
||||
}
|
||||
currPowerConsumption = powerConsumption;
|
||||
|
||||
if (Rand.Range(0.0f, 1.0f) < 0.05f && voltage < Rand.Range(0.0f, minVoltage))
|
||||
if (Rand.Range(0.0f, 1.0f) < 0.05f && Voltage < Rand.Range(0.0f, MinVoltage))
|
||||
{
|
||||
#if CLIENT
|
||||
if (voltage > 0.1f)
|
||||
if (Voltage > 0.1f)
|
||||
{
|
||||
SoundPlayer.PlaySound("zap", item.WorldPosition, hullGuess: item.CurrentHull);
|
||||
}
|
||||
@@ -237,7 +230,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
else
|
||||
{
|
||||
lightBrightness = MathHelper.Lerp(lightBrightness, Math.Min(voltage, 1.0f), 0.1f);
|
||||
lightBrightness = MathHelper.Lerp(lightBrightness, Math.Min(Voltage, 1.0f), 0.1f);
|
||||
}
|
||||
|
||||
if (blinkFrequency > 0.0f)
|
||||
@@ -262,8 +255,6 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
UpdateAITarget(item.AiTarget);
|
||||
}
|
||||
|
||||
voltage -= deltaTime;
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
using System.Globalization;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
class ModuloComponent : ItemComponent
|
||||
{
|
||||
private float modulus;
|
||||
[InGameEditable, Serialize(1.0f, false, description: "The modulus of the operation. Must be non-zero.")]
|
||||
public float Modulus
|
||||
{
|
||||
get { return modulus; }
|
||||
set
|
||||
{
|
||||
modulus = MathUtils.NearlyEqual(value, 0.0f) ? 1.0f : value;
|
||||
}
|
||||
}
|
||||
|
||||
public ModuloComponent(Item item, XElement element) : base(item, element)
|
||||
{
|
||||
IsActive = true;
|
||||
}
|
||||
|
||||
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0, float signalStrength = 1)
|
||||
{
|
||||
switch (connection.Name)
|
||||
{
|
||||
case "set_modulus":
|
||||
case "modulus":
|
||||
float.TryParse(signal, out float newModulus);
|
||||
Modulus = newModulus;
|
||||
break;
|
||||
case "signal_in":
|
||||
float.TryParse(signal, out float value);
|
||||
item.SendSignal(0, (value % modulus).ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,14 @@ namespace Barotrauma.Items.Components
|
||||
set;
|
||||
}
|
||||
|
||||
[Editable, Serialize(false, true, description: "Should the sensor ignore the bodies of dead characters?")]
|
||||
public bool IgnoreDead
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
|
||||
[InGameEditable, Serialize(0.0f, true, description: "Horizontal detection range.")]
|
||||
public float RangeX
|
||||
{
|
||||
@@ -109,6 +117,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
foreach (Character c in Character.CharacterList)
|
||||
{
|
||||
if (IgnoreDead && c.IsDead) { continue; }
|
||||
if (OnlyHumans && !c.IsHuman) { continue; }
|
||||
|
||||
//do a rough check based on the position of the character's collider first
|
||||
@@ -138,5 +147,15 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
detectOffset.Y = -detectOffset.Y;
|
||||
}
|
||||
public override XElement Save(XElement parentElement)
|
||||
{
|
||||
Vector2 prevDetectOffset = detectOffset;
|
||||
//undo flipping before saving
|
||||
if (item.FlippedX) { detectOffset.X = -detectOffset.X; }
|
||||
if (item.FlippedY) { detectOffset.Y = -detectOffset.Y; }
|
||||
XElement element = base.Save(parentElement);
|
||||
detectOffset = prevDetectOffset;
|
||||
return element;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Barotrauma.Networking;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Xml.Linq;
|
||||
@@ -8,9 +9,11 @@ namespace Barotrauma.Items.Components
|
||||
class RelayComponent : PowerTransfer, IServerSerializable
|
||||
{
|
||||
private float maxPower;
|
||||
|
||||
|
||||
private bool isOn;
|
||||
|
||||
private float throttlePowerOutput;
|
||||
|
||||
private static readonly Dictionary<string, string> connectionPairs = new Dictionary<string, string>
|
||||
{
|
||||
{ "power_in", "power_out"},
|
||||
@@ -21,6 +24,7 @@ namespace Barotrauma.Items.Components
|
||||
{ "signal_in4", "signal_out4" },
|
||||
{ "signal_in5", "signal_out5" }
|
||||
};
|
||||
public float DisplayLoad { get; set; }
|
||||
|
||||
[Editable, Serialize(1000.0f, true, description: "The maximum amount of power that can pass through the item.")]
|
||||
public float MaxPower
|
||||
@@ -31,7 +35,7 @@ namespace Barotrauma.Items.Components
|
||||
maxPower = Math.Max(0.0f, value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Editable, Serialize(false, true, description: "Can the relay currently pass power and signals through it.")]
|
||||
public bool IsOn
|
||||
{
|
||||
@@ -49,18 +53,46 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public RelayComponent(Item item, XElement element)
|
||||
: base (item, element)
|
||||
: base(item, element)
|
||||
{
|
||||
IsActive = true;
|
||||
}
|
||||
|
||||
throttlePowerOutput = MaxPower;
|
||||
}
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
base.Update(deltaTime, cam);
|
||||
RefreshConnections();
|
||||
|
||||
item.SendSignal(0, IsOn ? "1" : "0", "state_out", null);
|
||||
|
||||
if (!CanTransfer) { Voltage = 0.0f; return; }
|
||||
|
||||
if (isBroken)
|
||||
{
|
||||
SetAllConnectionsDirty();
|
||||
isBroken = false;
|
||||
}
|
||||
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
|
||||
|
||||
if (powerOut != null)
|
||||
{
|
||||
bool overloaded = false;
|
||||
foreach (Connection recipient in powerOut.Recipients)
|
||||
{
|
||||
var pt = recipient.Item.GetComponent<PowerTransfer>();
|
||||
if (pt != null)
|
||||
{
|
||||
float overload = -pt.CurrPowerConsumption - pt.PowerLoad;
|
||||
throttlePowerOutput += overload * deltaTime * 0.5f;
|
||||
overloaded = overload > 1.0f;
|
||||
}
|
||||
}
|
||||
throttlePowerOutput = overloaded ?
|
||||
MathHelper.Clamp(throttlePowerOutput, 0.0f, MaxPower):
|
||||
Math.Max(throttlePowerOutput - MaxPower * 0.1f * deltaTime, 0.0f);
|
||||
}
|
||||
|
||||
if (Math.Min(-currPowerConsumption, PowerLoad) > maxPower && CanBeOverloaded)
|
||||
{
|
||||
@@ -68,9 +100,56 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
public override void ReceivePowerProbeSignal(Connection connection, Item source, float power)
|
||||
{
|
||||
if (!IsOn) { return; }
|
||||
|
||||
//we've already received this signal
|
||||
if (lastPowerProbeRecipients.Contains(this)) { return; }
|
||||
lastPowerProbeRecipients.Add(this);
|
||||
|
||||
if (power < 0.0f)
|
||||
{
|
||||
if (!connection.IsOutput || powerIn == null) { return; }
|
||||
|
||||
//power being drawn from the power_out connection
|
||||
DisplayLoad -= Math.Min(power, 0.0f);
|
||||
powerLoad -= Math.Min(power + throttlePowerOutput, 0.0f);
|
||||
|
||||
//pass the load to items connected to the input
|
||||
powerIn.SendPowerProbeSignal(source, Math.Max(power, -MaxPower));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (connection.IsOutput || powerOut == null) { return; }
|
||||
//power being supplied to the power_in connection
|
||||
if (currPowerConsumption - power < -MaxPower)
|
||||
{
|
||||
power += MaxPower + (currPowerConsumption - power);
|
||||
}
|
||||
|
||||
currPowerConsumption -= power;
|
||||
|
||||
foreach (Connection recipient in powerOut.Recipients)
|
||||
{
|
||||
if (!recipient.IsPower) { continue; }
|
||||
var powered = recipient.Item.GetComponent<Powered>();
|
||||
if (powered == null) { continue; }
|
||||
|
||||
float load = powered.CurrPowerConsumption;
|
||||
var powerTransfer = powered as PowerTransfer;
|
||||
if (powerTransfer != null) { load = powerTransfer.PowerLoad; }
|
||||
|
||||
float powerOut = power * (load / Math.Max(powerLoad + throttlePowerOutput, 0.01f));
|
||||
powered.ReceivePowerProbeSignal(recipient, source, Math.Min(powerOut, power));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f)
|
||||
{
|
||||
if (connection.IsPower || item.Condition <= 0.0f) { return; }
|
||||
if (item.Condition <= 0.0f || connection.IsPower) { return; }
|
||||
|
||||
if (connectionPairs.TryGetValue(connection.Name, out string outConnection))
|
||||
{
|
||||
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
class TrigonometricFunctionComponent : ItemComponent
|
||||
{
|
||||
public enum FunctionType
|
||||
{
|
||||
Sin,
|
||||
Cos,
|
||||
Tan,
|
||||
Asin,
|
||||
Acos,
|
||||
Atan,
|
||||
}
|
||||
|
||||
protected float[] receivedSignal = new float[2];
|
||||
|
||||
[Serialize(FunctionType.Sin, false, description: "Which kind of function to run the input through.")]
|
||||
public FunctionType Function
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
|
||||
[InGameEditable, Serialize(false, true, description: "If set to true, the trigonometric function uses radians instead of degrees.")]
|
||||
public bool UseRadians
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
|
||||
public TrigonometricFunctionComponent(Item item, XElement element)
|
||||
: base(item, element)
|
||||
{
|
||||
IsActive = true;
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
//reset received signals
|
||||
receivedSignal[0] = float.NaN;
|
||||
receivedSignal[1] = float.NaN;
|
||||
}
|
||||
|
||||
|
||||
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0, float signalStrength = 1)
|
||||
{
|
||||
float.TryParse(signal, out float value);
|
||||
switch (Function)
|
||||
{
|
||||
case FunctionType.Sin:
|
||||
if (!UseRadians) { value = MathHelper.ToRadians(value); }
|
||||
item.SendSignal(0, ((float)Math.Sin(value)).ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
|
||||
break;
|
||||
case FunctionType.Cos:
|
||||
if (!UseRadians) { value = MathHelper.ToRadians(value); }
|
||||
item.SendSignal(0, ((float)Math.Cos(value)).ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
|
||||
break;
|
||||
case FunctionType.Tan:
|
||||
if (!UseRadians) { value = MathHelper.ToRadians(value); }
|
||||
item.SendSignal(0, ((float)Math.Tan(value)).ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
|
||||
break;
|
||||
case FunctionType.Asin:
|
||||
{
|
||||
float angle = (float)Math.Asin(value);
|
||||
if (!UseRadians) { angle = MathHelper.ToDegrees(angle); }
|
||||
item.SendSignal(0, angle.ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
|
||||
}
|
||||
break;
|
||||
case FunctionType.Acos:
|
||||
{
|
||||
float angle = (float)Math.Acos(value);
|
||||
if (!UseRadians) { angle = MathHelper.ToDegrees(angle); }
|
||||
item.SendSignal(0, angle.ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
|
||||
}
|
||||
break;
|
||||
case FunctionType.Atan:
|
||||
if (connection.Name == "signal_in_x")
|
||||
{
|
||||
float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out receivedSignal[0]);
|
||||
}
|
||||
else if (connection.Name == "signal_in_y")
|
||||
{
|
||||
float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out receivedSignal[1]);
|
||||
if (!float.IsNaN(receivedSignal[0]) && !float.IsNaN(receivedSignal[1]))
|
||||
{
|
||||
float angle = (float)Math.Atan2(receivedSignal[1], receivedSignal[0]);
|
||||
if (!UseRadians) { angle = MathHelper.ToDegrees(angle); }
|
||||
item.SendSignal(0, angle.ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
float angle = (float)Math.Atan(value);
|
||||
if (!UseRadians) { angle = MathHelper.ToDegrees(angle); }
|
||||
item.SendSignal(0, angle.ToString("G", CultureInfo.InvariantCulture), "signal_out", null);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new NotImplementedException($"Function {Function} has not been implemented.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ namespace Barotrauma.Items.Components
|
||||
[Serialize(Character.TeamType.None, false, description: "WiFi components can only communicate with components that have the same Team ID.")]
|
||||
public Character.TeamType TeamID { get; set; }
|
||||
|
||||
[Serialize(20000.0f, false, description: "How close the recipient has to be to receive a signal from this WiFi component.")]
|
||||
[Editable, Serialize(20000.0f, false, description: "How close the recipient has to be to receive a signal from this WiFi component.")]
|
||||
public float Range
|
||||
{
|
||||
get { return range; }
|
||||
@@ -174,8 +174,6 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
protected override void RemoveComponentSpecific()
|
||||
{
|
||||
base.RemoveComponentSpecific();
|
||||
|
||||
list.Remove(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,22 +83,16 @@ namespace Barotrauma.Items.Components
|
||||
public Wire(Item item, XElement element)
|
||||
: base(item, element)
|
||||
{
|
||||
#if CLIENT
|
||||
if (wireSprite == null)
|
||||
{
|
||||
wireSprite = new Sprite("Content/Items/wireHorizontal.png", new Vector2(0.5f, 0.5f))
|
||||
{
|
||||
Depth = 0.85f
|
||||
};
|
||||
}
|
||||
#endif
|
||||
|
||||
nodes = new List<Vector2>();
|
||||
sections = new List<WireSection>();
|
||||
connections = new Connection[2];
|
||||
IsActive = false;
|
||||
|
||||
InitProjSpecific(element);
|
||||
}
|
||||
|
||||
partial void InitProjSpecific(XElement element);
|
||||
|
||||
public Connection OtherConnection(Connection connection)
|
||||
{
|
||||
if (connection == connections[0]) { return connections[1]; }
|
||||
@@ -728,6 +722,11 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
ClearConnections();
|
||||
base.RemoveComponentSpecific();
|
||||
#if CLIENT
|
||||
overrideSprite?.Remove();
|
||||
overrideSprite = null;
|
||||
wireSprite = null;
|
||||
#endif
|
||||
}
|
||||
|
||||
public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime)
|
||||
|
||||
@@ -192,6 +192,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public override void OnItemLoaded()
|
||||
{
|
||||
base.OnItemLoaded();
|
||||
var lightComponents = item.GetComponents<LightComponent>();
|
||||
if (lightComponents != null && lightComponents.Count() > 0)
|
||||
{
|
||||
@@ -325,20 +326,24 @@ namespace Barotrauma.Items.Components
|
||||
failedLaunchAttempts = 0;
|
||||
|
||||
var batteries = item.GetConnectedComponents<PowerContainer>();
|
||||
float availablePower = 0.0f;
|
||||
foreach (PowerContainer battery in batteries)
|
||||
float neededPower = powerConsumption;
|
||||
|
||||
while (neededPower > 0.0001f && batteries.Count > 0)
|
||||
{
|
||||
float batteryPower = Math.Min(battery.Charge * 3600.0f, battery.MaxOutPut);
|
||||
float takePower = Math.Min(powerConsumption - availablePower, batteryPower);
|
||||
|
||||
battery.Charge -= takePower / 3600.0f;
|
||||
|
||||
#if SERVER
|
||||
if (GameMain.Server != null)
|
||||
batteries.RemoveAll(b => b.Charge <= 0.0001f || b.MaxOutPut <= 0.0001f);
|
||||
float takePower = neededPower / batteries.Count;
|
||||
takePower = Math.Min(takePower, batteries.Min(b => Math.Min(b.Charge * 3600.0f, b.MaxOutPut)));
|
||||
foreach (PowerContainer battery in batteries)
|
||||
{
|
||||
battery.Item.CreateServerEvent(battery);
|
||||
}
|
||||
neededPower -= takePower;
|
||||
battery.Charge -= takePower / 3600.0f;
|
||||
#if SERVER
|
||||
if (GameMain.Server != null)
|
||||
{
|
||||
battery.Item.CreateServerEvent(battery);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
Launch(projectiles[0].Item, character);
|
||||
@@ -477,12 +482,12 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
//enough shells and power
|
||||
Character closestEnemy = null;
|
||||
float closestDist = 10000.0f * 10000.0f;
|
||||
float closestDist = 3000 * 3000;
|
||||
foreach (Character enemy in Character.CharacterList)
|
||||
{
|
||||
//ignore humans and characters that are inside the sub
|
||||
if (enemy.IsDead|| enemy.AnimController.CurrentHull != null || !enemy.Enabled) { continue; }
|
||||
if (enemy.SpeciesName == character.SpeciesName && enemy.TeamID == character.TeamID) { continue; }
|
||||
// Ignore friendly and those that are inside the sub
|
||||
if (enemy.IsDead || enemy.AnimController.CurrentHull != null || !enemy.Enabled) { continue; }
|
||||
if (HumanAIController.IsFriendly(character, enemy)) { continue; }
|
||||
|
||||
float dist = Vector2.DistanceSquared(enemy.WorldPosition, item.WorldPosition);
|
||||
if (dist > closestDist) { continue; }
|
||||
@@ -510,8 +515,21 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (Math.Abs(MathUtils.GetShortestAngle(enemyAngle, turretAngle)) > 0.15f) { return false; }
|
||||
|
||||
var pickedBody = Submarine.PickBody(ConvertUnits.ToSimUnits(item.WorldPosition), closestEnemy.SimPosition, null);
|
||||
if (pickedBody != null && !(pickedBody.UserData is Limb)) { return false; }
|
||||
var pickedBody = Submarine.PickBody(ConvertUnits.ToSimUnits(item.WorldPosition), closestEnemy.SimPosition);
|
||||
if (pickedBody == null) { return false; }
|
||||
Character target = null;
|
||||
if (pickedBody.UserData is Character c)
|
||||
{
|
||||
target = c;
|
||||
}
|
||||
else if (pickedBody.UserData is Limb limb)
|
||||
{
|
||||
target = limb.character;
|
||||
}
|
||||
if (target == null || HumanAIController.IsFriendly(character, target))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (objective.Option.ToLowerInvariant() == "fireatwill")
|
||||
{
|
||||
@@ -554,8 +572,8 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
base.RemoveComponentSpecific();
|
||||
|
||||
if (barrelSprite != null) barrelSprite.Remove();
|
||||
if (railSprite != null) railSprite.Remove();
|
||||
barrelSprite?.Remove(); barrelSprite = null;
|
||||
railSprite?.Remove(); railSprite = null;
|
||||
|
||||
#if CLIENT
|
||||
moveSoundChannel?.Dispose(); moveSoundChannel = null;
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Networking;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -23,6 +24,7 @@ namespace Barotrauma
|
||||
|
||||
class WearableSprite
|
||||
{
|
||||
public string UnassignedSpritePath { get; private set; }
|
||||
public string SpritePath { get; private set; }
|
||||
public XElement SourceElement { get; private set; }
|
||||
|
||||
@@ -83,7 +85,7 @@ namespace Barotrauma
|
||||
if (value == _gender) { return; }
|
||||
_gender = value;
|
||||
IsInitialized = false;
|
||||
SpritePath = ParseSpritePath(SourceElement.GetAttributeString("texture", string.Empty));
|
||||
UnassignedSpritePath = ParseSpritePath(SourceElement.GetAttributeString("texture", string.Empty));
|
||||
Init(_gender);
|
||||
}
|
||||
}
|
||||
@@ -92,7 +94,7 @@ namespace Barotrauma
|
||||
{
|
||||
Type = type;
|
||||
SourceElement = subElement;
|
||||
SpritePath = subElement.GetAttributeString("texture", string.Empty);
|
||||
UnassignedSpritePath = subElement.GetAttributeString("texture", string.Empty);
|
||||
Init();
|
||||
switch (type)
|
||||
{
|
||||
@@ -122,42 +124,24 @@ namespace Barotrauma
|
||||
Type = WearableType.Item;
|
||||
WearableComponent = wearable;
|
||||
Variant = Math.Max(variant, 0);
|
||||
SpritePath = ParseSpritePath(subElement.GetAttributeString("texture", string.Empty));
|
||||
UnassignedSpritePath = ParseSpritePath(subElement.GetAttributeString("texture", string.Empty));
|
||||
SourceElement = subElement;
|
||||
}
|
||||
|
||||
private string ParseSpritePath(string texturePath) => texturePath.Contains("/") ? texturePath : $"{Path.GetDirectoryName(WearableComponent.Item.Prefab.ConfigFile)}/{texturePath}";
|
||||
|
||||
public void RefreshPath()
|
||||
{
|
||||
if (Variant > 0)
|
||||
{
|
||||
// Restore the tag so that we can parse it again.
|
||||
ReplaceNumbersWith("[VARIANT]");
|
||||
}
|
||||
ParsePath(true);
|
||||
}
|
||||
|
||||
private void ReplaceNumbersWith(string replacement)
|
||||
{
|
||||
var fileName = Path.GetFileName(SpritePath);
|
||||
var path = Path.GetDirectoryName(SpritePath);
|
||||
fileName = fileName.Replace(replacement, c => char.IsNumber(c));
|
||||
SpritePath = Path.Combine(path, fileName);
|
||||
}
|
||||
|
||||
private void ParsePath(bool parseSpritePath)
|
||||
public void ParsePath(bool parseSpritePath)
|
||||
{
|
||||
string tempPath = UnassignedSpritePath;
|
||||
if (_gender != Gender.None)
|
||||
{
|
||||
SpritePath = SpritePath.Replace("[GENDER]", (_gender == Gender.Female) ? "female" : "male");
|
||||
tempPath = tempPath.Replace("[GENDER]", (_gender == Gender.Female) ? "female" : "male");
|
||||
}
|
||||
SpritePath = SpritePath.Replace("[VARIANT]", Variant.ToString());
|
||||
SpritePath = tempPath.Replace("[VARIANT]", Variant.ToString());
|
||||
if (!File.Exists(SpritePath))
|
||||
{
|
||||
// If the variant does not exist, parse the path so that it uses first variant.
|
||||
Variant = 1;
|
||||
ReplaceNumbersWith(Variant.ToString());
|
||||
SpritePath = tempPath.Replace("[VARIANT]", "1");
|
||||
}
|
||||
if (parseSpritePath)
|
||||
{
|
||||
@@ -169,13 +153,13 @@ namespace Barotrauma
|
||||
public void Init(Gender gender = Gender.None)
|
||||
{
|
||||
if (IsInitialized) { return; }
|
||||
_gender = SpritePath.Contains("[GENDER]") ? gender : Gender.None;
|
||||
_gender = UnassignedSpritePath.Contains("[GENDER]") ? gender : Gender.None;
|
||||
ParsePath(false);
|
||||
if (Sprite != null)
|
||||
{
|
||||
Sprite.Remove();
|
||||
}
|
||||
Sprite = new Sprite(SourceElement, file: SpritePath);
|
||||
Sprite = new Sprite(SourceElement, file: SpritePath, preMultiplyAlpha: true);
|
||||
Limb = (LimbType)Enum.Parse(typeof(LimbType), SourceElement.GetAttributeString("limb", "Head"), true);
|
||||
HideLimb = SourceElement.GetAttributeBool("hidelimb", false);
|
||||
HideOtherWearables = SourceElement.GetAttributeBool("hideotherwearables", false);
|
||||
@@ -197,25 +181,60 @@ namespace Barotrauma
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
class Wearable : Pickable
|
||||
class Wearable : Pickable, IServerSerializable
|
||||
{
|
||||
private WearableSprite[] wearableSprites;
|
||||
private LimbType[] limbType;
|
||||
private Limb[] limb;
|
||||
private readonly XElement[] wearableElements;
|
||||
private readonly WearableSprite[] wearableSprites;
|
||||
private readonly LimbType[] limbType;
|
||||
private readonly Limb[] limb;
|
||||
|
||||
private List<DamageModifier> damageModifiers;
|
||||
private readonly List<DamageModifier> damageModifiers;
|
||||
|
||||
public List<DamageModifier> DamageModifiers
|
||||
public IEnumerable<DamageModifier> DamageModifiers
|
||||
{
|
||||
get { return damageModifiers; }
|
||||
}
|
||||
|
||||
private bool autoEquipWhenFull;
|
||||
public bool AutoEquipWhenFull
|
||||
public bool AutoEquipWhenFull { get; private set; }
|
||||
|
||||
public readonly int Variants;
|
||||
|
||||
private int variant;
|
||||
public int Variant
|
||||
{
|
||||
get { return autoEquipWhenFull; }
|
||||
}
|
||||
|
||||
get { return variant; }
|
||||
set
|
||||
{
|
||||
#if SERVER
|
||||
variant = value;
|
||||
item.CreateServerEvent(this);
|
||||
#elif CLIENT
|
||||
if (variant == value) { return; }
|
||||
|
||||
Character character = picker;
|
||||
if (character != null)
|
||||
{
|
||||
Unequip(character);
|
||||
}
|
||||
|
||||
for (int i = 0; i < wearableSprites.Length; i++)
|
||||
{
|
||||
var subElement = wearableElements[i];
|
||||
|
||||
wearableSprites[i]?.Sprite?.Remove();
|
||||
wearableSprites[i] = new WearableSprite(subElement, this, value);
|
||||
}
|
||||
|
||||
if (character != null)
|
||||
{
|
||||
Equip(character);
|
||||
}
|
||||
|
||||
variant = value;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
public Wearable(Item item, XElement element) : base(item, element)
|
||||
{
|
||||
this.item = item;
|
||||
@@ -223,12 +242,13 @@ namespace Barotrauma.Items.Components
|
||||
damageModifiers = new List<DamageModifier>();
|
||||
|
||||
int spriteCount = element.Elements().Count(x => x.Name.ToString() == "sprite");
|
||||
int variants = element.GetAttributeInt("variants", 0);
|
||||
int variant = variants > 0 ? Rand.Range(1, variants + 1, Rand.RandSync.Server) : 1;
|
||||
Variants = element.GetAttributeInt("variants", 0);
|
||||
variant = Rand.Range(1, Variants + 1, Rand.RandSync.Server);
|
||||
wearableSprites = new WearableSprite[spriteCount];
|
||||
wearableElements = new XElement[spriteCount];
|
||||
limbType = new LimbType[spriteCount];
|
||||
limb = new Limb[spriteCount];
|
||||
autoEquipWhenFull = element.GetAttributeBool("autoequipwhenfull", true);
|
||||
AutoEquipWhenFull = element.GetAttributeBool("autoequipwhenfull", true);
|
||||
int i = 0;
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
@@ -245,6 +265,7 @@ namespace Barotrauma.Items.Components
|
||||
subElement.GetAttributeString("limb", "Head"), true);
|
||||
|
||||
wearableSprites[i] = new WearableSprite(subElement, this, variant);
|
||||
wearableElements[i] = subElement;
|
||||
|
||||
foreach (XElement lightElement in subElement.Elements())
|
||||
{
|
||||
@@ -380,5 +401,39 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
public override XElement Save(XElement parentElement)
|
||||
{
|
||||
XElement componentElement = base.Save(parentElement);
|
||||
componentElement.Add(new XAttribute("variant", variant));
|
||||
return componentElement;
|
||||
}
|
||||
|
||||
private int loadedVariant = -1;
|
||||
public override void Load(XElement componentElement, bool usePrefabValues)
|
||||
{
|
||||
base.Load(componentElement, usePrefabValues);
|
||||
loadedVariant = componentElement.GetAttributeInt("variant", -1);
|
||||
}
|
||||
public override void OnItemLoaded()
|
||||
{
|
||||
base.OnItemLoaded();
|
||||
//do this here to prevent creating a network event before the item has been fully initialized
|
||||
if (loadedVariant > 0 && loadedVariant < Variants + 1)
|
||||
{
|
||||
Variant = loadedVariant;
|
||||
}
|
||||
}
|
||||
public override void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
|
||||
{
|
||||
msg.Write((byte)Variant);
|
||||
base.ServerWrite(msg, c, extraData);
|
||||
}
|
||||
|
||||
public override void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime)
|
||||
{
|
||||
Variant = (int)msg.ReadByte();
|
||||
base.ClientRead(type, msg, sendingTime);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -338,7 +338,7 @@ namespace Barotrauma
|
||||
|
||||
public Item FindItem(Func<Item, bool> predicate, bool recursive)
|
||||
{
|
||||
Item match = Items.FirstOrDefault(predicate);
|
||||
Item match = Items.FirstOrDefault(i => i != null && predicate(i));
|
||||
if (match == null && recursive)
|
||||
{
|
||||
foreach (var item in Items)
|
||||
@@ -360,13 +360,13 @@ namespace Barotrauma
|
||||
public Item FindItemByTag(string tag, bool recursive = false)
|
||||
{
|
||||
if (tag == null) { return null; }
|
||||
return FindItem(i => i != null && i.HasTag(tag), recursive);
|
||||
return FindItem(i => i.HasTag(tag), recursive);
|
||||
}
|
||||
|
||||
public Item FindItemByIdentifier(string identifier, bool recursive = false)
|
||||
{
|
||||
if (identifier == null) return null;
|
||||
return FindItem(i => i != null && i.Prefab.Identifier == identifier, recursive);
|
||||
return FindItem(i => i.Prefab.Identifier == identifier, recursive);
|
||||
}
|
||||
|
||||
public virtual void RemoveItem(Item item)
|
||||
|
||||
@@ -59,6 +59,8 @@ namespace Barotrauma
|
||||
|
||||
public readonly XElement StaticBodyConfig;
|
||||
|
||||
private bool transformDirty = true;
|
||||
|
||||
private float lastSentCondition;
|
||||
private float sendConditionUpdateTimer;
|
||||
private bool conditionUpdatePending;
|
||||
@@ -465,7 +467,7 @@ namespace Barotrauma
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
} = new List<Item>();
|
||||
} = new List<Item>(20);
|
||||
|
||||
public string ConfigFile
|
||||
{
|
||||
@@ -578,7 +580,7 @@ namespace Barotrauma
|
||||
|
||||
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
|
||||
|
||||
if (submarine == null || !submarine.Loading) FindHull();
|
||||
if (submarine == null || !submarine.Loading) { FindHull(); }
|
||||
|
||||
SetActiveSprite();
|
||||
|
||||
@@ -588,8 +590,35 @@ namespace Barotrauma
|
||||
{
|
||||
case "body":
|
||||
body = new PhysicsBody(subElement, ConvertUnits.ToSimUnits(Position), Scale);
|
||||
body.FarseerBody.AngularDamping = 0.2f;
|
||||
body.FarseerBody.LinearDamping = 0.1f;
|
||||
string collisionCategory = subElement.GetAttributeString("collisioncategory", null);
|
||||
if (Prefab.DamagedByProjectiles || Prefab.DamagedByMeleeWeapons)
|
||||
{
|
||||
//force collision category to Character to allow projectiles and weapons to hit
|
||||
//(we could also do this by making the projectiles and weapons hit CollisionItem
|
||||
//and check if the collision should be ignored in the OnCollision callback, but
|
||||
//that'd make the hit detection more expensive because every item would be included)
|
||||
body.CollisionCategories = Physics.CollisionCharacter;
|
||||
body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform | Physics.CollisionProjectile;
|
||||
}
|
||||
if (collisionCategory != null)
|
||||
{
|
||||
if (!Physics.TryParseCollisionCategory(collisionCategory, out Category cat))
|
||||
{
|
||||
DebugConsole.ThrowError("Invalid collision category in item \"" + Name+"\" (" + collisionCategory + ")");
|
||||
}
|
||||
else
|
||||
{
|
||||
body.CollisionCategories = cat;
|
||||
if (cat.HasFlag(Physics.CollisionCharacter))
|
||||
{
|
||||
body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform | Physics.CollisionProjectile;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body.FarseerBody.AngularDamping = element.GetAttributeFloat("angulardamping", 0.2f);
|
||||
body.FarseerBody.LinearDamping = element.GetAttributeFloat("lineardamping", 0.1f);
|
||||
body.UserData = this;
|
||||
break;
|
||||
case "trigger":
|
||||
case "inventoryicon":
|
||||
@@ -874,7 +903,7 @@ namespace Barotrauma
|
||||
rect.X = (int)(displayPos.X - rect.Width / 2.0f);
|
||||
rect.Y = (int)(displayPos.Y + rect.Height / 2.0f);
|
||||
|
||||
if (findNewHull) FindHull();
|
||||
if (findNewHull) { FindHull(); }
|
||||
}
|
||||
|
||||
public void SetActiveSprite()
|
||||
@@ -966,6 +995,8 @@ namespace Barotrauma
|
||||
return rootContainer;
|
||||
}
|
||||
|
||||
public bool IsOwnedBy(Character character) => FindParentInventory(i => i.Owner == character) != null;
|
||||
|
||||
public Inventory FindParentInventory(Func<Inventory, bool> predicate)
|
||||
{
|
||||
if (parentInventory != null)
|
||||
@@ -1214,11 +1245,13 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
if (Removed) { return; }
|
||||
|
||||
if (body != null && body.Enabled)
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(body.FarseerBody.FixtureList != null);
|
||||
|
||||
if (Math.Abs(body.LinearVelocity.X) > 0.01f || Math.Abs(body.LinearVelocity.Y) > 0.01f)
|
||||
if (Math.Abs(body.LinearVelocity.X) > 0.01f || Math.Abs(body.LinearVelocity.Y) > 0.01f || transformDirty)
|
||||
{
|
||||
UpdateTransform();
|
||||
if (CurrentHull == null && body.SimPosition.Y < ConvertUnits.ToSimUnits(Level.MaxEntityDepth))
|
||||
@@ -1255,6 +1288,8 @@ namespace Barotrauma
|
||||
|
||||
public void UpdateTransform()
|
||||
{
|
||||
if (body == null) { return; }
|
||||
|
||||
Submarine prevSub = Submarine;
|
||||
|
||||
FindHull();
|
||||
@@ -1283,6 +1318,8 @@ namespace Barotrauma
|
||||
MathHelper.Clamp(body.LinearVelocity.X, -NetConfig.MaxPhysicsBodyVelocity, NetConfig.MaxPhysicsBodyVelocity),
|
||||
MathHelper.Clamp(body.LinearVelocity.Y, -NetConfig.MaxPhysicsBodyVelocity, NetConfig.MaxPhysicsBodyVelocity));
|
||||
}
|
||||
|
||||
transformDirty = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1321,6 +1358,8 @@ namespace Barotrauma
|
||||
|
||||
private bool OnCollision(Fixture f1, Fixture f2, Contact contact)
|
||||
{
|
||||
if (transformDirty) { return false; }
|
||||
|
||||
Vector2 normal = contact.Manifold.LocalNormal;
|
||||
float impact = Vector2.Dot(f1.Body.LinearVelocity, -normal);
|
||||
|
||||
@@ -1503,34 +1542,37 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void SendSignal(int stepsTaken, string signal, string connectionName, Character sender, float power = 0.0f, Item source = null, float signalStrength = 1.0f)
|
||||
{
|
||||
LastSentSignalRecipients.Clear();
|
||||
if (connections == null) { return; }
|
||||
if (!connections.TryGetValue(connectionName, out Connection c)) { return; }
|
||||
SendSignal(stepsTaken, signal, c, sender, power, source, signalStrength);
|
||||
}
|
||||
|
||||
public void SendSignal(int stepsTaken, string signal, Connection connection, Character sender, float power = 0.0f, Item source = null, float signalStrength = 1.0f)
|
||||
{
|
||||
LastSentSignalRecipients.Clear();
|
||||
if (connections == null || connection == null) { return; }
|
||||
|
||||
stepsTaken++;
|
||||
|
||||
if (!connections.TryGetValue(connectionName, out Connection c)) { return; }
|
||||
|
||||
|
||||
if (stepsTaken > 10)
|
||||
{
|
||||
//use a coroutine to prevent infinite loops by creating a one
|
||||
//frame delay if the "signal chain" gets too long
|
||||
CoroutineManager.StartCoroutine(SendSignal(signal, c, sender, power, signalStrength));
|
||||
CoroutineManager.StartCoroutine(SendSignal(signal, connection, sender, power, signalStrength));
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (StatusEffect effect in c.Effects)
|
||||
foreach (StatusEffect effect in connection.Effects)
|
||||
{
|
||||
if (condition <= 0.0f && effect.type != ActionType.OnBroken) { continue; }
|
||||
if (signal != "0" && !string.IsNullOrEmpty(signal)) { ApplyStatusEffect(effect, ActionType.OnUse, (float)Timing.Step, null, null, false, false); }
|
||||
}
|
||||
c.SendSignal(stepsTaken, signal, source ?? this, sender, power, signalStrength);
|
||||
}
|
||||
connection.SendSignal(stepsTaken, signal, source ?? this, sender, power, signalStrength);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<object> SendSignal(string signal, Connection connection, Character sender, float power = 0.0f, float signalStrength = 1.0f)
|
||||
{
|
||||
//wait one frame
|
||||
@@ -1854,7 +1896,6 @@ namespace Barotrauma
|
||||
foreach (ItemComponent ic in components) ic.Unequip(character);
|
||||
}
|
||||
|
||||
|
||||
public List<Pair<object, SerializableProperty>> GetProperties<T>()
|
||||
{
|
||||
List<Pair<object, SerializableProperty>> allProperties = new List<Pair<object, SerializableProperty>>();
|
||||
|
||||
@@ -5,6 +5,8 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Xml.Linq;
|
||||
using System.Linq;
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.Extensions;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -247,6 +249,27 @@ namespace Barotrauma
|
||||
private set;
|
||||
}
|
||||
|
||||
[Serialize(false, false)]
|
||||
public bool DamagedByExplosions
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[Serialize(false, false)]
|
||||
public bool DamagedByProjectiles
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[Serialize(false, false)]
|
||||
public bool DamagedByMeleeWeapons
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[Serialize(false, false)]
|
||||
public bool FireProof
|
||||
{
|
||||
@@ -310,6 +333,17 @@ namespace Barotrauma
|
||||
private set;
|
||||
}
|
||||
|
||||
private HashSet<string> preferredContainers = new HashSet<string>();
|
||||
[Serialize("", true, description: "Define containers (by identifiers or tags) that this item should be placed in. These are preferences, which are not enforced.")]
|
||||
public string PreferredContainers
|
||||
{
|
||||
get { return string.Join(",", preferredContainers); }
|
||||
set
|
||||
{
|
||||
StringFormatter.ParseCommaSeparatedStringToCollection(value, preferredContainers);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// How likely it is for the item to spawn in a level of a given type.
|
||||
/// Key = name of the LevelGenerationParameters (empty string = default value)
|
||||
@@ -794,9 +828,24 @@ namespace Barotrauma
|
||||
}
|
||||
return prefab;
|
||||
}
|
||||
|
||||
public IEnumerable<PriceInfo> GetPrices()
|
||||
{
|
||||
return prices?.Values;
|
||||
}
|
||||
|
||||
public bool IsContainerPreferred(ItemContainer itemContainer, out bool isPreferencesDefined)
|
||||
{
|
||||
isPreferencesDefined = preferredContainers.Any();
|
||||
if (!isPreferencesDefined) { return true; }
|
||||
return preferredContainers.Any(id => itemContainer.Item.Prefab.Identifier == id || itemContainer.Item.HasTag(id));
|
||||
}
|
||||
|
||||
public bool IsContainerPreferred(string[] identifiersOrTags, out bool isPreferencesDefined)
|
||||
{
|
||||
isPreferencesDefined = preferredContainers.Any();
|
||||
if (!isPreferencesDefined) { return true; }
|
||||
return preferredContainers.Any(id => preferredContainers.Any(p => p == id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,10 +98,11 @@ namespace Barotrauma
|
||||
if (parentItem == null) { return false; }
|
||||
return CheckContained(parentItem);
|
||||
case RelationType.Container:
|
||||
if (parentItem == null || parentItem.Container == null) { return false; }
|
||||
if (parentItem == null || parentItem.Container == null) { return MatchOnEmpty; }
|
||||
return parentItem.Container.Condition > 0.0f && MatchesItem(parentItem.Container);
|
||||
case RelationType.Equipped:
|
||||
if (character == null) { return false; }
|
||||
if (MatchOnEmpty && character.SelectedItems.All(it => it == null)) { return true; }
|
||||
foreach (Item equippedItem in character.SelectedItems)
|
||||
{
|
||||
if (equippedItem == null) { continue; }
|
||||
@@ -158,7 +159,7 @@ namespace Barotrauma
|
||||
if (!string.IsNullOrWhiteSpace(Msg)) element.Add(new XAttribute("msg", Msg));
|
||||
}
|
||||
|
||||
public static RelatedItem Load(XElement element, string parentDebugName)
|
||||
public static RelatedItem Load(XElement element, bool returnEmpty, string parentDebugName)
|
||||
{
|
||||
string[] identifiers;
|
||||
if (element.Attribute("name") != null)
|
||||
@@ -205,10 +206,9 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
if (identifiers.Length == 0 && excludedIdentifiers.Length == 0) { return null; }
|
||||
if (identifiers.Length == 0 && excludedIdentifiers.Length == 0 && !returnEmpty) { return null; }
|
||||
|
||||
RelatedItem ri = new RelatedItem(identifiers, excludedIdentifiers);
|
||||
|
||||
string typeStr = element.GetAttributeString("type", "");
|
||||
if (string.IsNullOrEmpty(typeStr))
|
||||
{
|
||||
|
||||
@@ -110,9 +110,17 @@ namespace Barotrauma
|
||||
get { return aiTarget; }
|
||||
}
|
||||
|
||||
public double SpawnTime
|
||||
{
|
||||
get { return spawnTime; }
|
||||
}
|
||||
|
||||
private readonly double spawnTime;
|
||||
|
||||
public Entity(Submarine submarine)
|
||||
{
|
||||
this.Submarine = submarine;
|
||||
spawnTime = Timing.TotalTime;
|
||||
|
||||
//give a unique ID
|
||||
id = this is EntitySpawner ?
|
||||
|
||||
@@ -124,39 +124,55 @@ namespace Barotrauma
|
||||
if (powerContainer != null)
|
||||
{
|
||||
powerContainer.Charge -= powerContainer.Capacity * empStrength * distFactor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (force == 0.0f && attack.Stun == 0.0f && attack.GetTotalDamage(false) == 0.0f) return;
|
||||
if (MathUtils.NearlyEqual(force, 0.0f) && MathUtils.NearlyEqual(attack.Stun, 0.0f) && MathUtils.NearlyEqual(attack.GetTotalDamage(false), 0.0f))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DamageCharacters(worldPosition, attack, force, damageSource, attacker);
|
||||
|
||||
|
||||
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
|
||||
{
|
||||
if (flames)
|
||||
{
|
||||
foreach (Item item in Item.ItemList)
|
||||
{
|
||||
if (item.CurrentHull != hull || item.FireProof || item.Condition <= 0.0f) continue;
|
||||
if (item.CurrentHull != hull || item.FireProof || item.Condition <= 0.0f) { continue; }
|
||||
|
||||
//don't apply OnFire effects if the item is inside a fireproof container
|
||||
//(or if it's inside a container that's inside a fireproof container, etc)
|
||||
Item container = item.Container;
|
||||
bool fireProof = false;
|
||||
while (container != null)
|
||||
{
|
||||
if (container.FireProof) return;
|
||||
if (container.FireProof) { fireProof = true; break; }
|
||||
container = container.Container;
|
||||
}
|
||||
|
||||
if (Vector2.Distance(item.WorldPosition, worldPosition) > attack.Range * 0.1f) continue;
|
||||
if (fireProof || Vector2.Distance(item.WorldPosition, worldPosition) > attack.Range * 0.5f) { continue; }
|
||||
|
||||
item.ApplyStatusEffects(ActionType.OnFire, 1.0f);
|
||||
|
||||
if (item.Condition <= 0.0f && GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
|
||||
{
|
||||
GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnFire });
|
||||
}
|
||||
|
||||
if (item.Prefab.DamagedByExplosions && !item.Indestructible)
|
||||
{
|
||||
float limbRadius = item.body == null ? 0.0f : item.body.GetMaxExtent();
|
||||
float dist = Vector2.Distance(item.WorldPosition, worldPosition);
|
||||
dist = Math.Max(0.0f, dist - ConvertUnits.ToDisplayUnits(limbRadius));
|
||||
|
||||
if (dist > attack.Range) { continue; }
|
||||
|
||||
float distFactor = 1.0f - dist / attack.Range;
|
||||
float damageAmount = attack.GetItemDamage(1.0f);
|
||||
item.Condition -= damageAmount * distFactor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -197,7 +213,7 @@ namespace Barotrauma
|
||||
//calculate distance from the "outer surface" of the physics body
|
||||
//doesn't take the rotation of the limb into account, but should be accurate enough for this purpose
|
||||
float limbRadius = Math.Max(Math.Max(limb.body.width * 0.5f, limb.body.height * 0.5f), limb.body.radius);
|
||||
dist = Math.Max(0.0f, dist - FarseerPhysics.ConvertUnits.ToDisplayUnits(limbRadius));
|
||||
dist = Math.Max(0.0f, dist - ConvertUnits.ToDisplayUnits(limbRadius));
|
||||
|
||||
if (dist > attack.Range) { continue; }
|
||||
|
||||
@@ -240,7 +256,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
if (limb.WorldPosition != worldPosition && force > 0.0f)
|
||||
if (limb.WorldPosition != worldPosition && !MathUtils.NearlyEqual(force, 0.0f))
|
||||
{
|
||||
Vector2 limbDiff = Vector2.Normalize(limb.WorldPosition - worldPosition);
|
||||
if (!MathUtils.IsValid(limbDiff)) limbDiff = Rand.Vector(1.0f);
|
||||
|
||||
@@ -55,6 +55,8 @@ namespace Barotrauma
|
||||
set { open = MathHelper.Clamp(value, 0.0f, 1.0f); }
|
||||
}
|
||||
|
||||
public float Size => IsHorizontal ? Rect.Height : Rect.Width;
|
||||
|
||||
public Door ConnectedDoor;
|
||||
|
||||
public Structure ConnectedWall;
|
||||
|
||||
@@ -228,13 +228,16 @@ namespace Barotrauma
|
||||
|
||||
surface = rect.Y - rect.Height;
|
||||
|
||||
aiTarget = new AITarget(this)
|
||||
if (submarine != null)
|
||||
{
|
||||
MinSightRange = 2000,
|
||||
MaxSightRange = 5000,
|
||||
MaxSoundRange = 5000,
|
||||
SoundRange = 0
|
||||
};
|
||||
aiTarget = new AITarget(this)
|
||||
{
|
||||
MinSightRange = 2000,
|
||||
MaxSightRange = 5000,
|
||||
MaxSoundRange = 5000,
|
||||
SoundRange = 0
|
||||
};
|
||||
}
|
||||
|
||||
hullList.Add(this);
|
||||
|
||||
@@ -430,8 +433,11 @@ namespace Barotrauma
|
||||
|
||||
FireSource.UpdateAll(FireSources, deltaTime);
|
||||
|
||||
aiTarget.SightRange = Submarine == null ? aiTarget.MinSightRange : Submarine.Velocity.Length() / 2 * aiTarget.MaxSightRange;
|
||||
aiTarget.SoundRange -= deltaTime * 1000.0f;
|
||||
if (aiTarget != null)
|
||||
{
|
||||
aiTarget.SightRange = Submarine == null ? aiTarget.MinSightRange : Submarine.Velocity.Length() / 2 * aiTarget.MaxSightRange;
|
||||
aiTarget.SoundRange -= deltaTime * 1000.0f;
|
||||
}
|
||||
|
||||
if (!update)
|
||||
{
|
||||
@@ -594,16 +600,17 @@ namespace Barotrauma
|
||||
{
|
||||
adjacentHulls.Clear();
|
||||
int startStep = 0;
|
||||
return GetAdjacentHulls(includingThis, adjacentHulls, ref startStep, searchDepth);
|
||||
searchDepth = searchDepth ?? 100;
|
||||
return GetAdjacentHulls(includingThis, adjacentHulls, ref startStep, searchDepth.Value);
|
||||
}
|
||||
|
||||
private HashSet<Hull> GetAdjacentHulls(bool includingThis, HashSet<Hull> connectedHulls, ref int step, int? searchDepth)
|
||||
private HashSet<Hull> GetAdjacentHulls(bool includingThis, HashSet<Hull> connectedHulls, ref int step, int searchDepth)
|
||||
{
|
||||
if (includingThis)
|
||||
{
|
||||
connectedHulls.Add(this);
|
||||
}
|
||||
if (step > searchDepth.Value)
|
||||
if (step > searchDepth)
|
||||
{
|
||||
return connectedHulls;
|
||||
}
|
||||
@@ -642,7 +649,7 @@ namespace Barotrauma
|
||||
|
||||
foreach (Gap g in ConnectedGaps)
|
||||
{
|
||||
if (g.ConnectedDoor != null)
|
||||
if (g.ConnectedDoor != null && !g.ConnectedDoor.IsBroken)
|
||||
{
|
||||
//gap blocked if the door is not open or the predicted state is not open
|
||||
if (!g.ConnectedDoor.IsOpen || (g.ConnectedDoor.PredictedState.HasValue && !g.ConnectedDoor.PredictedState.Value))
|
||||
@@ -660,7 +667,7 @@ namespace Barotrauma
|
||||
if (g.linkedTo[i] is Hull hull && !connectedHulls.Contains(hull))
|
||||
{
|
||||
float dist = hull.GetApproximateHullDistance(g.Position, endPos, connectedHulls, target, distance + Vector2.Distance(startPos, g.Position), maxDistance);
|
||||
if (dist < float.MaxValue) return dist;
|
||||
if (dist < float.MaxValue) { return dist; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,12 +40,24 @@ namespace Barotrauma.RuinGeneration
|
||||
this.rect = rect;
|
||||
}
|
||||
|
||||
public void Split(float minDivRatio, float verticalProbability = 0.5f, int minWidth = 200)
|
||||
public void Split(float minDivRatio, float verticalProbability = 0.5f, int minWidth = 200, int minHeight = 200)
|
||||
{
|
||||
subRooms = new BTRoom[2];
|
||||
bool verticalSplit = Rand.Range(0.0f, rect.Height / (float)rect.Width, Rand.RandSync.Server) < verticalProbability;
|
||||
if (rect.Width * minDivRatio < minWidth && rect.Height * minDivRatio < minHeight)
|
||||
{
|
||||
minDivRatio = 0.5f;
|
||||
}
|
||||
else if (rect.Width * minDivRatio < minWidth)
|
||||
{
|
||||
verticalSplit = false;
|
||||
}
|
||||
else if (rect.Height * minDivRatio < minHeight)
|
||||
{
|
||||
verticalSplit = true;
|
||||
}
|
||||
|
||||
if (Rand.Range(0.0f, rect.Height / (float)rect.Width, Rand.RandSync.Server) < verticalProbability &&
|
||||
rect.Width * minDivRatio >= minWidth)
|
||||
subRooms = new BTRoom[2];
|
||||
if (verticalSplit)
|
||||
{
|
||||
SplitVertical(minDivRatio);
|
||||
}
|
||||
|
||||
@@ -69,12 +69,18 @@ namespace Barotrauma.RuinGeneration
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(400, false, description: "The splitting algorithm attempts to keep the dimensions the split areas larger than this. For example, if the width of the split areas would be smaller than this after a vertical split, the algorithm will do a horizontal split."), Editable]
|
||||
[Serialize(400, false, description: "The splitting algorithm attempts to keep the width of the split areas larger than this. If the width of the split areas would be smaller than this after a vertical split, the algorithm would do a horizontal split."), Editable]
|
||||
public int MinSplitWidth
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
[Serialize(400, false, description: "The splitting algorithm attempts to keep the height of the split areas larger than this. If the height of the split areas would be smaller than this after a vertical split, the algorithm would do a horizontal split."), Editable]
|
||||
public int MinSplitHeight
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize("0.5,0.9", false, description: "The minimum and maximum width of a room relative to the areas created by the split algorithm."), Editable]
|
||||
public Vector2 RoomWidthRange
|
||||
|
||||
@@ -239,7 +239,7 @@ namespace Barotrauma.RuinGeneration
|
||||
|
||||
for (int i = 0; i < iterations; i++)
|
||||
{
|
||||
rooms.ForEach(l => l.Split(0.3f, verticalProbability, generationParams.MinSplitWidth));
|
||||
rooms.ForEach(l => l.Split(0.3f, verticalProbability, generationParams.MinSplitWidth, generationParams.MinSplitHeight));
|
||||
rooms = baseRoom.GetLeaves();
|
||||
}
|
||||
|
||||
@@ -559,6 +559,11 @@ namespace Barotrauma.RuinGeneration
|
||||
foreach (MapEntity e in entities)
|
||||
{
|
||||
e.Move(doorOffset);
|
||||
Door doorComponent = (e as Item)?.GetComponent<Door>();
|
||||
if (doorComponent != null && !entities.Contains(doorComponent.LinkedGap))
|
||||
{
|
||||
doorComponent.LinkedGap.Move(doorOffset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ namespace Barotrauma
|
||||
LocationConnection connection = Connections[(MissionsCompleted + i) % Connections.Count];
|
||||
Location destination = connection.OtherLocation(this);
|
||||
|
||||
var mission = Mission.LoadRandom(new Location[] { this, destination }, rand, true, MissionType.Random, true);
|
||||
var mission = Mission.LoadRandom(new Location[] { this, destination }, rand, true, MissionType.All, true);
|
||||
if (mission == null) { continue; }
|
||||
if (availableMissions.Any(m => m.Prefab == mission.Prefab)) { continue; }
|
||||
if (GameSettings.VerboseLogging && mission != null)
|
||||
@@ -65,7 +65,11 @@ namespace Barotrauma
|
||||
|
||||
public int SelectedMissionIndex
|
||||
{
|
||||
get { return availableMissions.IndexOf(SelectedMission); }
|
||||
get
|
||||
{
|
||||
if (SelectedMission == null) { return -1; }
|
||||
return availableMissions.IndexOf(SelectedMission);
|
||||
}
|
||||
set
|
||||
{
|
||||
if (value < 0 || value >= AvailableMissions.Count())
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user