Unstable 0.1400.0.0

This commit is contained in:
Markus Isberg
2021-05-11 15:47:47 +03:00
parent 3f324b14e8
commit 92f0264af2
247 changed files with 8238 additions and 1911 deletions
@@ -0,0 +1,28 @@
using Microsoft.Xna.Framework;
namespace Barotrauma
{
public class CachedDistance
{
public readonly Vector2 StartWorldPos;
public readonly Vector2 EndWorldPos;
public readonly float Distance;
public double RecalculationTime;
public CachedDistance(Vector2 startWorldPos, Vector2 endWorldPos, float dist, double recalculationTime)
{
StartWorldPos = startWorldPos;
EndWorldPos = endWorldPos;
Distance = dist;
RecalculationTime = recalculationTime;
}
public bool ShouldUpdateDistance(Vector2 currentStartWorldPos, Vector2 currentEndWorldPos, float minDistanceToUpdate = 500.0f)
{
if (Timing.TotalTime < RecalculationTime) { return false; }
float minDistSquared = minDistanceToUpdate * minDistanceToUpdate;
return Vector2.DistanceSquared(StartWorldPos, currentStartWorldPos) > minDistSquared ||
Vector2.DistanceSquared(EndWorldPos, currentEndWorldPos) > minDistSquared;
}
}
}
@@ -73,6 +73,8 @@ namespace Barotrauma
get { return true; }
}
public virtual bool IsMentallyUnstable => false;
private IEnumerable<Hull> visibleHulls;
private float hullVisibilityTimer;
const float hullVisibilityInterval = 0.5f;
@@ -215,10 +217,26 @@ namespace Barotrauma
}
private readonly HashSet<Item> unequippedItems = new HashSet<Item>();
public bool TakeItem(Item item, Inventory targetInventory, bool equip, bool dropOtherIfCannotMove = true, bool allowSwapping = false, bool storeUnequipped = false)
public bool TakeItem(Item item, CharacterInventory targetInventory, bool equip, bool wear = false, bool dropOtherIfCannotMove = true, bool allowSwapping = false, bool storeUnequipped = false)
{
var pickable = item.GetComponent<Pickable>();
if (pickable == null) { return false; }
if (wear)
{
var wearable = item.GetComponent<Wearable>();
if (wearable != null)
{
pickable = wearable;
}
}
else
{
var holdable = item.GetComponent<Holdable>();
if (holdable != null)
{
pickable = holdable;
}
}
if (item.ParentInventory is ItemInventory itemInventory)
{
if (!itemInventory.Container.HasRequiredItems(Character, addMessage: false)) { return false; }
@@ -302,7 +320,7 @@ namespace Barotrauma
{
if (item != null && !item.Removed && Character.HasItem(item))
{
TakeItem(item, Character.Inventory, equip: true, dropOtherIfCannotMove: true, allowSwapping: true, storeUnequipped: false);
TakeItem(item, Character.Inventory, equip: true, wear: true, dropOtherIfCannotMove: true, allowSwapping: true, storeUnequipped: false);
}
}
unequippedItems.Clear();
@@ -144,21 +144,6 @@ namespace Barotrauma
}
}
public void Reset()
{
if (Static)
{
SightRange = MaxSightRange;
SoundRange = MaxSoundRange;
}
else
{
// Non-static ai targets must be kept alive by a custom logic (e.g. item components)
SightRange = StaticSight ? MaxSightRange : MinSightRange;
SoundRange = StaticSound ? MaxSoundRange : MinSoundRange;
}
}
public AITarget(Entity e, XElement element) : this(e)
{
SightRange = element.GetAttributeFloat("sightrange", 0.0f);
@@ -242,5 +227,20 @@ namespace Barotrauma
List.Remove(this);
entity = null;
}
public void Reset()
{
if (Static)
{
SightRange = MaxSightRange;
SoundRange = MaxSoundRange;
}
else
{
// Non-static ai targets must be kept alive by a custom logic (e.g. item components)
SightRange = StaticSight ? MaxSightRange : MinSightRange;
SoundRange = StaticSound ? MaxSoundRange : MinSoundRange;
}
}
}
}
@@ -381,6 +381,7 @@ namespace Barotrauma
SelectedAiTarget = target;
selectedTargetMemory = GetTargetMemory(target, true);
selectedTargetMemory.Priority = priority;
ignoredTargets.Remove(target);
}
private float movementMargin;
@@ -496,7 +497,7 @@ namespace Barotrauma
}
}
if (AIParams.Infiltrate)
if (AIParams.CanOpenDoors)
{
bool IsCloseEnoughToTargetSub(float threshold) => SelectedAiTarget?.Entity?.Submarine is Submarine sub && sub != null && Vector2.DistanceSquared(Character.WorldPosition, sub.WorldPosition) < MathUtils.Pow(Math.Max(sub.Borders.Size.X, sub.Borders.Size.Y) / 2 + threshold, 2);
@@ -787,7 +788,7 @@ namespace Barotrauma
if (pathSteering != null && !Character.AnimController.InWater)
{
// Wander around inside
pathSteering.Wander(deltaTime, ConvertUnits.ToDisplayUnits(colliderLength), stayStillInTightSpace: false);
pathSteering.Wander(deltaTime, Math.Max(ConvertUnits.ToDisplayUnits(colliderLength), 100.0f), stayStillInTightSpace: false);
}
else
{
@@ -1203,7 +1204,7 @@ namespace Barotrauma
}
canAttack = AttackingLimb != null && AttackingLimb.attack.CoolDownTimer <= 0;
}
if (!AIParams.Infiltrate)
if (!AIParams.CanOpenDoors)
{
if (!Character.AnimController.SimplePhysicsEnabled && SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null && (!canAttackDoors || !canAttackWalls || !AIParams.TargetOuterWalls))
{
@@ -1777,6 +1778,7 @@ namespace Barotrauma
}
if (!isFriendly && attackResult.Damage > 0.0f)
{
ignoredTargets.Remove(attacker.AiTarget);
bool canAttack = attacker.Submarine == Character.Submarine && canAttackCharacters || attacker.Submarine != null && canAttackWalls;
if (AIParams.AttackWhenProvoked && canAttack)
{
@@ -1893,16 +1895,28 @@ namespace Barotrauma
{
//simulate attack input to get the character to attack client-side
Character.SetInput(InputType.Attack, true, true);
#if SERVER
GameMain.NetworkMember.CreateEntityEvent(Character, new object[]
{
Networking.NetEntityEvent.Type.SetAttackTarget,
attackingLimb,
(damageTarget as Entity)?.ID ?? Entity.NullEntityID,
damageTarget is Character character && targetLimb != null ? Array.IndexOf(character.AnimController.Limbs, targetLimb) : 0,
SimPosition.X,
SimPosition.Y
});
#endif
if (attackingLimb.UpdateAttack(deltaTime, attackSimPos, damageTarget, out AttackResult attackResult, distance, targetLimb))
{
if (damageTarget.Health > 0)
if (damageTarget.Health > 0 && attackResult.Damage > 0)
{
// Managed to hit a living/non-destroyed target. Increase the priority more if the target is low in health -> dies easily/soon
selectedTargetMemory.Priority += GetRelativeDamage(attackResult.Damage, damageTarget.Health) * AIParams.AggressionGreed;
}
else
{
selectedTargetMemory.Priority = 0;
selectedTargetMemory.Priority -= Math.Max(selectedTargetMemory.Priority / 2, 1);
return selectedTargetMemory.Priority > 1;
}
}
return true;
@@ -2184,7 +2198,7 @@ namespace Barotrauma
bool targetingFromOutsideToInside = item.CurrentHull != null && character.CurrentHull == null;
if (targetingFromOutsideToInside)
{
if (door != null && (!canAttackDoors && !AIParams.Infiltrate) || !canAttackWalls)
if (door != null && (!canAttackDoors && !AIParams.CanOpenDoors) || !canAttackWalls)
{
// Can't reach
continue;
@@ -2610,7 +2624,7 @@ namespace Barotrauma
{
wallTarget = null;
if (State == AIState.Flee || State == AIState.Escape) { return; }
if (AIParams.Infiltrate && HasValidPath(requireNonDirty: true)) { return; }
if (AIParams.CanOpenDoors && HasValidPath(requireNonDirty: true)) { return; }
if (SelectedAiTarget == null) { return; }
if (SelectedAiTarget.Entity == null) { return; }
Vector2 rayStart = SimPosition;
@@ -29,6 +29,9 @@ namespace Barotrauma
private float flipTimer;
private const float FlipInterval = 0.5f;
private float teamChangeTimer;
private const float TeamChangeInterval = 0.5f;
public const float HULL_SAFETY_THRESHOLD = 40;
public const float HULL_LOW_OXYGEN_PERCENTAGE = 30;
@@ -121,6 +124,32 @@ namespace Barotrauma
}
}
public MentalStateManager MentalStateManager { get; private set; }
public void InitMentalStateManager()
{
if (MentalStateManager == null)
{
MentalStateManager = new MentalStateManager(Character, this);
}
MentalStateManager.Active = true;
}
public override bool IsMentallyUnstable =>
MentalStateManager?.CurrentMentalType != MentalStateManager.MentalType.Normal &&
MentalStateManager?.CurrentMentalType != MentalStateManager.MentalType.Confused;
public ShipCommandManager ShipCommandManager { get; private set; }
public void InitShipCommandManager()
{
if (ShipCommandManager == null)
{
ShipCommandManager = new ShipCommandManager(Character);
}
ShipCommandManager.Active = true;
}
public HumanAIController(Character c) : base(c)
{
if (!c.IsHuman)
@@ -204,9 +233,11 @@ namespace Barotrauma
}
}
}
if (Character.Submarine == null || !IsOnFriendlyTeam(Character.TeamID, Character.Submarine.TeamID))
if (Character.Submarine == null || !IsOnFriendlyTeam(Character.TeamID, Character.Submarine.TeamID) && !Character.IsEscorted)
{
// Spot enemies while staying outside or inside an enemy ship.
// does not apply for escorted characters, such as prisoners or terrorists who have their own behavior
enemycheckTimer -= deltaTime;
if (enemycheckTimer < 0)
{
@@ -287,6 +318,8 @@ namespace Barotrauma
}
else
{
Character.UpdateTeam();
if (Character.CurrentHull != null)
{
if (Character.IsOnPlayerTeam)
@@ -301,7 +334,7 @@ namespace Barotrauma
}
if (Character.SpeechImpediment < 100.0f)
{
if (Character.Submarine != null && Character.Submarine.TeamID == Character.TeamID && !Character.Submarine.Info.IsWreck)
if (Character.Submarine != null && (Character.Submarine.TeamID == Character.TeamID || Character.IsEscorted) && !Character.Submarine.Info.IsWreck)
{
ReportProblems();
}
@@ -314,7 +347,7 @@ namespace Barotrauma
if (objectiveManager.CurrentObjective == null) { return; }
objectiveManager.DoCurrentObjective(deltaTime);
bool run = objectiveManager.CurrentObjective.ForceRun || objectiveManager.GetCurrentPriority() > AIObjectiveManager.RunPriority;
bool run = objectiveManager.CurrentObjective.ForceRun || !objectiveManager.CurrentObjective.ForceWalk && objectiveManager.GetCurrentPriority() > AIObjectiveManager.RunPriority;
if (ObjectiveManager.CurrentObjective is AIObjectiveGoTo goTo && goTo.Target != null)
{
if (Character.CurrentHull == null)
@@ -395,6 +428,9 @@ namespace Barotrauma
flipTimer = FlipInterval;
}
}
MentalStateManager?.Update(deltaTime);
ShipCommandManager?.Update(deltaTime);
}
private void UnequipUnnecessaryItems()
@@ -442,9 +478,8 @@ namespace Barotrauma
Character.AnimController.InWater ||
Character.AnimController.HeadInWater ||
Character.CurrentHull == null ||
Character.Submarine?.TeamID != Character.TeamID ||
(Character.Submarine.TeamID != Character.TeamID && !Character.IsEscorted) || // these instances should maybe be combined to a method
ObjectiveManager.IsCurrentObjective<AIObjectiveFindSafety>() ||
ObjectiveManager.CurrentOrder is AIObjectiveGoTo goTo && goTo.Target == Character || // wait order
ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn);
if (oxygenLow && Character.CurrentHull.Oxygen > 0)
{
@@ -454,7 +489,7 @@ namespace Barotrauma
{
shouldKeepTheGearOn = true;
}
bool removeDivingSuit = !shouldKeepTheGearOn;
bool removeDivingSuit = !shouldKeepTheGearOn && Character.Submarine?.TeamID == Character.TeamID && (!(ObjectiveManager.CurrentOrder is AIObjectiveGoTo goTo) || goTo.Target != Character);
bool takeMaskOff = !shouldKeepTheGearOn;
if (!shouldKeepTheGearOn && !oxygenLow)
{
@@ -505,7 +540,7 @@ namespace Barotrauma
var divingSuit = Character.Inventory.FindItemByTag(AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR);
if (divingSuit != null)
{
if (oxygenLow || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
if (oxygenLow || Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
{
divingSuit.Drop(Character);
HandleRelocation(divingSuit);
@@ -550,7 +585,7 @@ namespace Barotrauma
{
if (!mask.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(mask, Character, new List<InvSlotType>() { InvSlotType.Any }))
{
if (ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
if (Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
{
mask.Drop(Character);
HandleRelocation(mask);
@@ -603,7 +638,7 @@ namespace Barotrauma
Item item = Character.Inventory.GetItemInLimbSlot(hand);
if (item == null) { continue; }
if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, new List<InvSlotType>() { InvSlotType.Any }))
if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, new List<InvSlotType>() { InvSlotType.Any }) && Character.Submarine?.TeamID == Character.TeamID )
{
findItemState = FindItemState.OtherItem;
if (FindSuitableContainer(item, out Item targetContainer))
@@ -743,6 +778,7 @@ namespace Barotrauma
{
Order newOrder = null;
Hull targetHull = null;
bool speak = true;
if (Character.CurrentHull != null)
{
bool isFighting = ObjectiveManager.HasActiveObjective<AIObjectiveCombat>();
@@ -759,6 +795,21 @@ namespace Barotrauma
var orderPrefab = Order.GetPrefab("reportintruders");
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
targetHull = hull;
if (target.IsEscorted)
{
if (!Character.IsPrisoner && target.IsPrisoner)
{
string msg = TextManager.GetWithVariables("orderdialog.prisonerescaped", new string[] { "[roomname]" }, new string[] { targetHull.DisplayName }, new bool[] { false, true }, true);
Character.Speak(msg, ChatMessageType.Order);
speak = false;
}
else if (!IsMentallyUnstable && target.AIController.IsMentallyUnstable)
{
string msg = TextManager.GetWithVariables("orderdialog.mentalcase", new string[] { "[roomname]" }, new string[] { targetHull.DisplayName }, new bool[] { false, true }, true);
Character.Speak(msg, ChatMessageType.Order);
speak = false;
}
}
}
}
}
@@ -771,7 +822,7 @@ namespace Barotrauma
targetHull = hull;
}
}
if (IsBallastFloraNoticeable(Character, hull))
if (IsBallastFloraNoticeable(Character, hull) && newOrder == null)
{
var orderPrefab = Order.GetPrefab("reportballastflora");
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
@@ -824,20 +875,24 @@ namespace Barotrauma
}
}
}
if (newOrder != null)
if (newOrder != null && speak)
{
if (Character.TeamID == CharacterTeamType.FriendlyNPC)
// for now, escorted characters use the report system to get targets but do not speak. escort-character specific dialogue could be implemented
if (!Character.IsEscorted)
{
Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName, givingOrderToSelf: false), ChatMessageType.Default,
identifier: newOrder.Prefab.Identifier + (targetHull?.DisplayName ?? "null"),
minDurationBetweenSimilar: 60.0f);
}
else if (Character.IsOnPlayerTeam && GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime))
{
Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName, givingOrderToSelf: false), ChatMessageType.Order);
if (Character.TeamID == CharacterTeamType.FriendlyNPC)
{
Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName, givingOrderToSelf: false), ChatMessageType.Default,
identifier: newOrder.Prefab.Identifier + (targetHull?.DisplayName ?? "null"),
minDurationBetweenSimilar: 60.0f);
}
else if (Character.IsOnPlayerTeam && GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime))
{
Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName, givingOrderToSelf: false), ChatMessageType.Order);
#if SERVER
GameMain.Server.SendOrderChatMessage(new OrderChatMessage(newOrder, "", CharacterInfo.HighestManualOrderPriority, targetHull, null, Character));
GameMain.Server.SendOrderChatMessage(new OrderChatMessage(newOrder, "", CharacterInfo.HighestManualOrderPriority, targetHull, null, Character));
#endif
}
}
}
}
@@ -977,11 +1032,11 @@ namespace Barotrauma
return;
}
float cumulativeDamage = GetDamageDoneByAttacker(attacker);
if (!Character.IsSecurity && attacker.IsBot && Character.CombatAction == null)
if (!Character.IsSecurity && attacker.IsBot && !IsMentallyUnstable && !attacker.AIController.IsMentallyUnstable && Character.CombatAction == null)
{
if (cumulativeDamage > 1)
{
// Don't retaliate on damage done by friendly NPC, because we know it's accidental
// Don't retaliate on damage done by friendly NPC, because we know it's accidental, unless if it's a berserking AI
AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker);
}
}
@@ -1039,8 +1094,11 @@ namespace Barotrauma
}
else
{
// Non-friendly
InformOtherNPCs(GetDamageDoneByAttacker(attacker));
if (Character.Submarine != null && Character.Submarine.GetConnectedSubs().Contains(attacker.Submarine))
{
// Non-friendly
InformOtherNPCs(GetDamageDoneByAttacker(attacker));
}
if (Character.IsBot)
{
AddCombatObjective(DetermineCombatMode(Character, cumulativeDamage: realDamage), attacker);
@@ -1070,12 +1128,27 @@ namespace Barotrauma
{
if (!IsFriendly(attacker))
{
return c.AIController is HumanAIController humanAI &&
if (Character.Submarine == null)
{
// Outside -> don't react.
return AIObjectiveCombat.CombatMode.None;
}
if (!Character.Submarine.GetConnectedSubs().Contains(attacker.Submarine))
{
// Attacked from an unconnected submarine.
return Character.SelectedConstruction?.GetComponent<Turret>() != null ? AIObjectiveCombat.CombatMode.None : AIObjectiveCombat.CombatMode.Retreat;
}
return c.AIController is HumanAIController humanAI &&
(humanAI.ObjectiveManager.IsCurrentOrder<AIObjectiveFightIntruders>() || humanAI.ObjectiveManager.Objectives.Any(o => o is AIObjectiveFightIntruders))
? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Defensive;
}
else
{
if (Character.Submarine == null || !Character.Submarine.GetConnectedSubs().Contains(attacker.Submarine))
{
// Outside or attacked from an unconnected submarine -> don't react.
return AIObjectiveCombat.CombatMode.None;
}
// If there are any enemies around, just ignore the friendly fire
if (Character.CharacterList.Any(ch => ch.Submarine == Character.Submarine && !ch.Removed && !ch.IsDead && !ch.IsIncapacitated && !IsFriendly(ch) && VisibleHulls.Contains(ch.CurrentHull)))
{
@@ -1090,7 +1163,7 @@ namespace Barotrauma
// The guards don't react when the player attacks instigators.
return c.IsSecurity ? AIObjectiveCombat.CombatMode.None : (Character.CombatAction != null ? Character.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.Retreat);
}
else if (attacker.TeamID == CharacterTeamType.FriendlyNPC)
else if (attacker.TeamID == CharacterTeamType.FriendlyNPC && !(attacker.AIController.IsMentallyUnstable || attacker.AIController.IsMentallyUnstable))
{
if (c.IsSecurity)
{
@@ -1132,7 +1205,7 @@ namespace Barotrauma
}
}
private void AddCombatObjective(AIObjectiveCombat.CombatMode mode, Character target, float delay = 0, Func<bool> abortCondition = null, Action onAbort = null, Action onCompleted = null, bool allowHoldFire = false)
public void AddCombatObjective(AIObjectiveCombat.CombatMode mode, Character target, float delay = 0, Func<AIObjective, bool> abortCondition = null, Action onAbort = null, Action onCompleted = null, bool allowHoldFire = false)
{
if (mode == AIObjectiveCombat.CombatMode.None) { return; }
if (Character.IsDead || Character.IsIncapacitated || Character.Removed) { return; }
@@ -1168,7 +1241,7 @@ namespace Barotrauma
Character.Info?.Job?.Prefab.Identifier == "watchman" ||
Character.CurrentHull == null ||
Character.IsOnPlayerTeam && !target.IsPlayer && ObjectiveManager.GetActiveObjective<AIObjectiveGoTo>()?.Target is Character followTarget && followTarget.IsPlayer,
abortCondition = abortCondition,
AbortCondition = abortCondition,
allowHoldFire = allowHoldFire,
};
if (onAbort != null)
@@ -1190,7 +1263,7 @@ namespace Barotrauma
public void SetForcedOrder(Order order, string option, Character orderGiver)
{
var objective = ObjectiveManager.CreateObjective(order, option, orderGiver, false);
var objective = ObjectiveManager.CreateObjective(order, option, orderGiver);
ObjectiveManager.SetForcedOrder(objective);
}
@@ -1273,7 +1346,8 @@ namespace Barotrauma
/// <summary>
/// Check whether the character has a diving suit in usable condition plus some oxygen.
/// </summary>
public static bool HasDivingSuit(Character character, float conditionPercentage = 0) => HasItem(character, AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR, out _, AIObjectiveFindDivingGear.OXYGEN_SOURCE, conditionPercentage, requireEquipped: true);
public static bool HasDivingSuit(Character character, float conditionPercentage = 0) => HasItem(character, AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR, out _, AIObjectiveFindDivingGear.OXYGEN_SOURCE, conditionPercentage, requireEquipped: true,
predicate: (Item item) => { return character.HasEquippedItem(item, InvSlotType.OuterClothes); });
/// <summary>
/// Check whether the character has a diving mask in usable condition plus some oxygen.
@@ -1464,7 +1538,7 @@ namespace Barotrauma
if (!humanAI.Character.IsSecurity) { return false; }
if (humanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveCombat>()) { return false; }
humanAI.AddCombatObjective(AIObjectiveCombat.CombatMode.Arrest, thief, delay: GetReactionTime(),
abortCondition: () => thief.Inventory.FindItem(it => it != null && it.StolenDuringRound, true) == null,
abortCondition: obj => thief.Inventory.FindItem(it => it != null && it.StolenDuringRound, true) == null,
onAbort: () =>
{
if (item != null && !item.Removed && humanAI != null && !humanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveGetItem>())
@@ -301,34 +301,33 @@ namespace Barotrauma
}
Ladder nextLadder = GetNextLadder();
var ladders = currentLadder ?? nextLadder;
if (canClimb && !isDiving && ladders != null && character.SelectedConstruction != ladders.Item)
bool useLadders = canClimb && ladders != null && (!isDiving || Math.Abs(steering.X) < 0.1f && Math.Abs(steering.Y) > 1);
if (useLadders && character.SelectedConstruction != ladders.Item)
{
if (IsNextNodeLadder || currentPath.Finished)
{
if (character.CanInteractWith(ladders.Item))
{
ladders.Item.TryInteract(character, false, true);
}
else
{
// Cannot interact with the current (or next) ladder,
// Try to select the previous ladder, unless it's already selected, unless the previous ladder is not adjacent to the current ladder.
// The intention of this code is to prevent the bots from dropping from the "double ladders".
var previousLadders = currentPath.PrevNode?.Ladders;
if (previousLadders != null && previousLadders != ladders && character.SelectedConstruction != previousLadders.Item &&
character.CanInteractWith(previousLadders.Item) && Math.Abs(previousLadders.Item.WorldPosition.X - ladders.Item.WorldPosition.X) < 5)
{
previousLadders.Item.TryInteract(character, false, true);
}
}
}
else if (!IsNextLadderSameAsCurrent && character.SelectedConstruction?.GetComponent<Ladder>() != null && character.CanInteractWith(ladders.Item))
if (character.CanInteractWith(ladders.Item))
{
ladders.Item.TryInteract(character, false, true);
}
else
{
// Cannot interact with the current (or next) ladder,
// Try to select the previous ladder, unless it's already selected, unless the previous ladder is not adjacent to the current ladder.
// The intention of this code is to prevent the bots from dropping from the "double ladders".
var previousLadders = currentPath.PrevNode?.Ladders;
if (previousLadders != null && previousLadders != ladders && character.SelectedConstruction != previousLadders.Item &&
character.CanInteractWith(previousLadders.Item) && Math.Abs(previousLadders.Item.WorldPosition.X - ladders.Item.WorldPosition.X) < 5)
{
previousLadders.Item.TryInteract(character, false, true);
}
}
}
var collider = character.AnimController.Collider;
if (character.IsClimbing && !isDiving)
if (character.IsClimbing && !useLadders)
{
character.AnimController.Anim = AnimController.Animation.None;
character.SelectedConstruction = null;
}
if (character.IsClimbing && useLadders)
{
Vector2 diff = currentPath.CurrentNode.SimPosition - pos;
bool nextLadderSameAsCurrent = IsNextLadderSameAsCurrent;
@@ -380,17 +379,12 @@ namespace Barotrauma
}
else if (character.AnimController.InWater)
{
// If the character is underwater, we don't need the ladders anymore
if (character.IsClimbing && isDiving)
{
character.AnimController.Anim = AnimController.Animation.None;
character.SelectedConstruction = null;
}
var door = currentPath.CurrentNode.ConnectedDoor;
if (door == null || door.CanBeTraversed)
{
float multiplier = MathHelper.Lerp(1, 10, MathHelper.Clamp(collider.LinearVelocity.Length() / 10, 0, 1));
float targetDistance = collider.GetSize().X * multiplier;
float margin = MathHelper.Lerp(1, 5, MathHelper.Clamp(collider.LinearVelocity.Length() / 10, 0, 1));
Vector2 colliderSize = collider.GetSize();
float targetDistance = Math.Max(Math.Max(colliderSize.X, colliderSize.Y) / 2 * margin, 0.5f);
float horizontalDistance = Math.Abs(character.WorldPosition.X - currentPath.CurrentNode.WorldPosition.X);
float verticalDistance = Math.Abs(character.WorldPosition.Y - currentPath.CurrentNode.WorldPosition.Y);
if (character.CurrentHull != currentPath.CurrentNode.CurrentHull)
@@ -404,24 +398,25 @@ namespace Barotrauma
}
}
}
else if (!canClimb || !IsNextLadderSameAsCurrent)
else
{
// Walking horizontally
Vector2 colliderBottom = character.AnimController.GetColliderBottom();
Vector2 colliderSize = collider.GetSize();
Vector2 velocity = collider.LinearVelocity;
// If the character is smaller than this, it would fail to use the waypoint nodes because they are always too high.
float minHeight = 1;
// If the character is very thin, without a min value, it would often fail to reach the waypoints, because the horizontal distance is too small.
float minWidth = 0.17f;
// If the character is very short, it would fail to use the waypoint nodes because they are always too high.
// If the character is very thin, it would often fail to reach the waypoints, because the horizontal distance is too small.
// Both values are based on the human size. So basically anything smaller than humans are considered as equal in size.
float minHeight = 1.6125001f;
float minWidth = 0.3225f;
// Cannot use the head position, because not all characters have head or it can be below the total height of the character
float characterHeight = Math.Max(colliderSize.Y + character.AnimController.ColliderHeightFromFloor, minHeight);
float horizontalDistance = Math.Abs(collider.SimPosition.X - currentPath.CurrentNode.SimPosition.X);
bool isAboveFeet = currentPath.CurrentNode.SimPosition.Y > colliderBottom.Y;
bool isNotTooHigh = currentPath.CurrentNode.SimPosition.Y < colliderBottom.Y + characterHeight;
var door = currentPath.CurrentNode.ConnectedDoor;
float margin = MathHelper.Lerp(1, 10, MathHelper.Clamp(Math.Abs(velocity.X) / 10, 0, 1));
float targetDistance = Math.Max(collider.radius * margin, minWidth);
float margin = MathHelper.Lerp(1, 10, MathHelper.Clamp(Math.Abs(velocity.X) / 5, 0, 1));
float targetDistance = Math.Max(colliderSize.X / 2 * margin, minWidth / 2);
if (horizontalDistance < targetDistance && isAboveFeet && isNotTooHigh && (door == null || door.CanBeTraversed))
{
currentPath.SkipToNextNode();
@@ -0,0 +1,176 @@
using Barotrauma.Extensions;
using System;
using System.Linq;
namespace Barotrauma
{
partial class MentalStateManager
{
private float mentalStateTimer;
private const float MentalStateInterval = 7.5f;
private float mentalBehaviorTimer;
private const float MentalBehaviorInterval = 7.5f;
private readonly Character character;
private readonly HumanAIController humanAIController;
public bool Active { get; set; }
public MentalType CurrentMentalType { get; private set; }
public enum MentalType
{
Normal,
Confused, // No effects other than special dialogue
Afraid, // Will retreat from whoever is nearby
Desperate, // Will defensively attack/arrest whoever is nearby
Berserk // turns fully hostile using team change logic
}
private const string MentalTeamChange = "mental";
public MentalStateManager(Character character, HumanAIController humanAIController)
{
this.character = character;
this.humanAIController = humanAIController;
}
public void Update(float deltaTime)
{
if (!Active) { return; }
mentalStateTimer -= deltaTime;
if (mentalStateTimer <= 0.0f)
{
UpdateMentalState();
mentalStateTimer = MentalStateInterval * Rand.Range(0.75f, 1.25f);
}
mentalBehaviorTimer = Math.Max(0f, mentalBehaviorTimer - deltaTime);
}
private void UpdateMentalState()
{
MentalType newMentalType = GetMentalType(character.CharacterHealth.GetAffliction("psychosis"));
bool createdCombat = false;
switch (newMentalType)
{
case MentalType.Normal:
case MentalType.Confused:
// remove combat if we became normal again
mentalBehaviorTimer = 0f;
break;
case MentalType.Afraid:
case MentalType.Desperate:
case MentalType.Berserk:
// berserk is not removed unless we drop to normal behavior again
if (CurrentMentalType == MentalType.Berserk)
{
newMentalType = MentalType.Berserk;
}
// give players a full interval to react to mental changes
if (newMentalType == CurrentMentalType)
{
createdCombat = CreateCombatBehavior(CurrentMentalType);
}
break;
}
if (!createdCombat)
{
CreateDialogueBehavior(newMentalType);
}
if (newMentalType != MentalType.Berserk)
{
character.TryRemoveTeamChange(MentalTeamChange);
}
CurrentMentalType = newMentalType;
}
private int mentalTypeCount;
private int MentalTypeCount
{
get
{
if (mentalTypeCount == 0)
{
mentalTypeCount = Enum.GetNames(typeof(MentalType)).Length;
}
return mentalTypeCount;
}
}
private MentalType GetMentalType(Affliction affliction)
{
if (affliction == null)
{
return MentalType.Normal;
}
// test this later
int psychosisIndex = (int)(affliction.Strength / (affliction.Prefab.MaxStrength / MentalTypeCount) * Rand.Range(1f, 1.2f));
psychosisIndex = Math.Clamp(psychosisIndex, 0, 4);
MentalType mentalType = psychosisIndex switch
{
0 => MentalType.Normal,
1 => MentalType.Confused,
2 => MentalType.Afraid,
3 => MentalType.Desperate,
4 => MentalType.Berserk,
_ => throw new ArgumentOutOfRangeException(psychosisIndex.ToString()),
};
return mentalType;
}
public bool CreateCombatBehavior(MentalType mentalType)
{
Character mentalAttackTarget = Character.CharacterList.Where(
possibleTarget => HumanAIController.IsActive(possibleTarget) &&
(possibleTarget.TeamID != character.TeamID || mentalType == MentalType.Berserk) &&
humanAIController.VisibleHulls.Contains(possibleTarget.CurrentHull) &&
possibleTarget != character).GetRandom();
if (mentalAttackTarget == null)
{
return false;
}
var combatMode = AIObjectiveCombat.CombatMode.None;
bool holdFire = mentalType == MentalType.Afraid && character.IsSecurity;
switch (mentalType)
{
case MentalType.Afraid:
combatMode = character.IsSecurity ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Retreat;
break;
case MentalType.Desperate:
// might be unnecessary to explicitly declare as arrest against non-humans
combatMode = character.IsSecurity && mentalAttackTarget.IsHuman ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Defensive;
break;
case MentalType.Berserk:
combatMode = AIObjectiveCombat.CombatMode.Offensive;
break;
}
// using this as an explicit time-out for the behavior. it's possible it will never run out because of the manager being disabled, but combat objective has failsafes for that
mentalBehaviorTimer = MentalBehaviorInterval;
humanAIController.AddCombatObjective(combatMode, mentalAttackTarget, allowHoldFire: holdFire, abortCondition: obj => mentalBehaviorTimer <= 0f);
string textIdentifier = $"dialogmentalstatereaction{combatMode.ToString().ToLowerInvariant()}";
character.Speak(TextManager.Get(textIdentifier), delay: Rand.Range(0.5f, 1.0f), identifier: textIdentifier, minDurationBetweenSimilar: 25f);
if (mentalType == MentalType.Berserk && !character.HasTeamChange(MentalTeamChange))
{
// TODO: could this be handled in the switch block above?
character.TryAddNewTeamChange(MentalTeamChange, new ActiveTeamChange(CharacterTeamType.None, ActiveTeamChange.TeamChangePriorities.Absolute, aggressiveBehavior: true));
}
return true;
}
public void CreateDialogueBehavior(MentalType mentalType)
{
if (mentalType == MentalType.Normal) { return; }
string textIdentifier = $"dialogmentalstate{mentalType.ToString().ToLowerInvariant()}";
character.Speak(TextManager.Get(textIdentifier), delay: Rand.Range(0.5f, 1.0f), identifier: textIdentifier, minDurationBetweenSimilar: 35f);
}
}
}
@@ -64,6 +64,10 @@ namespace Barotrauma
public readonly List<NPCConversation> Responses;
private readonly int speakerIndex;
private readonly List<string> allowedSpeakerTags;
private readonly bool requireNextLine;
// used primarily for team1 characters interacting with escorted personnel (TODO: not used anywhere)
private readonly bool requireSight;
public static void LoadAll(IEnumerable<ContentFile> files)
{
foreach (var file in files)
@@ -161,6 +165,8 @@ namespace Barotrauma
{
Responses.Add(new NPCConversation(subElement, filePath));
}
requireNextLine = element.GetAttributeBool("requirenextline", false);
requireSight = element.GetAttributeBool("requiresight", false);
}
private static List<string> GetCurrentFlags(Character speaker)
@@ -211,7 +217,7 @@ namespace Barotrauma
var afflictions = speaker.CharacterHealth.GetAllAfflictions();
foreach (Affliction affliction in afflictions)
{
var currentEffect = affliction.Prefab.GetActiveEffect(affliction.Strength);
var currentEffect = affliction.GetActiveEffect();
if (currentEffect != null && !string.IsNullOrEmpty(currentEffect.DialogFlag) && !currentFlags.Contains(currentEffect.DialogFlag))
{
currentFlags.Add(currentEffect.DialogFlag);
@@ -226,7 +232,6 @@ namespace Barotrauma
{
currentFlags.Add("CampaignNPC." + speaker.CampaignInteractionType);
}
if (GameMain.GameSession?.GameMode is CampaignMode campaignMode &&
(campaignMode.Map?.CurrentLocation?.Type?.Identifier.Equals("abandoned", StringComparison.OrdinalIgnoreCase) ?? false))
{
@@ -239,6 +244,10 @@ namespace Barotrauma
currentFlags.Add("Hostage");
}
}
if (speaker.IsEscorted)
{
currentFlags.Add("escort");
}
}
return currentFlags;
@@ -325,43 +334,15 @@ namespace Barotrauma
foreach (Character potentialSpeaker in availableSpeakers)
{
//check if the character has an appropriate job to say the line
if ((potentialSpeaker.Info?.Job != null && potentialSpeaker.Info.Job.Prefab.OnlyJobSpecificDialog) ||
selectedConversation.AllowedJobs.Count > 0)
if (CheckSpeakerViability(potentialSpeaker, selectedConversation, assignedSpeakers.Values.ToList(), ignoreFlags))
{
if (!selectedConversation.AllowedJobs.Contains(potentialSpeaker.Info?.Job.Prefab)) { continue; }
allowedSpeakers.Add(potentialSpeaker);
}
//check if the character has all required flags to say the line
if (!ignoreFlags)
{
var characterFlags = GetCurrentFlags(potentialSpeaker);
if (!selectedConversation.Flags.All(flag => characterFlags.Contains(flag))) { continue; }
}
//check if the character is close enough to hear the rest of the speakers
if (assignedSpeakers.Values.Any(s => !potentialSpeaker.CanHearCharacter(s))) { continue; }
//check if the character has an appropriate personality
if (selectedConversation.allowedSpeakerTags.Count > 0)
{
if (potentialSpeaker.Info?.PersonalityTrait == null) { continue; }
if (!selectedConversation.allowedSpeakerTags.Any(t => potentialSpeaker.Info.PersonalityTrait.AllowedDialogTags.Any(t2 => t2 == t))) { continue; }
}
else
{
if (potentialSpeaker.Info?.PersonalityTrait != null &&
!potentialSpeaker.Info.PersonalityTrait.AllowedDialogTags.Contains("none"))
{
continue;
}
}
allowedSpeakers.Add(potentialSpeaker);
}
if (allowedSpeakers.Count == 0)
if (allowedSpeakers.Count == 0 || NextLineFailure(selectedConversation, availableSpeakers, allowedSpeakers, ignoreFlags))
{
allowedSpeakers.Clear();
potentialLines.Remove(selectedConversation);
}
else
@@ -385,6 +366,62 @@ namespace Barotrauma
CreateConversation(availableSpeakers, assignedSpeakers, selectedConversation, lineList, availableConversations);
}
static bool NextLineFailure(NPCConversation selectedConversation, List<Character> availableSpeakers, List<Character> allowedSpeakers, bool ignoreFlags)
{
if (selectedConversation.requireNextLine)
{
foreach (NPCConversation nextConversation in selectedConversation.Responses)
{
foreach (Character potentialNextSpeaker in availableSpeakers)
{
if (CheckSpeakerViability(potentialNextSpeaker, nextConversation, allowedSpeakers, ignoreFlags))
{
return false;
}
}
}
return true;
}
return false;
}
static bool CheckSpeakerViability(Character potentialSpeaker, NPCConversation selectedConversation, List<Character> checkedSpeakers, bool ignoreFlags)
{
//check if the character has an appropriate job to say the line
if ((potentialSpeaker.Info?.Job != null && potentialSpeaker.Info.Job.Prefab.OnlyJobSpecificDialog) || selectedConversation.AllowedJobs.Count > 0)
{
if (!selectedConversation.AllowedJobs.Contains(potentialSpeaker.Info?.Job.Prefab)) { return false; }
}
//check if the character has all required flags to say the line
if (!ignoreFlags)
{
var characterFlags = GetCurrentFlags(potentialSpeaker);
if (!selectedConversation.Flags.All(flag => characterFlags.Contains(flag))) { return false; }
}
//check if the character is close enough to hear the rest of the speakers
if (checkedSpeakers.Any(s => !potentialSpeaker.CanHearCharacter(s))) { return false; }
//check if the character is close enough to see the rest of the speakers (this should be replaced with a more performant method)
if (checkedSpeakers.Any(s => !potentialSpeaker.CanSeeCharacter(s))) { return false; }
//check if the character has an appropriate personality
if (selectedConversation.allowedSpeakerTags.Count > 0)
{
if (potentialSpeaker.Info?.PersonalityTrait == null) { return false; }
if (!selectedConversation.allowedSpeakerTags.Any(t => potentialSpeaker.Info.PersonalityTrait.AllowedDialogTags.Any(t2 => t2 == t))) { return false; }
}
else
{
if (potentialSpeaker.Info?.PersonalityTrait != null &&
!potentialSpeaker.Info.PersonalityTrait.AllowedDialogTags.Contains("none"))
{
return false;
}
}
return true;
}
private static NPCConversation GetRandomConversation(List<NPCConversation> conversations, bool avoidPreviouslyUsed)
{
if (!avoidPreviouslyUsed)
@@ -6,16 +6,18 @@ using Barotrauma.Extensions;
namespace Barotrauma
{
abstract class AIObjective
abstract partial class AIObjective
{
public virtual float Devotion => AIObjectiveManager.baseDevotion;
public abstract string DebugTag { get; }
public abstract string Identifier { get; set; }
public virtual string DebugTag => Identifier;
public virtual bool ForceRun => false;
public virtual bool IgnoreUnsafeHulls => false;
public virtual bool AbandonWhenCannotCompleteSubjectives => true;
public virtual bool AllowSubObjectiveSorting => false;
public virtual bool ForceOrderPriority => true;
public virtual bool PrioritizeIfSubObjectivesActive => false;
/// <summary>
/// Can there be multiple objective instaces of the same type?
@@ -52,8 +54,17 @@ namespace Barotrauma
/// </summary>
public float Priority { get; set; }
public float BasePriority { get; set; }
public float PriorityModifier { get; private set; } = 1;
// For forcing the highest priority temporarily. Will reset after each priority calculation, so it will need to be kept alive by something.
public bool ForceHighestPriority { get; set; }
// For temporarily forcing walking. Will reset after each priority calculation, so it will need to be kept alive by something.
// The intention of this boolean to allow walking even when the priority is higher than AIObjectiveManager.RunPriority.
public bool ForceWalk { get; set; }
public bool IgnoreAtOutpost { get; set; }
public readonly Character character;
public readonly AIObjectiveManager objectiveManager;
public string Option { get; private set; }
@@ -102,6 +113,13 @@ namespace Barotrauma
return all;
}
#pragma warning disable CS0649
/// <summary>
/// Aborts the objective when this condition is true.
/// </summary>
public Func<AIObjective, bool> AbortCondition;
#pragma warning restore CS0649
/// <summary>
/// A single shot event. Automatically cleared after launching. Use OnCompleted method for implementing (internal) persistent behavior.
/// </summary>
@@ -217,18 +235,22 @@ namespace Barotrauma
protected bool IsAllowed
{
get
{
{
if (IgnoreAtOutpost && Level.IsLoadedOutpost && character.TeamID != CharacterTeamType.FriendlyNPC)
{
if (Submarine.MainSub != null && Submarine.MainSub.DockedTo.None(s => s.TeamID != CharacterTeamType.FriendlyNPC && s.TeamID != character.TeamID))
{
return false;
}
}
if (!AllowOutsideSubmarine && character.Submarine == null) { return false; }
if (AllowInAnySub) { return true; }
if (AllowInFriendlySubs && character.Submarine.TeamID == CharacterTeamType.FriendlyNPC) { return true; }
if ((AllowInFriendlySubs && character.Submarine.TeamID == CharacterTeamType.FriendlyNPC) || character.IsEscorted) { return true; }
return character.Submarine.TeamID == character.TeamID || character.Submarine.DockedTo.Any(sub => sub.TeamID == character.TeamID);
}
}
/// <summary>
/// Call this only when the priority needs to be recalculated. Use the cached Priority property when you don't need to recalculate.
/// </summary>
public virtual float GetPriority()
protected virtual float GetPriority()
{
bool isOrder = objectiveManager.IsOrder(this);
if (!IsAllowed)
@@ -248,6 +270,17 @@ namespace Barotrauma
return Priority;
}
/// <summary>
/// Call this only when the priority needs to be recalculated. Use the cached Priority property when you don't need to recalculate.
/// </summary>
public float CalculatePriority()
{
Priority = GetPriority();
ForceHighestPriority = false;
ForceWalk = false;
return Priority;
}
private void UpdateDevotion(float deltaTime)
{
var currentObjective = objectiveManager.CurrentObjective;
@@ -393,7 +426,17 @@ namespace Barotrauma
}
}
protected abstract bool Check();
protected virtual bool Check()
{
if (AbortCondition != null && AbortCondition(this))
{
Abandon = true;
return false;
}
return CheckObjectiveSpecific();
}
protected abstract bool CheckObjectiveSpecific();
private bool CheckState()
{
@@ -9,7 +9,7 @@ namespace Barotrauma
{
class AIObjectiveChargeBatteries : AIObjectiveLoop<PowerContainer>
{
public override string DebugTag => "charge batteries";
public override string Identifier { get; set; } = "charge batteries";
public override bool AllowAutomaticItemUnequipping => true;
private IEnumerable<PowerContainer> batteryList;
@@ -9,7 +9,7 @@ namespace Barotrauma
{
class AIObjectiveCleanupItem : AIObjective
{
public override string DebugTag => "cleanup item";
public override string Identifier { get; set; } = "cleanup item";
public override bool KeepDivingGearOn => true;
public override bool AllowAutomaticItemUnequipping => false;
@@ -26,7 +26,7 @@ namespace Barotrauma
this.item = item;
}
public override float GetPriority()
protected override float GetPriority()
{
if (!IsAllowed)
{
@@ -68,7 +68,7 @@ namespace Barotrauma
}
if (item.ParentInventory != null)
{
if (item.Container != null && !AIObjectiveCleanupItems.IsValidContainer(item.Container, character, allowUnloading: objectiveManager.HasOrders()))
if (item.Container != null && !AIObjectiveCleanupItems.IsValidContainer(item.Container, character, allowUnloading: objectiveManager.HasOrder<AIObjectiveCleanupItems>()))
{
// Target was picked up or moved by someone.
Abandon = true;
@@ -82,14 +82,14 @@ namespace Barotrauma
itemIndex = 0;
if (suitableContainer != null)
{
bool equip = item.HasTag(AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR) || (
item.GetComponent<Wearable>() == null &&
bool equip = item.GetComponent<Holdable>() != null ||
item.AllowedSlots.None(s =>
s == InvSlotType.Card ||
s == InvSlotType.Head ||
s == InvSlotType.Headset ||
s == InvSlotType.InnerClothes ||
s == InvSlotType.OuterClothes));
s == InvSlotType.Card ||
s == InvSlotType.Head ||
s == InvSlotType.Headset ||
s == InvSlotType.InnerClothes ||
s == InvSlotType.OuterClothes);
TryAddSubObjective(ref decontainObjective, () => new AIObjectiveDecontainItem(character, item, objectiveManager, targetContainer: suitableContainer.GetComponent<ItemContainer>())
{
Equip = equip,
@@ -131,7 +131,7 @@ namespace Barotrauma
}
}
protected override bool Check() => IsCompleted;
protected override bool CheckObjectiveSpecific() => IsCompleted;
public override void Reset()
{
@@ -8,7 +8,7 @@ namespace Barotrauma
{
class AIObjectiveCleanupItems : AIObjectiveLoop<Item>
{
public override string DebugTag => "cleanup items";
public override string Identifier { get; set; } = "cleanup items";
public override bool KeepDivingGearOn => true;
public override bool AllowAutomaticItemUnequipping => false;
public override bool ForceOrderPriority => false;
@@ -38,8 +38,8 @@ namespace Barotrauma
float prio = objectiveManager.GetOrderPriority(this);
if (subObjectives.All(so => so.SubObjectives.None()))
{
// If none of the subobjectives have subobjectives, no valid container was found. In this case, let's reduce the priority below the run threshold.
prio = Math.Min(prio, AIObjectiveManager.RunPriority - 1);
// If none of the subobjectives have subobjectives, no valid container was found. Don't allow running.
ForceWalk = true;
}
return prio;
}
@@ -80,8 +80,13 @@ namespace Barotrauma
return true;
}
public static bool IsValidContainer(Item item, Character character, bool allowUnloading = true) =>
!item.IgnoreByAI && item.IsInteractable(character) && item.HasTag("allowcleanup") && allowUnloading && item.ParentInventory == null && item.OwnInventory != null && item.OwnInventory.AllItems.Any() && IsItemInsideValidSubmarine(item, character);
public static bool IsValidContainer(Item container, Character character, bool allowUnloading = true) =>
allowUnloading &&
!container.IgnoreByAI &&
container.IsInteractable(character) &&
container.HasTag("allowcleanup") &&
container.ParentInventory == null && container.OwnInventory != null && container.OwnInventory.AllItems.Any() &&
IsItemInsideValidSubmarine(container, character);
public static bool IsValidTarget(Item item, Character character, bool checkInventory, bool allowUnloading = true)
{
@@ -91,7 +96,12 @@ namespace Barotrauma
if (item.SpawnedInOutpost) { return false; }
if (item.ParentInventory != null)
{
if (item.Container == null || !IsValidContainer(item.Container, character, allowUnloading)) { return false; }
if (item.Container == null)
{
// In a character inventory
return false;
}
if (!IsValidContainer(item.Container, character, allowUnloading)) { return false; }
}
if (character != null && !IsItemInsideValidSubmarine(item, character)) { return false; }
var pickable = item.GetComponent<Pickable>();
@@ -10,7 +10,7 @@ namespace Barotrauma
{
class AIObjectiveCombat : AIObjective
{
public override string DebugTag => "combat";
public override string Identifier { get; set; } = "combat";
public override bool KeepDivingGearOn => true;
public override bool IgnoreUnsafeHulls => true;
@@ -92,11 +92,6 @@ namespace Barotrauma
private readonly float distanceCheckInterval = 0.2f;
private float distanceTimer;
/// <summary>
/// Aborts the objective when this condition is true
/// </summary>
public Func<bool> abortCondition;
public bool allowHoldFire;
/// <summary>
@@ -152,7 +147,7 @@ namespace Barotrauma
HumanAIController.SortTimer = 0;
}
public override float GetPriority()
protected override float GetPriority()
{
if (character.TeamID == CharacterTeamType.FriendlyNPC && Enemy != null)
{
@@ -186,7 +181,7 @@ namespace Barotrauma
{
findSafety.Priority = 0;
}
if (!character.IsOnPlayerTeam && !objectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>())
if (!AllowCoolDown && !character.IsOnPlayerTeam && !objectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>())
{
distanceTimer -= deltaTime;
if (distanceTimer < 0)
@@ -197,7 +192,7 @@ namespace Barotrauma
}
}
protected override bool Check()
protected override bool CheckObjectiveSpecific()
{
if (sqrDistance > maxDistance * maxDistance)
{
@@ -209,11 +204,6 @@ namespace Barotrauma
protected override void Act(float deltaTime)
{
if (abortCondition != null && abortCondition())
{
Abandon = true;
return;
}
if (AllowCoolDown)
{
coolDownTimer -= deltaTime;
@@ -358,6 +348,7 @@ namespace Barotrauma
TryAddSubObjective(ref seekWeaponObjective,
constructor: () => new AIObjectiveGetItem(character, "weapon", objectiveManager, equip: true, checkInventory: false)
{
AllowStealing = HumanAIController.IsMentallyUnstable,
GetItemPriority = i =>
{
if (Weapon != null && (i == Weapon || i.Prefab.Identifier == Weapon.Prefab.Identifier)) { return 0; }
@@ -799,10 +790,13 @@ namespace Barotrauma
}
if (character.TeamID == CharacterTeamType.FriendlyNPC)
{
// Confiscate stolen goods.
// Confiscate stolen goods and all weapons
foreach (var item in Enemy.Inventory.AllItemsMod)
{
if (item.StolenDuringRound)
if (character.TeamID == CharacterTeamType.FriendlyNPC && item.StolenDuringRound ||
item.HasTag("weapon") ||
item.GetComponent<MeleeWeapon>() != null ||
item.GetComponent<RangedWeapon>() != null)
{
item.Drop(character);
character.Inventory.TryPutItem(item, character, CharacterInventory.anySlot);
@@ -894,7 +888,8 @@ namespace Barotrauma
if (ammunitionIdentifiers != null)
{
// Try reload ammunition from inventory
ammunition = character.Inventory.FindItem(i => ammunitionIdentifiers.Any(id => id == i.Prefab.Identifier || i.HasTag(id)) && i.Condition > 0, true);
bool IsInsideHeadset(Item i) => i.ParentInventory?.Owner is Item ownerItem && ownerItem.HasTag("mobileradio");
ammunition = character.Inventory.FindItem(i => ammunitionIdentifiers.Any(id => id == i.Prefab.Identifier || i.HasTag(id)) && i.Condition > 0 && !IsInsideHeadset(i), recursive: true);
if (ammunition != null)
{
var container = Weapon.GetComponent<ItemContainer>();
@@ -7,7 +7,7 @@ namespace Barotrauma
{
class AIObjectiveContainItem: AIObjective
{
public override string DebugTag => "contain item";
public override string Identifier { get; set; } = "contain item";
public Func<Item, float> GetItemPriority;
@@ -61,7 +61,7 @@ namespace Barotrauma
this.container = container;
}
protected override bool Check()
protected override bool CheckObjectiveSpecific()
{
if (IsCompleted) { return true; }
if (container == null || (container.Item != null && container.Item.IsThisOrAnyContainerIgnoredByAI()))
@@ -146,7 +146,7 @@ namespace Barotrauma
{
DialogueIdentifier = "dialogcannotreachtarget",
TargetName = container.Item.Name,
abortCondition = obj => !ItemToContain.IsOwnedBy(character),
AbortCondition = obj => !ItemToContain.IsOwnedBy(character),
SpeakIfFails = !objectiveManager.IsCurrentOrder<AIObjectiveCleanupItems>()
},
onAbandon: () => Abandon = true,
@@ -170,7 +170,8 @@ namespace Barotrauma
ignoredItems = containedItems,
AllowToFindDivingGear = AllowToFindDivingGear,
AllowDangerousPressure = AllowDangerousPressure,
TargetCondition = ConditionLevel
TargetCondition = ConditionLevel,
ItemFilter = (Item potentialItem) => RemoveEmpty ? container.CanBeContained(potentialItem) : container.Inventory.CanBePut(potentialItem)
}, onAbandon: () =>
{
Abandon = true;
@@ -6,7 +6,7 @@ namespace Barotrauma
{
class AIObjectiveDecontainItem : AIObjective
{
public override string DebugTag => "decontain item";
public override string Identifier { get; set; } = "decontain item";
public Func<Item, float> GetItemPriority;
@@ -59,7 +59,7 @@ namespace Barotrauma
this.targetContainer = targetContainer;
}
protected override bool Check() => IsCompleted;
protected override bool CheckObjectiveSpecific() => IsCompleted;
protected override void Act(float deltaTime)
{
@@ -0,0 +1,106 @@
using System;
using System.Linq;
namespace Barotrauma
{
class AIObjectiveEscapeHandcuffs : AIObjective
{
// Used for prisoner escorts to allow them to escape their binds
public override string Identifier { get; set; } = "escape handcuffs";
public override bool AllowAutomaticItemUnequipping => true;
public override bool AllowOutsideSubmarine => true;
public override bool AllowInAnySub => true;
private int escapeProgress;
private bool isBeingWatched;
private bool shouldSwitchTeams;
const string EscapeTeamChangeIdentifier = "escape";
public AIObjectiveEscapeHandcuffs(Character character, AIObjectiveManager objectiveManager, bool shouldSwitchTeams = true, bool beginInstantly = false, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier)
{
this.shouldSwitchTeams = shouldSwitchTeams;
if (beginInstantly)
{
escapeTimer = EscapeIntervalTimer;
}
}
public override bool CanBeCompleted => true;
public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace.CleanupStackTrace()); }
protected override bool CheckObjectiveSpecific() => false;
// escape timer is set to 60 by default to allow players to locate prisoners in time
private float escapeTimer = 60f;
private const float EscapeIntervalTimer = 7.5f;
private float updateTimer;
private const float UpdateIntervalTimer = 4f;
protected override float GetPriority()
{
Priority = !isBeingWatched && character.LockHands ? AIObjectiveManager.LowestOrderPriority - 1 : 0;
return Priority;
}
public override void Update(float deltaTime)
{
updateTimer -= deltaTime;
if (updateTimer <= 0.0f)
{
if (shouldSwitchTeams)
{
if (!character.LockHands)
{
if (!character.HasTeamChange(EscapeTeamChangeIdentifier))
{
character.TryAddNewTeamChange(EscapeTeamChangeIdentifier, new ActiveTeamChange(CharacterTeamType.None, ActiveTeamChange.TeamChangePriorities.Willful));
}
}
else
{
character.TryRemoveTeamChange(EscapeTeamChangeIdentifier);
}
}
isBeingWatched = false;
foreach (Character otherCharacter in Character.CharacterList)
{
if (HumanAIController.IsActive(otherCharacter) && otherCharacter.TeamID == CharacterTeamType.Team1 && HumanAIController.VisibleHulls.Contains(otherCharacter.CurrentHull)) // hasn't been tested yet
{
isBeingWatched = true; // act casual when player characters are around
escapeProgress = 0;
break;
}
}
updateTimer = UpdateIntervalTimer * Rand.Range(0.75f, 1.25f);
}
}
protected override void Act(float deltaTime)
{
SteeringManager.Reset();
escapeTimer -= deltaTime;
if (escapeTimer <= 0.0f)
{
escapeProgress += Rand.Range(2, 5);
if (escapeProgress > 15)
{
Item handcuffs = character.Inventory.FindItemByTag("handlocker");
if (handcuffs != null)
{
handcuffs.Drop(character);
}
}
escapeTimer = EscapeIntervalTimer * Rand.Range(0.75f, 1.25f);
}
}
public override void Reset()
{
base.Reset();
escapeProgress = 0;
}
}
}
@@ -8,7 +8,7 @@ namespace Barotrauma
{
class AIObjectiveExtinguishFire : AIObjective
{
public override string DebugTag => "extinguish fire";
public override string Identifier { get; set; } = "extinguish fire";
public override bool ForceRun => true;
public override bool ConcurrentObjectives => true;
public override bool KeepDivingGearOn => true;
@@ -27,7 +27,7 @@ namespace Barotrauma
this.targetHull = targetHull;
}
public override float GetPriority()
protected override float GetPriority()
{
if (!IsAllowed)
{
@@ -68,7 +68,7 @@ namespace Barotrauma
return Priority;
}
protected override bool Check() => targetHull.FireSources.None();
protected override bool CheckObjectiveSpecific() => targetHull.FireSources.None();
private float sinTime;
protected override void Act(float deltaTime)
@@ -8,7 +8,7 @@ namespace Barotrauma
{
class AIObjectiveExtinguishFires : AIObjectiveLoop<Hull>
{
public override string DebugTag => "extinguish fires";
public override string Identifier { get; set; } = "extinguish fires";
public override bool ForceRun => true;
public override bool AllowInAnySub => true;
@@ -6,7 +6,7 @@ namespace Barotrauma
{
class AIObjectiveFightIntruders : AIObjectiveLoop<Character>
{
public override string DebugTag => "fight intruders";
public override string Identifier { get; set; } = "fight intruders";
protected override float IgnoreListClearInterval => 30;
public override bool IgnoreUnsafeHulls => true;
@@ -21,13 +21,18 @@ namespace Barotrauma
protected override float TargetEvaluation()
{
// TODO: sorting criteria
return Targets.None() ? 0 : 100;
if (!character.IsOnPlayerTeam) { return Targets.None() ? 0 : 100; }
int totalEnemies = Targets.Count();
if (totalEnemies == 0) { return 0; }
if (character.IsSecurity) { return 100; }
if (objectiveManager.IsOrder(this)) { return 100; }
return HumanAIController.IsTrueForAnyCrewMember(c => c.Character.IsSecurity && !c.Character.IsIncapacitated && c.Character.Submarine == character.Submarine) ? 0 : 100;
}
protected override AIObjective ObjectiveConstructor(Character target)
{
var combatObjective = new AIObjectiveCombat(character, target, AIObjectiveCombat.CombatMode.Offensive, objectiveManager, PriorityModifier);
AIObjectiveCombat.CombatMode combatMode = target.IsEscorted && character.TeamID == CharacterTeamType.Team1 ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Offensive;
var combatObjective = new AIObjectiveCombat(character, target, combatMode, objectiveManager, PriorityModifier);
if (character.TeamID == CharacterTeamType.FriendlyNPC && target.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign)
{
var reputation = campaign.Map?.CurrentLocation?.Reputation;
@@ -7,7 +7,8 @@ namespace Barotrauma
{
class AIObjectiveFindDivingGear : AIObjective
{
public override string DebugTag => $"find diving gear ({gearTag})";
public override string Identifier { get; set; } = "find diving gear";
public override string DebugTag => $"{Identifier} ({gearTag})";
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
public override bool AbandonWhenCannotCompleteSubjectives => false;
@@ -23,7 +24,7 @@ namespace Barotrauma
public static string LIGHT_DIVING_GEAR = "lightdiving";
public static string OXYGEN_SOURCE = "oxygensource";
protected override bool Check() => targetItem != null && character.HasEquippedItem(targetItem);
protected override bool CheckObjectiveSpecific() => targetItem != null && character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.Head);
public AIObjectiveFindDivingGear(Character character, bool needsDivingSuit, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier)
{
@@ -38,7 +39,7 @@ namespace Barotrauma
return;
}
targetItem = character.Inventory.FindItemByTag(gearTag, true);
if (targetItem == null || !character.HasEquippedItem(targetItem) && targetItem.ContainedItems.Any(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > 0))
if (targetItem == null || !character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.Head | InvSlotType.InnerClothes) && targetItem.ContainedItems.Any(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > 0))
{
TryAddSubObjective(ref getDivingGear, () =>
{
@@ -48,9 +49,11 @@ namespace Barotrauma
}
return new AIObjectiveGetItem(character, gearTag, objectiveManager, equip: true)
{
AllowStealing = true,
AllowStealing = HumanAIController.NeedsDivingGear(character.CurrentHull, out _),
AllowToFindDivingGear = false,
AllowDangerousPressure = true
AllowDangerousPressure = true,
EquipSlotType = InvSlotType.OuterClothes | InvSlotType.Head | InvSlotType.InnerClothes,
Wear = true
};
},
onAbandon: () => Abandon = true,
@@ -58,8 +61,6 @@ namespace Barotrauma
}
else
{
HumanAIController.UnequipContainedItems(targetItem, it => !it.HasTag("oxygensource"));
HumanAIController.UnequipEmptyItems(targetItem);
// Seek oxygen that has at least 10% condition left, if we are inside a friendly sub.
// The margin helps us to survive, because we might need some oxygen before we can find more oxygen.
// When we are venturing outside of our sub, let's just suppose that we have enough oxygen with us and optimize it so that we don't keep switching off half used tanks.
@@ -119,6 +120,7 @@ namespace Barotrauma
int ReportOxygenTankCount()
{
if (character.Submarine != Submarine.MainSub) { return 1; }
int remainingOxygenTanks = Submarine.MainSub.GetItems(false).Count(i => i.HasTag("oxygensource") && i.Condition > 1);
if (remainingOxygenTanks == 0)
{
@@ -8,7 +8,7 @@ namespace Barotrauma
{
class AIObjectiveFindSafety : AIObjective
{
public override string DebugTag => "find safety";
public override string Identifier { get; set; } = "find safety";
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
public override bool IgnoreUnsafeHulls => true;
@@ -32,12 +32,12 @@ namespace Barotrauma
public AIObjectiveFindSafety(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { }
protected override bool Check() => false;
protected override bool CheckObjectiveSpecific() => false;
public override bool CanBeCompleted => true;
private bool resetPriority;
public override float GetPriority()
protected override float GetPriority()
{
if (!IsAllowed)
{
@@ -9,7 +9,7 @@ namespace Barotrauma
{
class AIObjectiveFixLeak : AIObjective
{
public override string DebugTag => "fix leak";
public override string Identifier { get; set; } = "fix leak";
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
public override bool AllowInAnySub => true;
@@ -29,9 +29,9 @@ namespace Barotrauma
this.isPriority = isPriority;
}
protected override bool Check() => Leak.Open <= 0 || Leak.Removed;
protected override bool CheckObjectiveSpecific() => Leak.Open <= 0 || Leak.Removed;
public override float GetPriority()
protected override float GetPriority()
{
if (!IsAllowed)
{
@@ -86,21 +86,22 @@ namespace Barotrauma
Abandon = true;
return;
}
HumanAIController.UnequipContainedItems(weldingTool, it => !it.HasTag("weldingfuel"));
HumanAIController.UnequipEmptyItems(weldingTool);
if (weldingTool.OwnInventory != null && weldingTool.OwnInventory.AllItems.None(i => i.HasTag("weldingfuel") && i.Condition > 0.0f))
{
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfuel", weldingTool.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC),
onAbandon: () =>
{
Abandon = true;
ReportWeldingFuelTankCount();
},
onCompleted: () =>
{
RemoveSubObjective(ref refuelObjective);
ReportWeldingFuelTankCount();
});
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfuel", weldingTool.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC)
{
RemoveExisting = true
},
onAbandon: () =>
{
Abandon = true;
ReportWeldingFuelTankCount();
},
onCompleted: () =>
{
RemoveSubObjective(ref refuelObjective);
ReportWeldingFuelTankCount();
});
void ReportWeldingFuelTankCount()
{
@@ -141,7 +142,7 @@ namespace Barotrauma
onAbandon: () => Abandon = true,
onCompleted: () =>
{
if (Check()) { IsCompleted = true; }
if (CheckObjectiveSpecific()) { IsCompleted = true; }
else
{
// Failed to operate. Probably too far.
@@ -160,7 +161,7 @@ namespace Barotrauma
},
onAbandon: () =>
{
if (Check()) { IsCompleted = true; }
if (CheckObjectiveSpecific()) { IsCompleted = true; }
else if ((Leak.WorldPosition - character.WorldPosition).LengthSquared() > MathUtils.Pow(reach * 2, 2))
{
// Too far
@@ -191,7 +192,7 @@ namespace Barotrauma
// This is an approximation, because we don't know the exact reach until the pose is taken.
// And even then the actual range depends on the direction we are aiming to.
// Found out that without any multiplier the value (209) is often too short.
return repairTool.Range + armLength * 1.2f;
return repairTool.Range + armLength * 1.3f;
}
}
}
@@ -6,7 +6,7 @@ namespace Barotrauma
{
class AIObjectiveFixLeaks : AIObjectiveLoop<Gap>
{
public override string DebugTag => "fix leaks";
public override string Identifier { get; set; } = "fix leaks";
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
public override bool AllowInAnySub => true;
@@ -40,7 +40,7 @@ namespace Barotrauma
{
int totalLeaks = Targets.Count();
if (totalLeaks == 0) { return 0; }
int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveFixLeaks>() && !c.Character.IsIncapacitated, onlyBots: true);
int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveFixLeaks>() && !c.Character.IsIncapacitated && c.Character.Submarine == character.Submarine, onlyBots: true);
bool anyFixers = otherFixers > 0;
if (objectiveManager.IsOrder(this))
{
@@ -51,7 +51,7 @@ namespace Barotrauma
{
int secondaryLeaks = Targets.Count(l => l.IsRoomToRoom);
int leaks = totalLeaks - secondaryLeaks;
float ratio = leaks == 0 ? 1 : anyFixers ? leaks / otherFixers : 1;
float ratio = leaks == 0 ? 1 : anyFixers ? leaks / (float)otherFixers : 1;
if (anyFixers && (ratio <= 1 || otherFixers > 5 || otherFixers / (float)HumanAIController.CountCrew(onlyBots: true) > 0.75f))
{
// Enough fixers
@@ -8,11 +8,10 @@ namespace Barotrauma
{
class AIObjectiveGetItem : AIObjective
{
public override string DebugTag => "get item";
public override string Identifier { get; set; } = "get item";
public override bool AbandonWhenCannotCompleteSubjectives => false;
private readonly bool equip;
public HashSet<Item> ignoredItems = new HashSet<Item>();
public Func<Item, float> GetItemPriority;
@@ -45,14 +44,17 @@ namespace Barotrauma
/// Is the character allowed to take the item from somewhere else than their own sub (e.g. an outpost)
/// </summary>
public bool AllowStealing { get; set; }
public bool TakeWholeStack { get; set; }
public bool Equip { get; set; }
public bool Wear { get; set; }
public InvSlotType? EquipSlotType { get; set; }
public AIObjectiveGetItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, bool equip = true, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
currSearchIndex = -1;
this.equip = equip;
Equip = equip;
originalTarget = targetItem;
this.targetItem = targetItem;
moveToTarget = targetItem?.GetRootInventoryOwner();
@@ -65,7 +67,7 @@ namespace Barotrauma
: base(character, objectiveManager, priorityModifier)
{
currSearchIndex = -1;
this.equip = equip;
Equip = equip;
this.identifiersOrTags = identifiersOrTags;
this.spawnItemIfNotFound = spawnItemIfNotFound;
for (int i = 0; i < identifiersOrTags.Length; i++)
@@ -197,7 +199,7 @@ namespace Barotrauma
Inventory itemInventory = targetItem.ParentInventory;
var slots = itemInventory?.FindIndices(targetItem);
if (HumanAIController.TakeItem(targetItem, character.Inventory, equip, storeUnequipped: true))
if (HumanAIController.TakeItem(targetItem, character.Inventory, Equip, Wear, storeUnequipped: true))
{
if (TakeWholeStack && slots != null)
{
@@ -227,7 +229,7 @@ namespace Barotrauma
return new AIObjectiveGoTo(moveToTarget, character, objectiveManager, repeat: false, getDivingGearIfNeeded: AllowToFindDivingGear, closeEnough: DefaultReach)
{
// If the root container changes, the item is no longer where it was (taken by someone -> need to find another item)
abortCondition = obj => targetItem == null || targetItem.GetRootInventoryOwner() != moveToTarget,
AbortCondition = obj => targetItem == null || targetItem.GetRootInventoryOwner() != moveToTarget,
SpeakIfFails = false
};
},
@@ -365,19 +367,33 @@ namespace Barotrauma
}
}
protected override bool Check()
protected override bool CheckObjectiveSpecific()
{
if (IsCompleted) { return true; }
if (targetItem != null)
{
return character.HasItem(targetItem, equip);
if (Equip && EquipSlotType.HasValue)
{
return character.HasEquippedItem(targetItem, EquipSlotType.Value);
}
else
{
return character.HasItem(targetItem, Equip);
}
}
else if (identifiersOrTags != null)
{
var matchingItem = character.Inventory.FindItem(i => CheckItem(i), recursive: true);
if (matchingItem != null)
{
return !equip || character.HasEquippedItem(matchingItem);
if (Equip && EquipSlotType.HasValue)
{
return character.HasEquippedItem(matchingItem, EquipSlotType.Value);
}
else
{
return !Equip || character.HasEquippedItem(matchingItem);
}
}
return false;
}
@@ -8,7 +8,7 @@ namespace Barotrauma
{
class AIObjectiveGoTo : AIObjective
{
public override string DebugTag => "go to";
public override string Identifier { get; set; } = "go to";
private AIObjectiveFindDivingGear findDivingGear;
private readonly bool repeat;
@@ -20,10 +20,6 @@ namespace Barotrauma
/// Doesn't allow the objective to complete if this condition is false
/// </summary>
public Func<bool> requiredCondition;
/// <summary>
/// Aborts the objective when this condition is true
/// </summary>
public Func<AIObjectiveGoTo, bool> abortCondition;
public Func<PathNode, bool> endNodeFilter;
public Func<float> priorityGetter;
@@ -38,6 +34,7 @@ namespace Barotrauma
private readonly float minDistance = 50;
private readonly float seekGapsInterval = 1;
private float seekGapsTimer;
private bool cannotFollow;
/// <summary>
/// Display units
@@ -81,7 +78,7 @@ namespace Barotrauma
public float? OverridePriority = null;
public override float GetPriority()
protected override float GetPriority()
{
bool isOrder = objectiveManager.IsOrder(this);
if (!IsAllowed)
@@ -177,6 +174,11 @@ namespace Barotrauma
character.AIController.SteeringManager.Reset();
return;
}
if (cannotFollow)
{
// Wait
character.AIController.SteeringManager.Reset();
}
waitUntilPathUnreachable -= deltaTime;
if (!character.IsClimbing)
{
@@ -263,16 +265,29 @@ namespace Barotrauma
if (findDivingGear != null && !findDivingGear.CanBeCompleted)
{
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager),
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref findDivingGear));
onAbandon: () => Abort(),
onCompleted: () =>
{
cannotFollow = false;
RemoveSubObjective(ref findDivingGear);
});
}
else
{
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager),
onCompleted: () => RemoveSubObjective(ref findDivingGear));
onAbandon: () => Abort(),
onCompleted: () =>
{
cannotFollow = false;
RemoveSubObjective(ref findDivingGear);
});
}
return;
}
else
{
cannotFollow = false;
}
}
if (repeat)
{
@@ -578,22 +593,15 @@ namespace Barotrauma
}
}
protected override bool Check()
protected override bool CheckObjectiveSpecific()
{
if (IsCompleted) { return true; }
// First check the distance
// Then the custom condition
// And finally check if can interact (heaviest)
// First check the distance and then if can interact (heaviest)
if (Target == null)
{
Abandon = true;
return false;
}
if (abortCondition != null && abortCondition(this))
{
Abandon = true;
return false;
}
if (repeat)
{
return false;
@@ -624,6 +632,18 @@ namespace Barotrauma
return IsCompleted;
}
private void Abort()
{
if (!objectiveManager.IsOrder(this))
{
Abandon = true;
}
else
{
cannotFollow = true;
}
}
protected override void OnAbandon()
{
StopMovement();
@@ -657,6 +677,7 @@ namespace Barotrauma
findDivingGear = null;
seekGapsTimer = 0;
TargetGap = null;
cannotFollow = false;
}
}
}
@@ -10,7 +10,7 @@ namespace Barotrauma
{
class AIObjectiveIdle : AIObjective
{
public override string DebugTag => "idle";
public override string Identifier { get; set; } = "idle";
public override bool AllowAutomaticItemUnequipping => true;
public override bool AllowInAnySub => true;
@@ -21,11 +21,6 @@ namespace Barotrauma
set
{
behavior = value;
if (behavior == BehaviorType.StayInHull && TargetHull == null)
{
DebugConsole.AddWarning($"Trying to set a character's behavior type to StayInHull, but target hull is not set. {character.Name} ({character.Info.Job.Prefab.Identifier})");
behavior = BehaviorType.Passive;
}
switch (behavior)
{
case BehaviorType.Passive:
@@ -93,7 +88,7 @@ namespace Barotrauma
CalculatePriority();
}
protected override bool Check() => false;
protected override bool CheckObjectiveSpecific() => false;
public override bool CanBeCompleted => true;
public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace.CleanupStackTrace()); }
@@ -110,21 +105,11 @@ namespace Barotrauma
Priority = 1;
}
public override float GetPriority() => Priority;
protected override float GetPriority() => Priority;
public override void Update(float deltaTime)
{
//if (objectiveManager.CurrentObjective == this)
//{
// if (randomTimer > 0)
// {
// randomTimer -= deltaTime;
// }
// else
// {
// CalculatePriority();
// }
//}
// Do nothing. Overrides the inherited devotion calculations.
}
private float timerMargin;
@@ -183,6 +168,11 @@ namespace Barotrauma
CleanupItems(deltaTime);
if (behavior == BehaviorType.StayInHull && TargetHull == null && character.CurrentHull != null)
{
TargetHull = character.CurrentHull;
}
if (behavior == BehaviorType.StayInHull)
{
currentTarget = TargetHull;
@@ -203,7 +193,7 @@ namespace Barotrauma
if (currentTarget != null && !currentTargetIsInvalid)
{
if (character.TeamID == CharacterTeamType.FriendlyNPC)
if (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted)
{
if (currentTarget.Submarine.TeamID != character.TeamID)
{
@@ -260,9 +250,9 @@ namespace Barotrauma
{
//choose a random available hull
currentTarget = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced);
bool isInWrongSub = character.TeamID == CharacterTeamType.FriendlyNPC && character.Submarine.TeamID != character.TeamID;
bool isInWrongSub = (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted) && character.Submarine.TeamID != character.TeamID;
bool isCurrentHullAllowed = !isInWrongSub && !IsForbidden(character.CurrentHull);
var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, errorMsgStr: $"AIObjectiveIdle {character.DisplayName}", nodeFilter: node =>
var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, errorMsgStr: null, nodeFilter: node =>
{
if (node.Waypoint.CurrentHull == null) { return false; }
// Check that there is no unsafe or forbidden hulls on the way to the target
@@ -419,7 +409,7 @@ namespace Barotrauma
if (HumanAIController.UnsafeHulls.Contains(hull)) { continue; }
if (hull.Submarine == null) { continue; }
if (character.Submarine == null) { break; }
if (character.TeamID == CharacterTeamType.FriendlyNPC)
if (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted)
{
if (hull.Submarine.TeamID != character.TeamID)
{
@@ -519,13 +509,7 @@ namespace Barotrauma
}
#endregion
public static bool IsForbidden(Hull hull)
{
if (hull == null) { return true; }
string hullName = hull.RoomName;
if (hullName == null) { return false; }
return hullName.Contains("ballast", StringComparison.OrdinalIgnoreCase) || hullName.Contains("airlock", StringComparison.OrdinalIgnoreCase);
}
public static bool IsForbidden(Hull hull) => hull == null || hull.AvoidStaying;
public override void Reset()
{
@@ -40,7 +40,7 @@ namespace Barotrauma
: base(character, objectiveManager, priorityModifier, option) { }
protected override void Act(float deltaTime) { }
protected override bool Check() => false;
protected override bool CheckObjectiveSpecific() => false;
public override bool CanBeCompleted => true;
public override bool AbandonWhenCannotCompleteSubjectives => false;
public override bool AllowSubObjectiveSorting => true;
@@ -106,7 +106,7 @@ namespace Barotrauma
UpdateTargets();
}
public override float GetPriority()
protected override float GetPriority()
{
if (!IsAllowed)
{
@@ -132,14 +132,14 @@ namespace Barotrauma
}
var order = new Order(orderPrefab, item ?? character.CurrentHull as Entity, orderPrefab.GetTargetItemComponent(item), orderGiver: character);
if (order == null) { continue; }
if (autonomousObjective.ignoreAtOutpost && Level.IsLoadedOutpost && character.TeamID != CharacterTeamType.FriendlyNPC)
if ((order.IgnoreAtOutpost || autonomousObjective.ignoreAtOutpost) && Level.IsLoadedOutpost && character.TeamID != CharacterTeamType.FriendlyNPC)
{
if (Submarine.MainSub != null && Submarine.MainSub.DockedTo.None(s => s.TeamID != CharacterTeamType.FriendlyNPC && s.TeamID != character.TeamID))
{
continue;
}
}
var objective = CreateObjective(order, autonomousObjective.option, character, isAutonomous: true, autonomousObjective.priorityModifier);
var objective = CreateObjective(order, autonomousObjective.option, character, autonomousObjective.priorityModifier);
if (objective != null && objective.CanBeCompleted)
{
AddObjective(objective, delay: Rand.Value() / 2);
@@ -184,7 +184,8 @@ namespace Barotrauma
{
var previousObjective = CurrentObjective;
var firstObjective = Objectives.FirstOrDefault();
if (CurrentOrder != null && firstObjective != null && CurrentOrder.Priority > firstObjective.Priority)
bool currentObjectiveIsOrder = CurrentOrder != null && firstObjective != null && CurrentOrder.Priority > firstObjective.Priority;
if (currentObjectiveIsOrder)
{
CurrentObjective = CurrentOrder;
}
@@ -197,6 +198,14 @@ namespace Barotrauma
previousObjective?.OnDeselected();
CurrentObjective?.OnSelected();
GetObjective<AIObjectiveIdle>().CalculatePriority(Math.Max(CurrentObjective.Priority - 10, 0));
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
GameMain.NetworkMember.CreateEntityEvent(character, new object[]
{
NetEntityEvent.Type.ObjectiveManagerState,
currentObjectiveIsOrder ? "order" : "objective"
});
}
}
return CurrentObjective;
}
@@ -269,38 +278,29 @@ namespace Barotrauma
public void SortObjectives()
{
ForcedOrder?.GetPriority();
ForcedOrder?.CalculatePriority();
AIObjective orderWithHighestPriority = null;
float highestPriority = 0;
foreach (var currentOrder in CurrentOrders)
{
var orderObjective = currentOrder.Objective;
if (orderObjective == null) { continue; }
orderObjective.GetPriority();
orderObjective.CalculatePriority();
if (orderWithHighestPriority == null || orderObjective.Priority > highestPriority)
{
orderWithHighestPriority = orderObjective;
highestPriority = orderObjective.Priority;
}
}
#if SERVER
if (orderWithHighestPriority != null && orderWithHighestPriority != currentOrder)
{
GameMain.NetworkMember.CreateEntityEvent(character, new object[] { NetEntityEvent.Type.ObjectiveManagerOrderState });
}
#endif
CurrentOrder = orderWithHighestPriority;
for (int i = Objectives.Count - 1; i >= 0; i--)
{
Objectives[i].GetPriority();
Objectives[i].CalculatePriority();
}
if (Objectives.Any())
{
Objectives.Sort((x, y) => y.Priority.CompareTo(x.Priority));
}
GetCurrentObjective()?.SortSubObjectives();
}
@@ -380,7 +380,7 @@ namespace Barotrauma
}
}
var newCurrentOrder = CreateObjective(order, option, orderGiver, isAutonomous: false);
var newCurrentOrder = CreateObjective(order, option, orderGiver);
if (newCurrentOrder != null)
{
CurrentOrders.Add(new OrderInfo(order, option, priority, newCurrentOrder));
@@ -441,7 +441,7 @@ namespace Barotrauma
}
}
public AIObjective CreateObjective(Order order, string option, Character orderGiver, bool isAutonomous, float priorityModifier = 1)
public AIObjective CreateObjective(Order order, string option, Character orderGiver, float priorityModifier = 1)
{
if (order == null || order.Identifier == "dismissed") { return null; }
AIObjective newObjective;
@@ -482,7 +482,6 @@ namespace Barotrauma
newObjective = new AIObjectiveRepairItems(character, this, priorityModifier: priorityModifier, prioritizedItem: order.TargetEntity as Item)
{
RelevantSkill = order.AppropriateSkill,
RequireAdequateSkills = isAutonomous
};
break;
case "pumpwater":
@@ -492,7 +491,7 @@ namespace Barotrauma
newObjective = new AIObjectiveOperateItem(targetPump, character, this, option, false, priorityModifier: priorityModifier)
{
IsLoop = true,
Override = orderGiver != null && orderGiver.IsPlayer
Override = orderGiver != null && orderGiver.IsCommanding
};
// ItemComponent.AIOperate() returns false by default -> We'd have to set IsLoop = false and implement a custom override of AIOperate for the Pump.cs,
// if we want that the bot just switches the pump on/off and continues doing something else.
@@ -519,7 +518,7 @@ namespace Barotrauma
{
IsLoop = true,
// Don't override unless it's an order by a player
Override = orderGiver != null && orderGiver.IsPlayer
Override = orderGiver != null && orderGiver.IsCommanding
};
break;
case "setchargepct":
@@ -563,6 +562,9 @@ namespace Barotrauma
newObjective = new AIObjectiveCleanupItems(character, this, priorityModifier: priorityModifier);
}
break;
case "escapehandcuffs":
newObjective = new AIObjectiveEscapeHandcuffs(character, this, priorityModifier: priorityModifier);
break;
default:
if (order.TargetItemComponent == null) { return null; }
if (!order.TargetItemComponent.Item.IsInteractable(character)) { return null; }
@@ -571,16 +573,22 @@ namespace Barotrauma
{
IsLoop = true,
// Don't override unless it's an order by a player
Override = orderGiver != null && orderGiver.IsPlayer
Override = orderGiver != null && orderGiver.IsCommanding
};
if (newObjective.Abandon) { return null; }
break;
}
if (newObjective != null)
{
newObjective.Identifier = order.Identifier;
}
newObjective.IgnoreAtOutpost = order.IgnoreAtOutpost;
return newObjective;
}
private bool IsAllowedToWait()
{
if (!character.IsOnPlayerTeam) { return false; }
if (HasOrders()) { return false; }
if (CurrentObjective is AIObjectiveCombat || CurrentObjective is AIObjectiveFindSafety) { return false; }
if (character.AnimController.InWater) { return false; }
@@ -627,7 +635,10 @@ namespace Barotrauma
public float GetOrderPriority(AIObjective objective)
{
if (objective == ForcedOrder) { return HighestOrderPriority; }
if (objective == ForcedOrder)
{
return HighestOrderPriority;
}
var currentOrder = CurrentOrders.FirstOrDefault(o => o.Objective == objective);
if (currentOrder.Objective == null)
{
@@ -635,7 +646,15 @@ namespace Barotrauma
}
else if (currentOrder.ManualPriority > 0)
{
return MathHelper.Lerp(LowestOrderPriority, HighestOrderPriority, MathUtils.InverseLerp(1, CharacterInfo.HighestManualOrderPriority, currentOrder.ManualPriority));
if (objective.ForceHighestPriority)
{
return HighestOrderPriority;
}
if (objective.PrioritizeIfSubObjectivesActive && objective.SubObjectives.Any())
{
return HighestOrderPriority;
}
return MathHelper.Lerp(LowestOrderPriority, HighestOrderPriority - 1, MathUtils.InverseLerp(1, CharacterInfo.HighestManualOrderPriority, currentOrder.ManualPriority));
}
#if DEBUG
DebugConsole.AddWarning("Error in order priority: shouldn't return 0!");
@@ -8,15 +8,18 @@ namespace Barotrauma
{
class AIObjectiveOperateItem : AIObjective
{
public override string DebugTag => $"operate item {component.Name}";
public override string Identifier { get; set; } = "operate item";
public override string DebugTag => $"{Identifier} {component.Name}";
public override bool AllowAutomaticItemUnequipping => true;
public override bool AllowMultipleInstances => true;
public override bool AllowInAnySub => true;
public override bool PrioritizeIfSubObjectivesActive => component != null && (component is Reactor || component is Turret);
private ItemComponent component, controller;
private Entity operateTarget;
private bool requireEquip;
private bool useController;
private readonly ItemComponent component, controller;
private readonly Entity operateTarget;
private readonly bool requireEquip;
private readonly bool useController;
private AIObjectiveGoTo goToObjective;
private AIObjectiveGetItem getItemObjective;
@@ -34,7 +37,7 @@ namespace Barotrauma
public Func<bool> completionCondition;
private bool isDoneOperating;
public override float GetPriority()
protected override float GetPriority()
{
bool isOrder = objectiveManager.IsOrder(this);
if (!IsAllowed || character.LockHands)
@@ -100,6 +103,16 @@ namespace Barotrauma
break;
}
}
else if (!isOrder)
{
var steering = component?.Item.GetComponent<Steering>();
if (steering != null && (steering.AutoPilot || HumanAIController.IsTrueForAnyCrewMember(c => c != HumanAIController && c.Character.IsCaptain)))
{
// Ignore if already set to autopilot or if there's a captain onboard
Priority = 0;
return Priority;
}
}
if (targetItem.CurrentHull == null ||
targetItem.Submarine != character.Submarine && !isOrder ||
targetItem.CurrentHull.FireSources.Any() ||
@@ -121,10 +134,10 @@ namespace Barotrauma
{
float value = CumulatedDevotion + (AIObjectiveManager.LowestOrderPriority * PriorityModifier);
float max = AIObjectiveManager.LowestOrderPriority - 1;
if (reactor != null && reactor.PowerOn && reactor.FissionRate > 1 && Option == "powerup")
if (reactor != null && reactor.PowerOn && reactor.FissionRate > 1 && reactor.AutoTemp && Option == "powerup")
{
// Decrease the priority when targeting a reactor that is already on.
value /= 2;
// Already on, no need to operate.
value = 0;
}
Priority = MathHelper.Clamp(value, 0, max);
}
@@ -268,7 +281,7 @@ namespace Barotrauma
}
}
protected override bool Check() => isDoneOperating && !IsLoop;
protected override bool CheckObjectiveSpecific() => isDoneOperating && !IsLoop;
public override void Reset()
{
@@ -9,7 +9,7 @@ namespace Barotrauma
{
class AIObjectivePumpWater : AIObjectiveLoop<Pump>
{
public override string DebugTag => "pump water";
public override string Identifier { get; set; } = "pump water";
public override bool KeepDivingGearOn => true;
public override bool AllowAutomaticItemUnequipping => true;
@@ -8,7 +8,7 @@ namespace Barotrauma
{
class AIObjectiveRepairItem : AIObjective
{
public override string DebugTag => "repair item";
public override string Identifier { get; set; } = "repair item";
public override bool AllowInAnySub => true;
@@ -31,7 +31,7 @@ namespace Barotrauma
this.isPriority = isPriority;
}
public override float GetPriority()
protected override float GetPriority()
{
if (!IsAllowed || Item.IgnoreByAI)
{
@@ -71,7 +71,7 @@ namespace Barotrauma
return Priority;
}
protected override bool Check()
protected override bool CheckObjectiveSpecific()
{
IsCompleted = Item.IsFullCondition;
if (character.IsOnPlayerTeam && IsCompleted && IsRepairing())
@@ -122,8 +122,6 @@ namespace Barotrauma
Abandon = true;
return;
}
HumanAIController.UnequipContainedItems(repairTool.Item, it => !it.HasTag("weldingfuel"));
HumanAIController.UnequipEmptyItems(repairTool.Item);
RelatedItem item = null;
Item fuel = null;
foreach (RelatedItem requiredItem in repairTool.requiredItems[RelatedItem.RelationType.Contained])
@@ -135,9 +133,12 @@ namespace Barotrauma
if (fuel == null)
{
RemoveSubObjective(ref goToObjective);
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, item.Identifiers, repairTool.Item.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC),
onCompleted: () => RemoveSubObjective(ref refuelObjective),
onAbandon: () => Abandon = true);
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, item.Identifiers, repairTool.Item.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC)
{
RemoveExisting = true
},
onCompleted: () => RemoveSubObjective(ref refuelObjective),
onAbandon: () => Abandon = true);
return;
}
}
@@ -9,12 +9,7 @@ namespace Barotrauma
{
class AIObjectiveRepairItems : AIObjectiveLoop<Item>
{
public override string DebugTag => "repair items";
/// <summary>
/// Should the character only attempt to fix items they have the skills to fix, or any damaged item
/// </summary>
public bool RequireAdequateSkills;
public override string Identifier { get; set; } = "repair items";
/// <summary>
/// If set, only fix items where required skill matches this.
@@ -28,7 +23,7 @@ namespace Barotrauma
public readonly static float RequiredSuccessFactor = 0.4f;
public override bool IsDuplicate<T>(T otherObjective) => otherObjective is AIObjectiveRepairItems repairObjective && repairObjective.RequireAdequateSkills == RequireAdequateSkills;
public override bool IsDuplicate<T>(T otherObjective) => otherObjective is AIObjectiveRepairItems repairObjective && objectiveManager.IsOrder(repairObjective) == objectiveManager.IsOrder(this);
public AIObjectiveRepairItems(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1, Item prioritizedItem = null)
: base(character, objectiveManager, priorityModifier)
@@ -69,16 +64,12 @@ namespace Barotrauma
protected override bool Filter(Item item)
{
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) && HumanAIController.IsActive(c))) { return false; }
if (!ViableForRepair(item, character, HumanAIController)) { return false; };
if (!Objectives.ContainsKey(item))
{
if (item != character.SelectedConstruction)
{
float condition = item.ConditionPercentage;
if (item.Repairables.All(r => condition >= r.RepairThreshold)) { return false; }
if (NearlyFullCondition(item)) { return false; }
}
}
if (!string.IsNullOrWhiteSpace(RelevantSkill))
@@ -88,6 +79,21 @@ namespace Barotrauma
return true;
}
public static bool ViableForRepair(Item item, Character character, HumanAIController humanAIController)
{
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) && HumanAIController.IsActive(c))) { return false; }
return true;
}
public static bool NearlyFullCondition(Item item)
{
float condition = item.ConditionPercentage;
return item.Repairables.All(r => condition >= r.RepairThreshold);
}
protected override float TargetEvaluation()
{
var selectedItem = character.SelectedConstruction;
@@ -115,14 +121,7 @@ namespace Barotrauma
// Enough fixers
return 0;
}
if (RequireAdequateSkills)
{
return Targets.Sum(t => GetTargetPriority(t, character, RequiredSuccessFactor)) * ratio;
}
else
{
return Targets.Sum(t => 100 - t.ConditionPercentage) * ratio;
}
return Targets.Sum(t => GetTargetPriority(t, character, RequiredSuccessFactor)) * ratio;
}
}
@@ -9,7 +9,7 @@ namespace Barotrauma
{
class AIObjectiveRescue : AIObjective
{
public override string DebugTag => "rescue";
public override string Identifier { get; set; } = "rescue";
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
@@ -374,7 +374,7 @@ namespace Barotrauma
}
}
protected override bool Check()
protected override bool CheckObjectiveSpecific()
{
if (character.LockHands || targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead)
{
@@ -390,6 +390,7 @@ namespace Barotrauma
bool isCompleted =
AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) >= AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager, character, targetCharacter) ||
targetCharacter.CharacterHealth.GetAllAfflictions().All(a => a.Strength < a.Prefab.TreatmentThreshold);
if (isCompleted && targetCharacter != character && character.IsOnPlayerTeam)
{
character.Speak(TextManager.GetWithVariable("DialogTargetHealed", "[targetname]", targetCharacter.Name),
@@ -398,7 +399,7 @@ namespace Barotrauma
return isCompleted;
}
public override float GetPriority()
protected override float GetPriority()
{
if (!IsAllowed)
{
@@ -7,7 +7,7 @@ namespace Barotrauma
{
class AIObjectiveRescueAll : AIObjectiveLoop<Character>
{
public override string DebugTag => "rescue all";
public override string Identifier { get; set; } = "rescue all";
public override bool ForceRun => true;
public override bool InverseTargetEvaluation => true;
public override bool AllowOutsideSubmarine => true;
@@ -154,12 +154,15 @@ namespace Barotrauma
public readonly Dictionary<string, Sprite> OptionSprites;
private readonly Dictionary<string, Sprite> minimapIcons;
public Dictionary<string, Sprite> MinimapIcons => IsPrefab ? minimapIcons : Prefab.minimapIcons;
public readonly bool MustSetTarget;
/// <summary>
/// Can the order be turned into a non-entity-targeting one if it was originally created with a target entity.
/// Note: if MustSetTarget is true, CanBeGeneralized will always be false.
/// </summary>
public readonly bool CanBeGeneralized;
public readonly string AppropriateSkill;
public readonly bool Hidden;
public readonly bool IgnoreAtOutpost;
public bool HasOptions => (IsPrefab ? Options : Prefab.Options).Length > 1;
public bool IsPrefab { get; private set; }
@@ -310,8 +313,10 @@ namespace Barotrauma
var category = orderElement.GetAttributeString("category", null);
if (!string.IsNullOrWhiteSpace(category)) { this.Category = (OrderCategory)Enum.Parse(typeof(OrderCategory), category, true); }
MustSetTarget = orderElement.GetAttributeBool("mustsettarget", false);
CanBeGeneralized = !MustSetTarget && orderElement.GetAttributeBool("canbegeneralized", true);
AppropriateSkill = orderElement.GetAttributeString("appropriateskill", null);
Hidden = orderElement.GetAttributeBool("hidden", false);
IgnoreAtOutpost = orderElement.GetAttributeBool("ignoreatoutpost", false);
var optionNames = TextManager.Get("OrderOptions." + Identifier, true)?.Split(',', '') ??
orderElement.GetAttributeStringArray("optionnames", new string[0]);
@@ -348,15 +353,6 @@ namespace Barotrauma
}
}
minimapIcons = new Dictionary<string, Sprite>();
var minimapIconElements = orderElement.GetChildElements("minimapicon");
foreach (XElement minimapIconElement in minimapIconElements)
{
var id = minimapIconElement.GetAttributeString("id", null);
if (string.IsNullOrWhiteSpace(id)) { continue; }
minimapIcons.Add(id, new Sprite(minimapIconElement.GetChildElement("sprite"), lazyLoad: true));
}
IsPrefab = true;
MustManuallyAssign = orderElement.GetAttributeBool("mustmanuallyassign", false);
IsIgnoreOrder = Identifier == "ignorethis" || Identifier == "unignorethis";
@@ -366,7 +362,7 @@ namespace Barotrauma
/// <summary>
/// Constructor for order instances
/// </summary>
public Order(Order prefab, Entity targetEntity, ItemComponent targetItem, Character orderGiver = null, bool isAutonomous = false)
public Order(Order prefab, Entity targetEntity, ItemComponent targetItem, Character orderGiver = null)
{
Prefab = prefab.Prefab ?? prefab;
@@ -384,12 +380,14 @@ namespace Barotrauma
AppropriateJobs = prefab.AppropriateJobs;
FadeOutTime = prefab.FadeOutTime;
MustSetTarget = prefab.MustSetTarget;
CanBeGeneralized = prefab.CanBeGeneralized;
AppropriateSkill = prefab.AppropriateSkill;
Category = prefab.Category;
MustManuallyAssign = prefab.MustManuallyAssign;
IsIgnoreOrder = prefab.IsIgnoreOrder;
DrawIconWhenContained = prefab.DrawIconWhenContained;
Hidden = prefab.Hidden;
IgnoreAtOutpost = prefab.IgnoreAtOutpost;
OrderGiver = orderGiver;
TargetEntity = targetEntity;
@@ -413,12 +411,18 @@ namespace Barotrauma
IsPrefab = false;
}
/// <summary>
/// Constructor for order instances
/// </summary>
public Order(Order prefab, OrderTarget target, Character orderGiver = null) : this(prefab, targetEntity: null, targetItem: null, orderGiver)
{
TargetPosition = target;
TargetType = OrderTargetType.Position;
}
/// <summary>
/// Constructor for order instances
/// </summary>
public Order(Order prefab, Structure wall, int? sectionIndex, Character orderGiver = null) : this(prefab, targetEntity: wall, null, orderGiver: orderGiver)
{
WallSectionIndex = sectionIndex;
@@ -410,7 +410,10 @@ namespace Barotrauma
if (end.state == 0 || end.Parent == null)
{
#if DEBUG
DebugConsole.NewMessage("Path not found. " + errorMsgStr, Color.Yellow);
if (errorMsgStr != null)
{
DebugConsole.NewMessage("Path not found. " + errorMsgStr, Color.Yellow);
}
#endif
return new SteeringPath(true);
}
@@ -370,7 +370,7 @@ namespace Barotrauma
if (c.Inventory != null)
{
var inventoryElement = new XElement("inventory");
c.SaveInventory(c.Inventory, inventoryElement);
Character.SaveInventory(c.Inventory, inventoryElement);
petElement.Add(inventoryElement);
}
@@ -0,0 +1,132 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
abstract class ShipIssueWorker
{
public const float MaxImportance = 100f;
public const float MinImportance = 0f;
public Order SuggestedOrderPrefab { get; }
private float importance;
public float Importance
{
get
{
return importance;
}
set
{
importance = MathHelper.Clamp(value, MinImportance, MaxImportance);
}
}
public float CurrentRedundancy { get; set; }
public readonly ShipCommandManager shipCommandManager;
public string Option { get; set; }
public Character OrderedCharacter { get; set; }
public Order CurrentOrder { get; private set; }
public ItemComponent TargetItemComponent { get; protected set; }
public Item TargetItem { get; protected set; }
public bool Active { get; protected set; } = true; // used to turn off the instance if errors are detected
protected virtual Character CommandingCharacter => shipCommandManager.character;
public virtual float TimeSinceLastAttempt { get; set; }
public virtual float RedundantIssueModifier => 0.5f;
public virtual bool StopDuringEmergency => true; // limit certain issue assessments when invaded by the enemies
public virtual bool AllowEasySwitching => false;
public ShipIssueWorker(ShipCommandManager shipCommandManager, Order suggestedOrderPrefab, string option = null)
{
this.shipCommandManager = shipCommandManager;
SuggestedOrderPrefab = suggestedOrderPrefab;
Option = option;
}
public void SetOrder(Character orderedCharacter)
{
OrderedCharacter = orderedCharacter;
if (orderedCharacter != CommandingCharacter)
{
CommandingCharacter.Speak(SuggestedOrderPrefab.GetChatMessage(OrderedCharacter.Name, "", false));
}
// not sure if new orders are supposed to be created each time. TODO m61: check later
CurrentOrder = new Order(SuggestedOrderPrefab, TargetItem, TargetItemComponent, CommandingCharacter);
OrderedCharacter.SetOrder(CurrentOrder, Option, priority: 3, CommandingCharacter, CommandingCharacter != OrderedCharacter);
TimeSinceLastAttempt = 0f;
}
public void RemoveOrder()
{
OrderedCharacter = null;
CurrentOrder = null;
}
protected virtual bool IsIssueViable()
{
return true;
}
public float CalculateImportance(bool isEmergency)
{
Importance = 0f; // reset anything that needs resetting
if (!Active)
{
return Importance;
}
Active = IsIssueViable();
if (isEmergency && StopDuringEmergency)
{
return Importance;
}
CalculateImportanceSpecific();
// if there are other orders of the same type already being attended to, such as fixing leaks
// reduce the relative importance of this issue
CurrentRedundancy = 1f;
foreach (ShipIssueWorker shipIssueWorker in shipCommandManager.ShipIssueWorkers)
{
if (shipIssueWorker.GetType() == GetType() && shipIssueWorker != this && shipIssueWorker.OrderAttendedTo())
{
CurrentRedundancy *= RedundantIssueModifier;
}
}
Importance *= CurrentRedundancy;
return Importance;
}
public bool OrderAttendedTo(float timeSinceLastCheck = 0f)
{
if (!HumanAIController.IsActive(OrderedCharacter))
{
return false;
}
// accept only the highest priority order
if (CurrentOrder != null && OrderedCharacter.GetCurrentOrderWithTopPriority()?.Order != CurrentOrder)
{
#if DEBUG
DebugConsole.NewMessage($"Order {CurrentOrder.Name} did not match current order for character {OrderedCharacter} in {this}");
#endif
return false;
}
if (!shipCommandManager.AbleToTakeOrder(OrderedCharacter))
{
#if DEBUG
DebugConsole.NewMessage(OrderedCharacter + " was unable to perform assigned order in " + this);
#endif
return false;
}
return true;
}
public abstract void CalculateImportanceSpecific();
}
}
@@ -0,0 +1,38 @@
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
class ShipGlobalIssueFixLeaks : ShipGlobalIssue
{
readonly List<float> hullSeverities = new List<float>();
public ShipGlobalIssueFixLeaks(ShipCommandManager shipCommandManager) : base(shipCommandManager) { }
public override void CalculateGlobalIssue()
{
hullSeverities.Clear();
foreach (Gap gap in Gap.GapList)
{
if (AIObjectiveFixLeaks.IsValidTarget(gap, shipCommandManager.character))
{
hullSeverities.Add(AIObjectiveFixLeaks.GetLeakSeverity(gap));
}
}
float averagePercentage = 0f;
if (hullSeverities.Any())
{
hullSeverities.Sort();
averagePercentage = hullSeverities.TakeLast(3).Average(); // get the 3 most damaged items on the ship and get their average
}
GlobalImportance = averagePercentage;
}
}
class ShipIssueWorkerFixLeaks : ShipIssueWorkerGlobal
{
public override bool StopDuringEmergency => false;
public ShipIssueWorkerFixLeaks(ShipCommandManager shipCommandManager, Order order, ShipGlobalIssueFixLeaks shipGlobalIssueFixLeaks) : base(shipCommandManager, order, shipGlobalIssueFixLeaks) { }
}
}
@@ -0,0 +1,29 @@
namespace Barotrauma
{
abstract class ShipGlobalIssue
{
public float GlobalImportance { get; set; }
protected ShipCommandManager shipCommandManager;
public ShipGlobalIssue(ShipCommandManager shipCommandManager)
{
this.shipCommandManager = shipCommandManager;
}
public abstract void CalculateGlobalIssue();
}
abstract class ShipIssueWorkerGlobal : ShipIssueWorker
{
private readonly ShipGlobalIssue shipGlobalIssue;
public ShipIssueWorkerGlobal(ShipCommandManager shipCommandManager, Order suggestedOrderPrefab, ShipGlobalIssue shipGlobalIssue) : base (shipCommandManager, suggestedOrderPrefab)
{
this.shipGlobalIssue = shipGlobalIssue;
}
public override void CalculateImportanceSpecific() // importances for global issues are precalculated, so that they don't need to be calculated per each attending character
{
Importance = shipGlobalIssue.GlobalImportance;
}
}
}
@@ -0,0 +1,32 @@
using Barotrauma.Items.Components;
namespace Barotrauma
{
abstract class ShipIssueWorkerItem : ShipIssueWorker
{
public ShipIssueWorkerItem(ShipCommandManager shipCommandManager, Order order, Item targetItem, ItemComponent targetItemComponent, string option = null) : base(shipCommandManager, order, option)
{
TargetItemComponent = targetItemComponent;
TargetItem = targetItem;
}
protected override bool IsIssueViable()
{
if (TargetItemComponent == null)
{
DebugConsole.ThrowError("TargetItemComponent was null in " + this);
return false;
}
if (TargetItem == null)
{
DebugConsole.ThrowError("TargetItem was null in " + this);
return false;
}
if (TargetItem.IgnoreByAI) { return false; }
return true;
}
}
}
@@ -0,0 +1,41 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
class ShipIssueWorkerOperateWeapons : ShipIssueWorkerItem
{
public override float RedundantIssueModifier => 0.65f;
private readonly List<float> targetingImportances = new List<float>();
public override bool AllowEasySwitching => true;
public ShipIssueWorkerOperateWeapons(ShipCommandManager shipCommandManager, Order order, Item targetItem, ItemComponent targetItemComponent) : base(shipCommandManager, order, targetItem, targetItemComponent) { }
float GetTargetingImportance(Entity entity)
{
float currentDistanceToEnemy = Vector2.Distance(entity.WorldPosition, TargetItem.WorldPosition);
return MathHelper.Clamp(100 - (currentDistanceToEnemy / 100f), MinImportance, MaxImportance);
}
public override void CalculateImportanceSpecific()
{
if (TargetItemComponent is Turret turret && !turret.HasPowerToShoot()) { return; }
targetingImportances.Clear();
foreach (Character character in shipCommandManager.EnemyCharacters)
{
targetingImportances.Add(GetTargetingImportance(character));
}
// there should maybe be additional logic for targeting and destroying spires, because they currently cause some issues with pathing
if (targetingImportances.Any())
{
targetingImportances.Sort();
Importance = targetingImportances.TakeLast(3).Average();
}
}
}
}
@@ -0,0 +1,21 @@
using Barotrauma.Items.Components;
namespace Barotrauma
{
class ShipIssueWorkerPowerUpReactor : ShipIssueWorkerItem
{
public ShipIssueWorkerPowerUpReactor(ShipCommandManager shipCommandManager, Order order, Item targetItem, ItemComponent targetItemComponent, string option) : base(shipCommandManager, order, targetItem, targetItemComponent, option)
{
}
public override void CalculateImportanceSpecific()
{
if (TargetItem.Condition <= 0f) { return; }
if (TargetItemComponent is Reactor reactor && -reactor.CurrPowerConsumption < float.Epsilon)
{
Importance = 40f;
}
}
}
}
@@ -0,0 +1,41 @@
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
class ShipGlobalIssueRepairSystems : ShipGlobalIssue
{
readonly List<Item> itemsNeedingRepair = new List<Item>();
public ShipGlobalIssueRepairSystems(ShipCommandManager shipCommandManager) : base(shipCommandManager) { }
public override void CalculateGlobalIssue()
{
itemsNeedingRepair.Clear();
foreach (Item item in shipCommandManager.CommandedSubmarine.GetItems(true))
{
if (!AIObjectiveRepairItems.ViableForRepair(item, shipCommandManager.character, shipCommandManager.character.AIController as HumanAIController)) { continue; }
if (AIObjectiveRepairItems.NearlyFullCondition(item)) { continue; }
itemsNeedingRepair.Add(item);
// merged this logic with AIObjectiveRepairItems
}
if (itemsNeedingRepair.Any())
{
itemsNeedingRepair.Sort((x, y) => y.ConditionPercentage.CompareTo(x.ConditionPercentage));
float modifiedPercentage = itemsNeedingRepair.TakeLast(3).Average(x => x.ConditionPercentage) * 0.6f + itemsNeedingRepair.TakeLast(10).Average(x => x.ConditionPercentage) * 0.4f;
// calculate a modified percentage with the most damaged items, with 60% the weight given to the top 3 damaged and the remaining given to top 10
GlobalImportance = 100 - modifiedPercentage;
}
// this system works reasonably well, though it could give extra importance to repairing critical items like reactors and junction boxes
}
}
class ShipIssueWorkerRepairSystems : ShipIssueWorkerGlobal // this class could be removed, but it might need special behavior later
{
public ShipIssueWorkerRepairSystems(ShipCommandManager shipCommandManager, Order order, ShipGlobalIssueRepairSystems shipGlobalIssueRepairSystems) : base(shipCommandManager, order, shipGlobalIssueRepairSystems)
{
}
}
}
@@ -0,0 +1,20 @@
using Barotrauma.Items.Components;
namespace Barotrauma
{
class ShipIssueWorkerSteer : ShipIssueWorkerItem
{
// The AI could be set to steer automatically through a specialized job or autonomous objectives
// but the logic involved doesn't really allow that without some annoyingly specific changes
// hence the AI will command itself to steer if steering is not being taken care of or the target location is wrong
public ShipIssueWorkerSteer(ShipCommandManager shipCommandManager, Order order, Item targetItem, ItemComponent targetItemComponent, string option) : base(shipCommandManager, order, targetItem, targetItemComponent, option) { }
public override void CalculateImportanceSpecific()
{
if (shipCommandManager.NavigationState == ShipCommandManager.NavigationStates.Inactive) { return; }
if (TargetItemComponent is Powered powered && powered.Voltage <= powered.MinVoltage) { return; }
if (TargetItem.Condition <= 0f) { return; }
Importance = 70f;
}
}
}
@@ -0,0 +1,383 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
class ShipCommandManager
{
public readonly Character character;
public readonly HumanAIController humanAIController;
private bool active;
public bool Active
{
get { return active; }
set
{
active = value ? TryInitializeShipCommandManager() : value;
}
}
public Submarine EnemySubmarine
{
get;
private set;
}
public Submarine CommandedSubmarine
{
get;
private set;
}
private Steering steering;
public readonly List<Vector2> patrolPositions = new List<Vector2>();
public enum NavigationStates
{
Inactive,
Patrol,
Aggressive
}
public NavigationStates NavigationState { get; private set; } = NavigationStates.Inactive;
float navigationTimer = 0f;
private readonly float navigationInterval = 4f;
float timeUntilRam;
private const float RamTimerMax = 17.5f;
public readonly List<ShipIssueWorker> ShipIssueWorkers = new List<ShipIssueWorker>();
private const float MinimumIssueThreshold = 10f;
private const float IssueDevotionBuffer = 5f;
private float decisionTimer = 6f;
private readonly float decisionInterval = 6f;
private float timeSinceLastCommandDecision;
private float timeSinceLastNavigation;
public readonly List<Character> AlliedCharacters = new List<Character>();
public readonly List<Character> EnemyCharacters = new List<Character>();
private readonly List<ShipIssueWorker> attendedIssues = new List<ShipIssueWorker>();
private readonly List<ShipIssueWorker> availableIssues = new List<ShipIssueWorker>();
private readonly List<ShipGlobalIssue> shipGlobalIssues = new List<ShipGlobalIssue>();
public ShipCommandManager(Character character)
{
this.character = character;
humanAIController = character.AIController as HumanAIController;
}
public void Update(float deltaTime)
{
if (!Active) { return; }
decisionTimer -= deltaTime;
if (decisionTimer <= 0.0f)
{
UpdateCommandDecision(timeSinceLastCommandDecision);
decisionTimer = decisionInterval * Rand.Range(0.8f, 1.2f);
timeSinceLastCommandDecision = decisionTimer;
}
navigationTimer -= deltaTime;
if (navigationTimer <= 0.0f)
{
UpdateNavigation(timeSinceLastNavigation);
navigationTimer = navigationInterval * Rand.Range(0.8f, 1.2f);
timeSinceLastNavigation = navigationTimer;
}
}
static void ShipCommandLog(string text)
{
if (GameSettings.VerboseLogging)
{
DebugConsole.NewMessage(text);
}
}
static bool WithinRange(float range, float distanceSquared)
{
return range * range > distanceSquared;
}
void UpdateNavigation(float timeSinceLastUpdate)
{
if (steering == null || EnemySubmarine == null)
{
return;
}
float distanceSquaredEnemy = Vector2.DistanceSquared(CommandedSubmarine.WorldPosition, EnemySubmarine.WorldPosition);
if (NavigationState != NavigationStates.Aggressive)
{
if (WithinRange(7000f, distanceSquaredEnemy))
{
#if DEBUG
ShipCommandLog("Ship " + CommandedSubmarine + " was within the aggro range of " + EnemySubmarine);
#endif
NavigationState = NavigationStates.Aggressive;
}
else if (WithinRange(40000f, distanceSquaredEnemy))
{
NavigationState = NavigationStates.Patrol;
}
}
if (NavigationState == NavigationStates.Aggressive)
{
steering.AITacticalTarget = EnemySubmarine.WorldPosition;
if (WithinRange(8500f, distanceSquaredEnemy) && !WithinRange(1500f, distanceSquaredEnemy)) // if we are within enemy ship's range for ramTimerMax, try to ram them instead (if we're not already very close)
{
if (steering.AIRamTimer > 0f)
{
#if DEBUG
ShipCommandLog("Ship " + CommandedSubmarine + " was still ramming, " + steering.AIRamTimer + " left");
#endif
}
else
{
timeUntilRam -= timeSinceLastUpdate;
#if DEBUG
ShipCommandLog("Ship " + CommandedSubmarine + " was close enough to ram, " + timeUntilRam + " left until ramming");
#endif
if (timeUntilRam <= 0f)
{
#if DEBUG
ShipCommandLog("Ship " + CommandedSubmarine + " is attempting to ram!");
#endif
steering.AIRamTimer = 50f;
timeUntilRam = RamTimerMax * Rand.Range(0.9f, 1.1f);
}
}
}
else
{
steering.AIRamTimer = 0f;
timeUntilRam = RamTimerMax * Rand.Range(0.9f, 1.1f);
}
}
else if (patrolPositions.Any())
{
float distanceSquaredPatrol = Vector2.DistanceSquared(CommandedSubmarine.WorldPosition, patrolPositions.First());
if (WithinRange(7000f, distanceSquaredPatrol))
{
Vector2 lastPosition = patrolPositions.First();
patrolPositions.RemoveAt(0);
patrolPositions.Add(lastPosition);
}
steering.AITacticalTarget = patrolPositions.First();
}
}
public bool AbleToTakeOrder(Character character)
{
return !character.IsIncapacitated && !character.LockHands && character.Submarine == CommandedSubmarine;
}
void UpdateCommandDecision(float timeSinceLastUpdate)
{
#if DEBUG
ShipCommandLog("Updating command for character " + character);
#endif
shipGlobalIssues.ForEach(c => c.CalculateGlobalIssue());
AlliedCharacters.Clear();
EnemyCharacters.Clear();
bool isEmergency = false;
foreach (Character potentialCharacter in Character.CharacterList)
{
if (!HumanAIController.IsActive(character)) { continue; }
if (HumanAIController.IsFriendly(character, potentialCharacter, true) && potentialCharacter.AIController is HumanAIController)
{
if (AbleToTakeOrder(potentialCharacter))
{
AlliedCharacters.Add(potentialCharacter);
}
}
else
{
EnemyCharacters.Add(potentialCharacter);
if (potentialCharacter.Submarine == CommandedSubmarine) // if enemies are on board, don't issue normal orders anymore
{
isEmergency = true;
}
}
}
attendedIssues.Clear();
availableIssues.Clear();
foreach (ShipIssueWorker shipIssueWorker in ShipIssueWorkers)
{
float importance = shipIssueWorker.CalculateImportance(isEmergency);
if (shipIssueWorker.OrderAttendedTo(timeSinceLastUpdate))
{
#if DEBUG
ShipCommandLog("Current importance for " + shipIssueWorker + " was " + importance + " and it was already being attended by " + shipIssueWorker.OrderedCharacter);
#endif
attendedIssues.Add(shipIssueWorker);
}
else
{
#if DEBUG
ShipCommandLog("Current importance for " + shipIssueWorker + " was " + importance + " and it is not attended to");
#endif
shipIssueWorker.RemoveOrder();
availableIssues.Add(shipIssueWorker);
}
}
availableIssues.Sort((x, y) => y.Importance.CompareTo(x.Importance));
attendedIssues.Sort((x, y) => x.Importance.CompareTo(y.Importance));
ShipIssueWorker mostImportantIssue = availableIssues.First();
float bestValue = 0f;
Character bestCharacter = null;
if (mostImportantIssue.Importance > MinimumIssueThreshold)
{
IEnumerable<Character> bestCharacters = CrewManager.GetCharactersSortedForOrder(mostImportantIssue.SuggestedOrderPrefab, AlliedCharacters, character, true);
foreach (Character orderedCharacter in bestCharacters)
{
float issueApplicability = mostImportantIssue.Importance;
// prefer not to switch if not qualified
issueApplicability *= mostImportantIssue.SuggestedOrderPrefab.AppropriateJobs.Contains(orderedCharacter.Info.Job.Prefab.Identifier) ? 1f : 0.75f;
ShipIssueWorker occupiedIssue = attendedIssues.FirstOrDefault(i => i.OrderedCharacter == orderedCharacter);
if (occupiedIssue != null)
{
if (occupiedIssue.GetType() == mostImportantIssue.GetType() && mostImportantIssue is ShipIssueWorkerGlobal && occupiedIssue is ShipIssueWorkerGlobal)
{
continue;
}
// reverse redundancy to ensure certain issues can be switched over easily (operating weapons)
if (mostImportantIssue.AllowEasySwitching && occupiedIssue.AllowEasySwitching)
{
issueApplicability /= mostImportantIssue.CurrentRedundancy;
}
// give slight preference if not qualified for current job
issueApplicability += occupiedIssue.SuggestedOrderPrefab.AppropriateJobs.Contains(orderedCharacter.Info.Job.Prefab.Identifier) ? 0 : 7.5f;
// prefer not to switch orders unless considerably more important
issueApplicability -= IssueDevotionBuffer;
if (issueApplicability + IssueDevotionBuffer < occupiedIssue.Importance)
{
continue;
}
}
// prefer first one in bestCharacters in tiebreakers
if (issueApplicability > bestValue)
{
bestValue = issueApplicability;
bestCharacter = orderedCharacter;
}
}
}
if (bestCharacter != null)
{
#if DEBUG
ShipCommandLog("Setting " + mostImportantIssue + " for character " + bestCharacter);
#endif
mostImportantIssue.SetOrder(bestCharacter);
}
else // if we didn't give an order, let's try to dismiss someone instead
{
foreach (ShipIssueWorker shipIssueWorker in ShipIssueWorkers)
{
if (shipIssueWorker.Importance <= 0f && shipIssueWorker.OrderAttendedTo())
{
#if DEBUG
ShipCommandLog("Dismissing " + shipIssueWorker + " for character " + shipIssueWorker.OrderedCharacter);
#endif
Order orderPrefab = Order.GetPrefab("dismissed");
character.Speak(orderPrefab.GetChatMessage(shipIssueWorker.OrderedCharacter.Name, "", givingOrderToSelf: false));
shipIssueWorker.OrderedCharacter.SetOrder(Order.GetPrefab("dismissed"), orderOption: null, priority: 3, character);
shipIssueWorker.RemoveOrder();
break;
}
}
}
}
bool TryInitializeShipCommandManager()
{
CommandedSubmarine = character.Submarine;
if (CommandedSubmarine == null)
{
DebugConsole.ThrowError("TryInitializeShipCommandManager failed: CommandedSubmarine was null for character " + character);
return false;
}
EnemySubmarine = Submarine.MainSubs[0] == CommandedSubmarine ? Submarine.MainSubs[1] : Submarine.MainSubs[0];
if (EnemySubmarine == null)
{
DebugConsole.ThrowError("TryInitializeShipCommandManager failed: EnemySubmarine was null for character " + character);
return false;
}
timeUntilRam = RamTimerMax * Rand.Range(0.9f, 1.1f);
ShipIssueWorkers.Clear();
// could have support for multiple reactors, todo m61
if (CommandedSubmarine.GetItems(false).Find(i => i.HasTag("reactor") && !i.NonInteractable)?.GetComponent<Reactor>() is Reactor reactor)
{
ShipIssueWorkers.Add(new ShipIssueWorkerPowerUpReactor(this, Order.GetPrefab("operatereactor"), reactor.Item, reactor, "powerup"));
}
if (CommandedSubmarine.GetItems(false).Find(i => i.HasTag("navterminal") && !i.NonInteractable) is Item nav && nav.GetComponent<Steering>() is Steering steeringComponent)
{
steering = steeringComponent;
ShipIssueWorkers.Add(new ShipIssueWorkerSteer(this, Order.GetPrefab("steer"), nav, steeringComponent, "navigatetactical"));
}
foreach (Item item in CommandedSubmarine.GetItems(true).FindAll(i => i.HasTag("turret")))
{
ShipIssueWorkers.Add(new ShipIssueWorkerOperateWeapons(this, Order.GetPrefab("operateweapons"), item, item.GetComponent<Turret>()));
}
int crewSizeModifier = 2;
// these issueworkers revolve around a singular, shared issue, which is injected into them to prevent redundant calculations
ShipGlobalIssueFixLeaks shipGlobalIssueFixLeaks = new ShipGlobalIssueFixLeaks(this);
for (int i = 0; i < crewSizeModifier; i++)
{
ShipIssueWorkers.Add(new ShipIssueWorkerFixLeaks(this, Order.GetPrefab("fixleaks"), shipGlobalIssueFixLeaks));
}
shipGlobalIssues.Add(shipGlobalIssueFixLeaks);
ShipGlobalIssueRepairSystems shipGlobalIssueRepairSystems = new ShipGlobalIssueRepairSystems(this);
for (int i = 0; i < crewSizeModifier; i++)
{
ShipIssueWorkers.Add(new ShipIssueWorkerRepairSystems(this, Order.GetPrefab("repairsystems"), shipGlobalIssueRepairSystems));
}
shipGlobalIssues.Add(shipGlobalIssueRepairSystems);
return true;
}
}
}
@@ -55,37 +55,59 @@ namespace Barotrauma
}
allItems = Wreck.GetItems(false);
thalamusItems = allItems.FindAll(i => IsThalamus(i.prefab));
var hulls = Wreck.GetHulls(false);
hulls.AddRange(Wreck.GetHulls(false));
var potentialBrainHulls = new Dictionary<Hull, float>();
brain = new Item(brainPrefab, Vector2.Zero, Wreck);
thalamusItems.Add(brain);
Vector2 negativeMargin = new Vector2(40, 20);
Vector2 minSize = brain.Rect.Size.ToVector2() - negativeMargin;
Vector2 maxSize = new Vector2(brain.Rect.Width * 3, brain.Rect.Height * 3);
// First try to get a room that is not too big and not in the edges of the sub.
// Also try not to create the brain in a room that already have carrier items inside.
// Ignore hulls that have any linked hulls to keep the calculations simple.
Point minSize = brain.Rect.Size.Multiply(brain.Scale);
// Bigger hulls are allowed, but not preferred more than what's sufficent.
Vector2 sufficentSize = new Vector2(minSize.X * 2, minSize.Y * 1.1f);
// Shrink the horizontal axis so that the brain is not placed in the left or right side, where we often have curved walls.
// Also ignore hulls that have open gaps, because we'll want the room to be full of water. The room will be filled with water when the brain is inserted in the room.
Rectangle shrinkedBounds = ToolBox.GetWorldBounds(Wreck.WorldPosition.ToPoint(), new Point(Wreck.Borders.Width - 500, Wreck.Borders.Height));
bool BaseCondition(Hull h) => h.RectWidth > minSize.X && h.RectHeight > minSize.Y && h.GetLinkedEntities<Hull>().None() && h.ConnectedGaps.None(g => g.Open > 0);
bool IsNotTooBig(Hull h) => h.RectWidth < maxSize.X && h.RectHeight < maxSize.Y;
bool IsNotInFringes(Hull h) => shrinkedBounds.ContainsWorld(h.WorldRect);
bool DoesNotContainOtherItems(Hull h) => thalamusItems.None(i => i.CurrentHull == h);
Hull brainHull = hulls.GetRandom(h => BaseCondition(h) && IsNotTooBig(h) && IsNotInFringes(h) && DoesNotContainOtherItems(h), Rand.RandSync.Server);
if (brainHull == null)
foreach (Hull hull in hulls)
{
brainHull = hulls.GetRandom(h => BaseCondition(h) && IsNotInFringes(h) && DoesNotContainOtherItems(h), Rand.RandSync.Server);
}
if (brainHull == null)
{
brainHull = hulls.GetRandom(h => BaseCondition(h) && (IsNotInFringes(h) || DoesNotContainOtherItems(h)), Rand.RandSync.Server);
}
if (brainHull == null)
{
brainHull = hulls.GetRandom(BaseCondition, Rand.RandSync.Server);
float distanceFromCenter = Vector2.Distance(Wreck.WorldPosition, hull.WorldPosition);
float distanceFactor = MathHelper.Lerp(1.0f, 0.5f, MathUtils.InverseLerp(0, Math.Max(shrinkedBounds.Width, shrinkedBounds.Height) / 2, distanceFromCenter));
float horizontalSizeFactor = MathHelper.Lerp(0.5f, 1.0f, MathUtils.InverseLerp(minSize.X, sufficentSize.X, hull.Rect.Width));
float verticalSizeFactor = MathHelper.Lerp(0.5f, 1.0f, MathUtils.InverseLerp(minSize.Y, sufficentSize.Y, hull.Rect.Height));
float weight = verticalSizeFactor * horizontalSizeFactor * distanceFactor;
if (hull.GetLinkedEntities<Hull>().Any())
{
// Ignore hulls that have any linked hulls to keep the calculations simple.
continue;
}
else if (hull.ConnectedGaps.Any(g => g.Open > 0 && (!g.IsRoomToRoom || g.Position.Y < hull.Position.Y)))
{
// Ignore hulls that have open gaps to outside or below the center point, because we'll want the room to be full of water and not be accessible without breaking the wall.
continue;
}
else if (thalamusItems.Any(i => i.CurrentHull == hull))
{
// Don't create the brain in a room that already has thalamus items inside it.
continue;
}
else if (hull.Rect.Width < minSize.X || hull.Rect.Height < minSize.Y)
{
// Don't select too small rooms.
continue;
}
if (weight > 0)
{
potentialBrainHulls.TryAdd(hull, weight);
}
}
Hull brainHull = ToolBox.SelectWeightedRandom(potentialBrainHulls.Keys.ToList(), potentialBrainHulls.Values.ToList(), Rand.RandSync.Server);
var thalamusStructurePrefabs = StructurePrefab.Prefabs.Where(p => IsThalamus(p));
if (brainHull == null) { return; }
if (brainHull == null)
{
DebugConsole.AddWarning("Wreck AI: Cannot find a proper room for the brain. Using a random room.");
brainHull = hulls.GetRandom(Rand.RandSync.Server);
}
if (brainHull == null)
{
DebugConsole.ThrowError("Wreck AI: Cannot find any room for the brain! Failed to create the Thalamus.");
return;
}
brainHull.WaterVolume = brainHull.Volume;
brain.SetTransform(brainHull.SimPosition, rotation: 0, findNewHull: false);
brain.CurrentHull = brainHull;
@@ -158,11 +180,12 @@ namespace Barotrauma
if (!spawnOrgans.Contains(item))
{
spawnOrgans.Add(item);
// Try to flood the hull so that the spawner won't die.
item.CurrentHull.WaterVolume = item.CurrentHull.Volume;
}
}
}
wayPoints.AddRange(Wreck.GetWaypoints(false));
hulls.AddRange(Wreck.GetHulls(false));
IsAlive = true;
thalamusStructures = GetThalamusEntities<Structure>(Wreck, Config.Entity).ToList();
}
@@ -307,9 +330,16 @@ namespace Barotrauma
public static void RemoveThalamusItems(Submarine wreck)
{
List<MapEntity> thalamusItems = new List<MapEntity>();
foreach (var wreckAiConfig in WreckAIConfig.List)
{
GetThalamusEntities(wreck, wreckAiConfig.Entity).ForEachMod(e => e.Remove());
thalamusItems.AddRange(GetThalamusEntities(wreck, wreckAiConfig.Entity));
}
thalamusItems = thalamusItems.Distinct().ToList();
foreach (MapEntity thalamusItem in thalamusItems)
{
thalamusItem.Remove();
wreck.PhysicsBody.FarseerBody.FixtureList.Where(f => f.UserData == thalamusItem).ForEachMod(f => wreck.PhysicsBody.FarseerBody.Remove(f));
}
}
@@ -323,15 +353,16 @@ namespace Barotrauma
private int MaxCellsPerRoom => CalculateCellCount(1, Config.MaxAgentsPerRoom);
private int MinCellsOutside => CalculateCellCount(0, Config.MinAgentsOutside);
private int MaxCellsOutside => CalculateCellCount(0, Config.MaxAgentsOutside);
private int MinCellsInside => CalculateCellCount(2, Config.MinAgentsInside);
private int MaxCellsInside => CalculateCellCount(3, Config.MaxAgentsInside);
private int MinCellsInside => CalculateCellCount(3, Config.MinAgentsInside);
private int MaxCellsInside => CalculateCellCount(5, Config.MaxAgentsInside);
private int MaxCellCount => CalculateCellCount(5, Config.MaxAgentCount);
private float MinWaterLevel => Config.MinWaterLevel;
private int CalculateCellCount(int minValue, int maxValue)
{
if (maxValue == 0) { return 0; }
return (int)Math.Round(MathHelper.Lerp(minValue, maxValue, Level.Loaded.Difficulty * 0.01f * Config.AgentSpawnCountDifficultyMultiplier));
float t = MathUtils.InverseLerp(0, 100, Level.Loaded.Difficulty * Config.AgentSpawnCountDifficultyMultiplier);
return (int)Math.Round(MathHelper.Lerp(minValue, maxValue, t));
}
private float GetSpawnTime()
@@ -51,7 +51,7 @@ namespace Barotrauma
public readonly Limb HitLimb;
public readonly List<DamageModifier> AppliedDamageModifiers;
public AttackResult(List<Affliction> afflictions, Limb hitLimb, List<DamageModifier> appliedDamageModifiers = null)
{
HitLimb = hitLimb;
@@ -137,6 +137,9 @@ namespace Barotrauma
set => _itemDamage = value;
}
[Serialize(0.0f, true, description: "Percentage of damage mitigation ignored when hitting armored body parts (deflecting limbs)."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1f)]
public float Penetration { get; private set; }
/// <summary>
/// Currently only used with variants. Used for multiplying all the damage.
/// </summary>
@@ -304,7 +307,7 @@ namespace Barotrauma
return totalDamage * DamageMultiplier;
}
public Attack(float damage, float bleedingDamage, float burnDamage, float structureDamage, float itemDamage, float range = 0.0f)
public Attack(float damage, float bleedingDamage, float burnDamage, float structureDamage, float itemDamage, float range = 0.0f, float penetration = 0f)
{
if (damage > 0.0f) Afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(damage), null);
if (bleedingDamage > 0.0f) Afflictions.Add(AfflictionPrefab.Bleeding.Instantiate(bleedingDamage), null);
@@ -314,6 +317,7 @@ namespace Barotrauma
DamageRange = range;
StructureDamage = LevelWallDamage = structureDamage;
ItemDamage = itemDamage;
Penetration = Penetration;
}
public Attack(XElement element, string parentDebugName)
@@ -478,8 +482,8 @@ namespace Barotrauma
if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
var targets = new List<ISerializableEntity>();
effect.GetNearbyTargets(worldPosition, targets);
targets.Clear();
targets.AddRange(effect.GetNearbyTargets(worldPosition, targets));
effect.Apply(effectType, deltaTime, targetEntity, targets);
}
if (effect.HasTargetType(StatusEffect.TargetType.UseTarget))
@@ -492,6 +496,7 @@ namespace Barotrauma
return attackResult;
}
readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
public AttackResult DoDamageToLimb(Character attacker, Limb targetLimb, Vector2 worldPosition, float deltaTime, bool playSound = true, PhysicsBody sourceBody = null)
{
if (targetLimb == null)
@@ -511,7 +516,7 @@ namespace Barotrauma
DamageParticles(deltaTime, worldPosition);
var attackResult = targetLimb.character.ApplyAttack(attacker, worldPosition, this, deltaTime, playSound, targetLimb);
var attackResult = targetLimb.character.ApplyAttack(attacker, worldPosition, this, deltaTime, playSound, targetLimb, penetration:Penetration);
var effectType = attackResult.Damage > 0.0f ? ActionType.OnUse : ActionType.OnFailure;
foreach (StatusEffect effect in statusEffects)
@@ -536,8 +541,8 @@ namespace Barotrauma
if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
var targets = new List<ISerializableEntity>();
effect.GetNearbyTargets(worldPosition, targets);
targets.Clear();
targets.AddRange(effect.GetNearbyTargets(worldPosition, targets));
effect.Apply(effectType, deltaTime, targetLimb.character, targets);
}
if (effect.HasTargetType(StatusEffect.TargetType.UseTarget))
@@ -94,7 +94,13 @@ namespace Barotrauma
public bool IsLocalPlayer => Controlled == this;
public bool IsPlayer => Controlled == this || IsRemotePlayer;
/// <summary>
/// Is the character player or does it have an active ship command manager (an AI controlled sub)? Bots in the player team are not treated as commanders.
/// </summary>
public bool IsCommanding => IsPlayer || (AIController is HumanAIController humanAI && humanAI.ShipCommandManager != null && humanAI.ShipCommandManager.Active);
public bool IsBot => !IsPlayer && AIController is HumanAIController humanAI && humanAI.Enabled;
public bool IsEscorted { get; set; }
public readonly Dictionary<string, SerializableProperty> Properties;
public Dictionary<string, SerializableProperty> SerializableProperties
@@ -120,6 +126,101 @@ namespace Barotrauma
}
}
protected readonly Dictionary<string, ActiveTeamChange> activeTeamChanges = new Dictionary<string, ActiveTeamChange>();
protected ActiveTeamChange currentTeamChange;
const string OriginalTeamIdentifier = "original";
public void SetOriginalTeam(CharacterTeamType newTeam)
{
TryRemoveTeamChange(OriginalTeamIdentifier);
currentTeamChange = new ActiveTeamChange(newTeam, ActiveTeamChange.TeamChangePriorities.Base);
TryAddNewTeamChange(OriginalTeamIdentifier, currentTeamChange);
}
protected void ChangeTeam(CharacterTeamType newTeam)
{
if (newTeam == teamID)
{
return;
}
teamID = newTeam;
if (info != null) { info.TeamID = newTeam; }
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
{
return;
}
// clear up any duties the character might have had from its old team (autonomous objectives are automatically recreated)
SetOrder(Order.GetPrefab("dismissed"), orderOption: null, priority: 3, orderGiver: this, speak: false);
#if SERVER
GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.TeamChange });
#endif
}
public bool HasTeamChange(string identifier)
{
return activeTeamChanges.ContainsKey(identifier);
}
public bool TryAddNewTeamChange(string identifier, ActiveTeamChange newTeamChange)
{
bool success = activeTeamChanges.TryAdd(identifier, newTeamChange);
if (success)
{
if (currentTeamChange == null)
{
// set team logic to use active team changes as soon as the first team change is added
SetOriginalTeam(TeamID);
}
}
else
{
#if DEBUG
DebugConsole.ThrowError("Tried to add an existing team change! Make sure to check if the team change exists first.");
#endif
}
return success;
}
public bool TryRemoveTeamChange(string identifier)
{
if (activeTeamChanges.TryGetValue(identifier, out ActiveTeamChange removedTeamChange))
{
if (currentTeamChange == removedTeamChange)
{
currentTeamChange = activeTeamChanges[OriginalTeamIdentifier];
}
}
return activeTeamChanges.Remove(identifier);
}
public void UpdateTeam()
{
if (currentTeamChange == null)
{
return;
}
ActiveTeamChange bestTeamChange = currentTeamChange;
foreach (var desiredTeamChange in activeTeamChanges) // order of iteration matters because newest is preferred when multiple same-priority team changes exist
{
if (bestTeamChange.TeamChangePriority < desiredTeamChange.Value.TeamChangePriority)
{
bestTeamChange = desiredTeamChange.Value;
}
}
if (TeamID != bestTeamChange.DesiredTeamId)
{
ChangeTeam(bestTeamChange.DesiredTeamId);
currentTeamChange = bestTeamChange;
if (bestTeamChange.AggressiveBehavior) // this seemed like the least disruptive way to induce aggressive behavior
{
SetOrder(Order.GetPrefab("fightintruders"), orderOption: null, priority: 3, orderGiver: this, speak: false);
}
}
}
public bool IsOnPlayerTeam => TeamID == CharacterTeamType.Team1 || TeamID == CharacterTeamType.Team2;
public bool IsInstigator => CombatAction != null && CombatAction.IsInstigator;
@@ -336,9 +437,10 @@ namespace Barotrauma
private Action<Character, Character> onCustomInteract;
public ConversationAction ActiveConversation;
public bool RequireConsciousnessForCustomInteract = true;
public bool AllowCustomInteract
{
get { return !IsIncapacitated && Stun <= 0.0f && !Removed; }
get { return (!RequireConsciousnessForCustomInteract || (!IsIncapacitated && Stun <= 0.0f)) && !Removed; }
}
private float lockHandsTimer;
@@ -919,7 +1021,6 @@ namespace Barotrauma
{
teamID = Info.TeamID;
}
keys = new Key[Enum.GetNames(typeof(InputType)).Length];
for (int i = 0; i < Enum.GetNames(typeof(InputType)).Length; i++)
{
@@ -1119,17 +1220,17 @@ namespace Barotrauma
switch (inputType)
{
case InputType.Left:
return !(dequeuedInput.HasFlag(InputNetFlags.Left)) && (prevDequeuedInput.HasFlag(InputNetFlags.Left));
return dequeuedInput.HasFlag(InputNetFlags.Left) && !prevDequeuedInput.HasFlag(InputNetFlags.Left);
case InputType.Right:
return !(dequeuedInput.HasFlag(InputNetFlags.Right)) && (prevDequeuedInput.HasFlag(InputNetFlags.Right));
return dequeuedInput.HasFlag(InputNetFlags.Right) && !prevDequeuedInput.HasFlag(InputNetFlags.Right);
case InputType.Up:
return !(dequeuedInput.HasFlag(InputNetFlags.Up)) && (prevDequeuedInput.HasFlag(InputNetFlags.Up));
return dequeuedInput.HasFlag(InputNetFlags.Up) && !prevDequeuedInput.HasFlag(InputNetFlags.Up);
case InputType.Down:
return !(dequeuedInput.HasFlag(InputNetFlags.Down)) && (prevDequeuedInput.HasFlag(InputNetFlags.Down));
return dequeuedInput.HasFlag(InputNetFlags.Down) && !prevDequeuedInput.HasFlag(InputNetFlags.Down);
case InputType.Run:
return !(dequeuedInput.HasFlag(InputNetFlags.Run)) && (prevDequeuedInput.HasFlag(InputNetFlags.Run));
return dequeuedInput.HasFlag(InputNetFlags.Run) && prevDequeuedInput.HasFlag(InputNetFlags.Run);
case InputType.Crouch:
return !(dequeuedInput.HasFlag(InputNetFlags.Crouch)) && (prevDequeuedInput.HasFlag(InputNetFlags.Crouch));
return dequeuedInput.HasFlag(InputNetFlags.Crouch) && !prevDequeuedInput.HasFlag(InputNetFlags.Crouch);
case InputType.Select:
return dequeuedInput.HasFlag(InputNetFlags.Select); //TODO: clean up the way this input is registered
case InputType.Deselect:
@@ -1139,11 +1240,11 @@ namespace Barotrauma
case InputType.Grab:
return dequeuedInput.HasFlag(InputNetFlags.Grab);
case InputType.Use:
return !(dequeuedInput.HasFlag(InputNetFlags.Use)) && (prevDequeuedInput.HasFlag(InputNetFlags.Use));
return dequeuedInput.HasFlag(InputNetFlags.Use) && !prevDequeuedInput.HasFlag(InputNetFlags.Use);
case InputType.Shoot:
return !(dequeuedInput.HasFlag(InputNetFlags.Shoot)) && (prevDequeuedInput.HasFlag(InputNetFlags.Shoot));
return dequeuedInput.HasFlag(InputNetFlags.Shoot) && !prevDequeuedInput.HasFlag(InputNetFlags.Shoot);
case InputType.Ragdoll:
return !(dequeuedInput.HasFlag(InputNetFlags.Ragdoll)) && (prevDequeuedInput.HasFlag(InputNetFlags.Ragdoll));
return dequeuedInput.HasFlag(InputNetFlags.Ragdoll) && !prevDequeuedInput.HasFlag(InputNetFlags.Ragdoll);
default:
return false;
}
@@ -1595,83 +1696,89 @@ namespace Barotrauma
}
}
#endif
if (attackCoolDown > 0.0f)
{
attackCoolDown -= deltaTime;
}
else if (IsKeyDown(InputType.Attack) && (IsRemotePlayer || Controlled == this || (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)))
else if (IsKeyDown(InputType.Attack))
{
Vector2 attackPos = SimPosition + ConvertUnits.ToSimUnits(cursorPosition - Position);
List<Body> ignoredBodies = AnimController.Limbs.Select(l => l.body.FarseerBody).ToList();
ignoredBodies.Add(AnimController.Collider.FarseerBody);
var body = Submarine.PickBody(
SimPosition,
attackPos,
ignoredBodies,
Physics.CollisionCharacter | Physics.CollisionWall);
IDamageable attackTarget = null;
if (body != null)
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
{
attackPos = Submarine.LastPickedPosition;
if (body.UserData is Submarine sub)
{
body = Submarine.PickBody(
SimPosition - ((Submarine)body.UserData).SimPosition,
attackPos - ((Submarine)body.UserData).SimPosition,
ignoredBodies,
Physics.CollisionWall);
if (body != null)
{
attackPos = Submarine.LastPickedPosition + sub.SimPosition;
attackTarget = body.UserData as IDamageable;
}
}
else
{
if (body.UserData is IDamageable)
{
attackTarget = (IDamageable)body.UserData;
}
else if (body.UserData is Limb)
{
attackTarget = ((Limb)body.UserData).character;
}
}
currentAttackTarget.AttackLimb?.UpdateAttack(deltaTime, currentAttackTarget.AttackPos, currentAttackTarget.DamageTarget, out _);
}
var currentContexts = GetAttackContexts();
var validLimbs = AnimController.Limbs.Where(l =>
else if (IsPlayer)
{
if (l.IsSevered || l.IsStuck) { return false; }
if (l.Disabled) { return false; }
var attack = l.attack;
if (attack == null) { return false; }
if (attack.CoolDownTimer > 0) { return false; }
if (!attack.IsValidContext(currentContexts)) { return false; }
if (attackTarget != null)
Vector2 attackPos = SimPosition + ConvertUnits.ToSimUnits(cursorPosition - Position);
List<Body> ignoredBodies = AnimController.Limbs.Select(l => l.body.FarseerBody).ToList();
ignoredBodies.Add(AnimController.Collider.FarseerBody);
var body = Submarine.PickBody(
SimPosition,
attackPos,
ignoredBodies,
Physics.CollisionCharacter | Physics.CollisionWall);
IDamageable attackTarget = null;
if (body != null)
{
if (!attack.IsValidTarget(attackTarget)) { return false; }
if (attackTarget is ISerializableEntity se && attackTarget is Character)
attackPos = Submarine.LastPickedPosition;
if (body.UserData is Submarine sub)
{
if (attack.Conditionals.Any(c => !c.Matches(se))) { return false; }
body = Submarine.PickBody(
SimPosition - ((Submarine)body.UserData).SimPosition,
attackPos - ((Submarine)body.UserData).SimPosition,
ignoredBodies,
Physics.CollisionWall);
if (body != null)
{
attackPos = Submarine.LastPickedPosition + sub.SimPosition;
attackTarget = body.UserData as IDamageable;
}
}
else
{
if (body.UserData is IDamageable)
{
attackTarget = (IDamageable)body.UserData;
}
else if (body.UserData is Limb)
{
attackTarget = ((Limb)body.UserData).character;
}
}
}
if (attack.Conditionals.Any(c => c.TargetSelf && !c.Matches(this))) { return false; }
return true;
});
var sortedLimbs = validLimbs.OrderBy(l => Vector2.DistanceSquared(ConvertUnits.ToDisplayUnits(l.SimPosition), cursorPosition));
// Select closest
var attackLimb = sortedLimbs.FirstOrDefault();
if (attackLimb != null)
{
attackLimb.UpdateAttack(deltaTime, attackPos, attackTarget, out AttackResult attackResult);
if (!attackLimb.attack.IsRunning)
var currentContexts = GetAttackContexts();
var validLimbs = AnimController.Limbs.Where(l =>
{
attackCoolDown = 1.0f;
if (l.IsSevered || l.IsStuck) { return false; }
if (l.Disabled) { return false; }
var attack = l.attack;
if (attack == null) { return false; }
if (attack.CoolDownTimer > 0) { return false; }
if (!attack.IsValidContext(currentContexts)) { return false; }
if (attackTarget != null)
{
if (!attack.IsValidTarget(attackTarget)) { return false; }
if (attackTarget is ISerializableEntity se && attackTarget is Character)
{
if (attack.Conditionals.Any(c => !c.Matches(se))) { return false; }
}
}
if (attack.Conditionals.Any(c => c.TargetSelf && !c.Matches(this))) { return false; }
return true;
});
var sortedLimbs = validLimbs.OrderBy(l => Vector2.DistanceSquared(ConvertUnits.ToDisplayUnits(l.SimPosition), cursorPosition));
// Select closest
var attackLimb = sortedLimbs.FirstOrDefault();
if (attackLimb != null)
{
attackLimb.UpdateAttack(deltaTime, attackPos, attackTarget, out AttackResult attackResult);
if (!attackLimb.attack.IsRunning)
{
attackCoolDown = 1.0f;
}
}
}
}
@@ -1746,6 +1853,24 @@ namespace Barotrauma
}
}
private struct AttackTargetData
{
public Limb AttackLimb { get; set; }
public IDamageable DamageTarget { get; set; }
public Vector2 AttackPos { get; set; }
}
private AttackTargetData currentAttackTarget;
public void SetAttackTarget(Limb attackLimb, IDamageable damageTarget, Vector2 attackPos)
{
currentAttackTarget = new AttackTargetData()
{
AttackLimb = attackLimb,
DamageTarget = damageTarget,
AttackPos = attackPos
};
}
public bool CanSeeCharacter(Character target)
{
if (target.Removed) { return false; }
@@ -1869,24 +1994,39 @@ namespace Barotrauma
/// </summary>
public bool IsFacing(Vector2 targetWorldPos) => AnimController.Dir > 0 && targetWorldPos.X > WorldPosition.X || AnimController.Dir < 0 && targetWorldPos.X < WorldPosition.X;
public bool HasItem(Item item, bool requireEquipped = false) => requireEquipped ? HasEquippedItem(item) : item.IsOwnedBy(this);
public bool HasItem(Item item, bool requireEquipped = false, InvSlotType? slotType = null) => requireEquipped ? HasEquippedItem(item) : item.IsOwnedBy(this);
public bool HasEquippedItem(Item item)
public bool HasEquippedItem(Item item, InvSlotType? slotType = null)
{
if (Inventory == null) { return false; }
for (int i = 0; i < Inventory.Capacity; i++)
{
if (Inventory.SlotTypes[i] != InvSlotType.Any && Inventory.GetItemAt(i) == item) { return true; }
if (slotType.HasValue)
{
if (!slotType.Value.HasFlag(Inventory.SlotTypes[i])) { continue; }
}
else if (Inventory.SlotTypes[i] == InvSlotType.Any)
{
continue;
}
if (Inventory.GetItemAt(i) == item) { return true; }
}
return false;
}
public bool HasEquippedItem(string tagOrIdentifier, bool allowBroken = true)
public bool HasEquippedItem(string tagOrIdentifier, bool allowBroken = true, InvSlotType? slotType = null)
{
if (Inventory == null) { return false; }
for (int i = 0; i < Inventory.Capacity; i++)
{
if (Inventory.SlotTypes[i] == InvSlotType.Any) { continue; }
if (slotType.HasValue)
{
if (!slotType.Value.HasFlag(Inventory.SlotTypes[i])) { continue; }
}
else if (Inventory.SlotTypes[i] == InvSlotType.Any)
{
continue;
}
var item = Inventory.GetItemAt(i);
if (item == null) { continue; }
if (!allowBroken && item.Condition <= 0.0f) { continue; }
@@ -1895,12 +2035,19 @@ namespace Barotrauma
return false;
}
public Item GetEquippedItem(string tagOrIdentifier)
public Item GetEquippedItem(string tagOrIdentifier, InvSlotType? slotType = null)
{
if (Inventory == null) { return null; }
for (int i = 0; i < Inventory.Capacity; i++)
{
if (Inventory.SlotTypes[i] == InvSlotType.Any) { continue; }
if (slotType.HasValue)
{
if (!slotType.Value.HasFlag(Inventory.SlotTypes[i])) { continue; }
}
else if (Inventory.SlotTypes[i] == InvSlotType.Any)
{
continue;
}
var item = Inventory.GetItemAt(i);
if (item == null) { continue; }
if (item.Prefab.Identifier == tagOrIdentifier || item.HasTag(tagOrIdentifier)) { return item; }
@@ -2022,7 +2169,7 @@ namespace Barotrauma
bool hidden = item.HiddenInGame;
#if CLIENT
if (Screen.Selected == GameMain.SubEditorScreen) { hidden = false; }
#endif
#endif
if (!CanInteract || hidden || !item.IsInteractable(this)) { return false; }
if (item.ParentInventory != null)
@@ -2360,29 +2507,26 @@ namespace Barotrauma
{
if (!(c is AICharacter) && !c.IsRemotePlayer) continue;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
if (c.IsPlayer || (c.IsBot && !c.IsDead))
{
c.Enabled = true;
}
else if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
//disable AI characters that are far away from all clients and the host's character and not controlled by anyone
if (c.IsPlayer || (c.IsBot && !c.IsDead))
float closestPlayerDist = c.GetDistanceToClosestPlayer();
if (closestPlayerDist > c.Params.DisableDistance)
{
c.Enabled = false;
if (c.IsDead && c.AIController is EnemyAIController)
{
Spawner?.AddToRemoveQueue(c);
}
}
else if (closestPlayerDist < c.Params.DisableDistance * 0.9f)
{
c.Enabled = true;
}
else
{
float closestPlayerDist = c.GetDistanceToClosestPlayer();
if (closestPlayerDist > c.Params.DisableDistance)
{
c.Enabled = false;
if (c.IsDead && c.AIController is EnemyAIController)
{
Spawner?.AddToRemoveQueue(c);
}
}
else if (closestPlayerDist < c.Params.DisableDistance * 0.9f)
{
c.Enabled = true;
}
}
}
else if (Submarine.MainSub != null)
{
@@ -2899,10 +3043,16 @@ namespace Barotrauma
return !string.IsNullOrEmpty(ChatMessage.ApplyDistanceEffect("message", messageType, speaker, this));
}
public void SetOrder(Order order, string orderOption, int priority, Character orderGiver, bool speak = true)
/// <param name="force">Force an order to be set for the character, bypassing hearing checks</param>
public void SetOrder(Order order, string orderOption, int priority, Character orderGiver, bool speak = true, bool force = false)
{
//set the character order only if the character is close enough to hear the message
if (orderGiver != null && !CanHearCharacter(orderGiver)) { return; }
if (!force && orderGiver != null && !CanHearCharacter(orderGiver)) { return; }
if (order.OrderGiver != orderGiver)
{
order.OrderGiver = orderGiver;
}
// If there's another character operating the same device, make them dismiss themself
if (order != null && order.Category == OrderCategory.Operate && order.TargetEntity != null)
@@ -2911,6 +3061,7 @@ namespace Barotrauma
{
if (character == this) { continue; }
if (character.TeamID != TeamID) { continue; }
if (!(character.AIController is HumanAIController)) { continue; }
if (!HumanAIController.IsActive(character)) { continue; }
foreach (var currentOrder in character.CurrentOrders)
{
@@ -2918,7 +3069,7 @@ namespace Barotrauma
if (currentOrder.Order.Category != OrderCategory.Operate) { continue; }
if (currentOrder.Order.Identifier != order.Identifier) { continue; }
if (currentOrder.Order.TargetEntity != order.TargetEntity) { continue; }
character.SetOrder(Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(currentOrder), currentOrder.ManualPriority, character);
character.SetOrder(Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(currentOrder), currentOrder.ManualPriority, character, speak: speak, force: force);
break;
}
}
@@ -2936,6 +3087,12 @@ namespace Barotrauma
SetOrderProjSpecific(order, orderOption, priority);
}
/// <param name="force">Force an order to be set for the character, bypassing hearing checks</param>
public void SetOrder(OrderInfo orderInfo, Character orderGiver, bool speak = true, bool force = false)
{
SetOrder(orderInfo.Order, orderInfo.OrderOption, orderInfo.ManualPriority, orderGiver, speak: speak, force: force);
}
private void AddCurrentOrder(OrderInfo newOrder)
{
if (newOrder.Order == null || newOrder.Order.Identifier == "dismissed")
@@ -3154,7 +3311,7 @@ namespace Barotrauma
/// <summary>
/// Apply the specified attack to this character. If the targetLimb is not specified, the limb closest to worldPosition will receive the damage.
/// </summary>
public AttackResult ApplyAttack(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = false, Limb targetLimb = null)
public AttackResult ApplyAttack(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = false, Limb targetLimb = null, float penetration = 0f)
{
if (Removed)
{
@@ -3170,7 +3327,7 @@ namespace Barotrauma
var attackResult = targetLimb == null ?
AddDamage(worldPosition, attack.Afflictions.Keys, attack.Stun, playSound, attackImpulse, out limbHit, attacker, attack.DamageMultiplier) :
DamageLimb(worldPosition, targetLimb, attack.Afflictions.Keys, attack.Stun, playSound, attackImpulse, attacker, attack.DamageMultiplier);
DamageLimb(worldPosition, targetLimb, attack.Afflictions.Keys, attack.Stun, playSound, attackImpulse, attacker, attack.DamageMultiplier, penetration: penetration);
if (limbHit == null) { return new AttackResult(); }
Vector2 forceWorld = attack.TargetImpulseWorld + attack.TargetForceWorld;
@@ -3302,7 +3459,7 @@ namespace Barotrauma
GameMain.Config.RecentlyEncounteredCreatures.Add(other.SpeciesName);
}
public AttackResult DamageLimb(Vector2 worldPosition, Limb hitLimb, IEnumerable<Affliction> afflictions, float stun, bool playSound, float attackImpulse, Character attacker = null, float damageMultiplier = 1, bool allowStacking = true)
public AttackResult DamageLimb(Vector2 worldPosition, Limb hitLimb, IEnumerable<Affliction> afflictions, float stun, bool playSound, float attackImpulse, Character attacker = null, float damageMultiplier = 1, bool allowStacking = true, float penetration = 0f)
{
if (Removed) { return new AttackResult(); }
@@ -3354,7 +3511,7 @@ namespace Barotrauma
}
bool wasDead = IsDead;
Vector2 simPos = hitLimb.SimPosition + ConvertUnits.ToSimUnits(dir);
AttackResult attackResult = hitLimb.AddDamage(simPos, afflictions, playSound, damageMultiplier: damageMultiplier);
AttackResult attackResult = hitLimb.AddDamage(simPos, afflictions, playSound, damageMultiplier: damageMultiplier, penetration: penetration);
CharacterHealth.ApplyDamage(hitLimb, attackResult, allowStacking);
if (attacker != this)
{
@@ -3412,9 +3569,9 @@ namespace Barotrauma
/// <summary>
/// Is the character knocked down regardless whether the technical state is dead, unconcious, paralyzed, or stunned.
/// With stunning, the parameter uses a half a second delay before the character is treated as knocked down. The purpose of this is to ignore minor stunning. If you don't want to to ignore any stun, use the Stun property.
/// With stunning, the parameter uses an one second delay before the character is treated as knocked down. The purpose of this is to ignore minor stunning. If you don't want to to ignore any stun, use the Stun property.
/// </summary>
public bool IsKnockedDown => IsDead || IsIncapacitated || CharacterHealth.StunTimer > 0.5f || IsRagdolled;
public bool IsKnockedDown => IsRagdolled || CharacterHealth.StunTimer > 1.0f || IsIncapacitated;
public void SetStun(float newStun, bool allowStunDecrease = false, bool isNetworkMessage = false)
{
@@ -3457,7 +3614,7 @@ namespace Barotrauma
statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
targets.Clear();
statusEffect.GetNearbyTargets(WorldPosition, targets);
targets.AddRange(statusEffect.GetNearbyTargets(WorldPosition, targets));
statusEffect.Apply(actionType, deltaTime, this, targets);
}
else
@@ -3500,6 +3657,7 @@ namespace Barotrauma
// OnDamaged is called only for the limb that is hit.
AnimController.Limbs.ForEach(l => l.ApplyStatusEffects(actionType, deltaTime));
}
CharacterHealth.ApplyAfflictionStatusEffects(actionType);
}
private void Implode(bool isNetworkMessage = false)
@@ -3738,8 +3896,9 @@ namespace Barotrauma
AnimController.FindHull(worldPos, true);
}
public void SaveInventory(Inventory inventory, XElement parentElement)
public static void SaveInventory(Inventory inventory, XElement parentElement)
{
if (inventory == null || parentElement == null) { return; }
var items = inventory.AllItems.Distinct();
foreach (Item item in items)
{
@@ -3758,6 +3917,14 @@ namespace Barotrauma
}
}
/// <summary>
/// Calls <see cref="SaveInventory(Barotrauma.Inventory, XElement)"/> using 'Inventory' and 'Info.InventoryData'
/// </summary>
public void SaveInventory()
{
SaveInventory(Inventory, Info?.InventoryData);
}
public void SpawnInventoryItems(Inventory inventory, XElement itemData)
{
SpawnInventoryItemsRecursive(inventory, itemData, new List<Item>());
@@ -4011,9 +4178,12 @@ namespace Barotrauma
public bool IsEngineer => HasJob("engineer");
public bool IsMechanic => HasJob("mechanic");
public bool IsMedic => HasJob("medicaldoctor");
public bool IsSecurity => HasJob("securityofficer");
public bool IsSecurity => HasJob("securityofficer") || HasJob("vipsecurityofficer");
public bool IsAssistant => HasJob("assistant");
public bool IsWatchman => HasJob("watchman");
public bool IsVip => HasJob("prisoner");
public bool IsPrisoner => HasJob("prisoner");
public Color? UniqueNameColor { get; set; } = null;
public bool HasJob(string identifier) => Info?.Job?.Prefab.Identifier == identifier;
@@ -4022,4 +4192,24 @@ namespace Barotrauma
return PressureProtection >= (Level.Loaded?.GetRealWorldDepth(WorldPosition.Y) ?? 1.0f);
}
}
class ActiveTeamChange
{
public CharacterTeamType DesiredTeamId { get; }
public enum TeamChangePriorities
{
Base, // given to characters when generated or when their base team is set
Willful, // cognitive, willful team changes, such as prisoners escaping
Absolute // possession, insanity, the like
}
public TeamChangePriorities TeamChangePriority { get; }
public bool AggressiveBehavior { get; }
public ActiveTeamChange(CharacterTeamType desiredTeamId, TeamChangePriorities teamChangePriority, bool aggressiveBehavior = false)
{
DesiredTeamId = desiredTeamId;
TeamChangePriority = teamChangePriority;
AggressiveBehavior = aggressiveBehavior;
}
}
}
@@ -149,6 +149,7 @@ namespace Barotrauma
public XElement InventoryData;
public XElement HealthData;
public XElement OrderData;
private static ushort idCounter;
private const string disguiseName = "???";
@@ -493,7 +494,7 @@ namespace Barotrauma
if (firstNamePath != "")
{
firstNamePath = firstNamePath.Replace("[GENDER]", (Head.gender == Gender.Female) ? "female" : "male");
Name = ToolBox.GetRandomLine(firstNamePath);
Name = ToolBox.GetRandomLine(firstNamePath, randSync);
}
string lastNamePath = CharacterConfigElement.Element("name").GetAttributeString("lastname", "");
@@ -501,7 +502,7 @@ namespace Barotrauma
{
lastNamePath = lastNamePath.Replace("[GENDER]", (Head.gender == Gender.Female) ? "female" : "male");
if (Name != "") Name += " ";
Name += ToolBox.GetRandomLine(lastNamePath);
Name += ToolBox.GetRandomLine(lastNamePath, randSync);
}
}
}
@@ -1019,7 +1020,273 @@ namespace Barotrauma
return charElement;
}
public void ApplyHealthData(Character character, XElement healthData)
public static void SaveOrders(XElement parentElement, params OrderInfo[] orders)
{
if (parentElement == null || orders == null || orders.None()) { return; }
// If an order is invalid, we discard the order and increase the priority of the following orders so
// 1) the highest priority value will remain equal to CharacterInfo.HighestManualOrderPriority; and
// 2) the order priorities will remain sequential.
int priorityIncrease = 0;
var linkedSubs = GetLinkedSubmarines();
foreach (var orderInfo in orders)
{
var order = orderInfo.Order;
if (order == null || string.IsNullOrEmpty(order.Identifier))
{
DebugConsole.ThrowError("Error saving an order - the order or its identifier is null");
priorityIncrease++;
continue;
}
int? linkedSubIndex = null;
bool targetAvailableInNextLevel = true;
if (order.TargetSpatialEntity != null)
{
var entitySub = order.TargetSpatialEntity.Submarine;
bool isOutside = entitySub == null;
bool canBeOnLinkedSub = !isOutside && Submarine.MainSub != null && entitySub != Submarine.MainSub && linkedSubs.Any();
bool isOnConnectedLinkedSub = false;
if (canBeOnLinkedSub)
{
for (int i = 0; i < linkedSubs.Count; i++)
{
var ls = linkedSubs[i];
if (!ls.LoadSub) { continue; }
if (ls.Sub != entitySub) { continue; }
linkedSubIndex = i;
isOnConnectedLinkedSub = Submarine.MainSub.GetConnectedSubs().Contains(entitySub);
break;
}
}
targetAvailableInNextLevel = !isOutside && GameMain.GameSession?.Campaign?.PendingSubmarineSwitch == null && (isOnConnectedLinkedSub || entitySub == Submarine.MainSub);
if (!targetAvailableInNextLevel)
{
if (!order.CanBeGeneralized)
{
DebugConsole.Log($"Trying to save an order ({order.Identifier}) targeting an entity that won't be connected to the main sub in the next level. The order requires a target so it won't be saved.");
priorityIncrease++;
continue;
}
else
{
DebugConsole.Log($"Saving an order ({order.Identifier}) targeting an entity that won't be connected to the main sub in the next level. The order will be saved as a generalized version.");
}
}
}
if (orderInfo.ManualPriority < 1)
{
DebugConsole.ThrowError($"Error saving an order ({order.Identifier}) - the order priority is less than 1");
priorityIncrease++;
continue;
}
var orderElement = new XElement("order",
new XAttribute("id", order.Identifier),
new XAttribute("priority", orderInfo.ManualPriority + priorityIncrease),
new XAttribute("targettype", (int)order.TargetType));
if (!string.IsNullOrEmpty(orderInfo.OrderOption))
{
orderElement.Add(new XAttribute("option", orderInfo.OrderOption));
}
if (order.OrderGiver != null)
{
orderElement.Add(new XAttribute("ordergiverinfoid", order.OrderGiver.Info.ID));
}
if (order.TargetSpatialEntity?.Submarine is Submarine targetSub)
{
if (targetSub == Submarine.MainSub)
{
orderElement.Add(new XAttribute("onmainsub", true));
}
else if(linkedSubIndex.HasValue)
{
orderElement.Add(new XAttribute("linkedsubindex", linkedSubIndex));
}
}
switch (order.TargetType)
{
case Order.OrderTargetType.Entity when targetAvailableInNextLevel && order.TargetEntity is Entity e:
orderElement.Add(new XAttribute("targetid", (uint)e.ID));
break;
case Order.OrderTargetType.Position when targetAvailableInNextLevel && order.TargetSpatialEntity is OrderTarget ot:
var orderTargetElement = new XElement("ordertarget");
var position = ot.WorldPosition;
if (ot.Hull != null)
{
orderTargetElement.Add(new XAttribute("hullid", (uint)ot.Hull.ID));
position -= ot.Hull.WorldPosition;
}
orderTargetElement.Add(new XAttribute("position", $"{position.X},{position.Y}"));
orderElement.Add(orderTargetElement);
break;
case Order.OrderTargetType.WallSection when targetAvailableInNextLevel && order.TargetEntity is Structure s && order.WallSectionIndex.HasValue:
orderElement.Add(new XAttribute("structureid", s.ID));
orderElement.Add(new XAttribute("wallsectionindex", order.WallSectionIndex.Value));
break;
}
parentElement.Add(orderElement);
}
}
/// <summary>
/// Save current orders to the parameter element
/// </summary>
public static void SaveOrderData(CharacterInfo characterInfo, XElement parentElement)
{
var currentOrders = new List<OrderInfo>(characterInfo.CurrentOrders);
// Sort the current orders to make sure the one with the highest priority comes first
currentOrders.Sort((x, y) => y.ManualPriority.CompareTo(x.ManualPriority));
SaveOrders(parentElement, currentOrders.ToArray());
}
/// <summary>
/// Save current orders to <see cref="OrderData"/>
/// </summary>
public void SaveOrderData()
{
OrderData = new XElement("orders");
SaveOrderData(this, OrderData);
}
public static void ApplyOrderData(Character character, XElement orderData)
{
if (character == null) { return; }
var orders = LoadOrders(orderData);
foreach (var order in orders)
{
character.SetOrder(order, order.Order?.OrderGiver, speak: false, force: true);
}
}
public void ApplyOrderData()
{
ApplyOrderData(Character, OrderData);
}
public static List<OrderInfo> LoadOrders(XElement ordersElement)
{
var orders = new List<OrderInfo>();
if (ordersElement == null) { return orders; }
// If an order is invalid, we discard the order and increase the priority of the following orders so
// 1) the highest priority value will remain equal to CharacterInfo.HighestManualOrderPriority; and
// 2) the order priorities will remain sequential.
int priorityIncrease = 0;
var linkedSubs = GetLinkedSubmarines();
foreach (var orderElement in ordersElement.GetChildElements("order"))
{
Order order = null;
string orderIdentifier = orderElement.GetAttributeString("id", "");
var orderPrefab = Order.GetPrefab(orderIdentifier);
if (orderPrefab == null)
{
DebugConsole.ThrowError($"Error loading a previously saved order - can't find an order prefab with the identifier \"{orderIdentifier}\"");
priorityIncrease++;
continue;
}
var targetType = (Order.OrderTargetType)orderElement.GetAttributeInt("targettype", 0);
int orderGiverInfoId = orderElement.GetAttributeInt("ordergiverinfoid", -1);
var orderGiver = orderGiverInfoId >= 0 ? Character.CharacterList.FirstOrDefault(c => c.Info?.ID == orderGiverInfoId) : null;
Entity targetEntity = null;
switch (targetType)
{
case Order.OrderTargetType.Entity:
ushort targetId = (ushort)orderElement.GetAttributeUInt("targetid", Entity.NullEntityID);
if (!GetTargetEntity(targetId, out targetEntity)) { continue; }
var targetComponent = orderPrefab.GetTargetItemComponent(targetEntity as Item);
order = new Order(orderPrefab, targetEntity, targetComponent, orderGiver: orderGiver);
break;
case Order.OrderTargetType.Position:
var orderTargetElement = orderElement.GetChildElement("ordertarget");
var position = orderTargetElement.GetAttributeVector2("position", Vector2.Zero);
ushort hullId = (ushort)orderTargetElement.GetAttributeUInt("hullid", 0);
if (!GetTargetEntity(hullId, out targetEntity)) { continue; }
if (!(targetEntity is Hull targetPositionHull))
{
DebugConsole.ThrowError($"Error loading a previously saved order ({orderIdentifier}) - entity with the ID {hullId} is of type {targetEntity?.GetType()} instead of Hull");
priorityIncrease++;
continue;
}
var orderTarget = new OrderTarget(targetPositionHull.WorldPosition + position, targetPositionHull);
order = new Order(orderPrefab, orderTarget, orderGiver: orderGiver);
break;
case Order.OrderTargetType.WallSection:
ushort structureId = (ushort)orderElement.GetAttributeInt("structureid", Entity.NullEntityID);
if (!GetTargetEntity(structureId, out targetEntity)) { continue; }
int wallSectionIndex = orderElement.GetAttributeInt("wallsectionindex", 0);
if (!(targetEntity is Structure targetStructure))
{
DebugConsole.ThrowError($"Error loading a previously saved order ({orderIdentifier}) - entity with the ID {structureId} is of type {targetEntity?.GetType()} instead of Structure");
priorityIncrease++;
continue;
}
order = new Order(orderPrefab, targetStructure, wallSectionIndex, orderGiver: orderGiver);
break;
}
string orderOption = orderElement.GetAttributeString("option", "");
int manualPriority = orderElement.GetAttributeInt("priority", 0) + priorityIncrease;
var orderInfo = new OrderInfo(order, orderOption, manualPriority);
orders.Add(orderInfo);
bool GetTargetEntity(ushort targetId, out Entity targetEntity)
{
targetEntity = null;
if (targetId == Entity.NullEntityID) { return true; }
Submarine parentSub = null;
if (orderElement.GetAttributeBool("onmainsub", false))
{
parentSub = Submarine.MainSub;
}
else
{
int linkedSubIndex = orderElement.GetAttributeInt("linkedsubindex", -1);
if (linkedSubIndex >= 0 && linkedSubIndex < linkedSubs.Count &&
linkedSubs[linkedSubIndex] is LinkedSubmarine linkedSub && linkedSub.LoadSub)
{
parentSub = linkedSub.Sub;
}
}
if (parentSub != null)
{
targetId = GetOffsetId(parentSub, targetId);
targetEntity = Entity.FindEntityByID(targetId);
}
else
{
if (!orderPrefab.CanBeGeneralized)
{
DebugConsole.ThrowError($"Error loading a previously saved order ({orderIdentifier}). Can't find the parent sub of the target entity. The order requires a target so it can't be loaded at all.");
priorityIncrease++;
return false;
}
else
{
DebugConsole.AddWarning($"Trying to load a previously saved order ({orderIdentifier}). Can't find the parent sub of the target entity. The order doesn't require a target so a more generic version of the order will be loaded instead.");
}
}
return true;
}
}
return orders;
}
private static List<LinkedSubmarine> GetLinkedSubmarines()
{
return Entity.GetEntities()
.OfType<LinkedSubmarine>()
.Where(ls => ls.Submarine == Submarine.MainSub)
.OrderBy(e => e.ID)
.ToList();
}
private static ushort GetOffsetId(Submarine parentSub, ushort id)
{
if (parentSub != null)
{
var idRemap = new IdRemap(parentSub.Info.SubmarineElement, parentSub.IdOffset);
return idRemap.GetOffsetId(id);
}
return id;
}
public static void ApplyHealthData(Character character, XElement healthData)
{
if (healthData != null) { character?.CharacterHealth.Load(healthData); }
}
@@ -14,7 +14,7 @@ namespace Barotrauma
public Dictionary<string, SerializableProperty> SerializableProperties { get; set; }
public float PendingAdditionStrenght { get; set; }
public float PendingAdditionStrength { get; set; }
public float AdditionStrength { get; set; }
protected float _strength;
@@ -32,7 +32,7 @@ namespace Barotrauma
float newValue = MathHelper.Clamp(value, 0.0f, Prefab.MaxStrength);
if (newValue > _strength)
{
PendingAdditionStrenght = Prefab.GrainBurst;
PendingAdditionStrength = Prefab.GrainBurst;
}
_strength = newValue;
}
@@ -64,7 +64,7 @@ namespace Barotrauma
public Affliction(AfflictionPrefab prefab, float strength)
{
Prefab = prefab;
PendingAdditionStrenght = Prefab.GrainBurst;
PendingAdditionStrength = Prefab.GrainBurst;
_strength = strength;
Identifier = prefab?.Identifier;
@@ -91,10 +91,12 @@ namespace Barotrauma
public override string ToString() => Prefab == null ? "Affliction (Invalid)" : $"Affliction ({Prefab.Name})";
public AfflictionPrefab.Effect GetActiveEffect() => Prefab.GetActiveEffect(Strength);
public float GetVitalityDecrease(CharacterHealth characterHealth)
{
if (Strength < Prefab.ActivationThreshold) { return 0.0f; }
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
AfflictionPrefab.Effect currentEffect = GetActiveEffect();
if (currentEffect == null) { return 0.0f; }
if (currentEffect.MaxStrength - currentEffect.MinStrength <= 0.0f) { return 0.0f; }
@@ -114,7 +116,7 @@ namespace Barotrauma
public float GetScreenGrainStrength()
{
if (Strength < Prefab.ActivationThreshold) { return 0.0f; }
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
AfflictionPrefab.Effect currentEffect = GetActiveEffect();
if (currentEffect == null) { return 0.0f; }
if (MathUtils.NearlyEqual(currentEffect.MaxGrainStrength, 0f)) { return 0.0f; }
@@ -125,7 +127,7 @@ namespace Barotrauma
if (Prefab.GrainBurst > 0 && AdditionStrength > amount)
{
return AdditionStrength;
return Math.Min(AdditionStrength, 1.0f);
}
return amount;
@@ -134,7 +136,7 @@ namespace Barotrauma
public float GetScreenDistortStrength()
{
if (Strength < Prefab.ActivationThreshold) { return 0.0f; }
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
AfflictionPrefab.Effect currentEffect = GetActiveEffect();
if (currentEffect == null) { return 0.0f; }
if (currentEffect.MaxScreenDistortStrength - currentEffect.MinScreenDistortStrength < 0.0f) { return 0.0f; }
@@ -147,7 +149,7 @@ namespace Barotrauma
public float GetRadialDistortStrength()
{
if (Strength < Prefab.ActivationThreshold) { return 0.0f; }
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
AfflictionPrefab.Effect currentEffect = GetActiveEffect();
if (currentEffect == null) { return 0.0f; }
if (currentEffect.MaxRadialDistortStrength - currentEffect.MinRadialDistortStrength < 0.0f) { return 0.0f; }
@@ -160,7 +162,7 @@ namespace Barotrauma
public float GetChromaticAberrationStrength()
{
if (Strength < Prefab.ActivationThreshold) { return 0.0f; }
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
AfflictionPrefab.Effect currentEffect = GetActiveEffect();
if (currentEffect == null) { return 0.0f; }
if (currentEffect.MaxChromaticAberrationStrength - currentEffect.MinChromaticAberrationStrength < 0.0f) { return 0.0f; }
@@ -173,7 +175,7 @@ namespace Barotrauma
public float GetScreenBlurStrength()
{
if (Strength < Prefab.ActivationThreshold) { return 0.0f; }
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
AfflictionPrefab.Effect currentEffect = GetActiveEffect();
if (currentEffect == null) { return 0.0f; }
if (currentEffect.MaxScreenBlurStrength - currentEffect.MinScreenBlurStrength < 0.0f) { return 0.0f; }
@@ -186,7 +188,7 @@ namespace Barotrauma
public float GetSkillMultiplier()
{
if (Strength < Prefab.ActivationThreshold) { return 1.0f; }
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
AfflictionPrefab.Effect currentEffect = GetActiveEffect();
if (currentEffect == null) { return 1.0f; }
float amount = MathHelper.Lerp(
@@ -210,11 +212,11 @@ namespace Barotrauma
public float GetResistance(string afflictionId)
{
if (Strength < Prefab.ActivationThreshold) return 0.0f;
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
if (currentEffect == null) return 0.0f;
if (currentEffect.MaxResistance - currentEffect.MinResistance <= 0.0f) return 0.0f;
if (afflictionId != null && afflictionId != currentEffect.ResistanceFor) return 0.0f;
if (Strength < Prefab.ActivationThreshold) { return 0.0f; }
AfflictionPrefab.Effect currentEffect = GetActiveEffect();
if (currentEffect == null) { return 0.0f; }
if (currentEffect.MaxResistance - currentEffect.MinResistance <= 0.0f) { return 0.0f; }
if (afflictionId != null && afflictionId != currentEffect.ResistanceFor) { return 0.0f; }
return MathHelper.Lerp(
currentEffect.MinResistance,
@@ -224,10 +226,10 @@ namespace Barotrauma
public float GetSpeedMultiplier()
{
if (Strength < Prefab.ActivationThreshold) return 1.0f;
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
if (currentEffect == null) return 1.0f;
if (currentEffect.MaxSpeedMultiplier - currentEffect.MinSpeedMultiplier <= 0.0f) return 1.0f;
if (Strength < Prefab.ActivationThreshold) { return 1.0f; }
AfflictionPrefab.Effect currentEffect = GetActiveEffect();
if (currentEffect == null) { return 1.0f; }
if (currentEffect.MaxSpeedMultiplier - currentEffect.MinSpeedMultiplier <= 0.0f) { return 1.0f; }
return MathHelper.Lerp(
currentEffect.MinSpeedMultiplier,
@@ -250,14 +252,14 @@ namespace Barotrauma
{
foreach (StatusEffect statusEffect in periodicEffect.StatusEffects)
{
ApplyStatusEffect(statusEffect, 1.0f, characterHealth, targetLimb);
ApplyStatusEffect(ActionType.OnActive, statusEffect, 1.0f, characterHealth, targetLimb);
PeriodicEffectTimers[periodicEffect] = Rand.Range(periodicEffect.MinInterval, periodicEffect.MaxInterval);
}
}
}
}
}
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
AfflictionPrefab.Effect currentEffect = GetActiveEffect();
if (currentEffect == null) { return; }
if (currentEffect.StrengthChange < 0) // Reduce diminishing of buffs if boosted
@@ -273,7 +275,7 @@ namespace Barotrauma
foreach (StatusEffect statusEffect in currentEffect.StatusEffects)
{
ApplyStatusEffect(statusEffect, deltaTime, characterHealth, targetLimb);
ApplyStatusEffect(ActionType.OnActive, statusEffect, deltaTime, characterHealth, targetLimb);
}
float amount = deltaTime;
@@ -281,10 +283,10 @@ namespace Barotrauma
{
amount /= Prefab.GrainBurst;
}
if (PendingAdditionStrenght >= 0)
if (PendingAdditionStrength >= 0)
{
AdditionStrength += amount;
PendingAdditionStrenght -= deltaTime;
PendingAdditionStrength -= deltaTime;
}
else if (AdditionStrength > 0)
{
@@ -292,27 +294,37 @@ namespace Barotrauma
}
}
public void ApplyStatusEffect(StatusEffect statusEffect, float deltaTime, CharacterHealth characterHealth, Limb targetLimb)
public void ApplyStatusEffects(ActionType type, float deltaTime, CharacterHealth characterHealth, Limb targetLimb)
{
var currentEffect = GetActiveEffect();
if (currentEffect != null)
{
currentEffect.StatusEffects.ForEach(se => ApplyStatusEffect(type, se, deltaTime, characterHealth, targetLimb));
}
}
private readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
public void ApplyStatusEffect(ActionType type, StatusEffect statusEffect, float deltaTime, CharacterHealth characterHealth, Limb targetLimb)
{
statusEffect.SetUser(Source);
if (statusEffect.HasTargetType(StatusEffect.TargetType.Character))
{
statusEffect.Apply(ActionType.OnActive, deltaTime, characterHealth.Character, characterHealth.Character);
statusEffect.Apply(type, deltaTime, characterHealth.Character, characterHealth.Character);
}
if (targetLimb != null && statusEffect.HasTargetType(StatusEffect.TargetType.Limb))
{
statusEffect.Apply(ActionType.OnActive, deltaTime, characterHealth.Character, targetLimb);
statusEffect.Apply(type, deltaTime, characterHealth.Character, targetLimb);
}
if (targetLimb != null && statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs))
{
statusEffect.Apply(ActionType.OnActive, deltaTime, targetLimb.character, targetLimb.character.AnimController.Limbs.Cast<ISerializableEntity>().ToList());
statusEffect.Apply(type, deltaTime, targetLimb.character, targets: targetLimb.character.AnimController.Limbs);
}
if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
var targets = new List<ISerializableEntity>();
statusEffect.GetNearbyTargets(characterHealth.Character.WorldPosition, targets);
statusEffect.Apply(ActionType.OnActive, deltaTime, characterHealth.Character, targets);
targets.Clear();
targets.AddRange(statusEffect.GetNearbyTargets(characterHealth.Character.WorldPosition, targets));
statusEffect.Apply(type, deltaTime, characterHealth.Character, targets);
}
}
@@ -69,7 +69,10 @@ namespace Barotrauma
if (Strength < DormantThreshold)
{
DeactivateHusk();
State = InfectionState.Dormant;
if (Strength > Math.Min(1.0f, DormantThreshold))
{
State = InfectionState.Dormant;
}
}
else if (Strength < ActiveThreshold)
{
@@ -678,7 +678,10 @@ namespace Barotrauma
{
foreach (Effect effect in effects)
{
if (currentStrength > effect.MinStrength && currentStrength <= effect.MaxStrength) return effect;
if (currentStrength > effect.MinStrength && currentStrength <= effect.MaxStrength)
{
return effect;
}
}
//if above the strength range of all effects, use the highest strength effect
@@ -42,7 +42,7 @@ namespace Barotrauma
private float GetDiminishMultiplier()
{
if (Strength < Prefab.ActivationThreshold) { return 1.0f; }
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
AfflictionPrefab.Effect currentEffect = GetActiveEffect();
if (currentEffect == null) { return 1.0f; }
float multiplier = MathHelper.Lerp(
@@ -833,6 +833,32 @@ namespace Barotrauma
#endif
}
// We need to use another list of the afflictions when we call the status effects triggered by afflictions,
// because those status effects may add or remove other afflictions while iterating the collection.
private readonly List<Affliction> afflictionsCopy = new List<Affliction>();
public void ApplyAfflictionStatusEffects(ActionType type)
{
for (int i = 0; i < limbHealths.Count; i++)
{
for (int j = limbHealths[i].Afflictions.Count - 1; j >= 0; j--)
{
var affliction = limbHealths[i].Afflictions[j];
Limb targetLimb = Character.AnimController.Limbs.LastOrDefault(l => !l.IsSevered && !l.Hidden && l.HealthIndex == i);
if (targetLimb == null)
{
targetLimb = Character.AnimController.MainLimb;
}
affliction.ApplyStatusEffects(type, 1.0f, this, targetLimb);
}
}
afflictionsCopy.Clear();
afflictionsCopy.AddRange(afflictions);
for (int i = afflictionsCopy.Count - 1; i >= 0; i--)
{
afflictionsCopy[i].ApplyStatusEffects(type, 1.0f, this, targetLimb: null);
}
}
public Pair<CauseOfDeathType, Affliction> GetCauseOfDeath()
{
List<Affliction> currentAfflictions = GetAllAfflictions(true);
@@ -90,6 +90,7 @@ namespace Barotrauma
public readonly Dictionary<XElement, float> ItemSets = new Dictionary<XElement, float>();
public readonly Dictionary<XElement, float> CustomNPCSets = new Dictionary<XElement, float>();
public HumanPrefab(XElement element, string filePath)
{
@@ -99,6 +100,7 @@ namespace Barotrauma
Job = Job.ToLowerInvariant();
Element = element;
element.GetChildElements("itemset").ForEach(e => ItemSets.Add(e, e.GetAttributeFloat("commonness", 1)));
element.GetChildElements("character").ForEach(e => CustomNPCSets.Add(e, e.GetAttributeFloat("commonness", 1)));
PreferredOutpostModuleTypes = element.GetAttributeStringArray("preferredoutpostmoduletypes", new string[0], convertToLowerInvariant: true).ToList();
}
@@ -160,18 +162,24 @@ namespace Barotrauma
var spawnItems = ToolBox.SelectWeightedRandom(ItemSets.Keys.ToList(), ItemSets.Values.ToList(), randSync);
foreach (XElement itemElement in spawnItems.GetChildElements("item"))
{
InitializeItems(character, itemElement, submarine, createNetworkEvents: createNetworkEvents);
InitializeItem(character, itemElement, submarine, this, createNetworkEvents: createNetworkEvents);
}
}
private void InitializeItems(Character character, XElement itemElement, Submarine submarine, Item parentItem = null, bool createNetworkEvents = true)
public CharacterInfo GetCharacterInfo()
{
var characterElement = ToolBox.SelectWeightedRandom(CustomNPCSets.Keys.ToList(), CustomNPCSets.Values.ToList(), Rand.RandSync.Unsynced);
return characterElement != null ? new CharacterInfo(characterElement) : null;
}
public static void InitializeItem(Character character, XElement itemElement, Submarine submarine, HumanPrefab humanPrefab, Item parentItem = null, bool createNetworkEvents = true)
{
ItemPrefab itemPrefab;
string itemIdentifier = itemElement.GetAttributeString("identifier", "");
itemPrefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab;
if (itemPrefab == null)
{
DebugConsole.ThrowError("Tried to spawn \"" + Identifier + "\" with the item \"" + itemIdentifier + "\". Matching item prefab not found.");
DebugConsole.ThrowError("Tried to spawn \"" + humanPrefab?.Identifier + "\" with the item \"" + itemIdentifier + "\". Matching item prefab not found.");
return;
}
Item item = new Item(itemPrefab, character.Position, null);
@@ -192,7 +200,11 @@ namespace Barotrauma
#endif
if (itemElement.GetAttributeBool("equip", false))
{
List<InvSlotType> allowedSlots = new List<InvSlotType>(item.AllowedSlots);
//if the item is both pickable and wearable, try to wear it instead of picking it up
List<InvSlotType> allowedSlots =
item.GetComponents<Pickable>().Count() > 1 ?
new List<InvSlotType>(item.GetComponent<Wearable>()?.AllowedSlots ?? item.GetComponent<Pickable>().AllowedSlots) :
new List<InvSlotType>(item.AllowedSlots);
allowedSlots.Remove(InvSlotType.Any);
character.Inventory.TryPutItem(item, null, allowedSlots);
@@ -237,7 +249,7 @@ namespace Barotrauma
}
foreach (XElement childItemElement in itemElement.Elements())
{
InitializeItems(character, childItemElement, submarine, item, createNetworkEvents);
InitializeItem(character, childItemElement, submarine, humanPrefab, item, createNetworkEvents);
}
}
}
@@ -158,9 +158,12 @@ namespace Barotrauma
if (itemElement.GetAttributeBool("equip", false))
{
List<InvSlotType> allowedSlots = new List<InvSlotType>(item.AllowedSlots);
//if the item is both pickable and wearable, try to wear it instead of picking it up
List<InvSlotType> allowedSlots =
item.GetComponents<Pickable>().Count() > 1 ?
new List<InvSlotType>(item.GetComponent<Wearable>()?.AllowedSlots ?? item.GetComponent<Pickable>().AllowedSlots) :
new List<InvSlotType>(item.AllowedSlots);
allowedSlots.Remove(InvSlotType.Any);
character.Inventory.TryPutItem(item, null, allowedSlots);
}
else
@@ -178,6 +178,14 @@ namespace Barotrauma
private set;
}
//whether the job should be available to NPCs
[Serialize(false, false)]
public bool HiddenJob
{
get;
private set;
}
public Sprite Icon;
public Sprite IconSmall;
@@ -195,7 +203,7 @@ namespace Barotrauma
SerializableProperty.DeserializeProperties(this, element);
Name = TextManager.Get("JobName." + Identifier);
Description = TextManager.Get("JobDescription." + Identifier);
Description = TextManager.Get("JobDescription." + Identifier, returnNull: true) ?? string.Empty;
Identifier = Identifier.ToLowerInvariant();
Element = element;
@@ -265,7 +273,7 @@ namespace Barotrauma
}
public static JobPrefab Random(Rand.RandSync sync = Rand.RandSync.Unsynced) => Prefabs.GetRandom(p => p.Identifier != "watchman", sync);
public static JobPrefab Random(Rand.RandSync sync = Rand.RandSync.Unsynced) => Prefabs.GetRandom(p => !p.HiddenJob, sync);
public static void LoadAll(IEnumerable<ContentFile> files)
{
@@ -683,7 +683,7 @@ namespace Barotrauma
private readonly List<DamageModifier> appliedDamageModifiers = new List<DamageModifier>();
private readonly List<DamageModifier> tempModifiers = new List<DamageModifier>();
private readonly List<Affliction> afflictionsCopy = new List<Affliction>();
public AttackResult AddDamage(Vector2 simPosition, IEnumerable<Affliction> afflictions, bool playSound, float damageMultiplier = 1)
public AttackResult AddDamage(Vector2 simPosition, IEnumerable<Affliction> afflictions, bool playSound, float damageMultiplier = 1, float penetration = 0f)
{
appliedDamageModifiers.Clear();
afflictionsCopy.Clear();
@@ -726,7 +726,12 @@ namespace Barotrauma
float finalDamageModifier = damageMultiplier;
foreach (DamageModifier damageModifier in tempModifiers)
{
finalDamageModifier *= damageModifier.DamageMultiplier;
float damageModifierValue = damageModifier.DamageMultiplier;
if (damageModifier.DeflectProjectiles && damageModifierValue < 1f)
{
damageModifierValue = MathHelper.Lerp(damageModifierValue, 1f, penetration);
}
finalDamageModifier *= damageModifierValue;
}
if (!MathUtils.NearlyEqual(finalDamageModifier, 1.0f))
{
@@ -981,13 +986,16 @@ namespace Barotrauma
NetEntityEvent.Type.ExecuteAttack,
this,
(damageTarget as Entity)?.ID ?? Entity.NullEntityID,
damageTarget is Character && targetLimb != null ? Array.IndexOf(((Character)damageTarget).AnimController.Limbs, targetLimb) : 0
damageTarget is Character && targetLimb != null ? Array.IndexOf(((Character)damageTarget).AnimController.Limbs, targetLimb) : 0,
attackSimPos.X,
attackSimPos.Y
});
#endif
}
Vector2 diff = attackSimPos - SimPosition;
bool applyForces = !attack.ApplyForcesOnlyOnce || !wasRunning;
if (applyForces)
{
if (attack.ForceOnLimbIndices != null && attack.ForceOnLimbIndices.Count > 0)
@@ -1143,7 +1151,7 @@ namespace Barotrauma
statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
targets.Clear();
statusEffect.GetNearbyTargets(WorldPosition, targets);
targets.AddRange(statusEffect.GetNearbyTargets(WorldPosition, targets));
statusEffect.Apply(actionType, deltaTime, character, targets);
}
else
@@ -547,8 +547,8 @@ namespace Barotrauma
[Serialize(false, true, description: "If enabled, the character chooses randomly from the available attacks. The priority is used as a weight for weighted random."), Editable]
public bool RandomAttack { get; private set; }
[Serialize(false, true, description:"Can the character open doors and hatches without a proper id card? Only applies on humanoids."), Editable]
public bool Infiltrate { get; private set; }
[Serialize(false, true, description:"Does the creature know how to open doors (still requires a proper ID card). Only applies on humanoids. Humans can always open doors (They don't use this AI definition)."), Editable]
public bool CanOpenDoors { get; private set; }
[Serialize(true, true, "Is the creature allowed to navigate from and into the depths of the abyss? When enabled, the creatures will try to avoid the depths."), Editable]
public bool AvoidAbyss { get; set; }
@@ -50,7 +50,8 @@ namespace Barotrauma
Corpses,
WreckAIConfig,
UpgradeModules,
MapCreature
MapCreature,
EnemySubmarine
}
public class ContentPackage
@@ -101,7 +102,8 @@ namespace Barotrauma
ContentType.Orders,
ContentType.Corpses,
ContentType.UpgradeModules,
ContentType.MapCreature
ContentType.MapCreature,
ContentType.EnemySubmarine
};
//at least one file of each these types is required in core content packages
@@ -132,7 +134,8 @@ namespace Barotrauma
ContentType.EventManagerSettings,
ContentType.Orders,
ContentType.Corpses,
ContentType.UpgradeModules
ContentType.UpgradeModules,
ContentType.EnemySubmarine
};
public static IEnumerable<ContentType> CorePackageRequiredFiles
@@ -212,7 +215,6 @@ namespace Barotrauma
private readonly List<ContentFile> filesToAdd;
private readonly List<ContentFile> filesToRemove;
public IReadOnlyList<ContentFile> Files
{
get { return files; }
@@ -609,7 +611,8 @@ namespace Barotrauma
catch (Exception e)
{
DebugConsole.ThrowError("Error while calculating content package hash: ", e);
DebugConsole.ThrowError($"Error while calculating the MD5 hash of the content package \"{Name}\" (file path: {Path}). The content package may be corrupted. You may want to delete or reinstall the package.", e);
break;
}
}
@@ -652,15 +652,20 @@ namespace Barotrauma
};
}, isCheat: true));
commands.Add(new Command("heal", "heal [character name]: Restore the specified character to full health. If the name parameter is omitted, the controlled character will be healed.", (string[] args) =>
commands.Add(new Command("heal", "heal [character name] [all]: Restore the specified character to full health. If the name parameter is omitted, the controlled character will be healed. By default only heals common afflictions such as physical damage and blood loss: use the \"all\" argument to heal everything, including poisonings/addictions/etc.", (string[] args) =>
{
Character healedCharacter = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args);
bool healAll = args.Length > 1 && args[1].Equals("all", StringComparison.OrdinalIgnoreCase);
Character healedCharacter = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(healAll ? args.Take(args.Length - 1).ToArray() : args);
if (healedCharacter != null)
{
healedCharacter.SetAllDamage(0.0f, 0.0f, 0.0f);
healedCharacter.Oxygen = 100.0f;
healedCharacter.Bloodloss = 0.0f;
healedCharacter.SetStun(0.0f, true);
if (healAll)
{
healedCharacter.CharacterHealth.RemoveAllAfflictions();
}
}
},
() =>
@@ -1449,6 +1454,20 @@ namespace Barotrauma
return new[] { primaries, identifiers };
}));
commands.Add(new Command("setdifficulty|forcedifficulty", "difficulty [0-100]. Leave the parameter empty to disable.", (string[] args) =>
{
if (args.Length == 0)
{
Level.ForcedDifficulty = null;
NewMessage($"Forced difficulty level disabled.", Color.Green);
}
else if (float.TryParse(args[0], out float difficulty))
{
Level.ForcedDifficulty = difficulty;
NewMessage($"Set the difficulty level to { Level.ForcedDifficulty }.", Color.Yellow);
}
}, isCheat: true));
commands.Add(new Command("difficulty|leveldifficulty", "difficulty [0-100]: Change the level difficulty setting in the server lobby.", null));
commands.Add(new Command("autoitemplacerdebug|outfitdebug", "autoitemplacerdebug: Toggle automatic item placer debug info on/off. The automatically placed items are listed in the debug console at the start of a round.", (string[] args) =>
@@ -1592,7 +1611,7 @@ namespace Barotrauma
commands.Add(new Command("control", "control [character name]: Start controlling the specified character (client-only).", null, () =>
{
return new string[][] { ListCharacterNames() };
}));
}, isCheat: true));
commands.Add(new Command("los", "Toggle the line of sight effect on/off (client-only).", null, isCheat: true));
commands.Add(new Command("lighting|lights", "Toggle lighting on/off (client-only).", null, isCheat: true));
commands.Add(new Command("ambientlight", "ambientlight [color]: Change the color of the ambient light in the level.", null, isCheat: true));
@@ -1885,6 +1904,8 @@ namespace Barotrauma
spawnPoint = WayPoint.GetRandom(human ? SpawnType.Human : SpawnType.Enemy);
}
CharacterTeamType teamType;
teamType = args.Length > 2 ? (CharacterTeamType)int.Parse(args[2]) : Character.Controlled != null ? Character.Controlled.TeamID : CharacterTeamType.Team1;
if (string.IsNullOrWhiteSpace(args[0])) { return; }
if (spawnPoint != null) { spawnPosition = spawnPoint.WorldPosition; }
@@ -1896,8 +1917,7 @@ namespace Barotrauma
spawnedCharacter = Character.Create(characterInfo, spawnPosition, ToolBox.RandomSeed(8));
if (GameMain.GameSession != null)
{
//TODO: a way to select which team to spawn to?
spawnedCharacter.TeamID = Character.Controlled != null ? Character.Controlled.TeamID : CharacterTeamType.Team1;
spawnedCharacter.TeamID = teamType;
#if CLIENT
GameMain.GameSession.CrewManager.AddCharacter(spawnedCharacter);
#endif
@@ -240,7 +240,7 @@ namespace Barotrauma
{
TryStartConversation(speaker);
}
else
else if (speaker.ActiveConversation != this)
{
speaker.CampaignInteractionType = CampaignMode.InteractionType.Talk;
speaker.ActiveConversation = this;
@@ -352,6 +352,14 @@ namespace Barotrauma
ShowDialog(speaker, targetCharacter);
dialogOpened = true;
if (speaker != null)
{
speaker.CampaignInteractionType = CampaignMode.InteractionType.None;
speaker.SetCustomInteract(null, null);
#if SERVER
GameMain.NetworkMember.CreateEntityEvent(speaker, new object[] { NetEntityEvent.Type.AssignCampaignInteraction });
#endif
}
}
partial void ShowDialog(Character speaker, Character targetCharacter);
@@ -0,0 +1,68 @@
using Barotrauma.Networking;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
class NPCChangeTeamAction : EventAction
{
[Serialize("", true)]
public string NPCTag { get; set; }
[Serialize(0, true)]
public int TeamTag { get; set; }
[Serialize(false, true)]
public bool AddToCrew { get; set; }
private bool isFinished = false;
public NPCChangeTeamAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { }
private List<Character> affectedNpcs = null;
public override void Update(float deltaTime)
{
if (isFinished) { return; }
affectedNpcs = ParentEvent.GetTargets(NPCTag).Where(c => c is Character).Select(c => c as Character).ToList();
foreach (var npc in affectedNpcs)
{
CharacterTeamType newTeam = (CharacterTeamType)TeamTag;
// characters will still remain on friendlyNPC team for rest of the tick
npc.SetOriginalTeam(newTeam);
if (AddToCrew && (newTeam == CharacterTeamType.Team1 || newTeam == CharacterTeamType.Team2))
{
npc.Info.StartItemsGiven = true;
GameMain.GameSession.CrewManager.AddCharacter(npc);
foreach (Item item in npc.Inventory.AllItems)
{
item.AllowStealing = true;
}
#if SERVER
GameMain.NetworkMember.CreateEntityEvent(npc, new object[] { NetEntityEvent.Type.AddToCrew });
#endif
}
}
isFinished = true;
}
public override bool IsFinished(ref string goTo)
{
return isFinished;
}
public override void Reset()
{
isFinished = false;
}
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(NPCChangeTeamAction)} -> (NPCTag: {NPCTag.ColorizeObject()})";
}
}
}
@@ -102,12 +102,12 @@ namespace Barotrauma
public override void Update(float deltaTime)
{
if (spawned) { return; }
if (!string.IsNullOrEmpty(NPCSetIdentifier) && !string.IsNullOrEmpty(NPCIdentifier))
{
HumanPrefab humanPrefab = NPCSet.Get(NPCSetIdentifier, NPCIdentifier);
ISpatialEntity spawnPos = GetSpawnPos();
Entity.Spawner.AddToSpawnQueue(CharacterPrefab.HumanSpeciesName, OffsetSpawnPos(spawnPos?.WorldPosition ?? Vector2.Zero, 100.0f), onSpawn: newCharacter =>
Entity.Spawner.AddToSpawnQueue(CharacterPrefab.HumanSpeciesName, OffsetSpawnPos(spawnPos?.WorldPosition ?? Vector2.Zero, 100.0f), humanPrefab.GetCharacterInfo(), onSpawn: newCharacter =>
{
newCharacter.TeamID = CharacterTeamType.FriendlyNPC;
newCharacter.EnableDespawn = false;
@@ -255,10 +255,9 @@ namespace Barotrauma
potentialSpawnPoints = potentialSpawnPoints.FindAll(wp => wp.ConnectedDoor == null && wp.Ladders == null && !wp.isObstructed);
var airlockSpawnPoints = potentialSpawnPoints.Where(wp => wp.CurrentHull?.OutpostModuleTags?.Contains("airlock") ?? false).ToList();
if (moduleFlags != null && moduleFlags.Any())
{
List<WayPoint> spawnPoints = potentialSpawnPoints.Where(wp => wp.CurrentHull?.OutpostModuleTags?.Any(moduleFlags.Contains) ?? false).ToList();
List<WayPoint> spawnPoints = potentialSpawnPoints.Where(wp => wp.CurrentHull?.OutpostModuleTags.Any(moduleFlags.Contains) ?? false).ToList();
if (spawnPoints.Any())
{
potentialSpawnPoints = spawnPoints;
@@ -267,8 +266,10 @@ namespace Barotrauma
if (spawnpointTags != null && spawnpointTags.Any())
{
var spawnPoints = potentialSpawnPoints.Where(wp => spawnpointTags.Any(tag => wp.Tags.Contains(tag)))
.Where(wp => wp.ConnectedDoor == null && !wp.isObstructed);
var spawnPoints = potentialSpawnPoints
.Where(wp => spawnpointTags.Any(tag => wp.Tags.Contains(tag)))
.Where(wp => wp.ConnectedDoor == null && !wp.isObstructed);
if (spawnPoints.Any())
{
potentialSpawnPoints = spawnPoints.ToList();
@@ -293,6 +294,7 @@ namespace Barotrauma
}
//don't spawn in an airlock module if there are other options
var airlockSpawnPoints = potentialSpawnPoints.Where(wp => wp.CurrentHull?.OutpostModuleTags.Contains("airlock") ?? false);
if (airlockSpawnPoints.Count() < validSpawnPoints.Count())
{
validSpawnPoints = validSpawnPoints.Except(airlockSpawnPoints);
@@ -1,3 +1,4 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System.Linq;
using System.Xml.Linq;
@@ -30,6 +31,9 @@ namespace Barotrauma
[Serialize(true, true, description: "If true, dead/unconscious characters cannot trigger the action.")]
public bool DisableIfTargetIncapacitated { get; set; }
[Serialize(false, true, description: "If true, one target must interact with the other to trigger the action.")]
public bool WaitForInteraction { get; set; }
private float distance;
public TriggerAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element)
@@ -44,12 +48,15 @@ namespace Barotrauma
}
public override void Reset()
{
ResetTargetIcons();
isRunning = false;
isFinished = false;
}
public bool isRunning = false;
private Either<Character, Item> npcOrItem = null;
public override void Update(float deltaTime)
{
if (isFinished) { return; }
@@ -81,20 +88,101 @@ namespace Barotrauma
if (DisableInCombat && IsInCombat(e2)) { continue; }
if (DisableIfTargetIncapacitated && e2 is Character character2 && (character2.IsDead || character2.IsIncapacitated)) { continue; }
Vector2 pos1 = e1.WorldPosition;
Vector2 pos2 = e2.WorldPosition;
distance = Vector2.Distance(pos1, pos2);
if (((e1 is MapEntity m1) && Submarine.RectContains(m1.WorldRect, pos2)) ||
((e2 is MapEntity m2) && Submarine.RectContains(m2.WorldRect, pos1)) ||
Vector2.DistanceSquared(pos1, pos2) < Radius * Radius)
if (WaitForInteraction)
{
Trigger(e1, e2);
return;
Character player = null;
Character npc = null;
Item item = null;
npcOrItem?.TryGet(out npc);
npcOrItem?.TryGet(out item);
if (e1 is Character char1)
{
if (char1.IsBot) { npc ??= char1; }
else { player = char1; }
}
else
{
item ??= e1 as Item;
}
if (e2 is Character char2)
{
if (char2.IsBot) { npc ??= char2; }
else { player = char2; }
}
else
{
item ??= e2 as Item;
}
if (player != null)
{
if (npc != null)
{
if (npc.CampaignInteractionType != CampaignMode.InteractionType.Examine)
{
npcOrItem = npc;
npc.CampaignInteractionType = CampaignMode.InteractionType.Examine;
#if CLIENT
npc.SetCustomInteract(
Trigger,
TextManager.GetWithVariable("CampaignInteraction.Examine", "[key]", GameMain.Config.KeyBindText(InputType.Use)));
#else
npc.SetCustomInteract(
Trigger,
TextManager.Get("CampaignInteraction.Talk"));
GameMain.NetworkMember.CreateEntityEvent(npc, new object[] { NetEntityEvent.Type.AssignCampaignInteraction });
#endif
npc.RequireConsciousnessForCustomInteract = false;
}
return;
}
else if (item != null)
{
npcOrItem = item;
item.CampaignInteractionType = CampaignMode.InteractionType.Examine;
if (player.SelectedConstruction == item ||
player.Inventory.Contains(item) ||
(player.FocusedItem == item && player.IsKeyHit(InputType.Use)))
{
Trigger(e1, e2);
return;
}
}
}
}
else
{
Vector2 pos1 = e1.WorldPosition;
Vector2 pos2 = e2.WorldPosition;
distance = Vector2.Distance(pos1, pos2);
if (((e1 is MapEntity m1) && Submarine.RectContains(m1.WorldRect, pos2)) ||
((e2 is MapEntity m2) && Submarine.RectContains(m2.WorldRect, pos1)) ||
Vector2.DistanceSquared(pos1, pos2) < Radius * Radius)
{
Trigger(e1, e2);
return;
}
}
}
}
}
private void ResetTargetIcons()
{
if (npcOrItem == null) { return; }
if (npcOrItem.TryGet(out Character npc))
{
npc.CampaignInteractionType = CampaignMode.InteractionType.None;
npc.SetCustomInteract(null, null);
npc.RequireConsciousnessForCustomInteract = true;
}
else if (npcOrItem.TryGet(out Item item))
{
item.CampaignInteractionType = CampaignMode.InteractionType.None;
}
}
private bool IsCloseEnoughToHull(Entity e, out Hull hull)
{
hull = null;
@@ -157,6 +245,7 @@ namespace Barotrauma
private void Trigger(Entity entity1, Entity entity2)
{
ResetTargetIcons();
if (!string.IsNullOrEmpty(ApplyToTarget1))
{
ParentEvent.AddTarget(ApplyToTarget1, entity1);
@@ -174,7 +263,14 @@ namespace Barotrauma
{
if (string.IsNullOrEmpty(TargetModuleType))
{
return $"{ToolBox.GetDebugSymbol(isFinished, isRunning)} {nameof(TriggerAction)} -> (Distance: {((int)distance).ColorizeObject()}, Radius: {Radius.ColorizeObject()}, TargetTags: {Target1Tag.ColorizeObject()}, {Target2Tag.ColorizeObject()})";
return
$"{ToolBox.GetDebugSymbol(isFinished, isRunning)} {nameof(TriggerAction)} -> (" +
(WaitForInteraction ?
$"Selected non-player target: {(npcOrItem?.ToString() ?? "<null>").ColorizeObject()}, " :
$"Distance: {((int)distance).ColorizeObject()}, ") +
$"Radius: {Radius.ColorizeObject()}, " +
$"TargetTags: {Target1Tag.ColorizeObject()}, " +
$"{Target2Tag.ColorizeObject()})";
}
else
{
@@ -50,7 +50,7 @@ namespace Barotrauma
private float calculateDistanceTraveledTimer;
private float distanceTraveled;
private float avgCrewHealth, avgHullIntegrity, floodingAmount, fireAmount, enemyDanger;
private float avgCrewHealth, avgHullIntegrity, floodingAmount, fireAmount, enemyDanger, monsterTotalStrength;
private float roundDuration;
@@ -694,47 +694,103 @@ namespace Barotrauma
// enemy amount --------------------------------------------------------
enemyDanger = 0.0f;
monsterTotalStrength = 0;
foreach (Character character in Character.CharacterList)
{
if (character.IsDead || character.IsIncapacitated || !character.Enabled || character.IsPet || character.Params.CompareGroup("human")) { continue; }
if (character.IsIncapacitated || !character.Enabled || character.IsPet || character.Params.CompareGroup("human")) { continue; }
if (!(character.AIController is EnemyAIController enemyAI)) { continue; }
if (!enemyAI.AIParams.StayInAbyss)
{
// Ignore abyss monsters because they can stay active for quite great distances. They'll be taken into account when they target the sub.
monsterTotalStrength += enemyAI.CombatStrength;
}
// Example combat strengths:
// Hammerheadspawn 1
// Moloch Pupa 1
// Terminal cell 20
// Leucocyte 40
// Husk 90
// Crawler 100
// Unarmored Mudraptor 140
// Spineling 150
// Tigerthresher 200
// Armored Mudraptor 210
// Watcher 400
// Golden Hammerhead 400
// Hammerhead 500
// Hammerhead Matriarch 550
// Bonethresher 600
// Moloch 1250
// Black Moloch 1500
// Endworm 10000
if (character.CurrentHull?.Submarine != null &&
(character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine)))
{
//crawler inside the sub adds 0.1f to enemy danger, mantis 0.25f
enemyDanger += enemyAI.CombatStrength / 100.0f;
// Enemy onboard -> Crawler inside the sub adds 0.2 to enemy danger, Mudraptor 0.42
enemyDanger += enemyAI.CombatStrength / 500.0f;
}
else if (enemyAI.SelectedAiTarget?.Entity?.Submarine != null)
{
//enemy outside and targeting the sub or something in it
//moloch adds 0.24 to enemy danger, a crawler 0.02
enemyDanger += enemyAI.CombatStrength / 1000.0f;
// Enemy outside targeting the sub or something in it
// -> One Crawler adds 0.02, a Mudraptor 0.042, a Hammerhead 0.1, and a Moloch 0.25.
enemyDanger += enemyAI.CombatStrength / 5000.0f;
}
}
// Add a portion of the total strength of active monsters to the enemy danger so that we don't spawn too many monsters around the sub.
// On top of the existing value, so if 10 crawlers are targeting the sub simultaneously from outside, the final value would be: 0.02 x 10 + 0.2 = 0.4.
// And if they get inside, we add 0.1 per crawler on that.
// So, in practice the danger per enemy that is attacking the sub is half of what it would be when the enemy is not targeting the sub.
// 10 Crawlers -> +0.2 (0.4 in total if all target the sub from outside).
// 5 Mudraptors -> +0.21 (0.42 in total, before they get inside).
// 3 Hammerheads -> +0.3 (0.6 in total, if they all target the sub).
// 2 Molochs -> +0.5 (1.0 in total, if both target the sub).
enemyDanger += monsterTotalStrength / 5000f;
enemyDanger = MathHelper.Clamp(enemyDanger, 0.0f, 1.0f);
// The definitions above aim for that we never spawn more monsters that the player (and the performance) can handle.
// Some examples that result in the max intensity even when the creatures would just idle around.
// The values are theoretical, because in practice many of the monsters are targeting the sub, which will double the danger of those monster and effectively halve the max monster count.
// In practice we don't use the max intensity. For example on level 50 we use max intensity 50, which would mean that we'd halve the numbers below.
// There's no hard cap for the monster count, but if the amount of monsters is higher than this, we don't spawn more monsters from the events:
// 50 Crawlers (We shouldn't actually ever spawn that many. 12 is the max per event, but theoretically 25 crawlers would result in max intensity).
// 25 Tigerthreshers (Max 9 per event. 12 targeting the sub at the same time results in max intensity).
// 10 Hammerheads (Max 3 per event. 5 targeting the sub at the same time results in max intensity).
// 4 Molochs (Max 2 per event and 2 targeting the sub at the same time results in max intensity).
// hull status (gaps, flooding, fire) --------------------------------------------------------
float holeCount = 0.0f;
float waterAmount = 0.0f;
float totalHullVolume = 0.0f;
float dryHullVolume = 0.0f;
foreach (Hull hull in Hull.hullList)
{
if (hull.Submarine == null || hull.Submarine.Info.Type != SubmarineType.Player) { continue; }
if (hull.RoomName != null && hull.RoomName.Contains("ballast", StringComparison.OrdinalIgnoreCase)) { continue; }
if (hull.Submarine == null || hull.Submarine.Info.Type != SubmarineType.Player) { continue; }
if (GameMain.GameSession?.GameMode is PvPMode)
{
if (hull.Submarine.TeamID != CharacterTeamType.Team1 && hull.Submarine.TeamID != CharacterTeamType.Team2) { continue; }
}
else
{
if (hull.Submarine.TeamID != CharacterTeamType.Team1) { continue; }
}
fireAmount += hull.FireSources.Sum(fs => fs.Size.X);
if (hull.IsWetRoom) { continue; }
foreach (Gap gap in hull.ConnectedGaps)
{
if (!gap.IsRoomToRoom) holeCount += gap.Open;
if (!gap.IsRoomToRoom)
{
holeCount += gap.Open;
}
}
waterAmount += hull.WaterVolume;
totalHullVolume += hull.Volume;
fireAmount += hull.FireSources.Sum(fs => fs.Size.X);
dryHullVolume += hull.Volume;
}
if (totalHullVolume > 0)
if (dryHullVolume > 0)
{
floodingAmount = waterAmount / totalHullVolume;
floodingAmount = waterAmount / dryHullVolume;
}
//hull integrity at 0.0 if there are 10 or more wide-open holes
@@ -149,7 +149,7 @@ namespace Barotrauma
OncePerOutpost = element.GetAttributeBool("onceperoutpost", false);
TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", true);
Commonness[""] = 1.0f;
Commonness[""] = element.GetAttributeFloat("commonness", 1.0f);
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
@@ -35,8 +35,8 @@ namespace Barotrauma
protected bool wasDocked;
public AbandonedOutpostMission(MissionPrefab prefab, Location[] locations) :
base(prefab, locations)
public AbandonedOutpostMission(MissionPrefab prefab, Location[] locations, Submarine sub) :
base(prefab, locations, sub)
{
characterConfig = prefab.ConfigElement.Element("Characters");
@@ -84,14 +84,7 @@ namespace Barotrauma
if (element.Attribute("identifier") != null && element.Attribute("from") != null)
{
string characterIdentifier = element.GetAttributeString("identifier", "");
string characterFrom = element.GetAttributeString("from", "");
HumanPrefab humanPrefab = NPCSet.Get(characterFrom, characterIdentifier);
if (humanPrefab == null)
{
DebugConsole.ThrowError("Couldn't spawn a character for abandoned outpost mission: character prefab \"" + characterIdentifier + "\" not found");
continue;
}
HumanPrefab humanPrefab = CreateHumanPrefabFromElement(element);
for (int i = 0; i < count; i++)
{
LoadHuman(humanPrefab, element, submarine);
@@ -128,32 +121,27 @@ namespace Barotrauma
spawnPos = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandom();
}
var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: humanPrefab.GetJobPrefab(Rand.RandSync.Server), randSync: Rand.RandSync.Server);
Character spawnedCharacter = Character.Create(characterInfo.SpeciesName, spawnPos.WorldPosition, ToolBox.RandomSeed(8), characterInfo, createNetworkEvent: false);
if (element.GetAttributeBool("requirerescue", false))
{
requireRescue.Add(spawnedCharacter);
spawnedCharacter.TeamID = CharacterTeamType.FriendlyNPC;
#if CLIENT
GameMain.GameSession.CrewManager.AddCharacterToCrewList(spawnedCharacter);
#endif
}
else
{
spawnedCharacter.TeamID = CharacterTeamType.None;
}
humanPrefab.InitializeCharacter(spawnedCharacter, spawnPos);
humanPrefab.GiveItems(spawnedCharacter, Submarine.MainSub, Rand.RandSync.Server, createNetworkEvents: false);
bool requiresRescue = element.GetAttributeBool("requirerescue", false);
Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, requiresRescue ? CharacterTeamType.FriendlyNPC : CharacterTeamType.None, spawnPos, giveTags: true);
if (spawnPos is WayPoint wp)
{
spawnedCharacter.GiveIdCardTags(wp);
}
if (requiresRescue)
{
requireRescue.Add(spawnedCharacter);
#if CLIENT
GameMain.GameSession.CrewManager.AddCharacterToCrewList(spawnedCharacter);
#endif
}
if (element.GetAttributeBool("requirekill", false))
{
requireKill.Add(spawnedCharacter);
}
characters.Add(spawnedCharacter);
characterItems.Add(spawnedCharacter, spawnedCharacter.Inventory.FindAllItems(recursive: true));
}
private void LoadMonster(CharacterPrefab monsterPrefab, XElement element, Submarine submarine)
@@ -13,7 +13,7 @@ namespace Barotrauma
private Point monsterCountRange;
private readonly string sonarLabel;
public BeaconMission(MissionPrefab prefab, Location[] locations) : base(prefab, locations)
public BeaconMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub)
{
swarmSpawned = false;
@@ -17,15 +17,97 @@ namespace Barotrauma
private int requiredDeliveryAmount;
public CargoMission(MissionPrefab prefab, Location[] locations)
: base(prefab, locations)
private readonly List<(XElement element, ItemContainer container)> itemsToSpawn = new List<(XElement element, ItemContainer container)>();
private int? rewardPerCrate;
private int calculatedReward;
private int maxItemCount;
private Submarine sub;
public CargoMission(MissionPrefab prefab, Location[] locations, Submarine sub)
: base(prefab, locations, sub)
{
this.sub = sub;
itemConfig = prefab.ConfigElement.Element("Items");
requiredDeliveryAmount = prefab.ConfigElement.GetAttributeInt("requireddeliveryamount", 0);
DetermineCargo();
}
private void DetermineCargo()
{
if (this.sub == null || itemConfig == null)
{
calculatedReward = Prefab.Reward;
return;
}
itemsToSpawn.Clear();
List<(ItemContainer container, int freeSlots)> containers = sub.GetCargoContainers();
containers.Sort((c1, c2) => { return c2.container.Capacity.CompareTo(c1.container.Capacity); });
maxItemCount = 0;
foreach (XElement subElement in itemConfig.Elements())
{
int maxCount = subElement.GetAttributeInt("maxcount", 10);
maxItemCount += maxCount;
}
for (int i = 0; i < containers.Count; i++)
{
foreach (XElement subElement in itemConfig.Elements())
{
int maxCount = subElement.GetAttributeInt("maxcount", 10);
if (itemsToSpawn.Count(it => it.element == subElement) >= maxCount) { continue; }
ItemPrefab itemPrefab = FindItemPrefab(subElement);
while (containers[i].freeSlots > 0 && containers[i].container.Inventory.CanBePut(itemPrefab))
{
containers[i] = (containers[i].container, containers[i].freeSlots - 1);
itemsToSpawn.Add((subElement, containers[i].container));
if (itemsToSpawn.Count(it => it.element == subElement) >= maxCount) { break; }
}
}
}
if (!itemsToSpawn.Any())
{
itemsToSpawn.Add((itemConfig.Elements().First(), null));
}
calculatedReward = 0;
foreach (var itemToSpawn in itemsToSpawn)
{
int price = itemToSpawn.element.GetAttributeInt("reward", Prefab.Reward / itemsToSpawn.Count);
if (rewardPerCrate.HasValue)
{
if (price != rewardPerCrate.Value) { rewardPerCrate = -1; }
}
else
{
rewardPerCrate = price;
}
calculatedReward += price;
}
if (rewardPerCrate.HasValue && rewardPerCrate < 0) { rewardPerCrate = null; }
string rewardText = $"‖color:gui.orange‖{string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:N0}", GetReward(sub))}‖end‖";
if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); }
}
public override int GetReward(Submarine sub)
{
if (sub != this.sub)
{
this.sub = sub;
DetermineCargo();
}
return calculatedReward;
}
private void InitItems()
{
this.sub = Submarine.MainSub;
DetermineCargo();
items.Clear();
parentInventoryIDs.Clear();
parentItemContainerIndices.Clear();
@@ -36,9 +118,9 @@ namespace Barotrauma
return;
}
foreach (XElement subElement in itemConfig.Elements())
foreach (var (element, container) in itemsToSpawn)
{
LoadItemAsChild(subElement, null);
LoadItemAsChild(element, container?.Item);
}
if (requiredDeliveryAmount == 0) { requiredDeliveryAmount = items.Count; }
@@ -49,7 +131,7 @@ namespace Barotrauma
}
}
private void LoadItemAsChild(XElement element, Item parent)
private ItemPrefab FindItemPrefab(XElement element)
{
ItemPrefab itemPrefab;
if (element.Attribute("name") != null)
@@ -60,7 +142,6 @@ namespace Barotrauma
if (itemPrefab == null)
{
DebugConsole.ThrowError("Couldn't spawn item for cargo mission: item prefab \"" + itemName + "\" not found");
return;
}
}
else
@@ -70,15 +151,15 @@ namespace Barotrauma
if (itemPrefab == null)
{
DebugConsole.ThrowError("Couldn't spawn item for cargo mission: item prefab \"" + itemIdentifier + "\" not found");
return;
}
}
return itemPrefab;
}
if (itemPrefab == null)
{
DebugConsole.ThrowError("Couldn't spawn item for cargo mission: item prefab \"" + element.Name.ToString() + "\" not found");
return;
}
private void LoadItemAsChild(XElement element, Item parent)
{
ItemPrefab itemPrefab = FindItemPrefab(element);
WayPoint cargoSpawnPos = WayPoint.GetRandom(SpawnType.Cargo, null, Submarine.MainSub, useSyncedRand: true);
if (cargoSpawnPos == null)
@@ -88,7 +169,6 @@ namespace Barotrauma
}
var cargoRoom = cargoSpawnPos.CurrentHull;
if (cargoRoom == null)
{
DebugConsole.ThrowError("A waypoint marked as Cargo must be placed inside a room!");
@@ -1,5 +1,4 @@
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using System.Collections.Generic;
namespace Barotrauma
@@ -7,6 +6,7 @@ namespace Barotrauma
partial class CombatMission : Mission
{
private Submarine[] subs;
// TODO: not used
private List<Character>[] crews;
private readonly string[] descriptions;
@@ -45,8 +45,8 @@ namespace Barotrauma
}
}
public CombatMission(MissionPrefab prefab, Location[] locations)
: base(prefab, locations)
public CombatMission(MissionPrefab prefab, Location[] locations, Submarine sub)
: base(prefab, locations, sub)
{
descriptions = new string[]
{
@@ -0,0 +1,301 @@
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
partial class EscortMission : Mission
{
private readonly XElement characterConfig;
private readonly XElement itemConfig;
private readonly List<Character> characters = new List<Character>();
private readonly Dictionary<Character, List<Item>> characterDictionary = new Dictionary<Character, List<Item>>();
private readonly int baseEscortedCharacters;
private readonly float scalingEscortedCharacters;
private readonly float terroristChance;
private Character vipCharacter;
private readonly List<Character> terroristCharacters = new List<Character>();
private bool terroristsShouldAct = false;
private float terroristDistanceSquared;
private const string TerroristTeamChangeIdentifier = "terrorist";
public EscortMission(MissionPrefab prefab, Location[] locations, Submarine sub)
: base(prefab, locations, sub)
{
characterConfig = prefab.ConfigElement.Element("Characters");
// Should reflect different escortables, prisoners, VIPs, passengers (where does this comment refer to?)
baseEscortedCharacters = prefab.ConfigElement.GetAttributeInt("baseescortedcharacters", 1);
scalingEscortedCharacters = prefab.ConfigElement.GetAttributeFloat("scalingescortedcharacters", 0);
terroristChance = prefab.ConfigElement.GetAttributeFloat("terroristchance", 0);
itemConfig = prefab.ConfigElement.Element("TerroristItems");
}
public override int Reward
{
get
{
int multiplier = CalculateScalingEscortedCharacterCount();
return Prefab.Reward * multiplier;
}
}
int CalculateScalingEscortedCharacterCount(bool inMission = false)
{
if (Submarine.MainSub == null || Submarine.MainSub.Info == null) // UI logic failing to get the correct value is not important, but the mission logic must succeed
{
if (inMission)
{
DebugConsole.ThrowError("MainSub was null when trying to retrieve submarine size for determining escorted character count!");
}
return 1;
}
return (int)Math.Round(baseEscortedCharacters + scalingEscortedCharacters * Submarine.MainSub.Info.RecommendedCrewSizeMin);
}
private void InitEscort()
{
characters.Clear();
characterDictionary.Clear();
// VIP transport mission characters stay in the same location; other characters roam at will
// could be replaced with a designated waypoint for VIPs, such as cargo or crew
WayPoint explicitStayInHullPos = WayPoint.GetRandom(SpawnType.Human, null, Submarine.MainSub);
Rand.RandSync randSync = Rand.RandSync.Server;
if (terroristChance > 0f)
{
// in terrorist missions, reroll characters each retry to avoid confusion as to who the terrorists are
randSync = Rand.RandSync.Unsynced;
}
foreach (XElement element in characterConfig.Elements())
{
int count = CalculateScalingEscortedCharacterCount(inMission: true);
for (int i = 0; i < count; i++)
{
Character spawnedCharacter = CreateHuman(CreateHumanPrefabFromElement(element), characters, characterDictionary, Submarine.MainSub, CharacterTeamType.FriendlyNPC, explicitStayInHullPos, humanPrefabRandSync: randSync);
if (spawnedCharacter.AIController is HumanAIController humanAI)
{
humanAI.InitMentalStateManager();
}
}
}
}
private void InitCharacters()
{
int scalingCharacterCount = CalculateScalingEscortedCharacterCount(inMission: true);
if (scalingCharacterCount * characterConfig.Elements().Count() != characters.Count)
{
DebugConsole.AddWarning("Character count did not match expected character count in InitCharacters of EscortMission");
return;
}
int i = 0;
foreach (XElement element in characterConfig.Elements())
{
string escortIdentifier = element.GetAttributeString("escortidentifier", string.Empty);
string colorIdentifier = element.GetAttributeString("color", string.Empty);
for (int k = 0; k < scalingCharacterCount; k++)
{
// for each element defined, we need to initialize that type of character equal to the scaling escorted character count
characters[k + i].IsEscorted = true;
if (escortIdentifier != string.Empty)
{
if (escortIdentifier == "vip")
{
vipCharacter = characters[k + i];
}
}
characters[k + i].UniqueNameColor = element.GetAttributeColor("color", Color.LightGreen);
}
i++;
}
if (!IsClient && terroristChance > 0f)
{
int terroristCount = (int)Math.Ceiling(terroristChance * Rand.Range(0.8f, 1.2f) * characters.Count);
terroristCount = Math.Clamp(terroristCount, 1, characters.Count);
terroristCharacters.Clear();
characters.Shuffle();
characters.GetRange(0, terroristCount).ForEach(c => terroristCharacters.Add(c));
terroristDistanceSquared = Vector2.DistanceSquared(Level.Loaded.StartPosition, Level.Loaded.EndPosition) * Rand.Range(0.35f, 0.65f);
#if DEBUG
DebugConsole.AddWarning("Terrorists will trigger at range " + Math.Sqrt(terroristDistanceSquared));
foreach (Character character in terroristCharacters)
{
DebugConsole.AddWarning(character.Name + " is a terrorist.");
}
#endif
}
}
protected override void StartMissionSpecific(Level level)
{
if (characters.Count > 0)
{
#if DEBUG
throw new Exception($"characters.Count > 0 ({characters.Count})");
#else
DebugConsole.AddWarning("Character list was not empty at the start of a escort mission. The mission instance may not have been ended correctly on previous rounds.");
characters.Clear();
#endif
}
if (characterConfig == null)
{
DebugConsole.ThrowError("Failed to initialize characters for escort mission (characterConfig == null)");
return;
}
if (!IsClient)
{
InitEscort();
InitCharacters();
}
}
void TryToTriggerTerrorists()
{
if (terroristsShouldAct)
{
// decoupled from range check to prevent from weirdness if players handcuff a terrorist and move backwards
foreach (Character character in terroristCharacters)
{
if (character.HasTeamChange(TerroristTeamChangeIdentifier))
{
// already triggered
continue;
}
if (IsAlive(character) && !character.IsIncapacitated && !character.LockHands)
{
character.TryAddNewTeamChange(TerroristTeamChangeIdentifier, new ActiveTeamChange(CharacterTeamType.None, ActiveTeamChange.TeamChangePriorities.Willful, aggressiveBehavior: true));
character.Speak(TextManager.Get("dialogterroristannounce"), null, Rand.Range(0.5f, 3f));
XElement randomElement = itemConfig.Elements().GetRandom(e => e.GetAttributeFloat(0f, "mindifficulty") <= Level.Loaded.Difficulty);
if (randomElement != null)
{
HumanPrefab.InitializeItem(character, randomElement, character.Submarine, humanPrefab: null, createNetworkEvents: true);
}
}
}
}
else if (Vector2.DistanceSquared(Submarine.MainSub.WorldPosition, Level.Loaded.EndPosition) < terroristDistanceSquared)
{
foreach (Character character in terroristCharacters)
{
if (character.AIController is HumanAIController humanAI)
{
humanAI.ObjectiveManager.AddObjective(new AIObjectiveEscapeHandcuffs(character, humanAI.ObjectiveManager, shouldSwitchTeams: false, beginInstantly: true));
}
}
terroristsShouldAct = true;
}
}
bool NonTerroristsStillAlive(IEnumerable<Character> characterList)
{
return characterList.Any(c => !terroristCharacters.Contains(c) && IsAlive(c));
}
public override void Update(float deltaTime)
{
if (!IsClient)
{
int newState = State;
TryToTriggerTerrorists();
switch (State)
{
case 0: // base
if (!NonTerroristsStillAlive(characters))
{
newState = 1;
}
if (terroristCharacters.Any() && terroristCharacters.All(c => !IsAlive(c)))
{
newState = 2;
}
break;
case 1: // failure
break;
case 2: // terrorists killed
if (!NonTerroristsStillAlive(characters))
{
newState = 1;
}
break;
}
State = newState;
}
}
private bool Survived(Character character)
{
return IsAlive(character) && character.CurrentHull != null && character.CurrentHull.Submarine == Submarine.MainSub;
}
private bool IsAlive(Character character)
{
return character != null && !character.Removed && !character.IsDead;
}
private bool IsCaptured(Character character)
{
return character.LockHands && character.HasTeamChange(TerroristTeamChangeIdentifier);
}
public override void End()
{
if (Submarine.MainSub != null && Submarine.MainSub.AtEndExit)
{
bool terroristsSurvived = terroristCharacters.Any(c => Survived(c) && !IsCaptured(c));
bool friendliesSurvived = characters.Except(terroristCharacters).Any(c => Survived(c));
bool vipDied = false;
if (vipCharacter != null)
{
vipDied = !Survived(vipCharacter);
}
if (friendliesSurvived && !terroristsSurvived && !vipDied)
{
GiveReward();
completed = true;
}
}
// characters that survived will take their items with them, in case players tried to be crafty and steal them
// this needs to run here in case players abort the mission by going back home
// TODO: I think this might feel like a bug.
foreach (var characterItem in characterDictionary)
{
if (Survived(characterItem.Key) || !completed)
{
foreach (Item item in characterItem.Value)
{
if (!item.Removed)
{
item.Remove();
}
}
}
}
characters.Clear();
characterDictionary.Clear();
failed = !completed;
}
}
}
@@ -26,7 +26,7 @@ namespace Barotrauma
}
}
public MineralMission(MissionPrefab prefab, Location[] locations) : base(prefab, locations)
public MineralMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub)
{
var configElement = prefab.ConfigElement.Element("Items");
foreach (var c in configElement.GetChildElements("Item"))
@@ -3,6 +3,7 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
@@ -61,14 +62,19 @@ namespace Barotrauma
//private set { description = value; }
}
protected string descriptionWithoutReward;
public virtual bool AllowUndocking
{
get { return true; }
}
public int Reward
public virtual int Reward
{
get { return Prefab.Reward; }
get
{
return Prefab.Reward;
}
}
public Dictionary<string, float> ReputationRewards
@@ -92,6 +98,16 @@ namespace Barotrauma
get { return true; }
}
public virtual int TeamCount
{
get { return 1; }
}
public virtual SubmarineInfo EnemySubmarineInfo
{
get { return null; }
}
public virtual IEnumerable<Vector2> SonarPositions
{
get { return Enumerable.Empty<Vector2>(); }
@@ -113,7 +129,7 @@ namespace Barotrauma
get { return Prefab.Difficulty; }
}
public Mission(MissionPrefab prefab, Location[] locations)
public Mission(MissionPrefab prefab, Location[] locations, Submarine sub)
{
System.Diagnostics.Debug.Assert(locations.Length == 2);
@@ -138,8 +154,12 @@ namespace Barotrauma
Messages[m] = Messages[m].Replace("[location" + (n + 1) + "]", locationName);
}
}
string rewardText = $"‖color:gui.orange‖{string.Format(CultureInfo.InvariantCulture, "{0:N0}", Reward)}‖end‖";
if (description != null) { description = description.Replace("[reward]", rewardText); }
string rewardText = $"‖color:gui.orange‖{string.Format(CultureInfo.InvariantCulture, "{0:N0}", GetReward(sub))}‖end‖";
if (description != null)
{
descriptionWithoutReward = description;
description = description.Replace("[reward]", rewardText);
}
if (successMessage != null) { successMessage = successMessage.Replace("[reward]", rewardText); }
if (failureMessage != null) { failureMessage = failureMessage.Replace("[reward]", rewardText); }
for (int m = 0; m < Messages.Count; m++)
@@ -181,7 +201,7 @@ namespace Barotrauma
{
if (randomNumber <= missionPrefab.Commonness)
{
return missionPrefab.Instantiate(locations);
return missionPrefab.Instantiate(locations, Submarine.MainSub);
}
randomNumber -= missionPrefab.Commonness;
}
@@ -189,6 +209,11 @@ namespace Barotrauma
return null;
}
public virtual int GetReward(Submarine sub)
{
return Prefab.Reward;
}
public void Start(Level level)
{
#if CLIENT
@@ -232,7 +257,7 @@ namespace Barotrauma
public void GiveReward()
{
if (!(GameMain.GameSession.GameMode is CampaignMode campaign)) { return; }
campaign.Money += Reward;
campaign.Money += GetReward(Submarine.MainSub);
foreach (KeyValuePair<string, float> reputationReward in ReputationRewards)
{
@@ -287,5 +312,48 @@ namespace Barotrauma
}
public virtual void AdjustLevelData(LevelData levelData) { }
// putting these here since both escort and pirate missions need them. could be tucked away into another class that they can inherit from (or use composition)
protected HumanPrefab CreateHumanPrefabFromElement(XElement element)
{
HumanPrefab humanPrefab = null;
if (element.Attribute("name") != null)
{
DebugConsole.ThrowError("Error in mission \"" + Name + "\" - use character identifiers instead of names to configure the characters.");
return null;
}
string characterIdentifier = element.GetAttributeString("identifier", "");
string characterFrom = element.GetAttributeString("from", "");
humanPrefab = NPCSet.Get(characterFrom, characterIdentifier);
if (humanPrefab == null)
{
DebugConsole.ThrowError("Couldn't spawn character for mission: character prefab \"" + characterIdentifier + "\" not found");
return null;
}
return humanPrefab;
}
protected Character CreateHuman(HumanPrefab humanPrefab, List<Character> characters, Dictionary<Character, List<Item>> characterItems, Submarine submarine, CharacterTeamType teamType, ISpatialEntity positionToStayIn = null, Rand.RandSync humanPrefabRandSync = Rand.RandSync.Server, bool giveTags = true)
{
if (positionToStayIn == null)
{
positionToStayIn = WayPoint.GetRandom(SpawnType.Human, null, submarine);
}
var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: humanPrefab.GetJobPrefab(humanPrefabRandSync), randSync: humanPrefabRandSync);
characterInfo.TeamID = teamType;
Character spawnedCharacter = Character.Create(characterInfo.SpeciesName, positionToStayIn.WorldPosition, ToolBox.RandomSeed(8), characterInfo, createNetworkEvent: false);
humanPrefab.InitializeCharacter(spawnedCharacter, positionToStayIn);
humanPrefab.GiveItems(spawnedCharacter, submarine, Rand.RandSync.Server, createNetworkEvents: false);
characters.Add(spawnedCharacter);
characterItems.Add(spawnedCharacter, spawnedCharacter.Inventory.FindAllItems(recursive: true));
return spawnedCharacter;
}
}
}
@@ -20,8 +20,9 @@ namespace Barotrauma
Combat = 0x40,
OutpostDestroy = 0x80,
OutpostRescue = 0x100,
All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | Combat | OutpostDestroy | OutpostRescue
Escort = 0x200,
Pirate = 0x400,
All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | Combat | OutpostDestroy | OutpostRescue | Escort | Pirate
}
partial class MissionPrefab
@@ -38,6 +39,8 @@ namespace Barotrauma
{ MissionType.Mineral, typeof(MineralMission) },
{ MissionType.OutpostDestroy, typeof(OutpostDestroyMission) },
{ MissionType.OutpostRescue, typeof(AbandonedOutpostMission) },
{ MissionType.Escort, typeof(EscortMission) },
{ MissionType.Pirate, typeof(PirateMission) }
};
public static readonly Dictionary<MissionType, Type> PvPMissionClasses = new Dictionary<MissionType, Type>()
{
@@ -286,16 +289,20 @@ namespace Barotrauma
if (CoOpMissionClasses.ContainsKey(Type))
{
constructor = CoOpMissionClasses[Type].GetConstructor(new[] { typeof(MissionPrefab), typeof(Location[]) });
constructor = CoOpMissionClasses[Type].GetConstructor(new[] { typeof(MissionPrefab), typeof(Location[]), typeof(Submarine) });
}
else if (PvPMissionClasses.ContainsKey(Type))
{
constructor = PvPMissionClasses[Type].GetConstructor(new[] { typeof(MissionPrefab), typeof(Location[]) });
constructor = PvPMissionClasses[Type].GetConstructor(new[] { typeof(MissionPrefab), typeof(Location[]), typeof(Submarine) });
}
else
{
DebugConsole.ThrowError("Error in mission prefab \"" + Name + "\" - unsupported mission type \"" + Type.ToString() + "\"");
}
if (constructor == null)
{
DebugConsole.ThrowError($"Failed to find a constructor for the mission type \"{Type}\"!");
}
InitProjSpecific(element);
}
@@ -333,9 +340,9 @@ namespace Barotrauma
return false;
}
public Mission Instantiate(Location[] locations)
public Mission Instantiate(Location[] locations, Submarine sub)
{
return constructor?.Invoke(new object[] { this, locations }) as Mission;
return constructor?.Invoke(new object[] { this, locations, sub }) as Mission;
}
}
}
@@ -33,8 +33,8 @@ namespace Barotrauma
}
}
public MonsterMission(MissionPrefab prefab, Location[] locations)
: base(prefab, locations)
public MonsterMission(MissionPrefab prefab, Location[] locations, Submarine sub)
: base(prefab, locations, sub)
{
string speciesName = prefab.ConfigElement.GetAttributeString("monsterfile", null);
if (!string.IsNullOrEmpty(speciesName))
@@ -46,8 +46,8 @@ namespace Barotrauma
}
}
public NestMission(MissionPrefab prefab, Location[] locations)
: base(prefab, locations)
public NestMission(MissionPrefab prefab, Location[] locations, Submarine sub)
: base(prefab, locations, sub)
{
itemConfig = prefab.ConfigElement.Element("Items");
@@ -49,8 +49,8 @@ namespace Barotrauma
}
}
public OutpostDestroyMission(MissionPrefab prefab, Location[] locations) :
base(prefab, locations)
public OutpostDestroyMission(MissionPrefab prefab, Location[] locations, Submarine sub) :
base(prefab, locations, sub)
{
itemConfig = prefab.ConfigElement.Element("Items");
itemTag = prefab.ConfigElement.GetAttributeString("targetitem", "");
@@ -96,10 +96,10 @@ namespace Barotrauma
spawnPoint = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandom();
}
Vector2 spawnPos = spawnPoint.WorldPosition;
if (spawnPoint is WayPoint wp && wp.CurrentHull != null)
if (spawnPoint is WayPoint wp && wp.CurrentHull != null && wp.CurrentHull.Rect.Width > 100)
{
spawnPos = new Vector2(
MathHelper.Clamp(wp.WorldPosition.X + Rand.Range(-200, 200), wp.CurrentHull.WorldRect.X, wp.CurrentHull.WorldRect.Right),
MathHelper.Clamp(wp.WorldPosition.X + Rand.Range(-200, 200), wp.CurrentHull.WorldRect.X + 50, wp.CurrentHull.WorldRect.Right - 50),
wp.CurrentHull.WorldRect.Y - wp.CurrentHull.Rect.Height + 16.0f);
}
var item = new Item(itemPrefab, spawnPos, null);
@@ -0,0 +1,290 @@
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
partial class PirateMission : Mission
{
private readonly XElement characterConfig;
private readonly XElement submarineConfig;
private Submarine enemySub;
private Item reactorItem;
private readonly List<Character> characters = new List<Character>();
private readonly Dictionary<Character, List<Item>> characterDictionary = new Dictionary<Character, List<Item>>();
// Update the last sighting periodically so that the players can find the pirate sub even if they have lost the track of it.
private readonly float pirateSightingUpdateFrequency = 30;
private float pirateSightingUpdateTimer;
private Vector2? lastSighting;
public override int TeamCount => 2;
private bool outsideOfSonarRange;
private readonly List<Vector2> patrolPositions = new List<Vector2>();
public override IEnumerable<Vector2> SonarPositions
{
get
{
var empty = Enumerable.Empty<Vector2>();
if (outsideOfSonarRange)
{
return State switch
{
0 => patrolPositions,
1 => lastSighting.HasValue ? lastSighting.Value.ToEnumerable() : empty,
_ => empty,
};
}
else
{
return empty;
}
}
}
private SubmarineInfo submarineInfo;
public override SubmarineInfo EnemySubmarineInfo => submarineInfo;
public PirateMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub)
{
submarineConfig = prefab.ConfigElement.Element("Submarine");
characterConfig = prefab.ConfigElement.Element("Characters");
string submarineIdentifier = submarineConfig.GetAttributeString("identifier", string.Empty);
if (submarineIdentifier == string.Empty)
{
DebugConsole.ThrowError("No identifier used for submarine for pirate mission!");
return;
}
// maybe a little redundant
var contentFile = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.EnemySubmarine).FirstOrDefault(x => x.Path == submarineIdentifier);
if (contentFile == null)
{
DebugConsole.ThrowError("No submarine file found with the identifier!");
return;
}
submarineInfo = new SubmarineInfo(contentFile.Path);
}
private void CreateMissionPositions(out Vector2 preferredSpawnPos)
{
Vector2 patrolPos = enemySub.WorldPosition;
Point subSize = enemySub.GetDockedBorders().Size;
if (!Level.Loaded.TryGetInterestingPosition(true, Level.PositionType.MainPath | Level.PositionType.SidePath, Level.Loaded.Size.X * 0.3f, out preferredSpawnPos))
{
DebugConsole.ThrowError("Could not spawn pirate submarine in an interesting location! " + this);
}
if (!Level.Loaded.TryGetInterestingPositionAwayFromPoint(true, Level.PositionType.MainPath | Level.PositionType.SidePath, Level.Loaded.Size.X * 0.3f, out patrolPos, preferredSpawnPos, minDistFromPoint: 10000f))
{
DebugConsole.ThrowError("Could not give pirate submarine an interesting location to patrol to! " + this);
}
patrolPos = enemySub.FindSpawnPos(patrolPos, subSize);
patrolPositions.Add(patrolPos);
patrolPositions.Add(preferredSpawnPos);
if (!IsClient)
{
PathFinder pathFinder = new PathFinder(WayPoint.WayPointList, false);
var path = pathFinder.FindPath(ConvertUnits.ToSimUnits(patrolPos), ConvertUnits.ToSimUnits(preferredSpawnPos));
if (!path.Unreachable)
{
preferredSpawnPos = path.Nodes[Rand.Range(0, path.Nodes.Count - 1)].WorldPosition; // spawn the sub in a random point in the path if possible
}
int graceDistance = 500; // the sub still spawns awkwardly close to walls, so this helps. could also be given as a parameter instead
preferredSpawnPos = enemySub.FindSpawnPos(preferredSpawnPos, new Point(subSize.X + graceDistance, subSize.Y + graceDistance));
}
}
private void InitPirateShip(Vector2 spawnPos)
{
enemySub.NeutralizeBallast();
if (enemySub.GetItems(alsoFromConnectedSubs: false).Find(i => i.HasTag("reactor") && !i.NonInteractable)?.GetComponent<Reactor>() is Reactor reactor)
{
reactor.PowerUpImmediately();
reactorItem = reactor.Item;
}
enemySub.EnableMaintainPosition();
enemySub.SetPosition(spawnPos);
enemySub.TeamID = CharacterTeamType.None;
}
private void InitPirates()
{
characters.Clear();
characterDictionary.Clear();
if (characterConfig == null)
{
DebugConsole.ThrowError("Failed to initialize characters for escort mission (characterConfig == null)");
return;
}
bool commanderAssigned = false;
foreach (XElement element in characterConfig.Elements())
{
Character spawnedCharacter = CreateHuman(CreateHumanPrefabFromElement(element), characters, characterDictionary, enemySub, CharacterTeamType.None, null);
if (!commanderAssigned)
{
bool isCommander = element.GetAttributeBool("iscommander", false);
if (isCommander && spawnedCharacter.AIController is HumanAIController humanAIController)
{
humanAIController.InitShipCommandManager();
foreach (var patrolPos in patrolPositions)
{
humanAIController.ShipCommandManager.patrolPositions.Add(patrolPos);
}
commanderAssigned = true;
}
}
}
}
protected override void StartMissionSpecific(Level level)
{
if (characters.Count > 0)
{
#if DEBUG
throw new Exception($"characters.Count > 0 ({characters.Count})");
#else
DebugConsole.AddWarning("Character list was not empty at the start of a pirate mission. The mission instance may not have been ended correctly on previous rounds.");
characters.Clear();
#endif
}
if (patrolPositions.Count > 0)
{
#if DEBUG
throw new Exception($"patrolPositions.Count > 0 ({patrolPositions.Count})");
#else
DebugConsole.AddWarning("Patrol point list was not empty at the start of a pirate mission. The mission instance may not have been ended correctly on previous rounds.");
patrolPositions.Clear();
#endif
}
enemySub = Submarine.MainSubs[1];
if (enemySub == null)
{
DebugConsole.ThrowError($"Enemy Submarine was not created. SubmarineInfo is likely not defined.");
// TODO: should we set the state to something here?
return;
}
Vector2 spawnPos = Level.Loaded.EndPosition; // in case TryGetInterestingPosition fails, though this should not happen
CreateMissionPositions(out spawnPos); // patrol positions are not explicitly replicated, instead they are acquired the same way the server acquires them
#if DEBUG
if (IsClient)
{
DebugConsole.NewMessage("The patrol positions set by client were: ");
}
else
{
DebugConsole.NewMessage("The patrol positions set by server were: ");
}
foreach (var patrolPos in patrolPositions)
{
DebugConsole.NewMessage("Patrol pos: " + patrolPos);
}
#endif
if (!IsClient)
{
InitPirateShip(spawnPos);
}
// flipping the sub on the frame it is moved into place must be done after it's been moved, or it breaks item connections to the submarine
// creating the pirates have to be done after the sub has been flipped, or it seems to break the AI pathing
enemySub.FlipX();
enemySub.ShowSonarMarker = false;
if (!IsClient)
{
InitPirates();
}
}
public override void Update(float deltaTime)
{
int newState = State;
float sqrSonarRange = MathUtils.Pow2(Sonar.DefaultSonarRange);
outsideOfSonarRange = Vector2.DistanceSquared(enemySub.WorldPosition, Submarine.MainSub.WorldPosition) > sqrSonarRange;
if (State < 2 && CheckWinState())
{
newState = 2;
}
else
{
switch (State)
{
case 0:
for (int i = patrolPositions.Count - 1; i >= 0; i--)
{
if (Vector2.DistanceSquared(patrolPositions[i], Submarine.MainSub.WorldPosition) < sqrSonarRange)
{
patrolPositions.RemoveAt(i);
}
}
if (!outsideOfSonarRange || patrolPositions.None())
{
newState = 1;
}
break;
case 1:
if (outsideOfSonarRange)
{
if (lastSighting.HasValue && Vector2.DistanceSquared(lastSighting.Value, Submarine.MainSub.WorldPosition) < sqrSonarRange)
{
lastSighting = null;
}
pirateSightingUpdateTimer -= deltaTime;
if (pirateSightingUpdateTimer < 0)
{
pirateSightingUpdateTimer = pirateSightingUpdateFrequency;
lastSighting = enemySub.WorldPosition;
}
}
else
{
lastSighting = enemySub.WorldPosition;
pirateSightingUpdateTimer = 0;
}
break;
}
}
State = newState;
}
private bool CheckWinState() => !IsClient && (characters.All(m => !Survived(m)) || reactorItem.Condition <= 0f);
private bool Survived(Character character)
{
return character != null && !character.Removed && !character.IsDead;
}
public override void End()
{
if (state == 2)
{
GiveReward();
completed = true;
}
characters.Clear();
characterDictionary.Clear();
failed = !completed;
}
}
}
@@ -43,8 +43,8 @@ namespace Barotrauma
}
}
public SalvageMission(MissionPrefab prefab, Location[] locations)
: base(prefab, locations)
public SalvageMission(MissionPrefab prefab, Location[] locations, Submarine sub)
: base(prefab, locations, sub)
{
containerTag = prefab.ConfigElement.GetAttributeString("containertag", "");
@@ -88,7 +88,7 @@ namespace Barotrauma
}
offset = prefab.ConfigElement.GetAttributeFloat("offset", 0);
scatter = Math.Clamp(prefab.ConfigElement.GetAttributeFloat("scatter", 1000), 0, 3000);
scatter = Math.Clamp(prefab.ConfigElement.GetAttributeFloat("scatter", 500), 0, 3000);
if (GameMain.NetworkMember != null)
{
@@ -192,8 +192,8 @@ namespace Barotrauma
spawnPos = Vector2.Zero;
var availablePositions = GetAvailableSpawnPositions();
var chosenPosition = new Level.InterestingPosition(Point.Zero, Level.PositionType.MainPath, isValid: false);
bool isSubOrWreck = spawnPosType == Level.PositionType.Ruin || spawnPosType == Level.PositionType.Wreck;
if (affectSubImmediately && !isSubOrWreck && spawnPosType != Level.PositionType.Abyss)
bool isRuinOrWreck = spawnPosType.HasFlag(Level.PositionType.Ruin) || spawnPosType.HasFlag(Level.PositionType.Wreck);
if (affectSubImmediately && !isRuinOrWreck && !spawnPosType.HasFlag(Level.PositionType.Abyss))
{
if (availablePositions.None())
{
@@ -264,7 +264,7 @@ namespace Barotrauma
}
else
{
if (!isSubOrWreck)
if (!isRuinOrWreck)
{
float minDistance = 20000;
var refSub = GetReferenceSub();
@@ -375,7 +375,7 @@ namespace Barotrauma
if (spawnPending)
{
//wait until there are no submarines at the spawnpos
if (spawnPosType == Level.PositionType.MainPath || spawnPosType == Level.PositionType.SidePath || spawnPosType == Level.PositionType.Abyss)
if (spawnPosType.HasFlag(Level.PositionType.MainPath) || spawnPosType.HasFlag(Level.PositionType.SidePath) || spawnPosType.HasFlag(Level.PositionType.Abyss))
{
foreach (Submarine submarine in Submarine.Loaded)
{
@@ -387,7 +387,7 @@ namespace Barotrauma
//if spawning in a ruin/cave, wait for someone to be close to it to spawning
//unnecessary monsters in places the players might never visit during the round
if (spawnPosType == Level.PositionType.Ruin || spawnPosType == Level.PositionType.Cave || spawnPosType == Level.PositionType.Wreck)
if (spawnPosType.HasFlag(Level.PositionType.Ruin) || spawnPosType.HasFlag(Level.PositionType.Cave) || spawnPosType.HasFlag(Level.PositionType.Wreck))
{
bool someoneNearby = false;
float minDist = Sonar.DefaultSonarRange * 0.8f;
@@ -415,16 +415,19 @@ namespace Barotrauma
}
if (spawnPosType == Level.PositionType.Abyss || spawnPosType == Level.PositionType.AbyssCave)
if (spawnPosType.HasFlag(Level.PositionType.Abyss) || spawnPosType.HasFlag(Level.PositionType.AbyssCave))
{
bool anyInAbyss = false;
foreach (Submarine submarine in Submarine.Loaded)
{
if (submarine.Info.Type != SubmarineType.Player) { continue; }
if (submarine.WorldPosition.Y > 0)
if (submarine.Info.Type != SubmarineType.Player || submarine == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle) { continue; }
if (submarine.WorldPosition.Y < 0)
{
return;
anyInAbyss = true;
break;
}
}
if (!anyInAbyss) { return; }
}
spawnPending = false;
@@ -432,7 +435,15 @@ namespace Barotrauma
//+1 because Range returns an integer less than the max value
int amount = Rand.Range(minAmount, maxAmount + 1);
monsters = new List<Character>();
float offsetAmount = spawnPosType == Level.PositionType.MainPath || spawnPosType == Level.PositionType.SidePath ? scatter : 100;
float scatterAmount = scatter;
if (spawnPosType.HasFlag(Level.PositionType.SidePath))
{
scatterAmount = Math.Min(scatter, Level.Loaded.Tunnels.Where(t => t.Type == Level.TunnelType.SidePath).Min(t => t.MinWidth) / 2);
}
else if (!spawnPosType.HasFlag(Level.PositionType.MainPath))
{
scatterAmount = 100;
}
for (int i = 0; i < amount; i++)
{
string seed = Level.Loaded.Seed + i.ToString();
@@ -443,8 +454,8 @@ namespace Barotrauma
System.Diagnostics.Debug.Assert(GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer, "Clients should not create monster events.");
Vector2 pos = spawnPos.Value + Rand.Vector(offsetAmount);
if (spawnPosType == Level.PositionType.MainPath || spawnPosType == Level.PositionType.SidePath)
Vector2 pos = spawnPos.Value + Rand.Vector(scatterAmount);
if (scatterAmount > 100)
{
if (Submarine.Loaded.Any(s => ToolBox.GetWorldBounds(s.Borders.Center, s.Borders.Size).ContainsWorld(pos)))
{
@@ -86,10 +86,11 @@ namespace Barotrauma.Extensions
/// <summary>
/// Executes an action that modifies the collection on each element (such as removing items from the list).
/// Creates a temporary list.
/// Creates a temporary list, unless the collection is empty.
/// </summary>
public static void ForEachMod<T>(this IEnumerable<T> source, Action<T> action)
{
if (source.None()) { return; }
var temp = new List<T>(source);
temp.ForEach(action);
}
@@ -1,4 +1,5 @@
using Microsoft.Xna.Framework;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -30,6 +31,8 @@ namespace Barotrauma
public ReadyCheck ActiveReadyCheck;
public XElement ActiveOrdersElement { get; set; }
public CrewManager(bool isSinglePlayer)
{
IsSinglePlayer = isSinglePlayer;
@@ -111,6 +114,9 @@ namespace Barotrauma
case "health":
characterInfo.HealthData = subElement;
break;
case "orders":
characterInfo.OrderData = subElement;
break;
}
}
}
@@ -189,7 +195,7 @@ namespace Barotrauma
spawnWaypoints = WayPoint.WayPointList.FindAll(wp =>
wp.SpawnType == SpawnType.Human &&
wp.Submarine == Level.Loaded.StartOutpost &&
wp.CurrentHull?.OutpostModuleTags != null &&
wp.CurrentHull != null &&
wp.CurrentHull.OutpostModuleTags.Contains("airlock"));
while (spawnWaypoints.Count > characterInfos.Count)
{
@@ -229,10 +235,14 @@ namespace Barotrauma
}
if (character.Info.HealthData != null)
{
character.Info.ApplyHealthData(character, character.Info.HealthData);
CharacterInfo.ApplyHealthData(character, character.Info.HealthData);
}
character.GiveIdCardTags(spawnWaypoints[i]);
character.Info.StartItemsGiven = true;
if (character.Info.OrderData != null)
{
character.Info.ApplyOrderData();
}
}
AddCharacter(character);
@@ -265,6 +275,14 @@ namespace Barotrauma
RemoveCharacterInfo(characterInfo);
}
public void ClearCurrentOrders()
{
foreach (var characterInfo in characterInfos)
{
characterInfo?.ClearCurrentOrders();
}
}
public void Update(float deltaTime)
{
foreach (Pair<Order, float?> order in ActiveOrders)
@@ -392,6 +410,88 @@ namespace Barotrauma
#endregion
public static Character GetCharacterForQuickAssignment(Order order, Character controlledCharacter, IEnumerable<Character> characters, bool includeSelf = false)
{
var controllingCharacter = controlledCharacter != null;
#if !DEBUG
if (!controllingCharacter) { return null; }
#endif
if (order.Category == OrderCategory.Operate && HumanAIController.IsItemOperatedByAnother(null, order.TargetItemComponent, out Character operatingCharacter) &&
(!controllingCharacter || operatingCharacter.CanHearCharacter(controlledCharacter)))
{
return operatingCharacter;
}
return GetCharactersSortedForOrder(order, characters, controlledCharacter, includeSelf).FirstOrDefault(c => !controllingCharacter || c.CanHearCharacter(controlledCharacter)) ?? controlledCharacter;
}
public static IEnumerable<Character> GetCharactersSortedForOrder(Order order, IEnumerable<Character> characters, Character controlledCharacter, bool includeSelf, IEnumerable<Character> extraCharacters = null)
{
var filteredCharacters = characters.Where(c => controlledCharacter == null || ((includeSelf || c != controlledCharacter) && c.TeamID == controlledCharacter.TeamID));
if (extraCharacters != null)
{
filteredCharacters = filteredCharacters.Union(extraCharacters);
}
return filteredCharacters
// 1. Prioritize those who are on the same submarine than the controlled character
.OrderByDescending(c => Character.Controlled == null || c.Submarine == Character.Controlled.Submarine)
// 2. Prioritize those who have been given the same maintenance or operate order as now issued
.ThenByDescending(c => c.CurrentOrders.Any(o =>
o.Order != null && o.Order.Identifier == order.Identifier &&
(order.Category == OrderCategory.Maintenance || order.Category == OrderCategory.Operate)))
// 3. Prioritize those with the appropriate job for the order
.ThenByDescending(c => order.HasAppropriateJob(c))
// 4. Prioritize bots over player controlled characters
.ThenByDescending(c => c.IsBot)
// 5. Use the priority value of the current objective
.ThenBy(c => c.AIController is HumanAIController humanAI ? humanAI.ObjectiveManager.CurrentObjective?.Priority : 0)
// 6. Prioritize those with the best skill for the order
.ThenByDescending(c => c.GetSkillLevel(order.AppropriateSkill));
}
partial void UpdateProjectSpecific(float deltaTime);
private void SaveActiveOrders(XElement parentElement)
{
ActiveOrdersElement = new XElement("activeorders");
// Only save orders with no fade out time (e.g. ignore orders)
var ordersToSave = new List<OrderInfo>();
foreach (var activeOrder in ActiveOrders)
{
var order = activeOrder?.First;
if (order == null || activeOrder.Second.HasValue) { continue; }
ordersToSave.Add(new OrderInfo(order, null, CharacterInfo.HighestManualOrderPriority));
}
CharacterInfo.SaveOrders(ActiveOrdersElement, ordersToSave.ToArray());
parentElement?.Add(ActiveOrdersElement);
}
public void LoadActiveOrders()
{
if (ActiveOrdersElement == null) { return; }
foreach (var orderInfo in CharacterInfo.LoadOrders(ActiveOrdersElement))
{
IIgnorable ignoreTarget = null;
if (orderInfo.Order.IsIgnoreOrder)
{
switch (orderInfo.Order.TargetType)
{
case Order.OrderTargetType.Entity:
ignoreTarget = orderInfo.Order.TargetEntity as IIgnorable;
break;
case Order.OrderTargetType.WallSection when orderInfo.Order.TargetEntity is Structure s && orderInfo.Order.WallSectionIndex.HasValue:
ignoreTarget = s.GetSection(orderInfo.Order.WallSectionIndex.Value) as IIgnorable;
break;
default:
DebugConsole.ThrowError("Error loading an ignore order - can't find a proper ignore target");
continue;
}
}
if (ignoreTarget != null)
{
ignoreTarget.OrderedToBeIgnored = true;
}
AddOrder(orderInfo.Order, null);
}
}
}
}
@@ -51,7 +51,7 @@ namespace Barotrauma
//there can be no events before this time has passed during the 1st campaign round
const float FirstRoundEventDelay = 30.0f;
public enum InteractionType { None, Talk, Map, Crew, Store, Repair, Upgrade, PurchaseSub }
public enum InteractionType { None, Talk, Examine, Map, Crew, Store, Repair, Upgrade, PurchaseSub }
public readonly CargoManager CargoManager;
public UpgradeManager UpgradeManager;
@@ -169,7 +169,7 @@ namespace Barotrauma
return Submarine.Loaded.FindAll(sub =>
sub != leavingSub &&
!leavingSub.DockedTo.Contains(sub) &&
sub.Info.Type == SubmarineType.Player &&
sub.Info.Type == SubmarineType.Player && sub.TeamID == CharacterTeamType.Team1 && // pirate subs are currently tagged as player subs as well
sub != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle &&
(sub.AtEndExit != leavingSub.AtEndExit || sub.AtStartExit != leavingSub.AtStartExit));
}
@@ -268,7 +268,7 @@ namespace Barotrauma
var beaconMissionPrefab = ToolBox.SelectWeightedRandom(beaconMissionPrefabs, beaconMissionPrefabs.Select(p => (float)p.Commonness).ToList(), rand);
if (!Missions.Any(m => m.Prefab.Type == beaconMissionPrefab.Type))
{
extraMissions.Add(beaconMissionPrefab.Instantiate(Map.SelectedConnection.Locations));
extraMissions.Add(beaconMissionPrefab.Instantiate(Map.SelectedConnection.Locations, Submarine.MainSub));
}
}
}
@@ -285,7 +285,7 @@ namespace Barotrauma
var huntingGroundsMissionPrefab = ToolBox.SelectWeightedRandom(huntingGroundsMissionPrefabs, huntingGroundsMissionPrefabs.Select(p => (float)p.Commonness).ToList(), rand);
if (!Missions.Any(m => m.Prefab.Tags.Any(t => t.Equals("huntinggrounds", StringComparison.OrdinalIgnoreCase))))
{
extraMissions.Add(huntingGroundsMissionPrefab.Instantiate(Map.SelectedConnection.Locations));
extraMissions.Add(huntingGroundsMissionPrefab.Instantiate(Map.SelectedConnection.Locations, Submarine.MainSub));
}
}
}
@@ -595,7 +595,6 @@ namespace Barotrauma
{
CrewManager.RemoveCharacterInfo(ci);
}
ci?.ClearCurrentOrders();
}
foreach (DockingPort port in DockingPort.List)
@@ -637,6 +636,7 @@ namespace Barotrauma
location.CreateStore(force: true);
location.ClearMissions();
location.Discovered = false;
location.LevelData?.EventHistory?.Clear();
}
Map.SetLocation(Map.Locations.IndexOf(Map.StartLocation));
Map.SelectLocation(-1);
@@ -26,6 +26,7 @@ namespace Barotrauma
private XElement itemData;
private XElement healthData;
public XElement OrderData { get; private set; }
partial void InitProjSpecific(Client client);
public CharacterCampaignData(Client client)
@@ -38,8 +39,10 @@ namespace Barotrauma
if (client.Character.Inventory != null)
{
itemData = new XElement("inventory");
client.Character.SaveInventory(client.Character.Inventory, itemData);
Character.SaveInventory(client.Character.Inventory, itemData);
}
OrderData = new XElement("orders");
CharacterInfo.SaveOrderData(client.Character.Info, OrderData);
}
public CharacterCampaignData(XElement element)
@@ -67,6 +70,9 @@ namespace Barotrauma
case "health":
healthData = subElement;
break;
case "orders":
OrderData = subElement;
break;
}
}
}
@@ -78,8 +84,10 @@ namespace Barotrauma
if (character.Inventory != null)
{
itemData = new XElement("inventory");
character.SaveInventory(character.Inventory, itemData);
Character.SaveInventory(character.Inventory, itemData);
}
OrderData = new XElement("orders");
CharacterInfo.SaveOrderData(character.Info, OrderData);
}
public XElement Save()
@@ -92,6 +100,7 @@ namespace Barotrauma
CharacterInfo?.Save(element);
if (itemData != null) { element.Add(itemData); }
if (healthData != null) { element.Add(healthData); }
if (OrderData != null) { element.Add(OrderData); }
return element;
}
@@ -21,7 +21,7 @@ namespace Barotrauma
Location[] locations = { GameMain.GameSession.StartLocation, GameMain.GameSession.EndLocation };
foreach (MissionPrefab missionPrefab in missionPrefabs)
{
missions.Add(missionPrefab.Instantiate(locations));
missions.Add(missionPrefab.Instantiate(locations, Submarine.MainSub));
}
}
@@ -149,12 +149,14 @@ namespace Barotrauma
case "metadata":
CampaignMetadata = new CampaignMetadata(this, subElement);
break;
case "upgrademanager":
case "pendingupgrades":
UpgradeManager = new UpgradeManager(this, subElement, isSingleplayer: false);
break;
case "bots" when GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer:
CrewManager.HasBots = subElement.GetAttributeBool("hasbots", false);
CrewManager.AddCharacterElements(subElement);
CrewManager.ActiveOrdersElement = subElement.GetChildElement("activeorders");
break;
case "cargo":
CargoManager?.LoadPurchasedItems(subElement);
@@ -353,9 +353,14 @@ namespace Barotrauma
}
}
}
if (GameMode is PvPMode && Submarine.MainSubs[1] == null)
if (Submarine.MainSubs[1] == null)
{
Submarine.MainSubs[1] = new Submarine(SubmarineInfo, true);
var enemySubmarineInfo = GameMode is PvPMode ? SubmarineInfo : GameMode.Missions.FirstOrDefault(m => m.EnemySubmarineInfo != null)?.EnemySubmarineInfo;
if (enemySubmarineInfo != null)
{
Submarine.MainSubs[1] = new Submarine(enemySubmarineInfo, true);
}
}
if (GameMain.NetworkMember?.ServerSettings?.LockAllDefaultWires ?? false)
@@ -28,6 +28,18 @@ namespace Barotrauma
}
}
internal class PurchasedItemSwap
{
public readonly Item ItemToRemove;
public readonly ItemPrefab ItemToInstall;
public PurchasedItemSwap(Item itemToRemove, ItemPrefab itemToInstall)
{
ItemToRemove = itemToRemove;
ItemToInstall = itemToInstall;
}
}
/// <summary>
/// This class handles all upgrade logic.
/// Storing, applying, checking and validation of upgrades.
@@ -75,6 +87,8 @@ namespace Barotrauma
public readonly List<PurchasedUpgrade> PendingUpgrades = new List<PurchasedUpgrade>();
public readonly List<PurchasedItemSwap> PurchasedItemSwaps = new List<PurchasedItemSwap>();
private CampaignMetadata Metadata => Campaign.CampaignMetadata;
private readonly CampaignMode Campaign;
private int spentMoney;
@@ -90,7 +104,67 @@ namespace Barotrauma
public UpgradeManager(CampaignMode campaign, XElement element, bool isSingleplayer) : this(campaign)
{
DebugConsole.Log($"Restored upgrade manager from save file, ({element.Elements().Count()} pending upgrades).");
LoadPendingUpgrades(element, isSingleplayer);
//backwards compatibility:
//upgrades used to be saved to a <pendingupgrades> element, now upgrades and item swaps are saved separately under a <upgrademanager> element
if (element.Name.LocalName.Equals("pendingupgrades", StringComparison.OrdinalIgnoreCase))
{
LoadPendingUpgrades(element, isSingleplayer);
}
else
{
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "pendingupgrades":
LoadPendingUpgrades(subElement, isSingleplayer);
break;
}
}
}
}
public int DetermineItemSwapCost(Item item, ItemPrefab replacement)
{
if (replacement == null)
{
replacement = ItemPrefab.Find("", item.Prefab.SwappableItem.ReplacementOnUninstall);
if (replacement == null)
{
DebugConsole.ThrowError("Failed to determine swap cost for item \"{}\". Trying to uninstall the item but no replacement item found.");
return 0;
}
}
int price = 0;
if (replacement == item.Prefab)
{
if (item.PendingItemSwap != null)
{
//refund the pending swap
price -= item.PendingItemSwap.SwappableItem.GetPrice(Campaign?.Map?.CurrentLocation);
//buy back the current item
price += item.Prefab.SwappableItem.GetPrice(Campaign?.Map?.CurrentLocation);
}
}
else
{
price = replacement.SwappableItem.GetPrice(Campaign?.Map?.CurrentLocation);
if (item.PendingItemSwap != null)
{
//refund the pending swap
price -= item.PendingItemSwap.SwappableItem.GetPrice(Campaign?.Map?.CurrentLocation);
//buy back the current item
price += item.Prefab.SwappableItem.GetPrice(Campaign?.Map?.CurrentLocation);
}
//refund the current item
if (replacement != item.prefab)
{
price -= item.Prefab.SwappableItem.GetPrice(Campaign?.Map?.CurrentLocation);
}
}
return price;
}
private DateTime lastUpgradeSpeak, lastErrorSpeak;
@@ -102,9 +176,6 @@ namespace Barotrauma
/// Purchased upgrades are temporarily stored in <see cref="PendingUpgrades"/> and they are applied
/// after the next round starts similarly how items are spawned in the stowage room after the round starts.
/// </remarks>
/// <param name="prefab"></param>
/// <param name="category"></param>
/// <param name="force"></param>
public void PurchaseUpgrade(UpgradePrefab prefab, UpgradeCategory category, bool force = false)
{
if (!CanUpgradeSub())
@@ -142,7 +213,7 @@ namespace Barotrauma
price = 0;
}
if (Campaign.Money > price)
if (Campaign.Money >= price)
{
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
{
@@ -184,6 +255,151 @@ namespace Barotrauma
}
}
/// <summary>
/// Purchases an item swap and handles logic for deducting the credit.
/// </summary>
public void PurchaseItemSwap(Item itemToRemove, ItemPrefab itemToInstall, bool force = false)
{
if (!CanUpgradeSub())
{
DebugConsole.ThrowError("Cannot swap items when switching to another submarine.");
return;
}
if (itemToRemove == null)
{
DebugConsole.ThrowError($"Cannot swap null item!");
return;
}
if (!UpgradeCategory.Categories.Any(c => c.ItemTags.Any(t => itemToRemove.HasTag(t)) && c.ItemTags.Any(t => itemToInstall.Tags.Contains(t))))
{
DebugConsole.ThrowError($"Failed to swap item \"{itemToRemove.Name}\" with \"{itemToInstall.Name}\" (not in the same upgrade category).");
return;
}
/*if (itemToRemove.PendingItemSwap != null)
{
CancelItemSwap(itemToRemove);
}
else */
if (itemToRemove.prefab == itemToInstall)
{
DebugConsole.ThrowError($"Failed to swap item \"{itemToRemove.Name}\" (trying to swap with the same item!).");
return;
}
SwappableItem? swappableItem = itemToRemove.Prefab.SwappableItem;
if (swappableItem == null)
{
DebugConsole.ThrowError($"Failed to swap item \"{itemToRemove.Name}\" (not configured as a swappable item).");
return;
}
int price = 0;
if (!itemToRemove.AvailableSwaps.Contains(itemToInstall))
{
price = itemToInstall.SwappableItem.GetPrice(Campaign.Map?.CurrentLocation);
}
if (force)
{
price = 0;
}
if (Campaign.Money >= price)
{
PurchasedItemSwaps.RemoveAll(p => p.ItemToRemove == itemToRemove);
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
{
// only make the NPC speak if more than 5 minutes have passed since the last purchased service
if (lastUpgradeSpeak == DateTime.MinValue || lastUpgradeSpeak.AddMinutes(5) < DateTime.Now)
{
UpgradeNPCSpeak(TextManager.Get("Dialog.UpgradePurchased"), Campaign.IsSinglePlayer);
lastUpgradeSpeak = DateTime.Now;
}
}
Campaign.Money -= price;
spentMoney += price;
itemToRemove.AvailableSwaps.Add(itemToRemove.Prefab);
if (itemToInstall != null) { itemToRemove.AvailableSwaps.Add(itemToInstall); }
if (itemToRemove.Prefab != itemToInstall && itemToInstall != null)
{
itemToRemove.PendingItemSwap = itemToInstall;
PurchasedItemSwaps.Add(new PurchasedItemSwap(itemToRemove, itemToInstall));
DebugLog($"CLIENT: Swapped item \"{itemToRemove.Name}\" with \"{itemToInstall.Name}\".", Color.Orange);
}
else
{
DebugLog($"CLIENT: Cancelled swapping the item \"{itemToRemove.Name}\" with \"{(itemToRemove.PendingItemSwap?.Name ?? null)}\".", Color.Orange);
}
OnUpgradesChanged?.Invoke();
}
else
{
DebugConsole.ThrowError("Tried to swap an item with insufficient funds, the transaction has not been completed.\n" +
$"Item to remove: {itemToRemove.Name}, Item to install: {itemToInstall.Name}, Cost: {price}, Have: {Campaign.Money}");
}
}
/// <summary>
/// Cancels the currently pending item swap, or uninstalls the item if there's no swap pending
/// </summary>
public void CancelItemSwap(Item itemToRemove, bool force = false)
{
if (!CanUpgradeSub())
{
DebugConsole.ThrowError("Cannot swap items when switching to another submarine.");
return;
}
if (itemToRemove?.PendingItemSwap == null && string.IsNullOrEmpty(itemToRemove?.Prefab.SwappableItem?.ReplacementOnUninstall))
{
DebugConsole.ThrowError($"Cannot uninstall item \"{itemToRemove?.Name}\" (no replacement item configured).");
return;
}
SwappableItem? swappableItem = itemToRemove.Prefab.SwappableItem;
if (swappableItem == null)
{
DebugConsole.ThrowError($"Failed to uninstall item \"{itemToRemove.Name}\" (not configured as a swappable item).");
return;
}
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
{
// only make the NPC speak if more than 5 minutes have passed since the last purchased service
if (lastUpgradeSpeak == DateTime.MinValue || lastUpgradeSpeak.AddMinutes(5) < DateTime.Now)
{
UpgradeNPCSpeak(TextManager.Get("Dialog.UpgradePurchased"), Campaign.IsSinglePlayer);
lastUpgradeSpeak = DateTime.Now;
}
}
if (itemToRemove.PendingItemSwap == null)
{
var replacement = MapEntityPrefab.Find("", swappableItem.ReplacementOnUninstall) as ItemPrefab;
if (replacement == null)
{
DebugConsole.ThrowError($"Failed to uninstall item \"{itemToRemove.Name}\". Could not find the replacement item \"{swappableItem.ReplacementOnUninstall}\".");
return;
}
PurchasedItemSwaps.RemoveAll(p => p.ItemToRemove == itemToRemove);
PurchasedItemSwaps.Add(new PurchasedItemSwap(itemToRemove, replacement));
DebugLog($"Uninstalled item item \"{itemToRemove.Name}\".", Color.Orange);
itemToRemove.PendingItemSwap = replacement;
}
else
{
PurchasedItemSwaps.RemoveAll(p => p.ItemToRemove == itemToRemove);
DebugLog($"Cancelled swapping the item \"{itemToRemove.Name}\" with \"{itemToRemove.PendingItemSwap.Name}\".", Color.Orange);
itemToRemove.PendingItemSwap = null;
}
#if CLIENT
OnUpgradesChanged?.Invoke();
#endif
}
/// <summary>
/// Applies all our pending upgrades to the submarine.
/// </summary>
@@ -201,24 +417,17 @@ namespace Barotrauma
public void ApplyUpgrades()
{
PurchasedUpgrades.Clear();
PurchasedItemSwaps.Clear();
if (Submarine.MainSub == null) { return; }
List<PurchasedUpgrade> pendingUpgrades = PendingUpgrades;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
{
if (Level.Loaded?.Type != LevelData.LevelType.Outpost)
if (loadedUpgrades != null)
{
if (loadedUpgrades != null)
{
// client receives pending upgrades from the save file
pendingUpgrades = loadedUpgrades;
}
}
else
{
// prevent the client from applying pending upgrades at an outpost when joining mid round
return;
// client receives pending upgrades from the save file
pendingUpgrades = loadedUpgrades;
}
}
@@ -592,7 +801,17 @@ namespace Barotrauma
OnUpgradesChanged?.Invoke();
}
public void SavePendingUpgrades(XElement? parent, List<PurchasedUpgrade> upgrades)
public void Save(XElement? parent)
{
if (parent == null) { return; }
var upgradeManagerElement = new XElement("upgrademanager");
parent.Add(upgradeManagerElement);
SavePendingUpgrades(upgradeManagerElement, PendingUpgrades);
}
private void SavePendingUpgrades(XElement? parent, List<PurchasedUpgrade> upgrades)
{
if (parent == null) { return; }
@@ -647,7 +866,7 @@ namespace Barotrauma
#endif
}
public static void LogError(string text, Dictionary<string, object?> data, Exception e = null)
public static void LogError(string text, Dictionary<string, object?> data, Exception? e = null)
{
string error = $"{text}\n";
foreach (var (label, value) in data)
@@ -616,7 +616,8 @@ namespace Barotrauma
f.Type == ContentType.Outpost ||
f.Type == ContentType.OutpostModule ||
f.Type == ContentType.Wreck ||
f.Type == ContentType.BeaconStation)) { SubmarineInfo.RefreshSavedSubs(); }
f.Type == ContentType.BeaconStation ||
f.Type == ContentType.EnemySubmarine)) { SubmarineInfo.RefreshSavedSubs(); }
if (files.Any(f => f.Type == ContentType.NPCSets)) { NPCSet.LoadSets(); }
if (files.Any(f => f.Type == ContentType.OutpostConfig)) { OutpostGenerationParams.LoadPresets(); }
if (files.Any(f => f.Type == ContentType.Factions)) { FactionPrefab.LoadFactions(); }
@@ -114,6 +114,17 @@ namespace Barotrauma
public bool IsInLimbSlot(Item item, InvSlotType limbSlot)
{
if (limbSlot == (InvSlotType.LeftHand | InvSlotType.RightHand))
{
int rightHandSlot = FindLimbSlot(InvSlotType.RightHand);
int leftHandSlot = FindLimbSlot(InvSlotType.LeftHand);
if (rightHandSlot > -1 && slots[rightHandSlot].Contains(item) &&
leftHandSlot > -1 && slots[leftHandSlot].Contains(item))
{
return true;
}
}
for (int i = 0; i < slots.Length; i++)
{
if (SlotTypes[i] == limbSlot && slots[i].Contains(item)) { return true; }
@@ -205,13 +216,41 @@ namespace Barotrauma
if (allowedSlots != null && !allowedSlots.Contains(InvSlotType.Any))
{
int slot = FindLimbSlot(allowedSlots.First());
if (slot > -1 && slots[slot].Items.Any(it => it != item) && slots[slot].First().AllowDroppingOnSwapWith(item))
bool allSlotsTaken = true;
foreach (var allowedSlot in allowedSlots)
{
foreach (Item existingItem in slots[slot].Items.ToList())
if (allowedSlot == (InvSlotType.RightHand | InvSlotType.LeftHand))
{
existingItem.Drop(user);
if (existingItem.ParentInventory != null) { existingItem.ParentInventory.RemoveItem(existingItem); }
int rightHandSlot = FindLimbSlot(InvSlotType.RightHand);
int leftHandSlot = FindLimbSlot(InvSlotType.LeftHand);
if (rightHandSlot > -1 && slots[rightHandSlot].CanBePut(item) &&
leftHandSlot > -1 && slots[leftHandSlot].CanBePut(item))
{
allSlotsTaken = false;
break;
}
}
else
{
int slot = FindLimbSlot(allowedSlot);
if (slot > -1 && slots[slot].CanBePut(item))
{
allSlotsTaken = false;
break;
}
}
}
if (allSlotsTaken)
{
int slot = FindLimbSlot(allowedSlots.First());
if (slot > -1 && slots[slot].Items.Any(it => it != item) && slots[slot].First().AllowDroppingOnSwapWith(item))
{
foreach (Item existingItem in slots[slot].Items.ToList())
{
existingItem.Drop(user);
if (existingItem.ParentInventory != null) { existingItem.ParentInventory.RemoveItem(existingItem); }
}
}
}
}
@@ -304,7 +343,7 @@ namespace Barotrauma
for (int i = 0; i < capacity; i++)
{
if (allowedSlot.HasFlag(SlotTypes[i]) && item.AllowedSlots.Any(s => s.HasFlag(SlotTypes[i])) && slots[i].Empty())
if (allowedSlot.HasFlag(SlotTypes[i]) && item.GetComponents<Pickable>().Any(p => p.AllowedSlots.Any(s => s.HasFlag(SlotTypes[i]))) && slots[i].Empty())
{
#if CLIENT
if (PersonalSlots.HasFlag(SlotTypes[i])) { hidePersonalSlots = false; }
@@ -390,7 +429,7 @@ namespace Barotrauma
if (SlotTypes[index] == InvSlotType.Any)
{
if (!item.AllowedSlots.Contains(InvSlotType.Any)) { return false; }
if (!item.GetComponents<Pickable>().Any(p => p.AllowedSlots.Contains(InvSlotType.Any))) { return false; }
if (slots[index].Any()) { return slots[index].Contains(item); }
PutItem(item, index, user, true, createNetworkEvent);
return true;
@@ -399,20 +438,23 @@ namespace Barotrauma
InvSlotType placeToSlots = InvSlotType.None;
bool slotsFree = true;
foreach (InvSlotType allowedSlot in item.AllowedSlots)
foreach (Pickable pickable in item.GetComponents<Pickable>())
{
if (!allowedSlot.HasFlag(SlotTypes[index])) { continue; }
#if CLIENT
if (PersonalSlots.HasFlag(allowedSlot)) { hidePersonalSlots = false; }
#endif
for (int i = 0; i < capacity; i++)
foreach (InvSlotType allowedSlot in pickable.AllowedSlots)
{
if (allowedSlot.HasFlag(SlotTypes[i]) && slots[i].Any() && !slots[i].Contains(item))
if (!allowedSlot.HasFlag(SlotTypes[index])) { continue; }
#if CLIENT
if (PersonalSlots.HasFlag(allowedSlot)) { hidePersonalSlots = false; }
#endif
for (int i = 0; i < capacity; i++)
{
slotsFree = false;
break;
if (allowedSlot.HasFlag(SlotTypes[i]) && slots[i].Any() && !slots[i].Contains(item))
{
slotsFree = false;
break;
}
placeToSlots = allowedSlot;
}
placeToSlots = allowedSlot;
}
}
@@ -319,6 +319,25 @@ namespace Barotrauma.Items.Components
public override void Equip(Character character)
{
//if the item has multiple Pickable components (e.g. Holdable and Wearable, check that we don't equip it in hands when the item is worn or vice versa)
if (item.GetComponents<Pickable>().Count() > 0)
{
bool inSuitableSlot = false;
for (int i = 0; i < character.Inventory.Capacity; i++)
{
if (character.Inventory.GetItemsAt(i).Contains(item))
{
if (character.Inventory.SlotTypes[i] != InvSlotType.Any &&
allowedSlots.Any(a => a.HasFlag(character.Inventory.SlotTypes[i])))
{
inSuitableSlot = true;
break;
}
}
}
if (!inSuitableSlot) { return; }
}
picker = character;
if (item.Removed)
@@ -327,6 +346,13 @@ namespace Barotrauma.Items.Components
return;
}
var wearable = item.GetComponent<Wearable>();
if (wearable != null)
{
//cannot hold and wear an item at the same time
wearable.Unequip(character);
}
if (character != null) { item.Submarine = character.Submarine; }
if (item.body == null)
{
@@ -75,6 +75,14 @@ namespace Barotrauma.Items.Components
IsActive = true;
}
public override void Move(Vector2 amount)
{
if (trigger != null && amount.LengthSquared() > 0.00001f)
{
trigger.SetTransform(item.SimPosition, 0.0f);
}
}
public override void Update(float deltaTime, Camera cam)
{
if (holdable != null && !holdable.Attached)
@@ -132,7 +132,12 @@ namespace Barotrauma.Items.Components
}
return false;
}
public override bool SecondaryUse(float deltaTime, Character character = null)
{
return characterUsable || character == null;
}
public override void Drop(Character dropper)
{
base.Drop(dropper);
@@ -90,6 +90,24 @@ namespace Barotrauma.Items.Components
public virtual bool OnPicked(Character picker)
{
//if the item has multiple Pickable components (e.g. Holdable and Wearable, check that we don't equip it in hands when the item is worn or vice versa)
if (item.GetComponents<Pickable>().Count() > 0)
{
bool alreadyEquipped = false;
for (int i = 0; i < picker.Inventory.Capacity; i++)
{
if (picker.Inventory.GetItemsAt(i).Contains(item))
{
if (picker.Inventory.SlotTypes[i] != InvSlotType.Any &&
!allowedSlots.Any(a => a.HasFlag(picker.Inventory.SlotTypes[i])))
{
alreadyEquipped = true;
break;
}
}
}
if (alreadyEquipped) { return false; }
}
if (picker.Inventory.TryPutItemWithAutoEquipCheck(item, picker, allowedSlots))
{
if (!picker.HeldItems.Contains(item) && item.body != null) { item.body.Enabled = false; }
@@ -151,6 +151,11 @@ namespace Barotrauma.Items.Components
return true;
}
public override bool SecondaryUse(float deltaTime, Character character = null)
{
return characterUsable || character == null;
}
public Projectile FindProjectile(bool triggerOnUseOnContainers = false)
{
var containedItems = item.OwnInventory?.AllItemsMod;
@@ -660,6 +660,7 @@ namespace Barotrauma.Items.Components
/// </summary>
public virtual bool HasAccess(Character character)
{
if (item.IgnoreByAI) { return false; }
if (!item.IsInteractable(character)) { return false; }
if (requiredItems.None()) { return true; }
if (character.Inventory != null)
@@ -678,7 +679,6 @@ namespace Barotrauma.Items.Components
public virtual bool HasRequiredItems(Character character, bool addMessage, string msg = null)
{
if (requiredItems.None()) { return true; }
if (!character.IsPlayer && character.Params.AI != null && character.Params.AI.Infiltrate) { return true; }
if (character.Inventory == null) { return false; }
bool hasRequiredItems = false;
bool canContinue = true;
@@ -204,6 +204,8 @@ namespace Barotrauma.Items.Components
return ContainableItems.Find(c => c.MatchesItem(itemPrefab)) != null;
}
readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
public override void Update(float deltaTime, Camera cam)
{
if (item.ParentInventory is CharacterInventory)
@@ -235,8 +237,8 @@ namespace Barotrauma.Items.Components
if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
var targets = new List<ISerializableEntity>();
effect.GetNearbyTargets(item.WorldPosition, targets);
targets.Clear();
targets.AddRange(effect.GetNearbyTargets(item.WorldPosition, targets));
effect.Apply(ActionType.OnActive, deltaTime, item, targets);
}
}
@@ -403,10 +405,26 @@ namespace Barotrauma.Items.Components
{
if (SpawnWithId.Length > 0)
{
ItemPrefab prefab = ItemPrefab.Prefabs.Find(m => m.Identifier == SpawnWithId);
if (prefab != null && Inventory != null && Inventory.CanBePut(prefab))
string[] splitIds = SpawnWithId.Split(',');
foreach (string id in splitIds)
{
Entity.Spawner?.AddToSpawnQueue(prefab, Inventory, spawnIfInventoryFull: false);
ItemPrefab prefab = ItemPrefab.Prefabs.Find(m => m.Identifier == id);
if (prefab != null && Inventory != null && Inventory.CanBePut(prefab))
{
bool isEditor = false;
#if CLIENT
isEditor = Screen.Selected == GameMain.SubEditorScreen;
#endif
if (!isEditor && (Entity.Spawner == null || Entity.Spawner.Removed) && GameMain.NetworkMember == null)
{
var spawnedItem = new Item(prefab, Vector2.Zero, null);
Inventory.TryPutItem(spawnedItem, null, spawnedItem.AllowedSlots, createNetworkEvent: false);
}
else
{
Entity.Spawner?.AddToSpawnQueue(prefab, Inventory, spawnIfInventoryFull: false);
}
}
}
}
}
@@ -67,6 +67,9 @@ namespace Barotrauma.Items.Components
public Character LastAIUser { get; private set; }
[Serialize(defaultValue: false, isSaveable: true)]
public bool LastUserWasPlayer { get; private set; }
private Character lastUser;
public Character LastUser
{
@@ -178,8 +181,6 @@ namespace Barotrauma.Items.Components
[Serialize(0.0f, true)]
public float AvailableFuel { get; set; }
public bool LastUserWasPlayer { get; private set; }
public Reactor(Item item, XElement element)
: base(item, element)
{
@@ -251,11 +252,13 @@ namespace Barotrauma.Items.Components
allowedFissionRate.X = Math.Min(allowedFissionRate.X, allowedFissionRate.Y - 10);
float heatAmount = GetGeneratedHeat(fissionRate);
float temperatureDiff = (heatAmount - turbineOutput) - Temperature;
Temperature += MathHelper.Clamp(Math.Sign(temperatureDiff) * 10.0f * deltaTime, -Math.Abs(temperatureDiff), Math.Abs(temperatureDiff));
//if (item.InWater && AvailableFuel < 100.0f) Temperature -= 12.0f * deltaTime;
FissionRate = MathHelper.Lerp(fissionRate, Math.Min(targetFissionRate, AvailableFuel), deltaTime);
TurbineOutput = MathHelper.Lerp(turbineOutput, targetTurbineOutput, deltaTime);
float temperatureFactor = Math.Min(temperature / 50.0f, 1.0f);
@@ -564,6 +567,7 @@ namespace Barotrauma.Items.Components
public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return false; }
character.AIController.SteeringManager.Reset();
bool shutDown = objective.Option.Equals("shutdown", StringComparison.OrdinalIgnoreCase);
IsActive = true;
@@ -598,6 +602,7 @@ namespace Barotrauma.Items.Components
void ReportFuelRodCount()
{
if (!character.IsOnPlayerTeam) { return; }
if (character.Submarine != Submarine.MainSub) { return; }
int remainingFuelRods = Submarine.MainSub.GetItems(false).Count(i => i.HasTag("reactorfuel") && i.Condition > 1);
if (remainingFuelRods == 0)
{

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